From a242537838a35eaa44e436ad7a979e47cfb4aa19 Mon Sep 17 00:00:00 2001 From: Niko1221 Date: Thu, 15 Oct 2026 10:21:48 +0200 Subject: [PATCH] setup: split build helpers (cmake, ninja) out of the runtime requirements requirements.txt keeps the runtime packages only; requirements-build.txt pins cmake and ninja. setup.cmake_build installs only the helper that is missing, right before a local engine or image-encoder build, so the ready-made engine path (and serving) never runs pip for build tools. ram_gb() reads hw.memsize on macOS and no longer crashes without /proc/meminfo. docs/INSTALL.md notes the split; two tests in tools/test_setup_pins.py cover it. From the owner's rebuild branch strata/rebuild-2026-10. setup.py merged onto 0.1.45 without conflicts (0.1.45's MIN_ENGINE and pins are unchanged). --- docs/INSTALL.md | 5 +++++ requirements-build.txt | 3 +++ requirements.txt | 5 ++--- setup.py | 37 ++++++++++++++++++++++++++++--------- tools/test_setup_pins.py | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 requirements-build.txt diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 1a7a43095..099db1179 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -27,6 +27,11 @@ you the first time: Python 3.12 if you have none (for your user account, no admi the build tools (Visual Studio Build Tools + CUDA Toolkit on Windows, `build-essential` + CUDA on Ubuntu) and compiles the engine for your GPU (asks first; 20-40 minutes once). More: [details](DETAILS.md#before-you-start). +The normal Python install uses `requirements.txt` and does not install CMake or Ninja. If setup has to compile an +engine or image encoder, it uses existing CMake/Ninja tools when available and installs missing pinned helpers from +`requirements-build.txt` into `.venv`. An older `.venv` keeps any build helpers it already has; setup does not remove +packages during an update. + ## Windows 1. [Download this project](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it (or diff --git a/requirements-build.txt b/requirements-build.txt new file mode 100644 index 000000000..ebdef8ebb --- /dev/null +++ b/requirements-build.txt @@ -0,0 +1,3 @@ +# Build helpers for a local engine or vision-encoder compile. Ready-made engines do not need these. +cmake==4.4.3 +ninja==1.13.2 diff --git a/requirements.txt b/requirements.txt index 3db8418ab..b6f4d38ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -# The Python packages setup.py installs into .venv, pinned so a checkout installs the same ones on any day (#214). +# Runtime Python packages setup.py installs into .venv, pinned so a checkout installs the same ones on any day (#214). +# CMake and Ninja are in requirements-build.txt and are installed only if a local build needs them. # setup.py's pip step reads this file; an install from before it keeps the packages it has. # Python 3.10+ (numpy's newest releases need a newer Python: one line per range). numpy==2.5.3; python_version >= "3.12" @@ -9,8 +10,6 @@ regex==2026.9.10 pyyaml==6.0.3 tqdm==4.70.1 requests==2.34.2 -cmake==4.4.3 -ninja==1.13.2 pillow==12.3.0 psutil==7.2.2 diff --git a/setup.py b/setup.py index 19b29532e..e90d33d6d 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ What the first run does (each step is skipped when it is already done): 2. asks the questions 3. installs the Python packages it needs into .venv (numpy, jinja2, ..., and NVIDIA's CUDA libraries) 4. gets the Strata engine: a ready-made build for RTX 20/30/40/50 cards (no compiler needed); if none fits your PC, - it installs the build tools (asks first) and compiles the engine for your GPU. AMD (--backend hip, chosen by + it installs the missing build tools (asks first) and compiles the engine for your GPU. AMD (--backend hip, chosen by itself on a PC with no usable NVIDIA card): the ready-made HIP engine on Windows, compiled here on Linux 5. downloads the model from Hugging Face (resumable), and the vision encoder if you want images 6. prepares the model for Strata and fetches the MTP draft layer (~5 GB, from the original Qwen checkpoint) @@ -196,8 +196,10 @@ ENGINE12_DIR = "engine-cuda12" MIN_ENGINE = (0, 1, 45) # versions compare all numbers; v0.1.45: Qwen3.6 parallel requests and pictures, fixes #2346 #2372 #2396; v0.1.44.1: CPUs without AVX2 start again (#2324: an illegal instruction before main in the NVFP4 CPU rows), the CUDA 12.0/12.1 toolkits build (#2322), a crashed engine start is named instead of retried as a GPU runtime start, Mac M2 (#2323); v0.1.44: UD-Q4/Q5/Q6_K_XL and Q8_0 Unsloth files in setup (K-quant experts, MMQ prompt library strata_kq), NVFP4 packs, Qwen3.6-35B-A3B, Intel Arc A-series kernels, the pipelined-decode and latency work; v0.1.43.1: the adaptive overlap no longer collapses decode on pinned-host desktops (#1964 #1975), Windows PLE mmap stall (#1984), pinned prompt uploads (#1996), the I/O watchdog allowance; v0.1.43: adaptive-tier copies overlap on CUDA, P100 (sm_60) kernel tables, RDNA4 (gfx1200/gfx1201) defaults (route tail skip, measured PCIe share, 16K prompt chunks, fused IQ3 prompt experts); v0.1.42.1: the GGUF-in-place file tier on Windows unmaps every shard, dual-socket starts judge each NUMA node and STRATA_DROP_CACHE_ON_EXIT, tools/strata_bench.py; v0.1.42: STRATA_ROUTE_TAIL_SKIP=7 default on CUDA (+13-16% decode), the AMD 2-GPU checkpoint fix (H13), Phase 2 fixes and opt-ins; v0.1.41.1 (folded into 0.1.42): the #1576 card order, #1616 stage size, #1595 CPU share, #1630/#1607 Windows HIP chunk cap and commit-limit warning, #1603 parallel-request bookkeeping; v0.1.41: CPU prefill share on by default (NVIDIA, chunks < 1024), --batch-groups auto, the file tier read in batches on Windows, Pascal decode fix, per-arch tables; v0.1.40.3: the #1357 MTP router guard, the #1376 Windows HIP cache floor, #461 runtime DLLs, Intel A750 first-request fix; v0.1.40.2: F4 verify windows, the Linux file tier (#1194), the stager wait (#1057), #1264/#1201/#1139 fixes, opt-in CPU share (#1282), Intel Arc; v0.1.40: --resident-experts on a layer split with the split+resident variant (#848), --kv k8v4 with KV streaming (#711); v0.1.39: the #577 file-tier regression fixed, the OpenAI Responses API (#451, Codex), a reply stuck on one token ended (#606), the head before the arena (#620), effort_position (#458), --vram-reserve hot resize opt-in (#533), PR batch; v0.1.38: prompts faster (one gather per expert group #372, the first chunk's PLE rows beside layer 0 #374, DeltaNet three heads per thread #413), --kv q4_0 prompts on tensor cores (#452), Q5_0 experts on the GPU (#473), IQ4_XS on AVX-2 (#415), unbuffered expert loading on Windows (#357 #362), --peer-device (#531), a 6 GB card starts (#496), PR batch; v0.1.37: a silent engine is restarted (#481), Windows AMD counts the desktop's VRAM (#380 #377 #497), a steadier PCIe probe (#485), fixes #496 #495 #498 #505 #493; v0.1.36: a cancelled prompt logged as read so far (#471), the draft-head hint (#474), UPDATE.bat (#475), --expert-profile-save (#477); v0.1.35: Windows AMD uses its bundled HIP runtime (#468 #461), the low-RAM resident mode on Windows 32 GB (#467), fixes #460 #459 #446 #447 #457 #448 #444; v0.1.34: AMD on Windows (a ready-made HIP engine), an MCP server for AI assistants (tools/strata_mcp.py), a shorter README; v0.1.33: a portable image encoder again (#411 #412), setup recommends instead of forcing (#406 #403 #364 #384), fixes #352 #365 #369 #371 #375 #393 #408 #414; v0.1.32: split prompts faster (#340), AMD router +12%, Unsloth Q4 in setup, faster Q4 prompts, #326/#327/#342/#344 fixes, PR batch; v0.1.31: Unsloth UD-Q4_K_XL (experimental), GGUF-in-place low-RAM mode, Windows GGUF load 2x, server race + tokenizer fixes, AMD intrinsics; v0.1.30: short prompts faster (streaming from 1024 tokens), resident low-RAM variant, multi-GPU session carve, RDNA4; v0.1.29: sampled answers faster (split top-k), #154 correctness fixes; v0.1.28: the expert cache reserves the draft head, a cancelled request no longer fails the next; v0.1.27: RTX 20 (sm_75) in the ready-made engine, the HIP build without CUDA headers; v0.1.26: the draft layer's prompt pass in batches; v0.1.25: faster prompts (grouping off the copy engine, fused hyper-connection kernels), AMD HIP backend, --kv k8v4; v0.1.24: long prompts faster (QSA select on tensor cores); v0.1.23: image requests honor sampling, 8 GB cards start, batched verify window; v0.1.22: faster prompts (tensor-core attention), multi-GPU across images/steering/KV streaming; v0.1.21: multi-GPU layer split (--gpus); v0.1.20: system-prompt checkpoint, PCIe probe, hit rate; v0.1.19: penalties # KV bytes per context token and attention layer: 8-bit 1056, rotated 4-bit 576, hybrid K8V4 (8-bit K, 4-bit V) 816 KV_CELL_BYTES = {"q4_0": 576, "k8v4": 816} -PY_PACKAGES = ["numpy", "jinja2", "regex", "pyyaml", "tqdm", "requests", "cmake", "ninja", "pillow", "psutil"] +PY_PACKAGES = ["numpy", "jinja2", "regex", "pyyaml", "tqdm", "requests", "pillow", "psutil"] REQUIREMENTS = ROOT / "requirements.txt" # the same packages and their dependencies, pinned (#214) +BUILD_PACKAGES = ["cmake", "ninja"] +BUILD_REQUIREMENTS = ROOT / "requirements-build.txt" # NVFP4 experts (MODEL-UPDATE Phase 4; the engine path is ported from sergqwer/strata-nvfp4, MIT). Ready-made Strata # packs on Hugging Face by the fork's author: nothing is converted here. Every file is pinned by its upload's commit, @@ -585,9 +587,20 @@ def _memory_status(): def ram_gb(): if WIN: return _memory_status().ullTotalPhys / 2**30 - for line in open("/proc/meminfo"): - if line.startswith("MemTotal"): - return int(line.split()[1]) * 1024 / 2**30 + if sys.platform == "darwin": + try: + value = subprocess.run(["sysctl", "-n", "hw.memsize"], capture_output=True, + text=True, timeout=3, check=True).stdout.strip() + return int(value) / 2**30 + except (OSError, ValueError, subprocess.CalledProcessError, subprocess.TimeoutExpired): + return 0.0 + try: + with open("/proc/meminfo") as stream: + for line in stream: + if line.startswith("MemTotal"): + return int(line.split()[1]) * 1024 / 2**30 + except (OSError, ValueError): + pass return 0.0 @@ -2038,7 +2051,7 @@ def req_name(line: str) -> str: def requirement_lines(path: Path | None = None) -> list[str]: - """requirements.txt's requirements, comments and blank lines left out.""" + """A requirements file's requirements, comments and blank lines left out.""" lines = [] for raw in (path or REQUIREMENTS).read_text(encoding="utf-8").splitlines(): line = raw.split("#", 1)[0].strip() @@ -3873,9 +3886,15 @@ def install_build_tools(gpu, yes): def cmake_build(src, bdir, target, defs, vcvars, bat_name): + # The ready-made engine path never calls this function. Keep build helpers out of + # the normal serving environment, and use already installed tools offline. + build_packages = requirement_lines(BUILD_REQUIREMENTS) if BUILD_REQUIREMENTS.exists() else BUILD_PACKAGES + missing = [package for package in build_packages if find_tool(req_name(package)) is None] + if missing: + pip_install(missing, "CMake and Ninja build tools") cmake, ninja = find_tool("cmake"), find_tool("ninja") if cmake is None or ninja is None: - fail("cmake / ninja not found after installing them", "run: .venv python -m pip install cmake ninja") + fail("cmake / ninja not found after installing them", "run: .venv python -m pip install -r requirements-build.txt") conf = [cmake, "-G", "Ninja", f"-DCMAKE_MAKE_PROGRAM={ninja}", "-S", str(src), "-B", str(bdir), "-DCMAKE_BUILD_TYPE=Release", *defs] build = [cmake, "--build", str(bdir), "--target", target, "-j", str(max(2, (os.cpu_count() or 4) // 2))] @@ -4882,7 +4901,7 @@ def update_install(have: list, a) -> int: say(" it finds an earlier install's model files next to it and reuses them.") return 0 pip_install(requirement_lines() if REQUIREMENTS.exists() else PY_PACKAGES, - "numpy, jinja2, regex, pyyaml, tqdm, requests, cmake, ninja, pillow, psutil") + "numpy, jinja2, regex, pyyaml, tqdm, requests, pillow, psutil") if not a.build: update_installed_engine(a.prebuilt) mtp_done: set = set() @@ -6647,7 +6666,7 @@ def main() -> int: # ---- 3. python packages step(3, "Python packages") pip_install(requirement_lines() if REQUIREMENTS.exists() else PY_PACKAGES, - "numpy, jinja2, regex, pyyaml, tqdm, requests, cmake, ninja, pillow, psutil") + "numpy, jinja2, regex, pyyaml, tqdm, requests, pillow, psutil") # ---- 4. the engine step(4, "the Strata engine") diff --git a/tools/test_setup_pins.py b/tools/test_setup_pins.py index d1beddf1a..11b19be13 100644 --- a/tools/test_setup_pins.py +++ b/tools/test_setup_pins.py @@ -319,6 +319,41 @@ class Requirements(unittest.TestCase): self.assertEqual(setup.req_name('numpy==2.5.3; python_version >= "3.12"'), "numpy") self.assertEqual(setup.req_name("charset_normalizer==3"), "charset-normalizer") + def test_build_helpers_are_pinned_but_absent_from_runtime_install(self): + runtime = {setup.req_name(x) for x in setup.requirement_lines()} + build = setup.requirement_lines(setup.BUILD_REQUIREMENTS) + self.assertEqual({setup.req_name(x) for x in build}, {"cmake", "ninja"}) + self.assertTrue(all("==" in x for x in build)) + self.assertTrue(runtime.isdisjoint({"cmake", "ninja"})) + + def test_local_build_installs_only_missing_helper(self): + installed = False + requests = [] + commands = [] + + def find_tool(name): + if name == "cmake": + return "/usr/bin/cmake" + return "/venv/bin/ninja" if installed else None + + def pip_install(packages, what): + nonlocal installed + requests.append((packages, what)) + installed = True + + def run(command, **kwargs): + commands.append(command) + return mock.Mock(returncode=0) + + with mock.patch.object(setup, "WIN", False), \ + mock.patch.object(setup, "find_tool", side_effect=find_tool), \ + mock.patch.object(setup, "pip_install", side_effect=pip_install), \ + mock.patch.object(setup, "run", side_effect=run): + setup.cmake_build(ROOT, ROOT / "build-test", "strata", [], None, "unused.bat") + self.assertEqual(requests, [(["ninja==1.13.2"], "CMake and Ninja build tools")]) + self.assertEqual(commands[0][:4], ["/usr/bin/cmake", "-G", "Ninja", "-DCMAKE_MAKE_PROGRAM=/venv/bin/ninja"]) + self.assertEqual(commands[1][:3], ["/usr/bin/cmake", "--build", str(ROOT / "build-test")]) + def pip(self, stamp, packages, installed=()): ran = [] with tempfile.TemporaryDirectory() as d: