730 lines
31 KiB
C
730 lines
31 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/*
|
|
* test_memwatch.c — Phase 1 MemoryAware unit tests (host signals + memwatch
|
|
* publish/read + level derivation + reserve policy). Self-contained: no model,
|
|
* no GPU, no external test harness — links only the mem module + libm, so it
|
|
* builds and runs on the Mac (the host-signal path is fully exercised there).
|
|
*
|
|
* Covers:
|
|
* - pn_host_signals_sample fills a plausible total/avail on this host
|
|
* - pn_host_reserve_bytes honors the max(1GB,15%) policy + env override
|
|
* - pn_die_reserve_bytes default + env override
|
|
* - seqlock read returns a self-consistent snapshot
|
|
* - the die feed republishes and drives per-die level escalation
|
|
* - level names are total
|
|
* - path-aware request admission (GPU-resident admits full context up to
|
|
* max_seq, CAPs past it; CPU path keeps the conservative LUT)
|
|
* - the model-load host-spike budget (PN_LOAD_SPIKE_BYTES) refuses a
|
|
* squeezed load
|
|
*/
|
|
|
|
#include "../src/core/mem/pn_memwatch.h"
|
|
#include "../src/core/mem/pn_host_signals.h"
|
|
#include "../src/core/mem/pn_admission.h"
|
|
#include "../src/core/mem/pn_ladder.h"
|
|
#include "../src/serve/signal_handlers.h"
|
|
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
/* signal_handlers.c calls into the serve registry on shutdown. Linking the real
|
|
* one would drag in batch_sched + the registry client and break this target's
|
|
* "no model, no GPU, builds on the Mac" contract, so the CPU-only test provides
|
|
* the one symbol itself. There is nothing registered in this process to
|
|
* deregister. (This target did not link at all before — Phase 10.) */
|
|
void serve_registry_deregister_all(void);
|
|
void serve_registry_deregister_all(void) { }
|
|
|
|
static int g_failed = 0;
|
|
|
|
#define CHECK(cond, msg) do { \
|
|
if (!(cond)) { \
|
|
fprintf(stderr, " FAIL: %s (%s:%d)\n", (msg), __FILE__, __LINE__); \
|
|
g_failed++; \
|
|
} \
|
|
} while (0)
|
|
|
|
static void test_host_sample(void)
|
|
{
|
|
printf("host_sample:\n");
|
|
pn_host_sample s;
|
|
pn_host_signals_sample(&s);
|
|
/* Every platform we build on has physical RAM; total must be non-zero and
|
|
* available must not exceed total. */
|
|
CHECK(s.total_bytes > 0, "total RAM should be > 0");
|
|
CHECK(s.avail_bytes <= s.total_bytes, "avail should not exceed total");
|
|
printf(" total=%llu MiB avail=%llu MiB psi_some=%.2f\n",
|
|
(unsigned long long)(s.total_bytes >> 20),
|
|
(unsigned long long)(s.avail_bytes >> 20), s.psi_some_avg10);
|
|
}
|
|
|
|
static void test_reserve_policy(void)
|
|
{
|
|
printf("reserve_policy:\n");
|
|
unsetenv("PN_HOST_RESERVE_BYTES");
|
|
unsetenv("PN_DIE_RESERVE_BYTES");
|
|
|
|
/* 8 GiB host: 15% = ~1.2 GiB, above the 1 GiB floor -> fraction wins. */
|
|
uint64_t eight_gib = 8ULL << 30;
|
|
uint64_t r8 = pn_host_reserve_bytes(eight_gib);
|
|
CHECK(r8 == (uint64_t)(0.15 * (double)eight_gib), "8GiB reserve = 15%");
|
|
CHECK(r8 > (1ULL << 30), "8GiB reserve above the 1GiB floor");
|
|
|
|
/* 2 GiB host: 15% = ~300 MiB, below the floor -> floor wins. */
|
|
uint64_t two_gib = 2ULL << 30;
|
|
uint64_t r2 = pn_host_reserve_bytes(two_gib);
|
|
CHECK(r2 == (1ULL << 30), "small host clamps to the 1GiB floor");
|
|
|
|
/* Env override takes precedence regardless of total. */
|
|
setenv("PN_HOST_RESERVE_BYTES", "500000000", 1);
|
|
CHECK(pn_host_reserve_bytes(eight_gib) == 500000000ULL, "host env override");
|
|
unsetenv("PN_HOST_RESERVE_BYTES");
|
|
|
|
CHECK(pn_die_reserve_bytes() == PN_DIE_RESERVE_BYTES, "die reserve default");
|
|
setenv("PN_DIE_RESERVE_BYTES", "2147483648", 1);
|
|
CHECK(pn_die_reserve_bytes() == 2147483648ULL, "die env override");
|
|
unsetenv("PN_DIE_RESERVE_BYTES");
|
|
|
|
/* PSI-full reject threshold: default, env override, garbage/zero fall back
|
|
* (a zero threshold would re-arm the transient-blip hypersensitivity). */
|
|
unsetenv("PN_PSI_FULL_REJECT");
|
|
CHECK(pn_psi_full_reject_pct() == PN_PSI_FULL_REJECT_DEFAULT,
|
|
"psi reject default");
|
|
setenv("PN_PSI_FULL_REJECT", "25.5", 1);
|
|
CHECK(pn_psi_full_reject_pct() == 25.5, "psi reject env override");
|
|
setenv("PN_PSI_FULL_REJECT", "0", 1);
|
|
CHECK(pn_psi_full_reject_pct() == PN_PSI_FULL_REJECT_DEFAULT,
|
|
"psi reject zero falls back to default");
|
|
setenv("PN_PSI_FULL_REJECT", "garbage", 1);
|
|
CHECK(pn_psi_full_reject_pct() == PN_PSI_FULL_REJECT_DEFAULT,
|
|
"psi reject garbage falls back to default");
|
|
unsetenv("PN_PSI_FULL_REJECT");
|
|
}
|
|
|
|
static void test_publish_read(void)
|
|
{
|
|
printf("publish_read:\n");
|
|
/* A synchronous poll publishes host truth without starting the thread. */
|
|
pn_memwatch_poll_now();
|
|
|
|
pn_mem_state a, b;
|
|
pn_memwatch_read(&a);
|
|
CHECK(a.host_total_bytes > 0, "read sees published host total");
|
|
|
|
/* seq is monotonic across a second poll. */
|
|
pn_memwatch_poll_now();
|
|
pn_memwatch_read(&b);
|
|
CHECK(b.seq > a.seq, "seq advances on republish");
|
|
}
|
|
|
|
static void test_die_feed_and_level(void)
|
|
{
|
|
printf("die_feed_and_level:\n");
|
|
pn_memwatch_poll_now();
|
|
|
|
uint64_t budget = 11ULL << 30; /* ~11 GiB usable on a 12 GiB die */
|
|
|
|
/* Plenty free -> die contributes OK. */
|
|
pn_memwatch_set_die(0, 10ULL << 30, budget, 1ULL << 30);
|
|
pn_mem_state s;
|
|
pn_memwatch_read(&s);
|
|
CHECK(s.n_dies >= 1, "die 0 registered");
|
|
CHECK(s.per_die_budget[0] == budget, "die 0 budget stored");
|
|
CHECK(s.per_die_used[0] == (1ULL << 30), "die 0 used stored");
|
|
|
|
/* Nearly exhausted die -> level escalates to at least TIGHT. */
|
|
pn_memwatch_set_die(0, budget / 32, budget, budget);
|
|
pn_memwatch_read(&s);
|
|
CHECK(s.level >= PN_MEM_LEVEL_TIGHT, "near-empty die escalates level");
|
|
|
|
/* Fully exhausted die -> CRITICAL. */
|
|
pn_memwatch_set_die(0, 0, budget, budget);
|
|
pn_memwatch_read(&s);
|
|
CHECK(s.level == PN_MEM_LEVEL_CRITICAL, "empty die is CRITICAL");
|
|
|
|
/* Out-of-range die id is a no-op (no crash, no n_dies growth past 16). */
|
|
pn_memwatch_set_die(999, 0, budget, 0);
|
|
pn_memwatch_read(&s);
|
|
CHECK(s.n_dies <= PN_MEM_MAX_DIES, "out-of-range die id ignored");
|
|
}
|
|
|
|
/* --- Phase 10: PSI noise floor + escalation dwell -------------------------- */
|
|
|
|
/* A state with ample byte headroom and no die reporting, so the ONLY thing that
|
|
* can move the level is PSI. 8 GiB total, 6 GiB available — miles above a
|
|
* max(1 GiB, 15%) = ~1.2 GiB reserve. Mirrors a healthy K80 host. */
|
|
static pn_mem_state healthy_state(double psi_some, double psi_full)
|
|
{
|
|
pn_mem_state s = {0};
|
|
s.host_total_bytes = 8ULL << 30;
|
|
s.host_avail_bytes = 6ULL << 30;
|
|
s.host_psi_some_avg10 = psi_some;
|
|
s.host_psi_full_avg10 = psi_full;
|
|
return s;
|
|
}
|
|
|
|
static void test_psi_noise_floor(void)
|
|
{
|
|
printf("psi_noise_floor:\n");
|
|
unsetenv("PN_PSI_FULL_REJECT");
|
|
unsetenv("PN_PSI_FULL_TIGHT");
|
|
unsetenv("PN_PSI_FULL_WATCH");
|
|
unsetenv("PN_PSI_SOME_TIGHT");
|
|
unsetenv("PN_PSI_SOME_WATCH");
|
|
unsetenv("PN_HOST_RESERVE_BYTES");
|
|
|
|
/* Hard signals alone say OK on a healthy box, at any PSI. */
|
|
pn_mem_state s = healthy_state(0.2, 0.2);
|
|
CHECK(pn_mem_hard_level(&s) == PN_MEM_LEVEL_OK,
|
|
"6 GiB avail of 8 GiB is OK on bytes");
|
|
|
|
/* THE BUG: the exact K80-host reading that tripped TIGHT on a healthy serve
|
|
* (2026-07-27, psi some 0.2 full 0.2 at 5578 MiB avail). 0.2% full-stall
|
|
* avg10 = 20 ms out of 10 s — noise, not pressure. */
|
|
CHECK(pn_mem_psi_level(&s) == PN_MEM_LEVEL_OK,
|
|
"psi(some 0.2 full 0.2) is below the noise floor");
|
|
|
|
/* And the recovery reading is obviously OK too. */
|
|
s = healthy_state(0.0, 0.0);
|
|
CHECK(pn_mem_psi_level(&s) == PN_MEM_LEVEL_OK, "psi(0,0) is OK");
|
|
|
|
/* Proportionate escalation above the floor. */
|
|
s = healthy_state(0.0, 1.5);
|
|
CHECK(pn_mem_psi_level(&s) == PN_MEM_LEVEL_WATCH, "full 1.5% -> WATCH");
|
|
s = healthy_state(0.0, 6.0);
|
|
CHECK(pn_mem_psi_level(&s) == PN_MEM_LEVEL_TIGHT, "full 6% -> TIGHT");
|
|
s = healthy_state(0.0, 20.0);
|
|
CHECK(pn_mem_psi_level(&s) == PN_MEM_LEVEL_CRITICAL, "full 20% -> CRITICAL");
|
|
|
|
/* "some" pressure is normal on any busy box; it needs a floor too. */
|
|
s = healthy_state(0.2, 0.0);
|
|
CHECK(pn_mem_psi_level(&s) == PN_MEM_LEVEL_OK, "some 0.2% is noise");
|
|
s = healthy_state(6.0, 0.0);
|
|
CHECK(pn_mem_psi_level(&s) == PN_MEM_LEVEL_WATCH, "some 6% -> WATCH");
|
|
s = healthy_state(12.0, 0.0);
|
|
CHECK(pn_mem_psi_level(&s) == PN_MEM_LEVEL_TIGHT, "some 12% -> TIGHT");
|
|
|
|
/* Thresholds are env-tunable, and garbage/zero falls back to the default. */
|
|
setenv("PN_PSI_FULL_TIGHT", "0.1", 1);
|
|
s = healthy_state(0.0, 0.2);
|
|
CHECK(pn_mem_psi_level(&s) == PN_MEM_LEVEL_TIGHT,
|
|
"PN_PSI_FULL_TIGHT override honored");
|
|
setenv("PN_PSI_FULL_TIGHT", "0", 1);
|
|
CHECK(pn_psi_full_tight_pct() == PN_PSI_FULL_TIGHT_DEFAULT,
|
|
"zero threshold falls back to the default");
|
|
unsetenv("PN_PSI_FULL_TIGHT");
|
|
|
|
/* HARD signals must still escalate regardless of PSI: real byte exhaustion
|
|
* is not noise. This is the "genuine pressure still protects" case. */
|
|
s = healthy_state(0.0, 0.0);
|
|
s.host_avail_bytes = 512ULL << 20; /* inside a ~1.2 GiB reserve */
|
|
CHECK(pn_mem_hard_level(&s) == PN_MEM_LEVEL_CRITICAL,
|
|
"avail inside the reserve is CRITICAL on bytes alone");
|
|
s.host_avail_bytes = (uint64_t)(1.35 * (1ULL << 30));
|
|
CHECK(pn_mem_hard_level(&s) >= PN_MEM_LEVEL_TIGHT,
|
|
"avail just above the reserve is TIGHT on bytes alone");
|
|
|
|
/* A near-empty die escalates on hard evidence too, PSI silent. */
|
|
s = healthy_state(0.0, 0.0);
|
|
s.n_dies = 1;
|
|
s.per_die_budget[0] = 11ULL << 30;
|
|
s.per_die_free[0] = (11ULL << 30) / 32;
|
|
CHECK(pn_mem_hard_level(&s) >= PN_MEM_LEVEL_TIGHT,
|
|
"near-empty die escalates hard level with PSI at zero");
|
|
}
|
|
|
|
static void test_psi_needs_corroboration(void)
|
|
{
|
|
printf("psi_needs_corroboration:\n");
|
|
|
|
/* Measured on a K80 host during a 3-model load: full-PSI 6.0% (over the TIGHT
|
|
* threshold) with host avail 6338 MiB of 7845 — 81% of the reserve headroom
|
|
* still free. Page-cache reclaim from streaming 17 GB of GGUF, not
|
|
* exhaustion. PSI alone must not push past WATCH here. */
|
|
CHECK(pn_mem_combine_level(PN_MEM_LEVEL_OK, PN_MEM_LEVEL_TIGHT)
|
|
== PN_MEM_LEVEL_WATCH,
|
|
"uncorroborated PSI-TIGHT is capped at WATCH");
|
|
|
|
/* WATCH on its own is allowed through — rung 4a only, near-zero cost. */
|
|
CHECK(pn_mem_combine_level(PN_MEM_LEVEL_OK, PN_MEM_LEVEL_WATCH)
|
|
== PN_MEM_LEVEL_WATCH, "PSI-WATCH passes through");
|
|
|
|
/* Once the bytes corroborate, PSI escalates in full — that is what a
|
|
* leading indicator is FOR. */
|
|
CHECK(pn_mem_combine_level(PN_MEM_LEVEL_WATCH, PN_MEM_LEVEL_TIGHT)
|
|
== PN_MEM_LEVEL_TIGHT,
|
|
"PSI-TIGHT escalates once bytes show a squeeze");
|
|
|
|
/* Whole-box thrash is never capped: at that point MemAvailable's opinion
|
|
* is irrelevant. */
|
|
CHECK(pn_mem_combine_level(PN_MEM_LEVEL_OK, PN_MEM_LEVEL_CRITICAL)
|
|
== PN_MEM_LEVEL_CRITICAL,
|
|
"PSI-CRITICAL is never capped by healthy bytes");
|
|
|
|
/* Hard evidence always wins outright, PSI silent. */
|
|
CHECK(pn_mem_combine_level(PN_MEM_LEVEL_CRITICAL, PN_MEM_LEVEL_OK)
|
|
== PN_MEM_LEVEL_CRITICAL, "hard CRITICAL published with PSI at 0");
|
|
CHECK(pn_mem_combine_level(PN_MEM_LEVEL_TIGHT, PN_MEM_LEVEL_OK)
|
|
== PN_MEM_LEVEL_TIGHT, "hard TIGHT published with PSI at 0");
|
|
CHECK(pn_mem_combine_level(PN_MEM_LEVEL_OK, PN_MEM_LEVEL_OK)
|
|
== PN_MEM_LEVEL_OK, "quiet box is OK");
|
|
}
|
|
|
|
static void test_psi_dwell(void)
|
|
{
|
|
printf("psi_dwell:\n");
|
|
unsetenv("PN_PSI_TRIP_POLLS");
|
|
int need = pn_psi_trip_polls();
|
|
CHECK(need >= 2, "default dwell is more than one sample");
|
|
|
|
pn_psi_dwell d = { 0, PN_MEM_LEVEL_OK };
|
|
|
|
/* A single elevated sample must NOT engage a rung. */
|
|
pn_psi_dwell_update(&d, PN_MEM_LEVEL_TIGHT);
|
|
CHECK(d.effective == PN_MEM_LEVEL_OK, "one TIGHT sample does not trip");
|
|
|
|
/* Nor does an on/off flap — the counter resets on every quiet sample. */
|
|
for (int i = 0; i < 20; i++) {
|
|
pn_psi_dwell_update(&d, PN_MEM_LEVEL_TIGHT);
|
|
pn_psi_dwell_update(&d, PN_MEM_LEVEL_OK);
|
|
CHECK(d.effective == PN_MEM_LEVEL_OK, "flapping PSI never trips");
|
|
}
|
|
|
|
/* Sustained pressure DOES engage, after the dwell. */
|
|
for (int i = 0; i < need; i++)
|
|
pn_psi_dwell_update(&d, PN_MEM_LEVEL_TIGHT);
|
|
CHECK(d.effective == PN_MEM_LEVEL_TIGHT, "sustained TIGHT engages");
|
|
|
|
/* Relax is immediate — this is what lets a clamp release. */
|
|
pn_psi_dwell_update(&d, PN_MEM_LEVEL_OK);
|
|
CHECK(d.effective == PN_MEM_LEVEL_OK, "relax is immediate (no latch)");
|
|
CHECK(d.trips == 0, "relax clears the counter");
|
|
|
|
/* CRITICAL bypasses the dwell entirely — never delay real protection. */
|
|
pn_psi_dwell d2 = { 0, PN_MEM_LEVEL_OK };
|
|
pn_psi_dwell_update(&d2, PN_MEM_LEVEL_CRITICAL);
|
|
CHECK(d2.effective == PN_MEM_LEVEL_CRITICAL,
|
|
"CRITICAL engages on the first sample");
|
|
|
|
/* NULL is a no-op, not a crash. */
|
|
pn_psi_dwell_update(NULL, PN_MEM_LEVEL_CRITICAL);
|
|
}
|
|
|
|
static void test_level_names(void)
|
|
{
|
|
printf("level_names:\n");
|
|
CHECK(strcmp(pn_mem_level_name(PN_MEM_LEVEL_OK), "OK") == 0, "OK name");
|
|
CHECK(strcmp(pn_mem_level_name(PN_MEM_LEVEL_CRITICAL), "CRITICAL") == 0,
|
|
"CRITICAL name");
|
|
}
|
|
|
|
static void test_thread_lifecycle(void)
|
|
{
|
|
printf("thread_lifecycle:\n");
|
|
CHECK(!pn_memwatch_running(), "not running before start");
|
|
CHECK(pn_memwatch_start() == 0, "start succeeds");
|
|
CHECK(pn_memwatch_running(), "running after start");
|
|
CHECK(pn_memwatch_start() == 0, "second start is an idempotent no-op");
|
|
pn_memwatch_stop();
|
|
CHECK(!pn_memwatch_running(), "stopped after stop");
|
|
pn_memwatch_stop(); /* double stop is safe */
|
|
}
|
|
|
|
/* --- Phase 2: admission cost model + gates --- */
|
|
|
|
static void test_admit_cost_monotonic(void)
|
|
{
|
|
printf("admit_cost_monotonic:\n");
|
|
/* Host cost must rise strictly with prompt length across the LUT range and
|
|
* beyond it (the O(N^2)-ish activation peak is the whole point). */
|
|
uint64_t prev = 0;
|
|
int lens[] = { 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536 };
|
|
for (size_t i = 0; i < sizeof(lens) / sizeof(lens[0]); i++) {
|
|
uint64_t c = pn_admit_host_cost(lens[i], true, 0);
|
|
CHECK(c > prev, "host cost strictly increasing");
|
|
prev = c;
|
|
}
|
|
/* 32k must project into multi-GiB territory — that is what OOM-rebooted an
|
|
* 8 GB box, so admission must see it as huge. */
|
|
CHECK(pn_admit_host_cost(32768, true, 0) > (4ULL << 30),
|
|
"32k host cost is multi-GiB");
|
|
CHECK(pn_admit_host_cost(0, true, 0) == 0, "zero-length costs nothing");
|
|
}
|
|
|
|
/* KV Phase 1: admission must price against the model's REAL per-token KV, not
|
|
* the generic 5898-byte constant. Ornith's true geometry (verified on a K80 host):
|
|
* 8 full-attention layers x 4 KV heads x 2(K+V) x 144-byte Q4 row = 9216
|
|
* bytes/token. A dense fp32 model with the same head geometry is far larger.
|
|
* Verify the geometry value drives the cost, and that 0 falls back to the
|
|
* generic constant (never zero-priced). */
|
|
static void test_admit_kv_geometry(void)
|
|
{
|
|
printf("admit_kv_geometry:\n");
|
|
const uint64_t ORNITH_Q4_PER_TOK = 8ULL * 4 * 2 * 144; /* 9216 */
|
|
CHECK(ORNITH_Q4_PER_TOK == 9216, "ornith Q4 KV/token is 9216 bytes");
|
|
|
|
/* The resolver returns the geometry value when given, else the constant. */
|
|
CHECK(pn_admit_kv_per_token(ORNITH_Q4_PER_TOK, true) == ORNITH_Q4_PER_TOK,
|
|
"geometry value wins over the generic constant");
|
|
CHECK(pn_admit_kv_per_token(0, true) == PN_ADMIT_KV_BYTES_PER_TOKEN_Q4,
|
|
"0 falls back to the generic Q4 constant (never zero-priced)");
|
|
CHECK(pn_admit_kv_per_token(0, false) == PN_ADMIT_KV_BYTES_PER_TOKEN_FP32,
|
|
"0 falls back to the generic fp32 constant");
|
|
|
|
/* At a 40K cap, ornith's on-die KV growth is ~368 MiB — comfortably within
|
|
* the die's ~4.8 GiB free budget (the design's single-tenant headroom). */
|
|
uint64_t kv_40k = ORNITH_Q4_PER_TOK * 40000ULL;
|
|
CHECK(kv_40k < (512ULL << 20), "40K ornith KV under 512 MiB");
|
|
CHECK(kv_40k > (256ULL << 20), "40K ornith KV over 256 MiB (real, not tiny)");
|
|
|
|
/* The die-cost must scale with the geometry per-token: the ornith value
|
|
* yields a materially different (larger) die cost than the generic constant
|
|
* at the same length — the mispricing the fix removes. */
|
|
uint64_t die_geom = pn_admit_die_cost(10000, true, ORNITH_Q4_PER_TOK);
|
|
uint64_t die_const = pn_admit_die_cost(10000, true, 0);
|
|
CHECK(die_geom > die_const, "geometry prices ornith above the generic const");
|
|
}
|
|
|
|
static void test_admit_request_verdicts(void)
|
|
{
|
|
printf("admit_request_verdicts:\n");
|
|
unsetenv("PN_HOST_RESERVE_BYTES");
|
|
|
|
pn_mem_state st = {0};
|
|
st.host_total_bytes = 8ULL << 30; /* an 8 GiB host */
|
|
st.n_dies = 0; /* host-only check */
|
|
|
|
pn_admit_result r;
|
|
|
|
/* Ample free RAM, small prompt -> ADMIT. */
|
|
st.host_avail_bytes = 6ULL << 30;
|
|
pn_admit_request(1024, true, 0, false, -1, 0, &st, &r);
|
|
CHECK(r.verdict == PN_ADMIT_OK, "small prompt with room -> ADMIT");
|
|
|
|
/* The catastrophe case: 32k full-attention prefill on ~4 GiB free must NOT
|
|
* be admitted as-is — it is refused or capped, never run whole. */
|
|
st.host_avail_bytes = 4ULL << 30;
|
|
pn_admit_request(32768, true, 0, false, -1, 0, &st, &r);
|
|
CHECK(r.verdict != PN_ADMIT_OK, "32k prefill on a squeezed host is not ADMIT");
|
|
printf(" 32k verdict=%s fit=%d need=%llu MiB\n",
|
|
pn_admit_verdict_name(r.verdict), r.fit_tokens,
|
|
(unsigned long long)(r.host_need_bytes >> 20));
|
|
|
|
/* A capped verdict must propose a strictly smaller, positive window. */
|
|
if (r.verdict == PN_ADMIT_CAP)
|
|
CHECK(r.fit_tokens > 0 && r.fit_tokens < 32768, "CAP window is smaller");
|
|
|
|
/* PSI-full below the reject threshold is a transient blip (avg10 decays
|
|
* over ~a minute after e.g. a model load) — healthy requests must still be
|
|
* ADMITTED on the byte math, not blanket-503'd. */
|
|
unsetenv("PN_PSI_FULL_REJECT");
|
|
st.host_avail_bytes = 7ULL << 30;
|
|
st.host_psi_full_avg10 = 3.5;
|
|
pn_admit_request(1024, true, 0, false, -1, 0, &st, &r);
|
|
CHECK(r.verdict == PN_ADMIT_OK, "transient PSI-full blip still admits");
|
|
|
|
/* Sustained PSI-full at/above the threshold -> REJECT regardless of the
|
|
* byte math: the box is genuinely thrashing. */
|
|
st.host_psi_full_avg10 = PN_PSI_FULL_REJECT_DEFAULT + 5.0;
|
|
pn_admit_request(1024, true, 0, false, -1, 0, &st, &r);
|
|
CHECK(r.verdict == PN_ADMIT_REJECT, "sustained PSI-full rejects new work");
|
|
|
|
/* The threshold is configurable: lower it under the blip and the same blip
|
|
* now rejects. */
|
|
setenv("PN_PSI_FULL_REJECT", "2.0", 1);
|
|
st.host_psi_full_avg10 = 3.5;
|
|
pn_admit_request(1024, true, 0, false, -1, 0, &st, &r);
|
|
CHECK(r.verdict == PN_ADMIT_REJECT, "lowered threshold rejects the blip");
|
|
unsetenv("PN_PSI_FULL_REJECT");
|
|
st.host_psi_full_avg10 = 0.0;
|
|
|
|
/* Die headroom: a nearly-full die caps/refuses even when host is fine. */
|
|
st.host_avail_bytes = 7ULL << 30;
|
|
st.n_dies = 1;
|
|
st.per_die_budget[0] = 11ULL << 30;
|
|
st.per_die_free[0] = 64ULL << 20; /* 64 MiB free — tiny */
|
|
pn_admit_request(16384, true, 0, false, 0, 0, &st, &r);
|
|
CHECK(r.verdict != PN_ADMIT_OK, "near-full die blocks a big prefill");
|
|
}
|
|
|
|
/* Path-aware admission (DECIDED 2026-07-09): a GPU-resident model has ~0
|
|
* per-request host cost, so admission admits the FULL context up to max_window
|
|
* and CAPs only past it — while real host pressure (sustained PSI, avail
|
|
* inside the reserve) still refuses, and the CPU path keeps the LUT. */
|
|
static void test_admit_request_gpu_path(void)
|
|
{
|
|
printf("admit_request_gpu_path:\n");
|
|
unsetenv("PN_HOST_RESERVE_BYTES");
|
|
unsetenv("PN_PSI_FULL_REJECT");
|
|
|
|
pn_mem_state st = {0};
|
|
st.host_total_bytes = 8ULL << 30; /* an 8 GiB host */
|
|
st.host_avail_bytes = 4ULL << 30; /* squeezes the CPU LUT, fine for GPU */
|
|
st.n_dies = 0;
|
|
pn_admit_result r;
|
|
|
|
/* The headline: a 32k prompt on a GPU-resident model ADMITs whole — the
|
|
* same state the CPU path caps (test_admit_request_verdicts above). */
|
|
pn_admit_request(32768, true, 0, true, -1, 32768, &st, &r);
|
|
CHECK(r.verdict == PN_ADMIT_OK, "gpu 32k admits the full context");
|
|
CHECK(r.fit_tokens == 32768, "gpu fit is the whole prompt");
|
|
CHECK(r.host_need_bytes == 0, "gpu per-request host need is ~0");
|
|
|
|
/* Past max_seq the flash->host-softmax fallback would fire: CAP there. */
|
|
pn_admit_request(40000, true, 0, true, -1, 32768, &st, &r);
|
|
CHECK(r.verdict == PN_ADMIT_CAP, "gpu caps past max_seq");
|
|
CHECK(r.fit_tokens == 32768, "gpu cap lands exactly on max_seq");
|
|
|
|
/* Sustained PSI stall still refuses regardless of path. */
|
|
st.host_psi_full_avg10 = PN_PSI_FULL_REJECT_DEFAULT + 5.0;
|
|
pn_admit_request(1024, true, 0, true, -1, 32768, &st, &r);
|
|
CHECK(r.verdict == PN_ADMIT_REJECT, "gpu path rejects a sustained stall");
|
|
st.host_psi_full_avg10 = 0.0;
|
|
|
|
/* MemAvailable already inside the host reserve is genuine pressure. */
|
|
st.host_avail_bytes = 512ULL << 20;
|
|
pn_admit_request(1024, true, 0, true, -1, 32768, &st, &r);
|
|
CHECK(r.verdict == PN_ADMIT_REJECT, "gpu path rejects inside the reserve");
|
|
}
|
|
|
|
/* Model-load host-spike budget: the ~2.4 GB GPU-upload transient (the real
|
|
* 8 GB-box squeeze) must be absorbable on top of the host reserve. */
|
|
static void test_admit_load_spike(void)
|
|
{
|
|
printf("admit_load_spike:\n");
|
|
unsetenv("PN_LOAD_SPIKE_BYTES");
|
|
unsetenv("PN_HOST_RESERVE_BYTES");
|
|
uint64_t spike = 0;
|
|
uint64_t total = 8ULL << 30;
|
|
|
|
/* 6 GiB available minus the ~1.2 GiB reserve absorbs the ~2.4 GB spike. */
|
|
CHECK(pn_admit_load_spike(6ULL << 30, total, &spike),
|
|
"roomy host absorbs the load spike");
|
|
CHECK(spike == PN_ADMIT_LOAD_SPIKE_BYTES_DEFAULT, "default spike budget");
|
|
|
|
/* 3 GiB available leaves only ~1.8 GiB over the reserve — refuse. */
|
|
CHECK(!pn_admit_load_spike(3ULL << 30, total, &spike),
|
|
"squeezed host refuses the load spike");
|
|
|
|
/* Env override: a 1 GiB budget fits where the default did not. */
|
|
setenv("PN_LOAD_SPIKE_BYTES", "1073741824", 1);
|
|
CHECK(pn_load_spike_bytes() == (1ULL << 30), "spike env override");
|
|
CHECK(pn_admit_load_spike(3ULL << 30, total, &spike),
|
|
"overridden spike fits the squeezed host");
|
|
|
|
/* Garbage/zero overrides fall back to the default (never disarm). */
|
|
setenv("PN_LOAD_SPIKE_BYTES", "0", 1);
|
|
CHECK(pn_load_spike_bytes() == PN_ADMIT_LOAD_SPIKE_BYTES_DEFAULT,
|
|
"zero spike falls back to default");
|
|
setenv("PN_LOAD_SPIKE_BYTES", "garbage", 1);
|
|
CHECK(pn_load_spike_bytes() == PN_ADMIT_LOAD_SPIKE_BYTES_DEFAULT,
|
|
"garbage spike falls back to default");
|
|
unsetenv("PN_LOAD_SPIKE_BYTES");
|
|
}
|
|
|
|
static void test_admit_model_load(void)
|
|
{
|
|
printf("admit_model_load:\n");
|
|
unsetenv("PN_DIE_RESERVE_BYTES");
|
|
uint64_t need = 0;
|
|
|
|
/* 16 GB model must NOT fit a 12 GB die. */
|
|
bool fit = pn_admit_model_load(16ULL << 30, 4096, true, 0, 12ULL << 30, &need);
|
|
CHECK(!fit, "16GB model refused on a 12GB die");
|
|
CHECK(need > (16ULL << 30), "need includes weights + KV + activation");
|
|
|
|
/* A 6 GB slice fits a 12 GB die with room for KV + reserve. */
|
|
fit = pn_admit_model_load(6ULL << 30, 4096, true, 0, 12ULL << 30, &need);
|
|
CHECK(fit, "6GB weights fit a 12GB die");
|
|
}
|
|
|
|
static void test_admit_model_load_die(void)
|
|
{
|
|
printf("admit_model_load_die:\n");
|
|
unsetenv("PN_DIE_RESERVE_BYTES");
|
|
uint64_t need = 0;
|
|
|
|
/* The working 2-die split (Q3_K_M @ layer 20, 32k q4 KV): ~8 GiB weights
|
|
* + ~96 MiB KV on one ~11.2 GiB-free die must ADMIT — the gate must never
|
|
* refuse the configuration that is known to run. */
|
|
uint64_t die_free = 11200ULL << 20;
|
|
bool fit = pn_admit_model_load_die(8ULL << 30, 96ULL << 20, die_free, &need);
|
|
CHECK(fit, "working split half fits an 11.2GiB-free die");
|
|
CHECK(need > (8ULL << 30), "need includes weights + KV + scratch");
|
|
|
|
/* A lopsided split (e.g. 38/40 layers -> ~15 GiB on die A) must REFUSE. */
|
|
fit = pn_admit_model_load_die(15ULL << 30, 91ULL << 20, die_free, &need);
|
|
CHECK(!fit, "oversized split share refused");
|
|
|
|
/* A die with stranded VRAM (only 4 GiB free) refuses the normal half. */
|
|
fit = pn_admit_model_load_die(8ULL << 30, 96ULL << 20, 4ULL << 30, &need);
|
|
CHECK(!fit, "stranded die refuses the normal half");
|
|
}
|
|
|
|
/* --- Phase 4: the reactive degradation ladder --- */
|
|
|
|
/* The ladder maps a pressure level onto concrete rungs. Reduce-only and
|
|
* monotonic: OK mirrors config exactly; each higher level pulls the rungs
|
|
* harder and never relaxes one a lower level engaged. */
|
|
static void test_ladder_plan(void)
|
|
{
|
|
printf("ladder_plan:\n");
|
|
unsetenv("PN_LADDER_DISABLE");
|
|
|
|
const int CFG_SLOTS = 16; /* PN_QWEN35MOE_GPU_MAX_SLOTS */
|
|
const int CFG_WINDOW = 32768;/* QWEN35_MAX_SEQ */
|
|
const bool CFG_Q4 = false;/* q4 not the operator default */
|
|
pn_ladder_plan p;
|
|
|
|
/* OK: byte-identical to config, nothing degraded — this is what makes
|
|
* wiring the rungs low-risk on an unpressured box. */
|
|
pn_ladder_plan_for(PN_MEM_LEVEL_OK, CFG_SLOTS, CFG_WINDOW, CFG_Q4, &p);
|
|
CHECK(p.max_slots == CFG_SLOTS, "OK keeps full slots");
|
|
CHECK(p.prefill_window == CFG_WINDOW, "OK keeps full window");
|
|
CHECK(!p.force_kv_q4, "OK does not force q4");
|
|
CHECK(!p.kv_evict, "OK does not evict");
|
|
CHECK(!p.degraded, "OK is not degraded");
|
|
|
|
/* WATCH: force q4 (cheap VRAM win) but no throughput/context hit yet. */
|
|
pn_ladder_plan_for(PN_MEM_LEVEL_WATCH, CFG_SLOTS, CFG_WINDOW, CFG_Q4, &p);
|
|
CHECK(p.force_kv_q4, "WATCH forces q4");
|
|
CHECK(p.max_slots == CFG_SLOTS, "WATCH keeps full slots");
|
|
CHECK(p.prefill_window == CFG_WINDOW, "WATCH keeps full window");
|
|
CHECK(!p.kv_evict, "WATCH does not evict yet");
|
|
CHECK(p.degraded, "WATCH is degraded (q4 forced)");
|
|
|
|
/* TIGHT: shrink concurrency, evict, clamp the window. */
|
|
pn_ladder_plan_for(PN_MEM_LEVEL_TIGHT, CFG_SLOTS, CFG_WINDOW, CFG_Q4, &p);
|
|
CHECK(p.max_slots == CFG_SLOTS / 2, "TIGHT halves slots");
|
|
CHECK(p.force_kv_q4, "TIGHT forces q4");
|
|
CHECK(p.kv_evict, "TIGHT evicts");
|
|
CHECK(p.prefill_window == CFG_WINDOW / 2, "TIGHT halves the window");
|
|
CHECK(p.degraded, "TIGHT is degraded");
|
|
|
|
/* CRITICAL: single slot, quartered window, q4 + evict. */
|
|
pn_ladder_plan_for(PN_MEM_LEVEL_CRITICAL, CFG_SLOTS, CFG_WINDOW, CFG_Q4, &p);
|
|
CHECK(p.max_slots == 1, "CRITICAL floors slots to 1");
|
|
CHECK(p.prefill_window == CFG_WINDOW / 4, "CRITICAL quarters the window");
|
|
CHECK(p.force_kv_q4 && p.kv_evict, "CRITICAL forces q4 + evict");
|
|
CHECK(p.degraded, "CRITICAL is degraded");
|
|
|
|
/* Monotonicity across the whole ladder: slots never rise, window never
|
|
* rises, and once q4/evict engage they stay engaged. */
|
|
int prev_slots = CFG_SLOTS + 1, prev_win = CFG_WINDOW + 1;
|
|
bool seen_q4 = false, seen_evict = false;
|
|
pn_mem_level levels[] = { PN_MEM_LEVEL_OK, PN_MEM_LEVEL_WATCH,
|
|
PN_MEM_LEVEL_TIGHT, PN_MEM_LEVEL_CRITICAL };
|
|
for (size_t i = 0; i < sizeof(levels) / sizeof(levels[0]); i++) {
|
|
pn_ladder_plan_for(levels[i], CFG_SLOTS, CFG_WINDOW, CFG_Q4, &p);
|
|
CHECK(p.max_slots <= prev_slots, "slots monotonically non-increasing");
|
|
CHECK(p.prefill_window <= prev_win, "window monotonically non-increasing");
|
|
CHECK(p.max_slots >= 1, "slots never drop below 1");
|
|
if (seen_q4) CHECK(p.force_kv_q4, "q4 stays engaged once on");
|
|
if (seen_evict) CHECK(p.kv_evict, "evict stays engaged once on");
|
|
seen_q4 = p.force_kv_q4; seen_evict = p.kv_evict;
|
|
prev_slots = p.max_slots; prev_win = p.prefill_window;
|
|
}
|
|
}
|
|
|
|
static void test_ladder_edges(void)
|
|
{
|
|
printf("ladder_edges:\n");
|
|
pn_ladder_plan p;
|
|
|
|
/* An operator who already runs q4: force_kv_q4 stays true, and q4 alone no
|
|
* longer counts as degradation at WATCH (it was already the config). */
|
|
pn_ladder_plan_for(PN_MEM_LEVEL_WATCH, 16, 32768, true, &p);
|
|
CHECK(p.force_kv_q4, "already-q4 config stays q4 under WATCH");
|
|
CHECK(!p.degraded, "WATCH with q4-config is not degraded");
|
|
|
|
/* No known base window (0): the rung-3 clamp is a no-op, stays 0. */
|
|
pn_ladder_plan_for(PN_MEM_LEVEL_CRITICAL, 16, 0, false, &p);
|
|
CHECK(p.prefill_window == 0, "no base window -> no clamp");
|
|
|
|
/* A tiny slot config still floors at 1 rather than 0 under TIGHT. */
|
|
pn_ladder_plan_for(PN_MEM_LEVEL_TIGHT, 1, 32768, false, &p);
|
|
CHECK(p.max_slots == 1, "1-slot config floors at 1 under TIGHT");
|
|
|
|
/* Disabled: mirror config at every level (the bare-Mac escape hatch). */
|
|
setenv("PN_LADDER_DISABLE", "1", 1);
|
|
CHECK(pn_ladder_disabled(), "PN_LADDER_DISABLE detected");
|
|
pn_ladder_plan_for(PN_MEM_LEVEL_CRITICAL, 16, 32768, false, &p);
|
|
CHECK(p.max_slots == 16, "disabled keeps full slots even at CRITICAL");
|
|
CHECK(p.prefill_window == 32768, "disabled keeps full window");
|
|
CHECK(!p.force_kv_q4 && !p.kv_evict && !p.degraded, "disabled = no rungs");
|
|
unsetenv("PN_LADDER_DISABLE");
|
|
|
|
/* from_state wrapper: derives the level from a snapshot; NULL == OK. */
|
|
pn_mem_state st = {0};
|
|
st.level = PN_MEM_LEVEL_TIGHT;
|
|
pn_ladder_plan_from_state(&st, 16, 32768, false, &p);
|
|
CHECK(p.max_slots == 8 && p.kv_evict, "from_state reads the snapshot level");
|
|
pn_ladder_plan_from_state(NULL, 16, 32768, false, &p);
|
|
CHECK(p.max_slots == 16 && !p.degraded, "NULL state treated as OK");
|
|
}
|
|
|
|
/* --- Phase 3: crash/shutdown signal handling --- */
|
|
|
|
static void test_signal_shutdown(void)
|
|
{
|
|
printf("signal_shutdown:\n");
|
|
/* Fork a child that installs the handlers and blocks; send SIGTERM and
|
|
* confirm it terminates via the signal (the handler drains — no live CUDA
|
|
* ctx in this CPU-only test — and re-raises SIG_DFL). The parent must not be
|
|
* affected. This exercises install + the controlled-shutdown path end to
|
|
* end without needing a GPU. */
|
|
pid_t pid = fork();
|
|
if (pid == 0) {
|
|
pn_install_signal_handlers();
|
|
for (;;)
|
|
pause(); /* wait for the signal */
|
|
_exit(0); /* unreached */
|
|
}
|
|
CHECK(pid > 0, "fork for signal test");
|
|
if (pid <= 0)
|
|
return;
|
|
|
|
usleep(100000); /* let the child install handlers */
|
|
kill(pid, SIGTERM);
|
|
|
|
int status = 0;
|
|
waitpid(pid, &status, 0);
|
|
CHECK(WIFSIGNALED(status), "child terminated by signal");
|
|
if (WIFSIGNALED(status))
|
|
CHECK(WTERMSIG(status) == SIGTERM, "child terminated by SIGTERM");
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
printf("MemoryAware Phase 1-4 Tests\n");
|
|
printf("===========================\n\n");
|
|
|
|
test_host_sample();
|
|
test_reserve_policy();
|
|
test_publish_read();
|
|
test_die_feed_and_level();
|
|
test_psi_noise_floor();
|
|
test_psi_needs_corroboration();
|
|
test_psi_dwell();
|
|
test_level_names();
|
|
test_thread_lifecycle();
|
|
test_admit_cost_monotonic();
|
|
test_admit_kv_geometry();
|
|
test_admit_request_verdicts();
|
|
test_admit_request_gpu_path();
|
|
test_admit_load_spike();
|
|
test_admit_model_load();
|
|
test_admit_model_load_die();
|
|
test_ladder_plan();
|
|
test_ladder_edges();
|
|
test_signal_shutdown();
|
|
|
|
printf("\n%s\n", g_failed == 0 ? "ALL PASS" : "FAILURES PRESENT");
|
|
return g_failed > 0 ? 1 : 0;
|
|
}
|