mirror of
https://github.com/Heretek-AI/Strata.git
synced 2026-11-07 10:05:58 -10:00
Merge f-029-par-amd (4772f0cd9)
This commit is contained in:
commit
16e08b50cd
12 files changed
+1025
-10
No files matched your search
@@ -1656,6 +1656,13 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(pinned_shared_test PRIVATE strata_core ${_strata_gpu_runtime_target})
|
||||
add_test(NAME pinned_shared_test COMMAND pinned_shared_test)
|
||||
endif()
|
||||
if(WIN32) # the replicas' shared expert arena as a named section (src/core/pinned.cu): two arenas, a child process, the last handle
|
||||
add_executable(pinned_shared_win_test src/core/pinned_shared_win_test.cpp)
|
||||
target_include_directories(pinned_shared_win_test PRIVATE include)
|
||||
target_link_libraries(pinned_shared_win_test PRIVATE strata_core ${_strata_gpu_runtime_target})
|
||||
add_test(NAME pinned_shared_win_test COMMAND pinned_shared_win_test)
|
||||
set_tests_properties(pinned_shared_win_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 180)
|
||||
endif()
|
||||
|
||||
# Plan v0.3 P2: the n-gram table read straight from the SSD. The selftest needs only the CPU and a temp file.
|
||||
add_executable(ple_reader_test src/ngram/ple_reader_test.cpp)
|
||||
|
||||
@@ -90,6 +90,9 @@ struct PinnedArena {
|
||||
std::vector<uint64_t> slice_starts;
|
||||
void* mapping_base = nullptr; ///< actual mapping start; differs from base when a shared-file header exists
|
||||
uint64_t mapping_bytes = 0; ///< bytes to release from mapping_base
|
||||
/// shared-file arena only: the load state word in the file's header (0: nobody has loaded it, a pid: that engine is
|
||||
/// loading it, ~0: it is complete). Shared by every engine that maps the file; null for a private arena.
|
||||
std::atomic<uint64_t>* shared_state = nullptr;
|
||||
~PinnedArena();
|
||||
PinnedArena(const PinnedArena&) = delete;
|
||||
PinnedArena& operator=(const PinnedArena&) = delete;
|
||||
|
||||
@@ -18,6 +18,18 @@ conversation's next turn therefore goes back to its replica, until that replica
|
||||
reading the prompt again is cheaper than waiting. What each replica holds is tracked as hashes of 64-token blocks of
|
||||
the prompts and answers it served (the engine itself checks the real prefix, as it always does).
|
||||
|
||||
Host RAM. Every engine keeps its own copy of the experts in RAM (about 47 GiB for Qwen3.8-Flash-Next IQ3_S), so two
|
||||
replicas need two copies and four need four. "replica_shared_arena": true (or STRATA_REPLICA_SHARED_ARENA=1; opt-in)
|
||||
backs the experts' arena of all replicas with ONE shared memory object (the engine's --shared-expert-arena, MAP_SHARED):
|
||||
the experts are in RAM once, whatever the number of replicas. On Linux the object is a memfd the server creates
|
||||
(os.memfd_create): it is not a file on a mount, so the size of /dev/shm does not limit it; the engines open it as
|
||||
/proc/<server pid>/fd/<n>. On Windows it is a named section (CreateFileMapping) the engines open by name. Without memfd (an old kernel, a sandbox that refuses it) a file in /dev/shm is used instead.
|
||||
The pack hash in the header refuses another model; the first engine to start reads the experts into it and the others
|
||||
wait for it and skip the read (a state word in the header: nobody / a pid reading / filled; an engine whose reader died
|
||||
takes the read over); the server closes the memfd (or unlinks the file) when it ends - an engine that still maps it
|
||||
keeps its pages, the kernel frees them with the last mapping - and a /dev/shm file whose server is gone is removed at
|
||||
the next start. See shared_arena_plan.
|
||||
|
||||
CPU cores. On Linux the replicas get disjoint physical cores (serve/cpu_affinity.py, from PR #1436 by agorevski), so
|
||||
the host threads of the engines (the expert workers) do not compete. "replica_cpus": false turns it off.
|
||||
"""
|
||||
@@ -26,6 +38,7 @@ from __future__ import annotations
|
||||
import collections
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
@@ -81,6 +94,154 @@ def _cards(g) -> list[int]:
|
||||
raise ReplicaConfigError(f"cards must be numbers, not {g!r}") from None
|
||||
|
||||
|
||||
ARENA_PREFIX = "strata-replicas-"
|
||||
_MEMFDS: dict = {} # the memfds this server created: path (/proc/<pid>/fd/<n>) -> fd
|
||||
ARENA_MIN_RAM = 8 << 30 # a memfd arena: at least this much free RAM to try (the engine reserves the real size)
|
||||
ARENA_MIN_FREE = 8 << 30 # /dev/shm must have at least this much free to try (the engine reserves the real size)
|
||||
ARENA_INCOMPATIBLE = ("--mmap-experts", "--resident-experts", "--shared-expert-arena", "--resident-budget-gib")
|
||||
|
||||
|
||||
def _pid_alive(pid: int) -> bool:
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
except PermissionError:
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
def sweep_stale_arenas(shm: str = "/dev/shm", uid: int | None = None) -> list[str]:
|
||||
"""Remove the arena files of servers that no longer run (a killed server cannot unlink its own). A file of a living
|
||||
server, or of another user, is never touched; a mapping an engine still holds stays valid either way."""
|
||||
if uid is None:
|
||||
uid = os.getuid() if hasattr(os, "getuid") else 0
|
||||
gone = []
|
||||
try:
|
||||
names = os.listdir(shm)
|
||||
except OSError:
|
||||
return gone
|
||||
head = f"{ARENA_PREFIX}{uid}-"
|
||||
for n in names:
|
||||
if not (n.startswith(head) and n.endswith(".bin")):
|
||||
continue
|
||||
try:
|
||||
pid = int(n[len(head):-4])
|
||||
except ValueError:
|
||||
continue
|
||||
if _pid_alive(pid):
|
||||
continue
|
||||
try:
|
||||
os.unlink(os.path.join(shm, n))
|
||||
gone.append(n)
|
||||
except OSError:
|
||||
pass
|
||||
return gone
|
||||
|
||||
|
||||
def _free(path: str) -> int:
|
||||
try:
|
||||
st = os.statvfs(path)
|
||||
return st.f_bavail * st.f_frsize
|
||||
except OSError:
|
||||
return 0
|
||||
|
||||
|
||||
def _mem_available() -> int:
|
||||
try:
|
||||
with open("/proc/meminfo") as f:
|
||||
for line in f:
|
||||
if line.startswith("MemAvailable:"):
|
||||
return int(line.split()[1]) * 1024
|
||||
except (OSError, ValueError, IndexError):
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def shared_arena_plan(cfg: dict, n_replicas: int, *, shm: str = "/dev/shm", platform: str | None = None,
|
||||
environ=None, free_bytes=None, use_memfd: bool | None = None) -> tuple[str | None, str]:
|
||||
"""(the file for --shared-expert-arena or None, a line for the log). Opt-in: "replica_shared_arena": true (or
|
||||
STRATA_REPLICA_SHARED_ARENA=1). Only on Linux (the engine's MAP_SHARED backing), for Qwen's arena (not DeepSeek,
|
||||
not the mapped or budgeted RAM modes, which have no arena), and with room in /dev/shm. The engine checks the real
|
||||
size and reserves it before it loads (a tmpfs that is too small is an error at start, not a bus error later)."""
|
||||
environ = os.environ if environ is None else environ
|
||||
platform = sys.platform if platform is None else platform
|
||||
want = cfg.get("replica_shared_arena")
|
||||
if want is None:
|
||||
want = str(environ.get("STRATA_REPLICA_SHARED_ARENA", "")).strip().lower() in ("1", "true", "on", "yes")
|
||||
if not want or n_replicas < 2:
|
||||
return None, ""
|
||||
if platform.startswith("win"):
|
||||
# a named pagefile-backed section the engines open by name (src/core/pinned.cu); it lives while any engine holds it
|
||||
name = f"Local{chr(92)}strata-replicas-{os.getpid()}"
|
||||
return f"win:{name}", (f"one shared expert arena for {n_replicas} replicas (a Windows section, {name}): "
|
||||
"the experts are in RAM once")
|
||||
if not platform.startswith("linux"):
|
||||
return None, "shared expert arena: Linux and Windows only; each replica keeps its own copy of the experts in RAM"
|
||||
args = [str(a) for a in cfg.get("args") or []]
|
||||
if isinstance(cfg.get("replicas"), list):
|
||||
for e in cfg["replicas"]:
|
||||
if isinstance(e, dict):
|
||||
args += [str(a) for a in e.get("args") or []]
|
||||
if cfg.get("arch") == "deepseek41" or any(a in args for a in ARENA_INCOMPATIBLE):
|
||||
return None, "shared expert arena: this model/config has no private expert arena to share; each replica as before"
|
||||
if use_memfd is None:
|
||||
use_memfd = hasattr(os, "memfd_create") and str(cfg.get("replica_shared_arena")).lower() != "devshm"
|
||||
if use_memfd:
|
||||
avail = free_bytes if free_bytes is not None else _mem_available()
|
||||
if avail < ARENA_MIN_RAM:
|
||||
return None, (f"shared expert arena: only {avail / 2**30:.1f} GiB of RAM is free; each replica keeps its own "
|
||||
"copy of the experts in RAM")
|
||||
try:
|
||||
fd = os.memfd_create("strata-replicas-arena", os.MFD_CLOEXEC)
|
||||
except OSError:
|
||||
fd = -1
|
||||
if fd >= 0:
|
||||
path = f"/proc/{os.getpid()}/fd/{fd}"
|
||||
_MEMFDS[path] = fd
|
||||
warn = ""
|
||||
try: # the arena once plus about 7 GB of private RAM per engine (measured); a warning, never a refusal
|
||||
from strata_control import replica_profile as _rp
|
||||
q = _rp.quant_of(cfg)
|
||||
if q:
|
||||
need = _rp.ARENA_GB[q] + n_replicas * _rp.PER_REPLICA_GB
|
||||
if avail / 2**30 < need + 4:
|
||||
warn = (f" WARNING: about {need:.0f} GB are needed for {n_replicas} engines and {avail / 2**30:.0f} GB "
|
||||
"are free: the last engine may not fit (fewer replicas, or the layer split, is safer)")
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return path, (f"one shared expert arena for {n_replicas} replicas (memory object of the server, not limited by "
|
||||
f"the size of /dev/shm): the experts are in RAM once.{warn}")
|
||||
if not (os.path.isdir(shm) and os.access(shm, os.W_OK)):
|
||||
return None, f"shared expert arena: {shm} is not usable; each replica keeps its own copy of the experts in RAM"
|
||||
free = free_bytes if free_bytes is not None else _free(shm)
|
||||
if free < ARENA_MIN_FREE:
|
||||
return None, (f"shared expert arena: {shm} has only {free / 2**30:.1f} GiB free (a tmpfs is half the RAM by "
|
||||
"default); each replica keeps its own copy of the experts in RAM")
|
||||
sweep_stale_arenas(shm)
|
||||
uid = os.getuid() if hasattr(os, "getuid") else 0
|
||||
path = os.path.join(shm, f"{ARENA_PREFIX}{uid}-{os.getpid()}.bin")
|
||||
return path, f"one shared expert arena for {n_replicas} replicas ({path}): the experts are in RAM once"
|
||||
|
||||
|
||||
def remove_arena(path: str | None) -> None:
|
||||
"""Close the memfd / unlink the arena file (the server's end, or a start that fell back): engines that map it keep
|
||||
their pages."""
|
||||
if not path or path.startswith("win:"): # (a Windows section goes with its last handle)
|
||||
return
|
||||
fd = _MEMFDS.pop(path, None)
|
||||
if fd is not None:
|
||||
try:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
return
|
||||
try:
|
||||
os.unlink(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _suffixed(path, i: int):
|
||||
if not isinstance(path, str) or not path:
|
||||
return path
|
||||
@@ -337,6 +498,7 @@ class ReplicaEngine:
|
||||
e.close()
|
||||
except BaseException as x: # noqa: BLE001
|
||||
err = err or x
|
||||
remove_arena(self.__dict__.get("arena_file")) # (an engine that still maps it keeps its pages)
|
||||
if err:
|
||||
raise err
|
||||
|
||||
|
||||
+44
-1
@@ -344,6 +344,12 @@ ADMIT_MAX_WAIT_S = _timeout_env("STRATA_ADMIT_MAX_WAIT_S", 30.0) or 0.0
|
||||
COALESCE_PREFILL = max(0, int(os.environ.get("STRATA_COALESCE_PREFILL") or 0))
|
||||
COALESCE_TOKENS = max(256, int(os.environ.get("STRATA_COALESCE_TOKENS") or 16384)) # prompt tokens read together at most
|
||||
COALESCE_WAIT_MS = max(0, int(os.environ.get("STRATA_COALESCE_WAIT_MS") or 0)) # a burst's later requests register within this long
|
||||
# Adaptive: a read is shared (and the leader waits for its followers) only when this engine has seen at least
|
||||
# COALESCE_MIN_REQS requests arrive within COALESCE_BURST_MS (the leader's own included). Fewer than that - one or two
|
||||
# requests per engine, as 8 requests on 4 replicas - read plainly, with no wait: nothing to share, and the wait and the
|
||||
# longer pass only cost (measured 4 x 1 replicas at 8 requests: 168 against 196 tok/s with an unconditional wait).
|
||||
COALESCE_MIN_REQS = max(2, int(os.environ.get("STRATA_COALESCE_MIN_REQS") or 3))
|
||||
COALESCE_BURST_MS = max(10, int(os.environ.get("STRATA_COALESCE_BURST_MS") or 250))
|
||||
STALL_CPU_EPS_S = 0.5 # CPU seconds the engine may use in the whole stall window and still count as frozen
|
||||
STALL_IO_EPS_B = 1 << 20 # bytes read or written in that window
|
||||
|
||||
@@ -1203,6 +1209,7 @@ class StrataEngine:
|
||||
self.admitting = 0 # #1603: ... and those that gave the lines back to wait for a slot
|
||||
self.par_counters = {"queued": 0, "queue_wait_s": 0.0}
|
||||
self.wait_lens: list[list[int]] = [] # ... their prompt lengths (a long read gives way to short ones)
|
||||
self.arrivals = collections.deque(maxlen=64) # monotonic times of the last requests that came for the control lines (STRATA_COALESCE_*)
|
||||
self.ctl_epoch = 0 # how often the control lines were taken
|
||||
self.ctl = threading.Lock() # one admission or solo request on the control lines at a time
|
||||
self.gen = self.__dict__.get("gen", 0) + 1 # which engine process this is (a request notes its own)
|
||||
@@ -1745,6 +1752,7 @@ class StrataEngine:
|
||||
with self.slot_cv:
|
||||
self.waiting += 1
|
||||
self.wait_lens.append(entry)
|
||||
self.arrivals.append(entry[1])
|
||||
beat = time.monotonic()
|
||||
try:
|
||||
while True:
|
||||
@@ -1812,6 +1820,16 @@ class StrataEngine:
|
||||
same layer-major pass as the BGEN's prompt. Each follower finds its slot in cancel._strata_preslot."""
|
||||
picked = []
|
||||
total = len(lead_prompt)
|
||||
def seen() -> int:
|
||||
now = time.monotonic()
|
||||
with self.slot_cv:
|
||||
return max(sum(1 for t in self.arrivals if now - t <= COALESCE_BURST_MS / 1000.0), 1 + len(self.wait_lens))
|
||||
recent = seen()
|
||||
if recent < COALESCE_MIN_REQS and COALESCE_WAIT_MS > 0:
|
||||
time.sleep(0.010) # (the first request of a burst holds the lines before its neighbours have arrived)
|
||||
recent = seen()
|
||||
if recent < COALESCE_MIN_REQS:
|
||||
return # one or two requests on this engine: a plain read, at most 10 ms lost
|
||||
if COALESCE_WAIT_MS > 0:
|
||||
time.sleep(COALESCE_WAIT_MS / 1000.0) # the requests of a burst reach the server a few ms apart
|
||||
with self.slot_cv:
|
||||
@@ -8569,9 +8587,14 @@ def start_replicas(cfg: dict, groups: list[list[int]], exe: str, effort_end: lis
|
||||
flush=True)
|
||||
engines: list = [None] * n
|
||||
errors: list = []
|
||||
arena, arena_note = rp.shared_arena_plan(cfg, n) # opt-in: one copy of the experts in RAM for all replicas
|
||||
if arena_note:
|
||||
print(f"[strata] {arena_note}", flush=True)
|
||||
|
||||
def one(i: int):
|
||||
rc = rp.replica_cfg(cfg, i, groups[i])
|
||||
if arena:
|
||||
rc["args"] = list(rc["args"]) + ["--shared-expert-arena", arena]
|
||||
try:
|
||||
engines[i] = StrataEngine(exe, engine_args(rc) + effort_end, cwd=rc.get("cwd"), log=rc.get("log"),
|
||||
env=child_env(rc, offline=offline), lazy=lazy, cpus=cpus[i] if cpus else None)
|
||||
@@ -8588,6 +8611,16 @@ def start_replicas(cfg: dict, groups: list[list[int]], exe: str, effort_end: lis
|
||||
t.start()
|
||||
for t in ts:
|
||||
t.join()
|
||||
if errors and arena and len(errors) < n:
|
||||
# a replica that died while it was reading the experts into the shared arena (or any other one that failed) while
|
||||
# another one is up: the living engine took the read over (the state word in the arena), so start the failed ones
|
||||
# once more on the same arena instead of giving up the sharing
|
||||
failed = sorted(i for i, _ in errors)
|
||||
errors.clear()
|
||||
print(f"[strata] replica(s) {', '.join(map(str, failed))} did not start; starting again on the shared expert arena",
|
||||
flush=True)
|
||||
for i in failed:
|
||||
one(i)
|
||||
if errors:
|
||||
for e in engines:
|
||||
if e is not None:
|
||||
@@ -8595,10 +8628,20 @@ def start_replicas(cfg: dict, groups: list[list[int]], exe: str, effort_end: lis
|
||||
e.close()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
rp.remove_arena(arena)
|
||||
i, e = errors[0]
|
||||
if arena: # a shared arena that could not be made (no room in /dev/shm, a file of another pack): as before
|
||||
print(f"[strata] replica {i} did not start with the shared expert arena ({e}); starting every replica with "
|
||||
"its own copy of the experts instead", flush=True)
|
||||
return start_replicas({**cfg, "replica_shared_arena": False}, groups, exe, effort_end, lazy, offline=offline)
|
||||
raise SystemExit(f"[strata] replica {i} did not start: {e}")
|
||||
load = int(cfg.get("replica_load_tokens") or rp.LOAD_TOKENS)
|
||||
return rp.ReplicaEngine(engines, load_tokens=load)
|
||||
eng = rp.ReplicaEngine(engines, load_tokens=load)
|
||||
if arena:
|
||||
eng.__dict__["arena_file"] = arena
|
||||
import atexit
|
||||
atexit.register(rp.remove_arena, arena) # (a killed server leaves the file: the next start sweeps it)
|
||||
return eng
|
||||
|
||||
|
||||
def start_split(cfg: dict, spec: dict, exe: str, effort_end: list[str], lazy: bool, *, offline: bool = False):
|
||||
|
||||
+126
-1
@@ -11,7 +11,8 @@ from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from serve.frontend import ChatTemplate
|
||||
from serve.replicas import BLOCK, ReplicaConfigError, ReplicaEngine, Router, cpu_sets, gpu_groups, replica_cfg
|
||||
from serve.replicas import (BLOCK, ReplicaConfigError, ReplicaEngine, Router, cpu_sets, gpu_groups, remove_arena,
|
||||
replica_cfg, shared_arena_plan, sweep_stale_arenas)
|
||||
from serve.server import ByteTokenizer, Service, StrataEngine, serve
|
||||
from serve.test_parallel import FAKE_BATCH
|
||||
|
||||
@@ -309,5 +310,129 @@ class ReplicaService(unittest.TestCase):
|
||||
self.assertEqual(self.served()[1], 3)
|
||||
|
||||
|
||||
class SharedArena(unittest.TestCase):
|
||||
"""The opt-in shared expert arena of the replicas (shared_arena_plan): when it is used, when it is not, what a stale
|
||||
file means."""
|
||||
CFG = {"replica_shared_arena": True, "args": ["--pack", "/p"], "gpu": [0, 1]}
|
||||
|
||||
def plan(self, cfg=None, n=2, tmp=None, **kw):
|
||||
return shared_arena_plan(cfg or self.CFG, n, shm=tmp, platform=kw.pop("platform", "linux"),
|
||||
environ=kw.pop("environ", {}), free_bytes=kw.pop("free_bytes", 100 << 30),
|
||||
use_memfd=kw.pop("use_memfd", False), **kw)
|
||||
|
||||
def test_off_by_default_and_for_one_engine(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
self.assertEqual(self.plan({"args": []}, tmp=d), (None, ""))
|
||||
self.assertEqual(self.plan(n=1, tmp=d), (None, ""))
|
||||
self.assertEqual(self.plan({"args": []}, tmp=d, environ={"STRATA_REPLICA_SHARED_ARENA": "0"}), (None, ""))
|
||||
|
||||
def test_on_by_config_or_environment(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path, note = self.plan(tmp=d)
|
||||
self.assertTrue(path and path.startswith(d) and path.endswith(".bin"), path)
|
||||
self.assertIn("once", note)
|
||||
path2, _ = self.plan({"args": []}, tmp=d, environ={"STRATA_REPLICA_SHARED_ARENA": "1"})
|
||||
self.assertTrue(path2)
|
||||
self.assertEqual(path, path2) # (one server: one file)
|
||||
|
||||
def test_windows_gets_a_named_section(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path, note = self.plan(tmp=d, platform="win32")
|
||||
self.assertTrue(path and path.startswith("win:Local") and "strata-replicas-" in path, path)
|
||||
self.assertIn("Windows section", note)
|
||||
remove_arena(path) # (nothing to remove: harmless)
|
||||
|
||||
def test_only_where_there_is_an_arena_to_share(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
self.assertIsNone(self.plan(tmp=d, platform="darwin")[0])
|
||||
for flag in ("--mmap-experts", "--resident-experts", "--resident-budget-gib", "--shared-expert-arena"):
|
||||
self.assertIsNone(self.plan({**self.CFG, "args": ["--pack", "/p", flag]}, tmp=d)[0], flag)
|
||||
self.assertIsNone(self.plan({**self.CFG, "replicas": [{"gpus": [0], "args": ["--mmap-experts"]}, {"gpus": [1]}]}, tmp=d)[0])
|
||||
self.assertIsNone(self.plan({**self.CFG, "arch": "deepseek41"}, tmp=d)[0])
|
||||
|
||||
def test_no_room_or_no_shm_says_so_and_falls_back(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
path, note = self.plan(tmp=d, free_bytes=2 << 30)
|
||||
self.assertIsNone(path)
|
||||
self.assertIn("2.0 GiB free", note)
|
||||
path, note = self.plan(tmp=d + "/nope")
|
||||
self.assertIsNone(path)
|
||||
self.assertIn("not usable", note)
|
||||
|
||||
def test_a_stale_file_goes_a_live_one_stays(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
uid = __import__("os").getuid() if hasattr(__import__("os"), "getuid") else 0
|
||||
dead = Path(d) / f"strata-replicas-{uid}-2999999.bin"
|
||||
live = Path(d) / f"strata-replicas-{uid}-{__import__('os').getpid()}.bin"
|
||||
other = Path(d) / f"strata-replicas-{uid + 1}-2999999.bin"
|
||||
junk = Path(d) / "strata-replicas-x.bin"
|
||||
for f in (dead, live, other, junk):
|
||||
f.write_bytes(b"x")
|
||||
with mock.patch("serve.replicas._pid_alive", side_effect=lambda pid: pid != 2999999):
|
||||
gone = sweep_stale_arenas(d, uid)
|
||||
self.assertEqual(gone, [dead.name])
|
||||
self.assertFalse(dead.exists())
|
||||
self.assertTrue(live.exists() and other.exists() and junk.exists())
|
||||
|
||||
@unittest.skipUnless(hasattr(__import__("os"), "memfd_create") and sys.platform.startswith("linux"), "Linux memfd")
|
||||
def test_a_memfd_is_not_limited_by_dev_shm_and_is_closed_at_the_end(self):
|
||||
import os
|
||||
path, note = shared_arena_plan(self.CFG, 2, platform="linux", environ={}, free_bytes=100 << 30, use_memfd=True)
|
||||
self.assertTrue(path and path.startswith(f"/proc/{os.getpid()}/fd/"), path)
|
||||
fd = int(path.rsplit("/", 1)[1])
|
||||
os.ftruncate(fd, 1 << 20)
|
||||
self.assertEqual(os.fstat(fd).st_size, 1 << 20)
|
||||
with open(path, "r+b") as other: # (how an engine opens it)
|
||||
other.seek(100)
|
||||
other.write(b"x")
|
||||
remove_arena(path)
|
||||
with self.assertRaises(OSError):
|
||||
os.fstat(fd)
|
||||
remove_arena(path) # (twice: harmless)
|
||||
|
||||
@unittest.skipUnless(hasattr(__import__("os"), "memfd_create") and sys.platform.startswith("linux"), "Linux memfd")
|
||||
def test_a_memfd_arena_warns_when_the_engines_will_not_all_fit(self):
|
||||
cfg = {**self.CFG, "model_name": "qwen3.8-flash-next-iq3_s"}
|
||||
path, note = shared_arena_plan(cfg, 2, platform="linux", environ={}, free_bytes=57 << 30, use_memfd=True)
|
||||
try:
|
||||
self.assertTrue(path)
|
||||
self.assertIn("WARNING", note)
|
||||
self.assertIn("may not fit", note)
|
||||
finally:
|
||||
remove_arena(path)
|
||||
path, note = shared_arena_plan(cfg, 2, platform="linux", environ={}, free_bytes=120 << 30, use_memfd=True)
|
||||
try:
|
||||
self.assertNotIn("WARNING", note)
|
||||
finally:
|
||||
remove_arena(path)
|
||||
|
||||
def test_a_memfd_needs_free_ram(self):
|
||||
path, note = shared_arena_plan(self.CFG, 2, platform="linux", environ={}, free_bytes=2 << 30, use_memfd=True)
|
||||
self.assertIsNone(path)
|
||||
self.assertIn("2.0 GiB of RAM", note)
|
||||
|
||||
def test_removing_twice_or_nothing_is_harmless(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
f = Path(d) / "a.bin"
|
||||
f.write_bytes(b"x")
|
||||
remove_arena(str(f))
|
||||
remove_arena(str(f))
|
||||
remove_arena(None)
|
||||
self.assertFalse(f.exists())
|
||||
|
||||
def test_the_replica_engine_unlinks_the_file_at_close(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
f = Path(d) / "a.bin"
|
||||
f.write_bytes(b"x")
|
||||
|
||||
class E:
|
||||
def close(self):
|
||||
pass
|
||||
r = ReplicaEngine([E(), E()])
|
||||
r.__dict__["arena_file"] = str(f)
|
||||
r.close()
|
||||
self.assertFalse(f.exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -8953,6 +8953,11 @@ def main() -> int:
|
||||
help="several GPUs: run N independent engines, each on its own equal group of the chosen cards (2 "
|
||||
"replicas on 4 cards = two 2-card engines), requests spread over them (opt-in; more total "
|
||||
"throughput for a model that fits fewer cards; each replica holds its own copy of the model)")
|
||||
ap.add_argument("--replica-profile", action="store_true", default=False,
|
||||
help="two or more GPUs, Qwen3.8 IQ3_S/IQ3_XXS/Q2_0: one engine per card behind the server instead of one "
|
||||
"layer split, the experts shared in RAM, the batch switches on (opt-in; about 1.8 times the "
|
||||
"throughput at 8-16 requests at once on 4 Radeon AI PRO R9700; needs about 61 GB of RAM for 2 "
|
||||
"engines and 66 GB for 4; a layer split you chose, replicas or --parallel always win)")
|
||||
ap.add_argument("--kv-streaming", choices=["auto", "on", "off"], default="auto",
|
||||
help="from a 64K context: keep the KV cache in RAM and only the attention's window in VRAM (more "
|
||||
"experts fit on the GPU); auto: when the RAM has room for it")
|
||||
@@ -10095,7 +10100,22 @@ def main() -> int:
|
||||
cfg["gpu_order"] = "as_given"
|
||||
ok(f"layer split across GPUs {multi} ({cfg['layer_split']})")
|
||||
recommend_remote_expert_opt(cfg, off=a.no_remote_expert_opt)
|
||||
if hip and a.layer_split in (None, "auto"): # four Radeon AI PRO R9700 cards and Qwen3.8 IQ3_S: the measured serving profile
|
||||
replica_done = False
|
||||
if a.replicas is None and a.layer_split in (None, "auto") and (getattr(a, "replica_profile", False) or
|
||||
os.environ.get("STRATA_REPLICA_PROFILE")):
|
||||
try: # the opt-in replica profile (strata_control/replica_profile.py)
|
||||
from strata_control import replica_profile
|
||||
if getattr(a, "replica_profile", False):
|
||||
cfg["replica_profile"] = True
|
||||
lines = replica_profile.apply([{"name": c.get("name"), "vram_gb": c.get("vram_gb"), "vendor": c.get("vendor"),
|
||||
"index": c.get("index")} for c in chosen], cfg, float(ram),
|
||||
parallel_given=a.parallel is not None)
|
||||
replica_done = bool(cfg.get("replicas"))
|
||||
for line in lines:
|
||||
(ok if replica_done else say)(line)
|
||||
except Exception as e: # noqa: BLE001 - a profile is never a reason to stop setup
|
||||
say(f" (the replica profile was not applied: {e})")
|
||||
if hip and a.layer_split in (None, "auto") and not replica_done: # four Radeon AI PRO R9700 cards and Qwen3.8 IQ3_S: the measured serving profile
|
||||
try: # (strata_control/amd_four_card.py); what the person chose always wins
|
||||
from strata_control import amd_four_card
|
||||
for line in amd_four_card.apply([{"name": c.get("name"), "vram_gb": c.get("vram_gb"), "vendor": "amd"} for c in chosen],
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
#include "strata/kernels/cpu/pool.hpp"
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
#ifndef _WIN32
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
@@ -72,6 +76,27 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// The pid of this process and whether a pid still exists (the shared arena's reader election, both OSes)
|
||||
static uint64_t arena_this_pid() {
|
||||
#ifdef _WIN32
|
||||
return (uint64_t) GetCurrentProcessId();
|
||||
#else
|
||||
return (uint64_t) getpid();
|
||||
#endif
|
||||
}
|
||||
static bool arena_pid_alive(uint64_t pid) {
|
||||
#ifdef _WIN32
|
||||
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, (DWORD) pid);
|
||||
if (h == nullptr) return GetLastError() == ERROR_ACCESS_DENIED; // (denied: it exists; invalid parameter: it does not)
|
||||
DWORD code = 0;
|
||||
const bool alive = GetExitCodeProcess(h, &code) && code == STILL_ACTIVE;
|
||||
CloseHandle(h);
|
||||
return alive;
|
||||
#else
|
||||
return !(kill((pid_t) pid, 0) != 0 && errno == ESRCH);
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace strata::core {
|
||||
|
||||
namespace detail {
|
||||
@@ -6000,6 +6025,43 @@ bool ArenaExpertSource::open(const std::string& pack_dir, int64_t n_layers, int6
|
||||
const bool unbuffered = experts_unbuffered(files, want + (uint64_t) blob, why);
|
||||
const int readers = unbuffered ? std::max(threads, 16) : threads; // 16 keep a PCIe 5 drive's queue full
|
||||
LoadStats st;
|
||||
// A shared arena (--shared-expert-arena, the replicas): ONE engine reads the experts into it, the others wait and skip the
|
||||
// read. The state word in the file's header says who: 0 nobody, a pid loading, ~0 filled. An engine that finds a
|
||||
// pid that is gone takes the load over (the previous one died mid-read: the arena is read again from the start), so a
|
||||
// dead loader never leaves the others waiting, and an engine that waits too long loads it itself.
|
||||
constexpr uint64_t kArenaFilled = ~uint64_t{0};
|
||||
std::atomic<uint64_t>* arena_state = padded_ ? nullptr : a->shared_state;
|
||||
bool arena_leader = false, arena_skip = false;
|
||||
if (arena_state != nullptr) {
|
||||
const uint64_t me = arena_this_pid();
|
||||
const auto t_begin = std::chrono::steady_clock::now();
|
||||
bool said = false;
|
||||
for (;;) {
|
||||
uint64_t cur = arena_state->load(std::memory_order_acquire);
|
||||
if (cur == kArenaFilled) { arena_skip = true; break; }
|
||||
if (cur == 0 || cur == me) {
|
||||
if (arena_state->compare_exchange_strong(cur, me, std::memory_order_acq_rel)) { arena_leader = true; break; }
|
||||
continue;
|
||||
}
|
||||
const bool alive = arena_pid_alive(cur);
|
||||
if (!alive) {
|
||||
if (arena_state->compare_exchange_strong(cur, me, std::memory_order_acq_rel)) {
|
||||
std::fprintf(stderr, "strata generate: shared expert arena: engine %llu died while reading it; reading it again\n",
|
||||
(unsigned long long) cur);
|
||||
arena_leader = true;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (std::chrono::steady_clock::now() - t_begin > std::chrono::minutes(45)) break; // (loads it on its own)
|
||||
if (!said) {
|
||||
said = true;
|
||||
std::fprintf(stderr, "strata generate: shared expert arena: engine %llu is reading the experts; waiting for it "
|
||||
"instead of reading them again\n", (unsigned long long) cur);
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
}
|
||||
}
|
||||
std::atomic<int> ready{0};
|
||||
std::thread reg;
|
||||
if (deferred) {
|
||||
@@ -6011,7 +6073,12 @@ bool ArenaExpertSource::open(const std::string& pack_dir, int64_t n_layers, int6
|
||||
});
|
||||
}
|
||||
const std::atomic<int>* rp = deferred ? &ready : nullptr;
|
||||
if (from_gguf) {
|
||||
if (arena_skip) {
|
||||
st.ok = true;
|
||||
st.bytes = want;
|
||||
st.seconds = 0.0;
|
||||
std::fprintf(stderr, "strata generate: shared expert arena: already filled by another engine; the read is skipped\n");
|
||||
} else if (from_gguf) {
|
||||
st = load_experts_gguf(gguf_, a->data(), lay, readers, unbuffered, rp, padded_ ? &pad_ : nullptr);
|
||||
} else {
|
||||
if (unbuffered && !padded_) st = load_experts_direct(path, a->data(), loff, lbytes, readers, /*chunk=*/8u << 20, rp);
|
||||
@@ -6022,15 +6089,18 @@ bool ArenaExpertSource::open(const std::string& pack_dir, int64_t n_layers, int6
|
||||
std::fprintf(stderr, "strata generate: expert arena read %s (%s)\n", unbuffered ? "unbuffered" : "through the file cache",
|
||||
why.c_str());
|
||||
if (!st.ok) {
|
||||
if (arena_leader) arena_state->store(0, std::memory_order_release); // (somebody else may try)
|
||||
delete a;
|
||||
err = "ArenaExpertSource: the expert load was refused: " + (st.error.empty() ? std::string("unknown") : st.error);
|
||||
return false;
|
||||
}
|
||||
if (st.bytes != want) {
|
||||
if (arena_leader) arena_state->store(0, std::memory_order_release);
|
||||
delete a;
|
||||
err = "ArenaExpertSource: the load read " + std::to_string(st.bytes) + " B of " + std::to_string(want);
|
||||
return false;
|
||||
}
|
||||
if (arena_leader) arena_state->store(kArenaFilled, std::memory_order_release); // the others may use (and skip reading) it
|
||||
if (std::getenv("STRATA_VERIFY_ARENA") != nullptr) { // tests: the loaded arena's checksum, any loader
|
||||
std::fprintf(stderr, "strata generate: expert arena checksum %016llx (%.2f GiB)\n",
|
||||
(unsigned long long) fnv1a64(a->data(), want), (double) want / (1ull << 30));
|
||||
|
||||
+214
-3
@@ -2,6 +2,7 @@
|
||||
#include "strata/core/pinned.hpp"
|
||||
#include "strata/platform/memory.hpp"
|
||||
#include "strata/core/pin_budget.hpp"
|
||||
#include <cstddef>
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
@@ -32,6 +33,7 @@
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/vfs.h>
|
||||
#include <unistd.h>
|
||||
#include <linux/mman.h>
|
||||
#ifndef MAP_HUGE_2MB
|
||||
@@ -56,6 +58,51 @@ struct SharedArenaHeader {
|
||||
};
|
||||
static_assert(sizeof(SharedArenaHeader) <= kSharedArenaHeaderBytes);
|
||||
|
||||
#ifdef _WIN32
|
||||
// Shared-section views: MapViewOfFile address -> the section handle, so release() unmaps and closes instead of VirtualFree.
|
||||
std::mutex g_views_mu;
|
||||
std::map<void*, HANDLE> g_views;
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
// MemAvailable and, in a cgroup v2 group with a limit, the room left under it; ~0 when neither can be read
|
||||
uint64_t free_ram_room() {
|
||||
uint64_t room = ~uint64_t{0};
|
||||
if (std::FILE* f = std::fopen("/proc/meminfo", "r")) {
|
||||
char line[256];
|
||||
while (std::fgets(line, sizeof line, f) != nullptr) {
|
||||
unsigned long long kb = 0;
|
||||
if (std::sscanf(line, "MemAvailable: %llu kB", &kb) == 1) { room = (uint64_t) kb * 1024u; break; }
|
||||
}
|
||||
std::fclose(f);
|
||||
}
|
||||
std::string cg;
|
||||
if (std::FILE* f = std::fopen("/proc/self/cgroup", "r")) {
|
||||
char line[512];
|
||||
while (std::fgets(line, sizeof line, f) != nullptr)
|
||||
if (std::strncmp(line, "0::", 3) == 0) { cg = line + 3; break; }
|
||||
std::fclose(f);
|
||||
}
|
||||
while (!cg.empty() && (cg.back() == '\n' || cg.back() == '\r')) cg.pop_back();
|
||||
if (!cg.empty()) {
|
||||
unsigned long long lim = 0, cur = 0;
|
||||
bool have_lim = false;
|
||||
if (std::FILE* f = std::fopen(("/sys/fs/cgroup" + cg + "/memory.max").c_str(), "r")) {
|
||||
have_lim = std::fscanf(f, "%llu", &lim) == 1; // ("max" does not parse: no limit)
|
||||
std::fclose(f);
|
||||
}
|
||||
if (have_lim) {
|
||||
if (std::FILE* f = std::fopen(("/sys/fs/cgroup" + cg + "/memory.current").c_str(), "r")) {
|
||||
if (std::fscanf(f, "%llu", &cur) != 1) cur = 0;
|
||||
std::fclose(f);
|
||||
}
|
||||
room = std::min<uint64_t>(room, lim > cur ? (uint64_t) (lim - cur) : 0);
|
||||
}
|
||||
}
|
||||
return room;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
// bytes in free blocks of 2 MiB or more (/proc/buddyinfo, order >= 9): what MADV_HUGEPAGE can get without compaction
|
||||
uint64_t free_huge_bytes() {
|
||||
@@ -96,8 +143,102 @@ void* reserve(uint64_t bytes, PageBacking& got, std::string& note, const std::st
|
||||
|
||||
#ifdef _WIN32
|
||||
if (!shared_file.empty()) {
|
||||
note = "shared-file arena backing is not implemented on Windows";
|
||||
return nullptr;
|
||||
// The shared arena on Windows is a named, pagefile-backed section (CreateFileMappingW) that every engine of the
|
||||
// replicas opens by name: "win:Local\\strata-replicas-<server pid>" (a name without the prefix is used as it is).
|
||||
// The first engine creates it at the full size, the others open it; it lives while any process holds a handle or
|
||||
// a view, so a replica that restarts joins the same pages. The first 4 KiB are the same header as on Linux (the
|
||||
// pack hash, the size, the load state word); a new section is all zeros, so "no magic" means "nobody has been
|
||||
// here" - two engines that both find it empty write the same header.
|
||||
if (shared_pack_hash == 0) {
|
||||
note = "shared arena requires a nonzero pack hash";
|
||||
return nullptr;
|
||||
}
|
||||
if (bytes > UINT64_MAX - kSharedArenaHeaderBytes) {
|
||||
note = "shared arena size overflows its header";
|
||||
return nullptr;
|
||||
}
|
||||
const uint64_t file_bytes = bytes + kSharedArenaHeaderBytes;
|
||||
std::string nm = shared_file.rfind("win:", 0) == 0 ? shared_file.substr(4) : shared_file;
|
||||
std::wstring wname(nm.begin(), nm.end());
|
||||
// the section is committed memory (RAM plus pagefile) from the moment it exists: say so before taking it
|
||||
MEMORYSTATUSEX ms{};
|
||||
ms.dwLength = sizeof ms;
|
||||
if (GlobalMemoryStatusEx(&ms) && file_bytes + (2ull << 30) > ms.ullAvailPageFile) {
|
||||
HANDLE probe = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, wname.c_str()); // (already there: nothing new is committed)
|
||||
if (probe == nullptr) {
|
||||
note = "not enough free memory (RAM + pagefile) for the shared arena " + nm + ": it needs " +
|
||||
std::to_string((unsigned long long) (file_bytes >> 20)) + " MiB and " +
|
||||
std::to_string((unsigned long long) (ms.ullAvailPageFile >> 20)) + " MiB are available (2 GiB are kept back)";
|
||||
return nullptr;
|
||||
}
|
||||
CloseHandle(probe);
|
||||
}
|
||||
HANDLE h = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, (DWORD) (file_bytes >> 32),
|
||||
(DWORD) (file_bytes & 0xffffffffu), wname.c_str());
|
||||
if (h == nullptr) {
|
||||
note = "cannot create shared arena section " + nm + ": Windows error " + std::to_string((unsigned long long) GetLastError());
|
||||
return nullptr;
|
||||
}
|
||||
void* view = MapViewOfFile(h, FILE_MAP_ALL_ACCESS, 0, 0, 0); // (0: the whole section, as big as it was created)
|
||||
if (view == nullptr) {
|
||||
const DWORD e = GetLastError();
|
||||
CloseHandle(h);
|
||||
note = "cannot map shared arena section " + nm + ": Windows error " + std::to_string((unsigned long long) e);
|
||||
return nullptr;
|
||||
}
|
||||
MEMORY_BASIC_INFORMATION mbi{};
|
||||
if (VirtualQuery(view, &mbi, sizeof mbi) == 0 || (uint64_t) mbi.RegionSize < file_bytes) {
|
||||
UnmapViewOfFile(view);
|
||||
CloseHandle(h);
|
||||
note = "shared arena section " + nm + " is smaller than the " + std::to_string((unsigned long long) file_bytes) +
|
||||
" B this engine needs (including its header)";
|
||||
return nullptr;
|
||||
}
|
||||
auto* hdr = static_cast<SharedArenaHeader*>(view);
|
||||
bool empty = true;
|
||||
for (char c : hdr->magic) empty = empty && c == 0;
|
||||
if (empty) {
|
||||
SharedArenaHeader fresh{};
|
||||
std::memcpy(fresh.magic, kSharedArenaMagic, sizeof fresh.magic);
|
||||
fresh.version = 1;
|
||||
fresh.header_bytes = (uint32_t) kSharedArenaHeaderBytes;
|
||||
fresh.arena_bytes = bytes;
|
||||
fresh.pack_hash = shared_pack_hash;
|
||||
// fields after the magic first, the magic last: an engine that sees the magic sees a complete header (the
|
||||
// state word, reserved[0], stays zero)
|
||||
std::memcpy(reinterpret_cast<char*>(hdr) + sizeof fresh.magic, reinterpret_cast<char*>(&fresh) + sizeof fresh.magic,
|
||||
sizeof fresh - sizeof fresh.magic);
|
||||
MemoryBarrier();
|
||||
std::memcpy(hdr->magic, fresh.magic, sizeof fresh.magic);
|
||||
} else {
|
||||
// (an engine that arrives in the instant between another one's header fields and its magic never sees a
|
||||
// half header: the magic is written last)
|
||||
if (std::memcmp(hdr->magic, kSharedArenaMagic, sizeof hdr->magic) != 0 || hdr->version != 1 ||
|
||||
hdr->header_bytes != kSharedArenaHeaderBytes || hdr->arena_bytes != bytes) {
|
||||
UnmapViewOfFile(view);
|
||||
CloseHandle(h);
|
||||
note = "shared arena section " + nm + " has an incompatible or missing header";
|
||||
return nullptr;
|
||||
}
|
||||
if (hdr->pack_hash != shared_pack_hash) {
|
||||
char b[256];
|
||||
std::snprintf(b, sizeof b, "shared arena %s was written for pack hash %016llx, expected %016llx", nm.c_str(),
|
||||
(unsigned long long) hdr->pack_hash, (unsigned long long) shared_pack_hash);
|
||||
UnmapViewOfFile(view);
|
||||
CloseHandle(h);
|
||||
note = b;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_views_mu);
|
||||
g_views[view] = h;
|
||||
}
|
||||
mapping_base = view;
|
||||
mapping_bytes = file_bytes;
|
||||
got = PageBacking::NormalPages;
|
||||
note = "shared section " + nm + " (pack hash checked)";
|
||||
return static_cast<uint8_t*>(view) + kSharedArenaHeaderBytes;
|
||||
}
|
||||
// MEM_LARGE_PAGES needs SeLockMemoryPrivilege. Having it assigned to the account is not enough: the
|
||||
// PROCESS must enable it in its own token (AdjustTokenPrivileges) before VirtualAlloc, or the call fails.
|
||||
@@ -200,7 +341,27 @@ void* reserve(uint64_t bytes, PageBacking& got, std::string& note, const std::st
|
||||
return nullptr;
|
||||
}
|
||||
SharedArenaHeader hdr{};
|
||||
const ssize_t got_header = pread(fd, &hdr, sizeof hdr, 0);
|
||||
ssize_t got_header = pread(fd, &hdr, sizeof hdr, 0);
|
||||
// the creator sizes the file first and writes the header right after: an engine that starts in that gap waits
|
||||
// for it (up to 10 s) instead of calling the file broken
|
||||
for (int w = 0; w < 100 && got_header == (ssize_t) sizeof hdr &&
|
||||
std::memcmp(hdr.magic, kSharedArenaMagic, sizeof hdr.magic) != 0 &&
|
||||
hdr.magic[0] == 0 && hdr.magic[1] == 0; ++w) {
|
||||
usleep(100 * 1000);
|
||||
got_header = pread(fd, &hdr, sizeof hdr, 0);
|
||||
}
|
||||
if (got_header == (ssize_t) sizeof hdr && std::memcmp(hdr.magic, kSharedArenaMagic, sizeof hdr.magic) != 0 &&
|
||||
hdr.magic[0] == 0 && hdr.magic[1] == 0) {
|
||||
// still no header after 10 s: the engine that sized the file died before writing it. Every engine
|
||||
// fills the arena itself, so nothing in the file is worth keeping: write the header as the creator would
|
||||
std::memset(&hdr, 0, sizeof hdr);
|
||||
std::memcpy(hdr.magic, kSharedArenaMagic, sizeof hdr.magic);
|
||||
hdr.version = 1;
|
||||
hdr.header_bytes = (uint32_t) kSharedArenaHeaderBytes;
|
||||
hdr.arena_bytes = bytes;
|
||||
hdr.pack_hash = shared_pack_hash;
|
||||
got_header = pwrite(fd, &hdr, sizeof hdr, 0);
|
||||
}
|
||||
if (got_header != (ssize_t) sizeof hdr ||
|
||||
std::memcmp(hdr.magic, kSharedArenaMagic, sizeof hdr.magic) != 0 ||
|
||||
hdr.version != 1 || hdr.header_bytes != kSharedArenaHeaderBytes || hdr.arena_bytes != bytes) {
|
||||
@@ -220,6 +381,36 @@ void* reserve(uint64_t bytes, PageBacking& got, std::string& note, const std::st
|
||||
}
|
||||
}
|
||||
|
||||
// tmpfs: take the pages now (ftruncate only sets the length). With /dev/shm too small the load would die with a bus
|
||||
// error part-way; here a full tmpfs is a plain error at start, for every engine that maps the file (pages already
|
||||
// taken cost nothing), and the server then starts its replicas with private copies. A file on a disk stays sparse.
|
||||
{
|
||||
struct statfs sf{};
|
||||
if (fstatfs(fd, &sf) == 0 && (unsigned long) sf.f_type == 0x01021994ul) {
|
||||
// a memfd (or any tmpfs file not capped by a mount size) would take the RAM and then the OOM killer: check the
|
||||
// free RAM and the cgroup's room for the pages that are not there yet before taking them
|
||||
struct stat st2{};
|
||||
const uint64_t have = fstat(fd, &st2) == 0 ? (uint64_t) st2.st_blocks * 512u : 0;
|
||||
const uint64_t need = file_bytes > have ? file_bytes - have : 0;
|
||||
if (need > 0) {
|
||||
const uint64_t room = free_ram_room();
|
||||
if (room != ~uint64_t{0} && need + (2ull << 30) > room) {
|
||||
close(fd);
|
||||
note = "not enough free RAM for the shared arena " + shared_file + ": it needs " +
|
||||
std::to_string((unsigned long long) (need >> 20)) + " MiB more and " +
|
||||
std::to_string((unsigned long long) (room >> 20)) + " MiB are free (2 GiB are kept back)";
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
const int fe = posix_fallocate(fd, 0, (off_t) file_bytes);
|
||||
if (fe != 0 && fe != EOPNOTSUPP && fe != EINVAL) {
|
||||
close(fd);
|
||||
note = "not enough room for the shared arena " + shared_file + " (" +
|
||||
std::to_string((unsigned long long) (file_bytes >> 20)) + " MiB): " + std::strerror(fe);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
void* map = mmap(nullptr, (size_t) file_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
const int e = errno;
|
||||
close(fd);
|
||||
@@ -322,6 +513,20 @@ void release(void* p, uint64_t bytes) {
|
||||
if (!p) return;
|
||||
#ifdef _WIN32
|
||||
(void) bytes;
|
||||
HANDLE section = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(g_views_mu);
|
||||
auto it = g_views.find(p);
|
||||
if (it != g_views.end()) {
|
||||
section = it->second;
|
||||
g_views.erase(it);
|
||||
}
|
||||
}
|
||||
if (section != nullptr) { // a shared-section view: the pages stay while another engine has the section
|
||||
UnmapViewOfFile(p);
|
||||
CloseHandle(section);
|
||||
return;
|
||||
}
|
||||
VirtualFree(p, 0, MEM_RELEASE);
|
||||
#else
|
||||
munmap(p, bytes);
|
||||
@@ -410,6 +615,9 @@ PinnedArena::PinnedArena(uint64_t bytes, const std::vector<uint64_t>& bounds,
|
||||
uint64_t shared_pack_hash) : capacity(bytes) {
|
||||
if (bytes == 0) return;
|
||||
base = reserve(bytes, backing, note, shared_file, shared_pack_hash, mapping_base, mapping_bytes);
|
||||
if (!shared_file.empty() && base != nullptr && mapping_base != nullptr)
|
||||
shared_state = reinterpret_cast<std::atomic<uint64_t>*>(static_cast<char*>(mapping_base) +
|
||||
offsetof(SharedArenaHeader, reserved));
|
||||
if (base != nullptr && mapping_base == nullptr) {
|
||||
mapping_base = base;
|
||||
mapping_bytes = bytes;
|
||||
@@ -529,6 +737,9 @@ PinnedArena::PinnedArena(uint64_t bytes, ByNeed, const std::vector<std::pair<uin
|
||||
by_need = true;
|
||||
if (bytes == 0) return;
|
||||
base = reserve(bytes, backing, note, shared_file, shared_pack_hash, mapping_base, mapping_bytes);
|
||||
if (!shared_file.empty() && base != nullptr && mapping_base != nullptr)
|
||||
shared_state = reinterpret_cast<std::atomic<uint64_t>*>(static_cast<char*>(mapping_base) +
|
||||
offsetof(SharedArenaHeader, reserved));
|
||||
if (base != nullptr && mapping_base == nullptr) {
|
||||
mapping_base = base;
|
||||
mapping_bytes = bytes;
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// Windows selftest for the named-section backing of PinnedArena (the replicas' shared expert arena).
|
||||
// Parent: two arenas on one section name (same bytes, header refused for another pack / a bigger size, the load state word),
|
||||
// then a CHILD PROCESS that opens the same section, reads what the parent wrote and writes back; the parent closes its first
|
||||
// arena while the second still holds the section (the pages must stay), and a third arena joins the live section. After the
|
||||
// last close, a new arena with the same name starts empty (the section is gone with its last handle).
|
||||
#ifdef _WIN32
|
||||
#include "strata/core/pinned.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
constexpr uint64_t kBytes = 64u << 10;
|
||||
constexpr uint64_t kPackHash = 0x0123456789abcdefull;
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const std::vector<uint64_t> no_bounds;
|
||||
if (argc >= 3 && std::strcmp(argv[1], "child") == 0) {
|
||||
const std::string name = argv[2];
|
||||
strata::core::PinnedArena a(kBytes, no_bounds, 0, name, kPackHash);
|
||||
if (!a.valid()) { std::fprintf(stderr, "child: cannot open: %s\n", a.note.c_str()); return 2; }
|
||||
if (a.data()[123] != 0x5a) { std::fprintf(stderr, "child: did not see the parent's byte\n"); return 3; }
|
||||
if (a.shared_state == nullptr || a.shared_state->load() != 4242u) { std::fprintf(stderr, "child: state word is not the parent's\n"); return 4; }
|
||||
a.data()[456] = 0xa5;
|
||||
a.shared_state->store(~uint64_t{0}); // (filled)
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fail = 0;
|
||||
const std::string name = "win:Local\\strata-pinned-test-" + std::to_string((unsigned long long) GetCurrentProcessId());
|
||||
{
|
||||
auto* first = new strata::core::PinnedArena(kBytes, no_bounds, 0, name, kPackHash);
|
||||
if (!first->valid()) {
|
||||
std::fprintf(stderr, "first section failed: %s\n", first->note.c_str());
|
||||
return 1;
|
||||
}
|
||||
first->data()[123] = 0x5a;
|
||||
if (first->shared_state == nullptr) { std::fprintf(stderr, "no state word\n"); fail = 1; }
|
||||
else if (first->shared_state->load() != 0) { std::fprintf(stderr, "a new section's state word is not 0\n"); fail = 1; }
|
||||
else first->shared_state->store(4242u);
|
||||
|
||||
auto* second = new strata::core::PinnedArena(kBytes, no_bounds, 0, name, kPackHash);
|
||||
if (!second->valid()) { std::fprintf(stderr, "second section failed: %s\n", second->note.c_str()); fail = 1; }
|
||||
else {
|
||||
if (second->data()[123] != 0x5a) { std::fprintf(stderr, "same name did not expose the same bytes\n"); fail = 1; }
|
||||
if (second->shared_state == nullptr || second->shared_state->load() != 4242u) { std::fprintf(stderr, "state word not shared\n"); fail = 1; }
|
||||
}
|
||||
{
|
||||
strata::core::PinnedArena wrong_pack(kBytes, no_bounds, 0, name, kPackHash + 1);
|
||||
if (wrong_pack.valid() || wrong_pack.note.find("pack hash") == std::string::npos) {
|
||||
std::fprintf(stderr, "pack mismatch was not refused: %s\n", wrong_pack.note.c_str());
|
||||
fail = 1;
|
||||
}
|
||||
strata::core::PinnedArena wrong_size(kBytes * 2, no_bounds, 0, name, kPackHash);
|
||||
if (wrong_size.valid()) { std::fprintf(stderr, "a bigger size was not refused\n"); fail = 1; }
|
||||
}
|
||||
|
||||
// a child process opens the live section
|
||||
char exe[MAX_PATH];
|
||||
GetModuleFileNameA(nullptr, exe, MAX_PATH);
|
||||
std::string cmd = std::string("\"") + exe + "\" child \"" + name + "\"";
|
||||
STARTUPINFOA si{};
|
||||
si.cb = sizeof si;
|
||||
PROCESS_INFORMATION pi{};
|
||||
std::vector<char> cl(cmd.begin(), cmd.end());
|
||||
cl.push_back('\0');
|
||||
if (!CreateProcessA(nullptr, cl.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) {
|
||||
std::fprintf(stderr, "cannot start the child (error %lu)\n", GetLastError());
|
||||
fail = 1;
|
||||
} else {
|
||||
WaitForSingleObject(pi.hProcess, 120000);
|
||||
DWORD code = 99;
|
||||
GetExitCodeProcess(pi.hProcess, &code);
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
if (code != 0) { std::fprintf(stderr, "the child failed with %lu\n", code); fail = 1; }
|
||||
if (first->data()[456] != 0xa5) { std::fprintf(stderr, "the child's byte did not arrive\n"); fail = 1; }
|
||||
if (first->shared_state->load() != ~uint64_t{0}) { std::fprintf(stderr, "the child's state word did not arrive\n"); fail = 1; }
|
||||
}
|
||||
|
||||
delete first; // the creator is gone; the second still holds the section
|
||||
if (second != nullptr && second->valid()) {
|
||||
if (second->data()[123] != 0x5a || second->data()[456] != 0xa5) { std::fprintf(stderr, "bytes lost when the first arena closed\n"); fail = 1; }
|
||||
strata::core::PinnedArena third(kBytes, no_bounds, 0, name, kPackHash);
|
||||
if (!third.valid() || third.data()[123] != 0x5a) { std::fprintf(stderr, "a late arena did not join the live section\n"); fail = 1; }
|
||||
}
|
||||
delete second;
|
||||
}
|
||||
{
|
||||
strata::core::PinnedArena again(kBytes, no_bounds, 0, name, kPackHash); // (every handle was closed: a new, empty section)
|
||||
if (!again.valid() || again.data()[123] != 0 || again.shared_state->load() != 0) {
|
||||
std::fprintf(stderr, "the section survived its last handle\n");
|
||||
fail = 1;
|
||||
}
|
||||
}
|
||||
std::printf("pinned_shared_win_test: %s\n", fail ? "FAILED" : "OK");
|
||||
return fail;
|
||||
}
|
||||
#else
|
||||
int main() { return 77; } // (skipped: the Linux build has pinned_shared_test)
|
||||
#endif
|
||||
@@ -5949,9 +5949,15 @@ int64_t Prefill::lm_prepare(int64_t n, std::string& why, int64_t cap) {
|
||||
Impl& m = *impl_;
|
||||
if (m.g == nullptr) { why = "not initialized"; return 0; }
|
||||
#if defined(STRATA_USE_HIP) || defined(STRATA_HIP_GFX906)
|
||||
// only NVIDIA CUDA has run this path; on an AMD build the prompt stays on the chunk-major path
|
||||
why = "layer-major prefill has only been run on NVIDIA CUDA";
|
||||
return 0;
|
||||
// only NVIDIA CUDA has run this path by default; on an AMD build the prompt stays on the chunk-major path unless
|
||||
// STRATA_LM_HIP=1 (opt-in, PAR-A 2026-10-30: gfx1201 is being qualified)
|
||||
{
|
||||
static const bool lm_hip = [] { const char* v = std::getenv("STRATA_LM_HIP"); return v != nullptr && v[0] == '1'; }();
|
||||
if (!lm_hip) {
|
||||
why = "layer-major prefill has only been run on NVIDIA CUDA";
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
const core::ModelGeometry& g = *m.g;
|
||||
const core::OnDevice on_device(m.device); // a layer split's later stage is sized on its own GPU
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
"""The replica serving profile (opt-in): on two or more GPUs, one engine per card behind the server instead of one layer split.
|
||||
|
||||
Measured on 4 x Radeon AI PRO R9700 (Qwen3.8-Flash-Next IQ3_S, 1k-token prompts, 128 tokens out, total tokens per second;
|
||||
docs/BATCHING.md, "Replicas as a serving profile"):
|
||||
|
||||
* 2 cards: 2 engines of one card each reach 1.8 to 1.9 times what a layer split of the two cards does with `parallel` 8 or
|
||||
16 (116.6 against 63.8 tok/s at 8 requests, 126.9 against 65.7 at 16);
|
||||
* 4 cards: 4 engines reach 1.8 to 1.9 times the shipped four-card profile (190 against 107 at 8, 220 against 116 at 16).
|
||||
|
||||
Nothing is lost for one user: a request alone runs on one card at the same speed (13 ms a token on one card, 13 ms on four).
|
||||
|
||||
The price is host RAM: every engine keeps its own copy of the experts. The profile therefore turns on the shared expert
|
||||
arena (serve/replicas.py: one memory object holds the experts for all engines, measured 61 GB for 2 engines and 66 GB for 4
|
||||
instead of 109 and 211 GB) and chooses the number of engines the RAM allows. Without the shared arena (Windows) it needs the
|
||||
private copies to fit.
|
||||
|
||||
Recommend, never force: this profile is opt-in (`setup.py --replica-profile`, STRATA_REPLICA_PROFILE=1, or
|
||||
"replica_profile": true in the run config; "replica_profile": false or STRATA_REPLICA_PROFILE=0 turn it off), and what the
|
||||
person chose (the config's `replicas`, `--parallel`, `--layer-split`, `--peer-device`, an environment variable) always wins.
|
||||
It only touches the measured model class; every other computer gets exactly what it got before.
|
||||
|
||||
Pure functions, no I/O (the callers pass the cards, the RAM and the run config).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# the experts' size in RAM per quant (setup.py MODELS "arena_gb"): the shared arena holds this once
|
||||
ARENA_GB = {"iq3_s": 50.3, "iq3_xxs": 42.9, "q2_0": 34.0}
|
||||
PER_REPLICA_GB = 8.0 # measured: about 7 GB of private RAM per engine beside the arena (2 engines: 61 GB in all, 47 of them shared)
|
||||
HEADROOM_GB = 12.0 # what the OS, the server and the page cache keep
|
||||
MIN_CARD_GB = 24.0 # measured on 32 GB cards; a smaller card is not claimed (an A4000 16 GB is measured separately)
|
||||
MIN_REPLICAS = 2
|
||||
SWITCHES = {"STRATA_QSA_SLOTS": "1", "STRATA_BATCH_SELF_COMMIT": "1", "STRATA_SLOT_COPY_DIRECT": "1",
|
||||
"STRATA_CKPT_POOL": "1", "STRATA_CKPT_POOL_READY": "4", "STRATA_SLOT_CKPT_MOVE": "1",
|
||||
"STRATA_SLOT_COPY_BATCHSYNC": "1"}
|
||||
OFF_VALUES = ("0", "off", "no", "false")
|
||||
ON_VALUES = ("1", "on", "yes", "true")
|
||||
|
||||
|
||||
def requested(cfg: dict | None = None, environ=None) -> bool:
|
||||
"""Opt-in, and an explicit no wins: config "replica_profile" true/false, else STRATA_REPLICA_PROFILE."""
|
||||
environ = os.environ if environ is None else environ
|
||||
if cfg and cfg.get("replica_profile") is not None:
|
||||
return cfg.get("replica_profile") is True
|
||||
return str(environ.get("STRATA_REPLICA_PROFILE", "")).strip().lower() in ON_VALUES
|
||||
|
||||
|
||||
def quant_of(cfg: dict) -> str | None:
|
||||
"""The measured model: Qwen3.8-Flash-Next at a quant whose experts size is known."""
|
||||
text = " ".join([str(cfg.get("model_name") or "")] + [str(a) for a in cfg.get("args") or []]).lower().replace("-", "_").replace(".", "_")
|
||||
if "qwen3_8" not in text:
|
||||
return None
|
||||
for q in ARENA_GB:
|
||||
if q in text:
|
||||
return q
|
||||
return None
|
||||
|
||||
|
||||
def shared_arena_possible(platform: str | None = None) -> bool:
|
||||
return (sys.platform if platform is None else platform).startswith(("linux", "win"))
|
||||
|
||||
|
||||
def replicas_for(n_cards: int, arena_gb: float, ram_gb: float, shared: bool) -> int:
|
||||
"""How many one-card engines the RAM allows (at most one per card)."""
|
||||
if shared:
|
||||
room = ram_gb - HEADROOM_GB - arena_gb
|
||||
k = int(room // PER_REPLICA_GB) if room > 0 else 0
|
||||
else:
|
||||
k = int((ram_gb - HEADROOM_GB) // (arena_gb + PER_REPLICA_GB))
|
||||
return max(0, min(n_cards, k))
|
||||
|
||||
|
||||
def applies(cards: list[dict], cfg: dict, ram_gb: float, *, platform: str | None = None, environ=None) -> tuple[bool, str]:
|
||||
"""(True, "") when the profile would be applied; else (False, why not) - the reason is for the log, '' when nothing was asked."""
|
||||
if not requested(cfg, environ):
|
||||
return False, ""
|
||||
args = [str(a) for a in cfg.get("args") or []]
|
||||
if cfg.get("replicas") or cfg.get("arch") == "deepseek41" or "--peer-device" in args:
|
||||
return False, "replica profile: the config already names replicas, a peer device or another model family"
|
||||
if str(cfg.get("layer_split") or "auto") != "auto" or "--layer-split" in args:
|
||||
return False, "replica profile: you chose a layer split; kept"
|
||||
if len(cards) < MIN_REPLICAS:
|
||||
return False, "replica profile: needs two or more GPUs"
|
||||
q = quant_of(cfg)
|
||||
if q is None:
|
||||
return False, "replica profile: measured only for Qwen3.8-Flash-Next IQ3_S, IQ3_XXS and Q2_0"
|
||||
if any(float(c.get("vram_gb") or 0.0) < MIN_CARD_GB for c in cards):
|
||||
return False, f"replica profile: a card has under {MIN_CARD_GB:.0f} GB of VRAM (measured on 32 GB cards)"
|
||||
shared = shared_arena_possible(platform)
|
||||
k = replicas_for(len(cards), ARENA_GB[q], ram_gb, shared)
|
||||
if k < MIN_REPLICAS:
|
||||
need = HEADROOM_GB + ARENA_GB[q] + MIN_REPLICAS * PER_REPLICA_GB if shared else HEADROOM_GB + MIN_REPLICAS * (ARENA_GB[q] + PER_REPLICA_GB)
|
||||
return False, (f"replica profile: {ram_gb:.0f} GB of RAM; two engines need about {need:.0f} GB"
|
||||
f"{'' if shared else ' (this OS cannot share the experts between engines)'}; the layer split stays")
|
||||
return True, ""
|
||||
|
||||
|
||||
def apply(cards: list[dict], cfg: dict, ram_gb: float, *, parallel_given: bool = False, platform: str | None = None,
|
||||
environ=None) -> list[str]:
|
||||
"""Fill the profile into `cfg` (in place) where the person has not chosen; returns the plain lines to show."""
|
||||
ok, why = applies(cards, cfg, ram_gb, platform=platform, environ=environ)
|
||||
if not ok:
|
||||
return [why] if why else []
|
||||
environ = os.environ if environ is None else environ
|
||||
q = quant_of(cfg)
|
||||
shared = shared_arena_possible(platform)
|
||||
k = replicas_for(len(cards), ARENA_GB[q], ram_gb, shared)
|
||||
gpus = cfg.get("gpu") if isinstance(cfg.get("gpu"), list) else [c.get("index", i) for i, c in enumerate(cards)]
|
||||
chosen = list(gpus)[:k]
|
||||
cfg["gpu"] = chosen
|
||||
cfg["replicas"] = [{"gpus": [g]} for g in chosen]
|
||||
if shared:
|
||||
cfg.setdefault("replica_shared_arena", True)
|
||||
env = dict(cfg.get("env") or {})
|
||||
for key, val in SWITCHES.items():
|
||||
if key not in environ: # an environment variable of the person's own wins
|
||||
env.setdefault(key, val)
|
||||
cfg["env"] = env
|
||||
notes = [f"{k} engines of one card each instead of one layer split of {len(cards)}: requests spread over them "
|
||||
f"(about 1.8 times the throughput at 8 to 16 requests at once; one request alone is as fast as before)"]
|
||||
if k < len(cards):
|
||||
notes.append(f"{len(cards) - k} card(s) left out: the RAM ({ram_gb:.0f} GB) holds {k} engines")
|
||||
notes.append("the experts are in RAM once for all engines (shared expert arena)" if shared else
|
||||
"each engine keeps its own copy of the experts in RAM (this OS cannot share them)")
|
||||
if not parallel_given and "parallel" not in cfg and not {"--batch", "--slots"} & set(str(a) for a in cfg.get("args") or []):
|
||||
cfg["parallel"] = 8 if k == 2 else 4
|
||||
notes.append(f"{cfg['parallel']} requests at once on each engine (--parallel N changes it)")
|
||||
return notes
|
||||
@@ -0,0 +1,130 @@
|
||||
"""strata_control/replica_profile.py: the opt-in replica serving profile (one engine per card, shared expert arena)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from strata_control import replica_profile as P
|
||||
|
||||
R9700 = {"name": "AMD Radeon AI PRO R9700", "vram_gb": 31.9, "vendor": "amd"}
|
||||
|
||||
|
||||
def cards(n, **kw):
|
||||
return [{**R9700, **kw, "index": i} for i in range(n)]
|
||||
|
||||
|
||||
def cfg(**kw):
|
||||
c = {"model_name": "qwen3.8-flash-next-iq3_s", "gpu": [0, 1, 2, 3], "layer_split": "auto",
|
||||
"args": ["--pack", "/p/iq3_s", "--spec", "4"], "replica_profile": True}
|
||||
c.update(kw)
|
||||
return c
|
||||
|
||||
|
||||
class Request(unittest.TestCase):
|
||||
def test_opt_in_only(self):
|
||||
self.assertFalse(P.requested({}, {}))
|
||||
self.assertTrue(P.requested({"replica_profile": True}, {}))
|
||||
self.assertTrue(P.requested({}, {"STRATA_REPLICA_PROFILE": "1"}))
|
||||
|
||||
def test_an_explicit_no_wins(self):
|
||||
self.assertFalse(P.requested({"replica_profile": False}, {"STRATA_REPLICA_PROFILE": "1"}))
|
||||
|
||||
def test_without_the_request_nothing_changes(self):
|
||||
c = cfg(replica_profile=None)
|
||||
c.pop("replica_profile")
|
||||
before = dict(c)
|
||||
self.assertEqual(P.apply(cards(4), c, 256.0, platform="linux", environ={}), [])
|
||||
self.assertEqual(c, before)
|
||||
|
||||
|
||||
class Count(unittest.TestCase):
|
||||
def test_ram_sets_the_number_of_engines(self):
|
||||
self.assertEqual(P.replicas_for(4, 50.3, 251.0, True), 4)
|
||||
self.assertEqual(P.replicas_for(4, 50.3, 128.0, True), 4)
|
||||
self.assertEqual(P.replicas_for(4, 50.3, 96.0, True), 4)
|
||||
self.assertEqual(P.replicas_for(4, 50.3, 80.0, True), 2) # 80 - 12 - 50.3 = 17.7 -> 2
|
||||
self.assertEqual(P.replicas_for(4, 50.3, 64.0, True), 0) # one engine's room only
|
||||
self.assertEqual(P.replicas_for(2, 50.3, 251.0, False), 2) # private copies: 2 x 58.3 + 12
|
||||
self.assertEqual(P.replicas_for(4, 50.3, 200.0, False), 3)
|
||||
self.assertEqual(P.replicas_for(2, 50.3, 100.0, False), 1)
|
||||
|
||||
|
||||
class Apply(unittest.TestCase):
|
||||
def test_four_cards_with_plenty_of_ram(self):
|
||||
c = cfg()
|
||||
notes = P.apply(cards(4), c, 251.0, platform="linux", environ={})
|
||||
self.assertTrue(notes)
|
||||
self.assertEqual(c["replicas"], [{"gpus": [0]}, {"gpus": [1]}, {"gpus": [2]}, {"gpus": [3]}])
|
||||
self.assertIs(c["replica_shared_arena"], True)
|
||||
self.assertEqual(c["parallel"], 4)
|
||||
self.assertEqual(c["env"]["STRATA_CKPT_POOL"], "1")
|
||||
self.assertEqual(c["env"]["STRATA_QSA_SLOTS"], "1")
|
||||
|
||||
def test_two_cards_get_eight_slots_each(self):
|
||||
c = cfg(gpu=[0, 1])
|
||||
P.apply(cards(2), c, 128.0, platform="linux", environ={})
|
||||
self.assertEqual(c["parallel"], 8)
|
||||
self.assertEqual(len(c["replicas"]), 2)
|
||||
|
||||
def test_less_ram_drops_cards_and_says_so(self):
|
||||
c = cfg()
|
||||
notes = P.apply(cards(4), c, 80.0, platform="linux", environ={})
|
||||
self.assertEqual(c["replicas"], [{"gpus": [0]}, {"gpus": [1]}])
|
||||
self.assertEqual(c["gpu"], [0, 1])
|
||||
self.assertTrue(any("left out" in n for n in notes))
|
||||
|
||||
def test_too_little_ram_keeps_the_layer_split(self):
|
||||
c = cfg()
|
||||
notes = P.apply(cards(4), c, 64.0, platform="linux", environ={})
|
||||
self.assertNotIn("replicas", c)
|
||||
self.assertEqual(c["layer_split"], "auto")
|
||||
self.assertTrue(notes and "layer split stays" in notes[0])
|
||||
|
||||
def test_windows_shares_through_a_section(self):
|
||||
c = cfg(gpu=[0, 1])
|
||||
P.apply(cards(2), c, 80.0, platform="win32", environ={})
|
||||
self.assertEqual(len(c["replicas"]), 2)
|
||||
self.assertIs(c["replica_shared_arena"], True)
|
||||
|
||||
def test_an_os_that_cannot_share_needs_the_private_copies_to_fit(self):
|
||||
c = cfg(gpu=[0, 1])
|
||||
self.assertEqual(P.apply(cards(2), c, 100.0, platform="darwin", environ={})[0].split(":")[0], "replica profile")
|
||||
self.assertNotIn("replicas", c)
|
||||
c = cfg(gpu=[0, 1])
|
||||
P.apply(cards(2), c, 160.0, platform="darwin", environ={})
|
||||
self.assertEqual(len(c["replicas"]), 2)
|
||||
self.assertNotIn("replica_shared_arena", c)
|
||||
|
||||
def test_what_the_person_chose_wins(self):
|
||||
for kw in ({"replicas": 2}, {"layer_split": "13,26,39"}, {"args": ["--pack", "/p/iq3_s", "--peer-device", "1"]},
|
||||
{"args": ["--pack", "/p/iq3_s", "--layer-split", "20,40"]}, {"arch": "deepseek41"}):
|
||||
c = cfg(**kw)
|
||||
before = dict(c)
|
||||
P.apply(cards(4), c, 251.0, platform="linux", environ={})
|
||||
self.assertEqual(c.get("replicas"), before.get("replicas"), kw)
|
||||
self.assertEqual(c.get("layer_split"), before.get("layer_split"), kw)
|
||||
|
||||
def test_parallel_and_environment_variables_of_the_person_win(self):
|
||||
c = cfg(parallel=6)
|
||||
P.apply(cards(4), c, 251.0, platform="linux", environ={"STRATA_CKPT_POOL": "0"})
|
||||
self.assertEqual(c["parallel"], 6)
|
||||
self.assertNotIn("STRATA_CKPT_POOL", c["env"])
|
||||
c = cfg()
|
||||
P.apply(cards(4), c, 251.0, parallel_given=True, platform="linux", environ={})
|
||||
self.assertNotIn("parallel", c)
|
||||
|
||||
def test_only_the_measured_hardware_and_model(self):
|
||||
for kw in ({"model_name": "qwen3.6-35b-a3b-q4"}, {"model_name": "qwen3.8-flash-next-iq2_xs", "args": ["--pack", "/p/x"]}):
|
||||
c = cfg(**kw)
|
||||
P.apply(cards(4), c, 251.0, platform="linux", environ={})
|
||||
self.assertNotIn("replicas", c, kw)
|
||||
c = cfg(gpu=[0, 1])
|
||||
P.apply(cards(2, vram_gb=16.0), c, 251.0, platform="linux", environ={})
|
||||
self.assertNotIn("replicas", c)
|
||||
c = cfg(gpu=[0])
|
||||
P.apply(cards(1), c, 251.0, platform="linux", environ={})
|
||||
self.assertNotIn("replicas", c)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user