Metal: platform-aware expert-cache defaults via honest storage probe (#379)#386
Open
monotophic wants to merge 3 commits into
Open
Metal: platform-aware expert-cache defaults via honest storage probe (#379)#386monotophic wants to merge 3 commits into
monotophic wants to merge 3 commits into
Conversation
Metal's per-buffer residency cost makes the engine's own expert LRU cache anti-productive on fast NVMe (issue JustVugg#379, maintainer-endorsed root cause): the OS page cache already streams cold experts cheaper than Metal can re-pin them. Deciding "fast enough to prefer the page cache" needs a real measurement, not a guess -- buffered reads lie (page-cache hits read 23-97 GB/s on the reference box; the real F_NOCACHE number was 14 GB/s). Adds coli_ssd_probe_raw()/coli_ssd_probe_cached(): open a real model shard with F_NOCACHE (mirrors compat_open_direct()/iobench.c's __APPLE__ branch), random 16K-aligned pread, single thread, budget ~0.35s wall time. The result is cached in <model>/.coli_ssd -- written atomically (tmp+rename, like stats_dump_q: the model dir is shared with concurrent serve+run engines) -- so every startup after the first reads a file instead of re-probing; a missing or unparsable cache re-probes. Two probers racing on a virgin dir both measure under mutual contention, i.e. a LOW reading: the safe direction (the platform default just stays off). darwin-only (#ifdef __APPLE__), not yet called from main() -- this commit only adds the capability. Wiring it into the cap/CAP_RAISE defaults follows in the next commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ce (JustVugg#379, S2/S3) Wires the F_NOCACHE probe (previous commit) into the expert-cache-cap and CAP_RAISE defaults. When Metal is active, darwin, and the probe measures the model volume as fast (>= COLI_SSD_FAST_GBS, default 4.0 GB/s), the engine's own expert LRU cache is anti-productive: the OS page cache streams cold experts cheaper than Metal can re-pin them (issue JustVugg#379). Default cap drops to 1/layer and CAP_RAISE defaults to off; a one-line stderr notice explains the choice. Silent when the probe ran but storage was slow -- defaults stay unchanged. Precedence (coli_resolve_cap(), pure and unit-tested in isolation): explicit --cap CLI > explicit CAP env > platform default (1) > historic default The historic default preserves both prior behaviors exactly: a bare `./glm` (no positional argument) keeps the engine's old 64; the coli wrapper's "0 = auto" sentinel keeps the 8 coli always forced. Every non-qualifying invocation is byte-identical to before this change. Same shape for CAP_RAISE: an explicit CAP_RAISE env always wins over the platform default. An explicit setting is never overridden by the probe. On a qualifying box an explicit --cap N intentionally does NOT re-enable auto-raise (CAP_RAISE still defaults to 0 there): Metal's residency cost scales with cache growth, so auto-raise is the same mechanism the minimal-cache default avoids; a user who explicitly sizes the cache gets exactly that size. CAP_RAISE=1 re-enables it. coli's own --cap default changes from a hardcoded 8 to 0 ("not explicitly set"), so the platform default can actually reach `coli chat`/`coli run` users instead of being permanently shadowed by Python's own default. openai_server.py's --cap flag and its internal Engine/serve defaults get the same sentinel for consistency (it forwards straight to the engine, bypassing coli). Note this makes an explicit `--cap 0`/`CAP=0` mean "auto" on every platform: literal cap=0 (a zero-slot, always-cold-load cache) was undocumented and unused repo-wide. Adds tests/test_cap_precedence.c: exhaustive precedence-table coverage of coli_resolve_cap() including bare-vs-sentinel-vs-explicit, portable (no Metal/Apple hardware needed). Updates docs/SETTINGS.md and docs/ENVIRONMENT.md (--cap 0=auto, CAP, conditional CAP_RAISE default, COLI_SSD_FAST_GBS); eval_glm.py keeps its fixed --cap 64 deliberately (benchmark reproducibility) with a comment saying so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… S4)
`coli doctor` and `coli plan` now read <model>/.coli_ssd, if the
engine has already written it, and display the measured GB/s. Purely
read-and-display: never re-measures, never guesses, and shows nothing
before the first Metal+darwin engine startup.
resource_plan.read_ssd_probe() is the shared reader; build_plan()
threads it into plan["ssd_probe_gbs"] (None if not yet cached),
format_plan() adds one line when present, and doctor.run_doctor() adds
a storage.ssd_probe check ("pass" with the cached value, or "skip"
before the first probe / when the model itself is invalid).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 18, 2026
Owner
|
Heads-up: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Authored by Fable 5 in Claude Code, analysis in partnership with @monotophic
Problem
On Apple Silicon (Metal backend, unified memory, fast NVMe), colibri's stock
defaults leave a large performance win on the table. Measured campaign
(issue #379, M5 Max 128 GB):
MTP=0 CAP_RAISE=0 --ram 90 --cap 1)Scope caveat, to prevent over-reading: the tuned row is a four-knob
configuration. This PR changes the defaults of exactly two of those knobs
(cap, CAP_RAISE); MTP and RAM sizing remain manual and out of scope. Stock
Metal runs with these defaults should therefore land somewhere between 0.4
and 2.0 tok/s, not at 2.0 — this PR is a partial capture of the measured
configuration, and how much of the 5x the two cache knobs alone recover is
exactly what the on-box validation run (below) will measure.
Issues #180 (M3 Ultra) and #107 (M4 Pro) corroborate the same shape on other
Apple Silicon boxes. As the maintainer noted in #379: no one maintaining this
repo has Metal hardware, and this PR was explicitly invited from the field.
Mechanism (#379)
Metal's per-buffer residency cost makes the engine's own expert LRU cache
anti-productive on fast NVMe. The OS page cache already streams cold
experts back in cheaper than Metal can re-pin them into GPU-visible buffers,
so a large expert cache just adds residency churn on top of I/O that was
already going to happen. On this class of hardware, a minimal cache (so the
engine gets out of the page cache's way) beats a large one.
Whether a volume is "fast" has to be measured, not assumed: buffered
reads lie (page-cache hits read 23-97 GB/s on the reference box; a real
F_NOCACHEread measured 14 GB/s on the same file). That's why this PR addsan honest storage probe rather than just flipping the default unconditionally
on every Metal build.
Design
S1 -- honest storage probe (
glm.c,coli_ssd_probe_raw/coli_ssd_probe_cached)C-side, in the engine's own startup path, so it covers
glmdirect users andthe
coliwrapper alike -- Python-only logic would miss direct users.Mirrors
compat_open_direct()/iobench.c's existing__APPLE__branch: opena real model shard (readdir order -- any shard is representative for a
bandwidth probe),
fcntl(F_NOCACHE,1), disable readahead, random 16K-alignedpread, single thread, ~0.35s wall-clock budget. The result is cached in<model>/.coli_ssd(plain-text GB/s) so every startup after the first readsa file instead of re-probing; a missing or unparsable cache file just
re-probes. The cache write is atomic (tmp+rename, the same model-dir
convention as
stats_dump_q); if two engines race on a virgin dir (e.g. aconcurrent serve + run pair), both probe under mutual contention and may
cache a low reading -- the safe direction (defaults simply stay historic);
delete
.coli_ssdto re-measure, and an unparsable file re-probesautomatically on the next start.
S2 -- platform-aware cache defaults (
glm.c,coli,openai_server.py)When
g_metal_enabled && darwin && probe-fast:CAP_RAISEdefaults to 0 (previously 1) -- raising the cache back upunder a generous RAM budget is the same anti-pattern the cap default exists
to avoid.
MTP, RAM sizing, and autopin are untouched -- out of scope for this PR.
Precedence (
coli_resolve_cap(), pure function, unit-tested in isolation):--cap NCLICAPenvCAP_RAISEenvcoliwrapper ("0 = auto" sentinel), 64 for a bare./glmwith no positional argumentAn explicit setting is never overridden by the probe, at any tier.
Note on the cap/CAP_RAISE coupling (deliberate): on a qualifying box, an
explicit
--cap 16does not re-enable auto-raise --CAP_RAISEstilldefaults to 0 there. Rationale: the Metal residency cost scales with cache
growth, so auto-raise is the same mechanism the minimal-cache default avoids;
a user who explicitly sizes the cache gets exactly that size. Explicit
CAP_RAISE=1re-enables auto-raise.S3 -- the one-line notice (
glm.c, stderr, alongside the existing config banner)Printed only when the platform cap default actually engages:
Silent when the probe ran but storage was slow -- defaults stay at their
historic values with no extra output.
S4 -- doctor/plan surfacing (
doctor.py,resource_plan.py)coli doctorandcoli planread<model>/.coli_ssdif present and displayit. Read-and-display only: neither command re-measures or guesses. A new
storage.ssd_probedoctor check reports "pass" with the cached GB/s, or"skip" before the first Metal+darwin startup.
What's explicitly out of scope
--ram), autopin -- their defaults are untouched (and arethe other half of the measured 5x; see the scope caveat at the top).
CACHE_ROUTE, sampling -- nothing here can change a generatedtoken. Cache sizing is a pure performance knob: it changes how often an
expert is re-read from disk, never which expert gets computed for a given
token.
(
g_metal_enabledstays 0, or the measured GB/s is under the threshold,default 4.0 GB/s,
COLI_SSD_FAST_GBSto override), and every invocationresolves byte-identically to today -- including the bare
./glmfallbackof 64.
Behavioral disclosures (small, but stated plainly)
--cap 0/CAP=0now mean "auto" on every platform, not just Metal.This is the sentinel that lets the wrapper say "not explicitly set."
Previously a literal cap=0 was accepted and produced a working zero-slot,
always-cold-load cache mode; that mode was undocumented and unused
repo-wide (no test, tool, doc, or wrapper passes 0). Anyone who somehow
relied on it can get the nearest supported equivalent with
--cap 1.coli's--capargparse default changed from8to0(auto).Without this,
colialways forced an explicit8onto the engine commandline, which (per the precedence table) would have permanently shadowed the
platform default for every wrapped invocation.
openai_server.py's--capflag and its internalEngine/serve()defaults got the samesentinel so programmatic callers don't silently bypass auto.
tools/eval_glm.py) keeps its fixed--cap 64deliberately -- benchmarks need a reproducible cache size, not a
platform-dependent auto (commented in the source).
Durable vs. current-state (a review map)
Two kinds of change ship here, and they age differently:
Durable machinery -- the F_NOCACHE probe and
.coli_ssdcache, thecoli_resolve_cap()precedence chain and cap sentinel, the noticemechanism, and the doctor/plan surfacing. None of these encode an opinion
about how big the cache should be; they survive any change in residency
economics.
Current-state calibration -- the qualifying-platform values: cap 1
and CAP_RAISE 0. These encode today's measured economics (per-buffer
Metal residency cost makes a growing expert cache anti-productive, #379)
and are deliberately concentrated so that revisiting them is a two-constant
edit. "Today" is a checkable coordinate, not rhetoric: measured 2026-07 on
macOS 26.5 (M5 Max, 128 GB) against engine base
caa49f7(dev) -- thein-code marker comments carry the same stamp. A reader far from those
coordinates (newer macOS Metal runtime, or an engine that has absorbed the
#379 residency work) should treat the constants as suspect and re-run the
#379 A/B (cap 1 vs cap 16, hit rate vs tok/s) before trusting them. Two
foreseeable developments would flip them:
already flips the sign of caching on the reference box (cap16 ~ +5% over
cap1 with heap-backed slabs); a follow-up PR is planned from the same
investigation. If it lands, the right qualifying default likely becomes a
larger, RAM-scaled cap -- not 1.
runtime-managed residency in current and upcoming macOS releases). If
expert storage migrates there, the per-buffer registration cost -- the
entire reason cap 1 wins today -- changes wholesale.
In both cases the edit is confined to the platform-default branch of
coli_resolve_cap()and the singleCAP_RAISEdefault expression (bothcarry a marker comment in the code); probe, precedence, notice, and
surfacing are unaffected. The
COLI_SSD_FAST_GBSthreshold (4.0) islikewise a current-economics number, with the env override as escape hatch.
Testing
Author-side testing was parse-level and unit-level only -- no model was
loaded and no inference was run:
tests/test_cap_precedence.c-- exhaustive precedence-table coverage ofcoli_resolve_cap(), including bare-invocation vs wrapper-sentinel vsexplicit CLI/env on fast and slow platforms. Portable (no Metal/Apple
hardware required), part of the default
make test-cgate.tests/test_resource_plan.py/tests/test_doctor.py-- cache-fileparsing (
read_ssd_probe: missing, empty, malformed, negative, valid) andthe doctor/plan surfacing contract.
make glm METAL=1and plainmake glmboth compile clean(
-Wall -Wextra); fullmake test-c+ python unittest suites pass.dummy
*.safetensorsfile (noconfig.json, so every run exits beforemodel_init-- deliberately): notice printed +cap=1on a fast volume;.coli_ssdwritten once (atomically) and reused on the next start;--cap 16/CAP=4override silently; forced-slow threshold andnon-Metal builds stay silent at historic defaults; corrupted
.coli_ssdre-probes; bare
./glmstill resolves cap 64.On-box validation (performed 2026-07-18): M5 Max 128 GB, macOS 26.5.2,
real GLM-5.2 int4 model, engine at this branch's HEAD (base
caa49f7,dev). Three-run block, fixed prompt,
temp 0, 32 decoded tokens each:
(
METAL: fast SSD (9.7 GB/s) -- page cache favored, expert cache minimal (cap 1); override with --cap), cap resolved to 1,.coli_ssdwrittenonce. Decode 1.48 tok/s vs 0.4 at the old defaults -- the two cache
knobs alone recover roughly 3.7x of the measured 5x (MTP and RAM sizing
remain manual, as scoped above).
--cap 16: silent (no notice), cap honored at 16/layer,.coli_ssduntouched. Decode 0.86 tok/s despite the hit rate rising24% -> 50% -- a same-session replication of the Metal — growing the expert cache makes decode SLOWER #379 mechanism this PR
encodes: growing the Metal-registered cache makes decode slower even as
it hits more.
.coli_ssdmtime unchanged,same 9.7 GB/s in the notice), cap=1 again, decode 1.49 tok/s
replicating run 1; temp-0 output text identical across the two stock runs.