Compare commits

...
52 Commits
Author SHA1 Message Date
Niko1221 5b734e427c Engine 0.1.35: Windows AMD uses its bundled HIP runtime (#468 #461), the low-RAM resident mode on Windows 32 GB PCs (#467), fixes #460 #459 #446 #447 #457 #448 #444; setup requires it 2026-10-02 09:44:21 +02:00
Niko1221 82d7e82926 Merge branch 'f-135-small' into rel-0.1.35 2026-10-02 09:41:57 +02:00
Niko1221 772c9330e9 Merge branch 'f-135-eng' into rel-0.1.35 2026-10-02 09:41:57 +02:00
Niko1221 6f8536aed1 docs: the Coder is weaker outside code, Chinese/CJK text included; the unpruned sizes for that (#438)
README and docs/MODELS.md said only "Weaker outside coding". #438 measured Chinese answers wrong or looping on the
Coder where English was fine; both now say plainly that this includes Chinese and other CJK text and point to the
sizes that keep every expert (Q2_0, IQ2_XS, IQ3_S).
2026-10-02 09:39:14 +02:00
Niko1221 6cc16bee09 setup (--gguf-dir): GGUFs Strata cannot run are named as such, with the --family/--model to use (#444)
With Unsloth's UD-IQ3_XXS or UD-Q2_K_XL in the folder, setup either said only "has no IQ3_XXS model file"
(--family unsloth --model IQ3_XXS) or took the UD-IQ3_XXS file for the GSQ-RCO IQ3_XXS by the size in its name.
Now:
- a chosen shard whose name is a quantization setup has no size for (UD-IQ3_XXS, UD-Q2_K_XL, a K-quant) stops
  with "<file> is UD-IQ3_XXS, a GGUF Strata cannot run" and what Strata runs: ISTA-DASLab's GSQ-RCO files (and
  Swift 1.5's and the Coder's) and Unsloth's UD-Q4_K_XL only;
- a folder without the chosen file but with other GGUFs lists the ones Strata cannot use and the --family/--model
  of the ones it can (an empty folder keeps check_shards' "missing");
- a size the family does not have names the families that do (--family qwen --model IQ3_XXS), and with
  --gguf-dir also what Strata runs.
2026-10-02 09:38:48 +02:00
Niko1221 95f2860231 /metrics: each request's speculative draft counts, and their sums in the totals (#457)
The server already read drafts_offered / drafts_accepted from the engine's DONE line but only returned them in a
response's timings (draft_n / draft_n_accepted). Each /metrics request entry now carries both (null when the
engine did not report them) and the totals keep their running sums since the server started, so a dashboard such
as llm-visuals can show the acceptance. Additive: no existing field changes.
2026-10-02 09:36:41 +02:00
Niko1221 22ee8d0e9a serve, setup: name the small split card that caps the prompt chunk; setup recommends the big card alone (#448)
- engine (layer splits only): when a stage's cache cannot fund the chunk CUDA0 alone would choose, a WARNING
  names it (CUDA index, name, cache slots, the slots the chunk borrows) and points at the helper cache
  (--expert-cache-device1, docs/SECOND_GPU.md).  The chunk choice itself is unchanged: fits() is split into
  fits_one() with the same tests; one GPU never reaches the new log.
- setup: a pair whose second card is under 12 GB beside a first card of 12 GB or more is still offered, but
  the first card alone is recommended (the default with --yes and Enter; offer_together asks "n" by default),
  with a note.  A named --gpus pair is kept.  Equal pairs (2x 8/16/24 GB) are unchanged.
- The automatic drop of the small card from an auto split is not done: stages load their weights before the
  boundary search and the exact prompt-buffer bytes need the sessions made after it.
2026-10-02 09:35:59 +02:00
Niko1221 0e4c3e8727 calibrate: the engine's arguments come from the server's engine_args; a failed tuning says why (#447)
tools/calibrate.py read any "gpu" list as a layer split and added --layer-split auto, so a one-card config with a
helper card for the expert tier ("gpu": [0] + --expert-cache-device1) was refused by the engine before it read a
weight, a "0,2" string never got its split, and split_skip_if_fits was ignored. The tuner now builds the arguments
with serve.server.engine_args, the same as a normal start.

When the tuning fails, setup prints the engine's own last error line from the log (only lines written during the
tuning), and says the failure again where it is not lost: before --calibrate starts the model, and in the
"All set." summary of an install that tuned.
2026-10-02 09:35:26 +02:00
Niko1221 cd49a498c8 setup (--backend hip): a system ROCm without the HIP development files falls back to the wheels (#446)
rocm_root took /opt/rocm when bin/hipcc and lib/libhipblas.so* were there and the version was 7.0 or newer. A
runtime-only ROCm 7 (no -dev packages) has both, and the engine's cmake configure then failed on the missing
hip-lang package. The system ROCm now also needs lib/cmake/hip-lang/hip-lang-config.cmake (lib, lib64 or the
multiarch lib folder, where CMake looks) and include/hip/hip_runtime.h; without them setup warns, naming what is
missing, and takes the TheRock wheels path, as it already does for a ROCm older than 7.0.
2026-10-02 09:33:17 +02:00
Niko1221 ce2d83b62d setup: an earlier config that does not parse is skipped, not a crash; configs are written atomically (#459)
A fresh copy of Strata set itself up like the newest strata-*.json of another Strata folder and crashed with
JSONDecodeError when that file was empty or cut off. previous_config now takes the newest one that parses as a JSON
object; each one that does not gets a one-line warning naming it, and with none the copy is set up as a fresh
install. Every place setup writes a run config now goes through write_config (a .tmp file, then os.replace), so a
setup stopped half-way no longer leaves an empty config behind.
2026-10-02 09:31:55 +02:00
Niko1221 42bc3dd9ad engine: the low-RAM resident mode engages on Windows 32 GB PCs (#467)
- Windows: the process working set is trimmed before the resident complement's RAM reading, so the experts
  the GPU cache pre-fill touched through the mapping count as available again (standby list); one line
  prints the reading before and after.
- --resident-experts (soft): when the whole complement does not fit, a second try keeps what fits, by
  profile rank, through the #403 budget path (RAM room minus the 256 MiB margin) instead of falling back
  to mmap entirely; a WARNING names it.  Same file bytes either way, so the answers are unchanged.
- Only the resident mode calls pin_cache_complement: the default and mmap paths are untouched.
2026-10-02 09:31:35 +02:00
Niko1221 a2edcb12f9 Windows AMD: the HIP runtime next to strata.exe, not an AMD driver's System32 copy (#468, #461)
Windows looks for an imported DLL in the exe's folder, then System32, then PATH (where the zip's rocm/bin is): a
driver that installs its own amdhip64_7.dll in System32 won, and the bundled rocBLAS/hipBLAS ran on that runtime - an
access violation on a W7900, hipErrorInvalidDeviceFunction on a 7900 XTX, on the first prompt. setup copies
amdhip64_7.dll and amd_comgr.dll from engine/rocm/bin next to the exes after the unzip, before the device probe and
on every start (so a 0.1.34 install is fixed too); package_windows.py ships them there; the engine and strata-device
log which runtime they loaded. rocBLAS/hipBLASLt stay in rocm/bin (their kernel libraries and .kpack are found from
there).
2026-10-02 09:31:18 +02:00
Niko1221 8896607268 serve: "messages" sent as a JSON string is decoded; a non-object entry is a 400 (#460)
openai_to_messages and anthropic_to_messages iterated req["messages"] directly, so a client that double-encodes
the array as a JSON string had its characters iterated and the request crashed on m.get. The new _object_list
decodes such a string and requires a list of objects; anything else raises ValueError("messages must be a list
of objects"), which the server answers with a 400. A message's tool_calls get the same guard.
2026-10-02 09:30:24 +02:00
Niko1221 eb8bb8bcd9 README, MODELS: the AMD baseline - RX 9070 XT + Ryzen 9 3900X + 47 GB RAM (Q2_0, IQ2_XS, the Coder)
Measured with setup's own install and the README's speed method on the AMD box.
2026-10-02 03:08:05 +02:00
Niko1221 43685766ae docs: byte-identical server repeats also need --pcie-frac 0 (#410, measured) 2026-10-02 00:59:21 +02:00
Niko1221 3997ca29fd docs (AI setup): AMD on Windows - the ready-made engine, the device check before the download 2026-10-02 00:55:01 +02:00
Niko1221 1867a8d6a1 README: pictures on AMD (Linux through the CPU, not on Windows yet) 2026-10-02 00:31:47 +02:00
Niko1221 dd23aa676c docs (AMD): Windows ships in 0.1.34 2026-10-02 00:30:46 +02:00
Niko1221 348ac7ef0e docs (AMD): the router's decode gain as measured by a user too (+1-4% over A/B/A/B), WMMA +29-34% with TheRock 7.10 (#432, jkuepker) 2026-10-02 00:30:25 +02:00
Niko1221 d5871c1af1 prefill: an MMQ product without a tile on some GPU takes the non-MMQ path instead of aborting (#420, qni-live)
llama.cpp's tile choice aborts the process ("J_best=0") when no tile size of a type fits the card's shared memory.
The plan now asks the same question for every visible GPU (the experts' gate+up and down shapes, the draft layer's
Q8_0 projections) and keeps a product with no tile on the existing FP16 path, saying which type, card and limits.
Where every product fits (all cards tested here), the plan is the same as before.
2026-10-02 00:28:48 +02:00
Niko1221 af53393b94 setup: UD-Q4_K_XL on AMD is asked before the download (#429); a resumed download needs room only for the rest (#425)
#429 (jkuepker): the HIP engine has no prompt kernels for its Q4_K/Q5_K experts and it has not been run on AMD -
--check says so, and setup asks (--model UD-Q4_K_XL --yes tries it). #425 (jctaborda): the finished shards and the
.part files already on the disk count against the download's size.
2026-10-02 00:27:13 +02:00
Niko1221 77668b2e3c serve: a request is cancelled within a second of its client hanging up (#430, #431, jkuepker)
A non-streamed request wrote nothing until its end, and a streamed one only a keep-alive per prompt chunk (the
first write after a hang-up usually still succeeds), so a dropped request kept the engine reading the whole prompt
or generating to max_tokens while the next request waited. A watcher per request checks the client's socket for EOF
every 0.5 s and cancels; the engine then stops at its next prompt chunk.
2026-10-02 00:24:20 +02:00
Niko1221 14dbd1be5f Engine 0.1.34: AMD on Windows (a ready-made HIP engine, #247 #325), an MCP server so an AI assistant can set Strata up (#133), a shorter README; setup requires it 2026-10-02 00:13:57 +02:00
Niko1221 3629d22c76 Merge branch 'f-133-docs' into rel-0.1.34
# Conflicts:
#	README.md
#	docs/AMD_HIP.md
#	setup.py
2026-10-02 00:09:58 +02:00
Niko1221 524b091daf Merge branch 'f-133-mcp' into rel-0.1.34 2026-10-02 00:09:12 +02:00
Niko1221 6edbe9d6ed Merge branch 'f-133-winamd' into rel-0.1.34
# Conflicts:
#	serve/test_server.py
2026-10-02 00:09:12 +02:00
Niko1221 8554b6bc53 hip_compat: cudaDevAttrComputeCapabilityMinor (#371's check, the HIP build) 2026-10-01 22:23:10 +02:00
Niko1221 30a0f3269a Engine 0.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; setup requires it 2026-10-01 21:17:17 +02:00
Niko1221 decfc96c98 Merge branch 'f-133-choice' into rel-0.1.33 2026-10-01 21:12:04 +02:00
Niko1221 95ade2d5ee docs: the RAM rule recommends 128K, the low-RAM mode on several GPUs, --resident-budget-gib / --kv-streaming (#406 #403 #364 #384) 2026-10-01 21:03:56 +02:00
Niko1221 f0ee8ce54f setup: recommend, never force - longer contexts, more GPUs, a bigger RAM budget are kept with their risk (#406 #403 #364 #384)
The recommended answers stay the defaults, and --yes / Enter write the same config as before
(tools/test_setup_golden.py, five PCs x five sizes).  What a user asks for beyond the recommendation is kept and
its risk said:

- #406: the RAM rule for IQ3_XXS / IQ3_S (experts + KV + 24 GB) is part of the recommended context (the smaller of
  it and the GPU's rule), no longer a cap: an explicit --context, a pick in the list or the earlier install's
  context is kept with a note ("256K with IQ3_S needs ~78 GB of RAM by setup's estimate; this PC has 64. Kept as
  you chose ..."), and the list marks the sizes past it.  The low-RAM mode keeps its KV cache in VRAM, so there
  the context does not count against RAM.
- #364 #384: the low-RAM mode recommends one GPU (the resident variant has no layer split) and asks: 2 = all the
  chosen GPUs with the experts read through the OS file cache (--mmap-experts, layer_split auto; faster in both
  reports, RAM can fill up during long prompts).  An explicit --gpus (or the earlier install's cards) is kept;
  --yes keeps one GPU.  The GPUs' share counts every card.  The one-GPU text no longer sends users to --low-ram off.
  At start, --gpus on a --resident-experts config (and offer_together's "y", whose default is now n for such a
  config) switches it to --mmap-experts with a note instead of an engine refusal.
- S4: --resident-budget-gib N for UD-Q4_K_XL (default: the RAM less 24 GB, as before); more is kept with a note,
  and a given N is not lowered for KV streaming.
- confirm_risk(msg, explicit, yes): one helper for the stops a user may take on - asked with default n, and with
  --yes an explicit flag is the consent (--yes alone keeps the stop): UD-Q4_K_XL under 48 GB (--model), the RAM
  floor (--model), the paging question (--model, besides --low-ram off), a card under 8 GB named with --gpus.
- --kv-streaming on|off|auto (auto: the RAM test, as before); on past the RAM test with a note, never with k8v4 or
  under WSL.

The refusals that stay are the impossible ones: rope none past 262144, disk space, engine version, driver < 580,
no AVX2, a size outside the family.
2026-10-01 21:03:56 +02:00
Niko1221 7349d9112b engine: --resident-experts with a layer split runs as --mmap-experts with a warning (#364 #384)
The low-RAM mode's resident variant has no layer split yet, and the engine refused the pair.  When the residency
comes only from setup's soft --resident-experts (no --resident-cpu-experts, no --resident-budget-gib, no remote
caches), it now prints a WARNING - the experts the GPUs do not hold are read through the OS file cache and RAM may
fill up during long prompts - and continues exactly as --mmap-experts: the resident flags are cleared before
anything reads them, so the source, the placement and the per-stage caches are the mapped mode's (#364's hand-edited
config).  An explicit --resident-cpu-experts, a budget, or remote caches are still refused.

The explicit --expert-cache reduced under a layer split is now a WARNING that names --vram-reserve-mib.
2026-10-01 21:00:49 +02:00
Niko1221 152f7198b9 engine: a RAM budget clamped to the free RAM no longer fails its own safety check (#403)
FileExpertSource::pin_cache_complement read the available RAM twice: the budget was clamped to the first reading
(less the 4 GiB headroom) and the resident-memory check then failed it on a second reading a few MB lower, so a
64 GB PC with setup's --resident-budget-gib 40 could exit at start.  With a budget, the check now reuses the reading
the budget was sized from, and a clamped budget stops 256 MiB under the room.  An unclamped budget holds the same
experts as before (the lend region is off with a budget, so `budget` there is unused).

If the budget still cannot be kept (no RAM reading, an allocation that fails), generate.cpp warns and serves every
expert the GPU does not hold from the files through the OS file cache (--mmap-experts) instead of exiting; the
complement is left unbuilt on every failure path.
2026-10-01 21:00:48 +02:00
Niko1221 41e9ecb016 docs: byte-identical repeats through the server also need --prompt-cache 0 --adapt-swaps 0 (#410) 2026-10-01 20:53:18 +02:00
Niko1221 18a0ec2d47 tests: golden --yes / Enter configs on five PCs (#403 #406 #364 #384)
setup.main() with --yes, and with Enter for every question, on 64 GB + 32 GB, 32 GB + 2x24 GB, 47 GB + 2x16 GB,
96 GB + 16 GB and 128 GB + 24 GB, for the recommended size, Q2_0, IQ3_XXS, IQ3_S and UD-Q4_K_XL: the run config
recorded by this setup (tools/test_setup_golden.json), so the choices that follow cannot change the default.
2026-10-01 20:52:24 +02:00
sergiywith 17ebb94433 gr_parity: compare the split read (STRATA_GR_V3=1) within float rounding
With STRATA_GR_V3=1 "fused GR multi max-T=8 LDS launch and changing graph replay" failed: the test compares the
multi-token read with the single-token kernel bit for bit, and v3 sums `down` in another order (row group x stream
blocks). It also compared `lo`, the default kernels' workspace between down and up, which v3 keeps in shared
memory and never writes. Under v3 the test now checks R_out, rs, inject and mixed within 2e-6 of the largest
reference value and leaves `lo` out; without it, bit for bit as before. Passes both ways.
2026-10-01 20:51:59 +02:00
Niko1221 e878547256 fused_gr: STRATA_GR_V3=1 keeps the default read where it needs the two-half split (Turing; #375, kenh0u)
Opt-in path only; the default read is unchanged.
2026-10-01 20:51:54 +02:00
Niko1221 950c1199dd expert cache: --expert-cache-per-layer starts on native packs (#369, dag08)
open_sized seeded every layer's cursor at slot 0, so all layers wrote the same slots and the startup check failed;
the profile fill ended at the first full layer. Now each layer starts in its own range, a full layer skips only
its own pairs, and per-layer mode keeps slots of the largest blob (its ranges ignore the rank a sized slot was cut
for). The default (shared) cache is unchanged.
2026-10-01 20:51:16 +02:00
Niko1221 37fe5b5328 serve: the image encoder on its own GPU, "cuda_device" in the vision section (#408, Efs-O)
Opt-in; without it the encoder gets the engine's environment as before.
2026-10-01 20:49:41 +02:00
Niko1221 6aa51c24d8 qsa_prompt_attn: the tensor-core kernels need sm_75 - a V100 (sm_70) keeps the old kernel (#371)
The host check compared the major version only (>= 7) while the device code traps below sm_75, so an sm_70 card
took the m16n8k8 kernel and hit __trap(). sm_75 and newer: the same choice as before.
2026-10-01 20:47:40 +02:00
Niko1221 0c8b8dc4d3 tests: find_nvcc's test ignores a real CUDA toolkit on the host (#414) 2026-10-01 20:46:32 +02:00
Bortlesboat 36beee903b setup: stop on EOF instead of accepting prompt defaults 2026-10-01 20:46:19 +02:00
Niko1221 318d74d9d2 serve: a template refusal answers 400 (#365); the temporary image goes when the encoder's pipe fails (#352) 2026-10-01 20:46:18 +02:00
Niko1221 082d59640f docs (AMD): Windows - the ready-made HIP engine, what it needs, what is validated, how to report 2026-10-01 20:34:37 +02:00
26cc76347a setup: AMD on Windows - the ready-made HIP engine, chosen by itself without a usable NVIDIA card
START-HERE.bat on a Windows PC whose AMD Radeon is the card Strata can use now
sets it up like an NVIDIA one: no compiler, no ROCm install, only the AMD driver.

- Detection without any AMD software: the display adapters present
  (Win32_VideoController: name + PCI id) with each one's 64-bit VRAM size from
  the display-class registry (readable without admin; #247/#325's source),
  the arch by PCI device id, else by name. An integrated or older Radeon is
  listed and refused, as on Linux. The NVIDIA-or-AMD choice is the Linux one:
  AMD by itself when no NVIDIA card can be used, asked when both can.
- The engine: strata-windows-x64-hip.zip from the release (this version's,
  then the latest; --prebuilt for a local folder), unpacked into engine/ and
  checked for this card's arch and version >= 0.1.33; an older one is
  replaced on a later start like the NVIDIA zip. Without a published zip setup
  says how to make one (tools\hip\build_windows.bat + --prebuilt).
- Before the model download, the engine's own strata-device --list-devices
  (with its rocm/bin on PATH) must see the card - otherwise it stops and
  points to the AMD driver - and gives the card's HIP ordinal (#325: an
  integrated Radeon is HIP's device 0). From then on setup lists the AMD
  cards in HIP's order, so "gpu" in the config and --gpu N are HIP numbers.
- The config: backend hip, lib_dirs = engine\rocm\bin, the hipBLASLt table
  for the zip's hipBLASLt version (BUILD.json). Images on the CPU and
  several AMD cards (--gpus) stay Linux-only for now (said, not silent).
- Texts: setup's description and step 1 name NVIDIA or AMD; START-HERE.bat
  needs "an NVIDIA or AMD graphics driver"; the "AMD backend runs on Linux
  only" stop is gone.
- An installed HIP engine's BUILD.json ("gfx1201" archs) is no longer read as
  an NVIDIA one by get_prebuilt.

Linux is unchanged (KFD detection, TheRock wheels, compiled here).
Tests: tools/test_setup_amd.py WindowsDetection (mocked WMI/registry rows,
the list-devices parser on this PC's real output, the iGPU-first ordinal
match, the driver-missing stop, the zip install/refusals, the dispatch);
test_setup_choices' Linux CPU-encoder case pins WIN=False.

Co-authored-by: Dimitri Vasdekis <[email protected]>
Co-authored-by: isshin <[email protected]>
2026-10-01 20:34:03 +02:00
Niko1221 b8349a8144 tools/hip: build_windows.bat and package_windows.py - the ready-made Windows HIP engine zip
build_windows.bat builds strata.exe + strata-device.exe (and with `tests` every
HIP test) for gfx1100;gfx1101;gfx1102;gfx1200;gfx1201;gfx1030 with no admin
rights and no AMD GPU: ROCm 10.2.0a20260930 from AMD's TheRock wheels
(whl-next: rocm[libraries,devel,device-<arch>...], the version #325 ran on an
RX 9070 XT) in a private venv (.rocm-win, ~7 GB), ROCm's clang as host and HIP
compiler with MSVC's headers/linker from vcvars, STRATA_PORTABLE (AVX2 ggml).

package_windows.py makes dist/strata-windows-x64-hip.zip (524 MiB, 810 MiB
unpacked): strata.exe (61 MiB), strata-device.exe, BUILD.json (backend hip,
archs, ROCm and hipBLASLt versions, lib_dirs rocm/bin), rocm/bin with the DLLs
the programs import followed through ROCm (amdhip64_7, hipblas, rocblas,
rocsolver, libhipblaslt, libtensilelite-host, origami, rocm_kpack) + amd_comgr
(loaded by name) + the MSVC runtime, rocBLAS/hipBLASLt kernels for these archs,
rocBLAS's kpack device code (found at ../.kpack from rocblas.dll), licenses and
a NOTICE. The user needs only the AMD driver (the HIP runtime uses its
user-mode components).

Checked on this PC's Ryzen iGPU (gfx1036, a test build of the same tree): with
only the zip's rocm/bin on PATH, strata-device lists the card and
hipblasGemmEx BF16 -> FP32 (T=100) matches the CPU (max err 4.7e-7); ctest
52 of 56 pass, the 4 failures the same as #325's RX 9070 XT run (hip_handoff,
and ple_parity / expert_parity / pool_test without the pack fixture).
2026-10-01 20:33:37 +02:00
36c11637f5 HIP on Windows: the hipBLAS sticky error (Windows only) and the mapped embedding fallback (#247, #325)
- prefill gemm: on Windows hipBLAS can return success with the right BF16/FP16
  product and still leave hipErrorInvalidValue set (#247 on gfx1201: hc up once
  T >= 96, the router), which the next kernel check turns into an exit. That
  one stale error is cleared after a GEMM that succeeded; any other error
  still stops the engine. Only under __HIPCC__ && _WIN32: on Linux a stale
  hipErrorInvalidValue is a real error and keeps being reported (our review
  of #247). CUDA compiles a ((void) 0).
- native embedding (HIP builds): when the runtime refuses a device alias for
  the mapped table, the table goes into VRAM (as when it cannot be pinned)
  instead of failing the start; the pinned copy is freed so the destructor's
  one-of-two rule holds (#325's version leaked it and freed dev_owned_ twice).
  CUDA keeps the old code path.
- tests/hip/mapped_alias (#325): what a hipHostGetDevicePointer alias is on
  this stack. On Windows it is the host pointer itself, and a D2D copy into it
  does not land - hip_handoff's timeout there; kernels reading it work (the
  engine serves on Windows). Rewritten to read the kernel's output back with
  hipMemcpy (the PR's version dereferenced device memory on the host) and to
  try the kernel read in both cases. Diagnostic: exits 0, 77 without a device.

Co-authored-by: Dimitri Vasdekis <[email protected]>
Co-authored-by: isshin <[email protected]>
2026-10-01 20:20:09 +02:00
Niko1221andDimitri Vasdekis 51fdc43c80 strata-device --list-devices, and HIP's numbering for AMD cards on Windows (#325)
On Windows the AMD cards are found in the display-adapter order, but
HIP_VISIBLE_DEVICES counts the devices the HIP runtime enumerates, and an
integrated Radeon takes HIP's device 0 and pushes the discrete card to 1 - so
the engine was pointed at the iGPU, which the arch check then refuses (found
by dvasdekis on an RX 9070 XT + Ryzen iGPU).

- device_count() and device_summary(): enumerate without device_info's arch
  check (the cards this binary has no code for are part of the answer).
- strata-device --list-devices: every device the runtime enumerates, as
  "device N: <name>", then "arch gfx1201, 15.9 GiB, wave32" (CUDA: compute
  capability and VRAM), then "cannot run: <why>" for a card this binary
  cannot run. setup.py parses it (next commit).
- server.py hip_visible(): a one-card config's "hip_ordinal" (written by
  #325's branch) wins over "gpu"; a layer split keeps its list.

Checked on this PC's own Ryzen iGPU through TheRock's Windows HIP runtime:
"device 0: AMD Radeon(TM) Graphics / arch gfx1036, 28.1 GiB, wave32 / cannot
run: ... not an architecture this Strata engine was compiled for (...)".

Co-authored-by: Dimitri Vasdekis <[email protected]>
2026-10-01 20:19:56 +02:00
isshinandDimitri Vasdekis eecb32abf6 HIP on Windows: the build with ROCm's clang as the host compiler (#247, #325)
CMake refuses to mix cl.exe with Clang for HIP, so on Windows ROCm's clang++
compiles the host code too (targeting the MSVC ABI, with MSVC's headers and
linker from vcvars). The CUDA->HIP shim is force-included with /FI when the
host front end is MSVC-style (cl / clang-cl) and with -include otherwise, its
path in forward slashes; CMAKE_HIP_ARCHITECTURES may also be space-separated.
On Windows the shim keeps <windows.h>'s min/max macros out (NOMINMAX,
WIN32_LEAN_AND_MEAN).

Taken from jagsan-cyber's #247 as ported by dvasdekis in #325 (2373c75); the
arch lists and the arch check stay 0.1.32's. Built on this PC with ROCm
10.2.0a20260930 (TheRock wheels, the version #325 ran on an RX 9070 XT) for
gfx1100;gfx1101;gfx1102;gfx1200;gfx1201;gfx1030: strata.exe and all 253
targets with the tests. CUDA builds do not read these files.

Co-authored-by: Dimitri Vasdekis <[email protected]>
2026-10-01 20:19:37 +02:00
Niko1221 885e52052b tools/strata_mcp.py: an MCP server so an AI assistant can install, start, check and stop Strata (#133)
Claude Code, Claude Desktop, Cursor, VS Code, Codex ... add it over stdio (JSON-RPC 2.0, MCP 2025-06-18, also
2025-03-26 / 2024-11-05); standard library only, so it runs before setup made .venv. Eight tools: strata_status
(running server via /health, /v1/status, /status, /metrics; installed configs, engine, data folder; hardware and
setup's recommendation), strata_models, strata_install (setup --yes --no-start in the background, plan first and
confirm=true to start, progress / cancel on later calls), strata_start (the run script's command, waits for
/health), strata_stop (POST /unload, then ends the server it started), strata_logs, strata_benchmark,
strata_connect_info.

Every argument is checked against a schema and setup's own tables; the only path (data_dir) must be the data
folder, inside the Strata folder, or a new Strata* folder outside system folders. Processes are tracked by pid and
start time in .strata-mcp/, so only processes it started are ever ended; a Strata started elsewhere is only
unloaded. docs/MCP_SERVER.md has every client's config; docs/DETAILS.md links it. Tests use a fake setup and
server (Windows and Linux).
2026-10-01 20:18:07 +02:00
Niko1221 f938412499 docs: a short README for regular people; the details move to INSTALL, MODELS, TROUBLESHOOTING, HOW_IT_WORKS
README.md (279 -> ~180 lines): what it is, speed on an NVIDIA and an AMD baseline PC side by side (the AMD
numbers are TODO-AMD placeholders until the RX 9070 XT baseline is measured), what you need with NVIDIA and AMD
as equals, install by asking an AI assistant (docs/AI_SETUP.md, MCP server) or by hand, pick-by-RAM table,
using it, the four common problems, how it works with the two diagrams, Buy Me a Coffee button below the speed
table and at the end. Its anchors other pages use (#which-model-should-i-pick, #install, #using-it,
#something-went-wrong, #how-does-it-work) are kept.

Everything the README had moves, nothing dropped: docs/INSTALL.md (requirements, Windows, Linux, AMD cards,
several cards, Docker, updating, where things are stored, setup's questions incl. rope scaling / images /
speed projection, calibrate, all options), docs/MODELS.md (speed per size, sizes, will it fit, low-RAM, Coder,
Swift 1.5, Unsloth UD-Q4_K_XL, Orca), docs/TROUBLESHOOTING.md (the README's problems plus AMD ones),
docs/HOW_IT_WORKS.md (the longer explanation, credits, license). New docs/AI_SETUP.md (instructions for an AI
agent installing Strata for a user) and AGENTS.md. .github/FUNDING.yml (buy_me_a_coffee: strataengine).
AMD_HIP.md loses "Experimental" in its title and intro; DETAILS.md lists the AMD cards and points to the new pages.
2026-10-01 20:08:58 +02:00
Niko1221 0ad23b584d setup: the AMD backend is no longer called experimental in its messages
--backend's help, the NVIDIA-or-AMD question and the GPU line drop "experimental" (the Unsloth model, the
speed projection and the Pascal/Volta build keep it); the AMD choice says images run on the CPU (0.1.32)
instead of "no images". setup.sh's header comment likewise.
2026-10-01 20:08:58 +02:00
54 changed files with 7483 additions and 429 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
buy_me_a_coffee: strataengine
+2
View File
@@ -37,6 +37,7 @@ td.log
/strata-*.json
/strata-*.log
/build-strata.bat
/.strata-mcp/
# image encoder (setup.py --vision): build output and the generated build script
build-vision*/
@@ -49,3 +50,4 @@ build-vision.bat
*.done
/third_party/*.zip
/.rocm-win/
+23
View File
@@ -0,0 +1,23 @@
# AGENTS.md
Strata runs the Qwen3.8-Flash-Next mixture-of-experts model (and its Coder, Swift 1.5 and Unsloth variants) on a
normal PC: one NVIDIA or AMD graphics card plus system RAM, on Windows or Linux. It has a C++/CUDA/HIP engine
(`src/`, `include/`), a Python server with an OpenAI- and Anthropic-compatible API and a web app (`serve/`), and a
one-click installer (`setup.py`, started by `START-HERE.bat` / `setup.sh`).
## Installing Strata for a user
Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**: check the PC, pick the model by RAM, run setup non-interactively,
start and verify the server, and connect the user's apps. Never expose the server beyond `127.0.0.1` without
`--api-key`. As an alternative to shell commands, Strata's MCP server ([docs/MCP_SERVER.md](docs/MCP_SERVER.md))
offers the same steps as tools.
## Working on the code
- How the engine works, every measured number, the API and all settings: [docs/DETAILS.md](docs/DETAILS.md) and
the [paper](docs/paper/Strata-Paper.pdf).
- AMD (HIP) build and validation: [docs/AMD_HIP.md](docs/AMD_HIP.md); multi-GPU: [docs/MULTI_GPU.md](docs/MULTI_GPU.md).
- Setup's own tests run without a GPU or downloads: `python tools/test_setup_<name>.py` (for example
`tools/test_setup_amd.py`, `tools/test_setup_choices.py`).
- Keep the docs' style: plain words, measured numbers with what they were measured on, no claims without a
measurement.
+15 -1
View File
@@ -8,7 +8,7 @@
# below refuses anything older: silently building for a pre-Ampere arch would produce a binary that runs
# and is wrong about its own performance, which is the worst of both outcomes.
cmake_minimum_required(VERSION 3.24)
project(strata VERSION 0.1.32 LANGUAGES CXX)
project(strata VERSION 0.1.35 LANGUAGES CXX)
add_compile_definitions(STRATA_VERSION="${PROJECT_VERSION}")
# issue #31: release builds keep a PDB beside the exe (not shipped in it), so a minidump from a stalled engine can
# be read back to the exact source line
@@ -362,6 +362,11 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_link_libraries(native_dense_ple_key_test PRIVATE strata_engine)
add_test(NAME native_dense_ple_key_test COMMAND native_dense_ple_key_test)
set_tests_properties(native_dense_ple_key_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
# #369: per-layer admission on a sized (native pack) cache starts each layer in its own range
add_executable(expert_cache_per_layer_test tests/core/expert_cache_per_layer_test.cpp)
target_link_libraries(expert_cache_per_layer_test PRIVATE strata_engine)
add_test(NAME expert_cache_per_layer_test COMMAND expert_cache_per_layer_test)
set_tests_properties(expert_cache_per_layer_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
endif()
if(STRATA_BUILD_CONVERSATION_TESTS)
add_executable(conversation_snapshot_test src/core/conversation_snapshot_test.cpp)
@@ -421,6 +426,15 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
add_test(NAME hip_handoff COMMAND hip_handoff)
list(APPEND _strata_hip_tests hip_handoff)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/hip/mapped_alias.cpp")
# #325: reports what a hipHostGetDevicePointer alias is on this stack (on Windows: the host pointer, and a
# D2D copy into it does not land - hip_handoff's timeout there). A diagnosis: exits 0 whenever it ran.
add_executable(hip_mapped_alias tests/hip/mapped_alias.cpp)
target_link_libraries(hip_mapped_alias PRIVATE strata_kernels strata_hip_runtime)
add_test(NAME hip_mapped_alias COMMAND hip_mapped_alias)
set_tests_properties(hip_mapped_alias PROPERTIES SKIP_RETURN_CODE 77)
list(APPEND _strata_hip_tests hip_mapped_alias)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/hip/native_qsa_score.cpp")
add_executable(hip_native_qsa_score tests/hip/native_qsa_score.cpp)
target_link_libraries(hip_native_qsa_score PRIVATE strata_kernels strata_hip_runtime)
+121 -216
View File
@@ -1,279 +1,184 @@
<h1 align="center">Strata</h1>
<p align="center"><b>Run a 125-billion-parameter AI model on a normal gaming PC</b><br>
one NVIDIA card (12-24 GB) + 64 GB of RAM · Windows or Linux · one click to install</p>
<p align="center"><b>Run a 125-billion-parameter AI model on your own gaming PC</b><br>
NVIDIA or AMD graphics card (12 GB or more) · Windows or Linux · free and open source</p>
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
<sub>A voxel pagoda garden, 1 shot prompt running on an RTX 5070 with Strata (IQ3_S, 128K context) ·
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">full video (49 s)</a></sub></p>
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** - a large, smart AI model that
normally needs a server - on your own PC. It writes its answers at **60-95 tokens per second** (a token is about ¾
of a word): faster than you can read.
- **Free and open source.**
> **Jump to:** [How fast?](#how-fast-is-it) · [Which model?](#which-model-should-i-pick) · [Install](#install) ·
> [Using it](#using-it) · [Problems?](#something-went-wrong) · [How it works](#how-does-it-work) ·
> [All the details](docs/DETAILS.md)
---
normally needs a server - on a normal PC. It chats, writes code, reads pictures and works with your apps and coding
agents, and nothing leaves your PC.
## How fast is it?
Measured on an RTX 5070 (12 GB), a Ryzen 5 7600 and 64 GB of RAM:
Measured on two ordinary gaming PCs. "Writes answers" is how fast the reply appears in a short chat; "reads your
prompt" is how fast it takes in what you send (a 32K-token document, code or chat history). A token is about ¾ of a
word, so 60 tokens per second is faster than you can read.
| Size | Writes answers (short chat) | Writes answers (128K context) | Reads your prompt |
| --- | ---: | ---: | ---: |
| **Q2_0** | 93 tokens/s | 74 tokens/s | 2,170 tokens/s |
| **IQ2_XS** | 79 tokens/s | 63 tokens/s | 2,090 tokens/s |
| **IQ3_XXS** | 62 tokens/s | 49 tokens/s | 1,750 tokens/s |
| **IQ3_S** | 53 tokens/s | 46 tokens/s | 1,620 tokens/s |
| **Coder** (IQ1_M) | 55 tokens/s | 43 tokens/s | 2,180 tokens/s |
<table>
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
<tr><td>
- **Writes answers** = how fast the reply appears (tokens per second).
- **Reads your prompt** = how fast it takes in what you send (long documents, code, chat history), measured on a
32K-token prompt; a 4K prompt reads at 910-1,580 tokens/s. A 32K prompt takes about 15 seconds with Q2_0.
| Size | Writes answers | Reads your prompt |
| --- | ---: | ---: |
| **Q2_0** | 93 tokens/s | 2,170 tokens/s |
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
| **Coder** | 55 tokens/s | 2,180 tokens/s |
A card with more VRAM is faster, because more of the model fits on the GPU: an RTX 3090 (24 GB) should do roughly
100-140 tokens per second. All measurements, long-context numbers and estimates for other cards are in the
[details](docs/DETAILS.md#speed-measured).
</td><td>
Every PC is different: `START-HERE.bat --calibrate` measures a few engine settings on yours and keeps the fastest
(about 5-10 minutes; on the PC above it made the Coder 7% faster).
| Size | Writes answers | Reads your prompt |
| --- | ---: | ---: |
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
| **Coder** | 44 tokens/s | 1,420 tokens/s |
Measured Strata on your own PC? See [Community benchmark results](docs/COMMUNITY_BENCHMARKS.md)
for a report template and how to share your results in a pull request.
</td></tr>
</table>
**Two or three NVIDIA cards?** Just run `START-HERE.bat`: it lists your cards, says which ones Strata can use, and
asks whether to share the model across them (recommended when two can). An install made on one card asks once at
its next start. Or choose yourself: `START-HERE.bat --gpus 0,2` (both, remembered), `--gpus all`, or `--gpu 0` (one
card, this start only). Each card keeps the experts of its own layers, and prompts flow through the cards in a
pipeline: on an RTX 5080 + RTX 3090 prompts were read 18-20% faster than on the 5080 alone, decoding on par.
Every card must be an RTX 20 series or newer with 8 GB or more. See [docs/MULTI_GPU.md](docs/MULTI_GPU.md).
A card with more VRAM is faster: an RTX 3090 (24 GB) should write roughly 100-140 tokens per second. Long chats,
other cards: [speed of each model](docs/MODELS.md#how-fast-is-each-size), [community results](docs/COMMUNITY_BENCHMARKS.md).
## Which model should I pick?
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
<sub>Strata is free. If it runs well on your PC, a coffee keeps the work on it going.</sub></p>
**The size** (the same model, compressed more or less):
## What you need
| Model | RAM+VRAM Requirements | Speed | Quality |
| --- | ---: | --- | --- |
| **Q2_0** | 37.6 GB | fastest | good |
| **IQ2_XS** | 39.2 GB | fast | better (**recommended**) |
| **IQ3_XXS** | 47.0 GB | slower | great |
| **IQ3_S** | 54.8 GB | slowest | best: matches the full model on the published tests (original model only) |
| | |
| --- | --- |
| **Graphics card** | **NVIDIA** GeForce RTX 20, 30, 40 or 50 series, or **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 or RX 6800 / 6900 series - with **12 GB of VRAM or more** |
| **RAM** | 32 GB or more - how much decides [which model](#which-model-should-i-pick) fits; 64 GB runs every size |
| **Disk** | about 80 GB free, on an SSD if you can (the first start is much faster) |
| **System** | Windows 10 / 11 or Linux, and a current graphics driver from NVIDIA or AMD |
**Will it fit?** Shard 1 is the part of the model that gets loaded when it starts: its experts go into your **RAM**,
the rest onto your graphics card (the second shard, a 29 GB lookup table, stays on the SSD). So it fits when your
**RAM is at least shard 1 + about 10 GB** for Windows and your other programs. With 64 GB of RAM every size fits
(IQ3_S with little else open); with 48 GB, Q2_0 and IQ2_XS. A bigger graphics card makes it faster, but it doesn't
lower the RAM needed.
**The version:**
- **Qwen3.8-Flash-Next** - the original.
- **[Coder](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF)** - ISTA-DASLab's coding
version: half of the experts removed, keeping the ones that code, tool use and images need (91% of the full model's
SWE-bench Verified score, 99% of LiveCodeBench, by its authors). One size (IQ1_M: its experts stored like IQ3_S):
shard 1 is **29.6 GB**, so it fits a PC with **32 GB of RAM**, runs 262K context on 64 GB, and reads long prompts
the fastest of all. Weaker outside coding.
- **[Swift 1.5](https://huggingface.co/ukisai/Swift-1.5-Qwen3.8-Flash-Next-GSQ-RCO-GGUF)** - a fine-tune by UkisAI
that thinks much shorter before answering, so you get the answer sooner, with about the same quality. Same speed per
token, and about the same RAM as the same size of the original (no IQ3_S). Its own license applies (see its page).
Not sure? Take **IQ2_XS** - or the **Coder** if you mainly write code, or have 32-48 GB of RAM. You can add another
one later with `SETUP.bat` (the same as `START-HERE.bat --setup`; on Linux `./setup.sh --setup`).
For **OrcaRouter's Flash-Next Uncensored IQ3_XXS**, see the [manual compatibility setup](docs/ORCA.md).
It needs an explicit packing conversion and is not an installer menu option.
**Unsloth's 4-bit UD-Q4_K_XL** (experimental) is the fourth version in setup's menu (`--family unsloth`): the closest
to the full model, but a 111 GB download whose 77 GB of experts do not fit in RAM. Strata keeps your RAM minus 24 GB
of them in RAM and reads the rest from the SSD while it answers: 7-8.5 tokens/s on a 64 GB PC with a 12 GB GPU, several
times slower than the sizes above, and long prompts are slow. It needs 48 GB of RAM or more, an NVMe SSD and one
NVIDIA GPU (no images yet). Details and measurements: [UD-Q4_K_XL](docs/UNSLOTH_Q4.md).
An **AMD Radeon RX 7900 XT / XTX, RX 9070 / 9070 XT or Radeon AI PRO R9700 on Linux** works too (experimental; the
RX 7800 XT / 7700 XT and RX 9060 XT were validated by their owners; the RX 6800 / 6900 series, gfx1030, is community-reported):
`./setup.sh --backend hip`, chosen by itself on a PC with no NVIDIA card Strata can use. It installs ROCm without sudo
and compiles the engine (no images yet; several cards with `--gpus`). Details: [AMD HIP](docs/AMD_HIP.md).
Everything else is installed for you. Two or three cards can share the model ([multi-GPU](docs/MULTI_GPU.md)).
The full list: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
## Install
**You need:** an NVIDIA RTX 20, 30, 40 or 50 card with 12 GB of VRAM or more (RTX 20 since 0.1.27), enough RAM for the size you pick (above;
a big GPU makes up for less RAM - the [low-RAM mode](docs/DETAILS.md)),
~80 GB of free disk space (an SSD makes the first start much faster), and Windows 10/11 or Linux. The only thing you
install yourself is a current **NVIDIA driver** ([nvidia.com/drivers](https://www.nvidia.com/drivers) or the NVIDIA
App). Everything else - Python, the engine, the model - is set up for you.
### Let your AI set it up
**Windows**
Use an AI coding assistant (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Paste this into it:
1. [Download this project](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it (or `git clone` it).
2. Double-click **`START-HERE.bat`**.
3. Answer a few questions - or just press Enter each time for the recommended choice:
- **Which model and size?** The original or Swift 1.5, and Q2_0, IQ2_XS, IQ3_XXS or IQ3_S - see [above](#which-model-should-i-pick)
- **How much context?** How much text it can keep in mind at once (it suggests one for your card). 384K and
512K (experimental) extend the model past its trained 262K by rope scaling - the setup turns it on itself (yarn and a
covering factor; `--rope-scaling`/`--rope-scale` override) ([details](docs/DETAILS.md))
- **Images?** Whether it should also read pictures
- **Experimental speed projection?** Off unless you say yes - [read what it does](docs/DETAILS.md#experimental-speed-projection-experimental-off-by-default) first
```text
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
```
Then it downloads everything (the model is ~70 GB, so the first time takes a while - you can stop and it picks up
where it left off) and **starts the model**. Your browser opens the Strata app at `http://127.0.0.1:8080`.
It checks your graphics card, RAM and disk, picks the model that fits, installs it, starts it and tells you how to
connect your apps. AI tools can also install, start and stop Strata themselves through its
[MCP server](docs/MCP_SERVER.md).
### Or do it yourself
[Download Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it (or `git clone` it).
**Windows:** double-click **`START-HERE.bat`**. **Linux:** run **`./setup.sh`** in the Strata folder.
The same steps for NVIDIA and AMD: the installer finds your card and sets up the right engine for it. It asks which
model, which size, how much context (how much text it keeps in mind) and whether it should read pictures - press
Enter each time for the recommended answer. Then it downloads the model (~70 GB; you can stop and it continues where
it left off) and starts it. Your browser opens the Strata app at `http://127.0.0.1:8080`.
> **While the model starts, your PC can be slow or stop responding for 1-3 minutes** (longest the first time): Strata
> loads 35-55 GB into your RAM and locks part of it for the graphics card. That's normal - wait, and don't close the
> window. The window tells you what it is doing.
**Next time**, just double-click `START-HERE.bat` again: it starts right away, nothing is downloaded twice. Close its
window to stop the model.
**Next time**, run `START-HERE.bat` (or `./setup.sh`) again: it starts right away, nothing is downloaded twice. Close
its window to stop the model. Updating, Docker, several cards, where the files go and every option:
[docs/INSTALL.md](docs/INSTALL.md).
**Updating:** download the new version and unzip it anywhere (or `git pull`), then run `START-HERE.bat` in it. The
model files are kept in a `Strata-data` folder next to your Strata folder, so a new copy finds them and sets itself up
the same way - nothing big is downloaded again.
## Which model should I pick?
**Linux:** run `./setup.sh` - same questions, same result.
The installer recommends one for your RAM. The same model comes in sizes that are compressed more or less: smaller
is faster, larger is a bit smarter.
**Docker (Linux):** the same idea, in a container.
| Your RAM | Take | Why |
| --- | --- | --- |
| **32 GB** | **Coder** | it fits 32 GB, and it is made for code (with a 24 GB card, Q2_0 and IQ2_XS run too) |
| **48 GB** | **IQ2_XS** (or Q2_0, the fastest) | the larger sizes do not fit |
| **64 GB** | **IQ2_XS** (recommended), or IQ3_XXS / IQ3_S | every size fits; IQ3_S is the best, and the slowest |
| **96 GB or more** | **IQ3_S**, or Unsloth's 4-bit (experimental) | room for the largest sizes with everything else open |
1. Host: Docker with the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
and a driver **580 or newer** (CUDA 13.0).
2. Build (this compiles the engine into the image, so the container never compiles):
`docker build -t strata .`
`docker build -t strata --build-arg CUDA_ARCHITECTURES=89 .` builds for one card only (faster).
The default covers RTX 30 (86), RTX 40 (89), RTX 50 (120) and A-series (80); a card outside that
set needs a rebuild with its own arch. Add `--build-arg BUILD_VISION=0` to skip the image encoder.
3. Run (the first start downloads the ~70 GB model, then starts; later starts go straight to serving):
`docker run --rm --gpus all -p 8080:8080 --ulimit memlock=-1 -v strata-data:/data strata`
- **[Coder](docs/MODELS.md#coder)** - a coding version with half of the experts removed: 91% of the full model's
SWE-bench Verified score (by its authors), fits 32 GB of RAM. Weaker outside code, including Chinese and other
CJK text (#438): for those, take Q2_0, IQ2_XS or IQ3_S, which keep every expert.
- **[Swift 1.5](docs/MODELS.md#swift-15)** - a fine-tune that thinks much shorter before it answers, so you get the
answer sooner, at about the same quality.
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental) - the closest to the full
model, but most of it is read from the SSD while it answers: 7-8.5 tokens/s on a 64 GB PC.
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** - a manual setup, not in the
installer's menu.
The setup choices are env vars: `-e MODEL=IQ2_XS -e FAMILY=qwen -e CONTEXT=32768 -e VISION=no`
(or `MODEL=Q2_0|IQ3_XXS|IQ3_S`, `FAMILY=swift|coder`; the defaults above are the recommended ones).
`-e VISION=cpu` keeps the image encoder on the CPU. `-e KV=int8|q4_0|k8v4` picks the KV cache
precision; `k8v4` is INT8 K with 4-bit V and keeps its KV in VRAM from 64K up.
Only the model files, the prepared pack, the MTP layer and the install config live in the
`strata-data` volume; the engine is part of the image. Switching between models already on the
volume needs no setup pass: `-e MODEL=Q2_0 -e FAMILY=coder` picks that model's config. Add
`-e REINSTALL=1` only to change settings for a model already set up (context, vision, KV, host,
api_key, LOW_RAM), since those are recorded in its config.
Strata loads 32-62 GB into RAM. `--gpus all` on a host with two usable cards takes both: the
layer split is setup's recommended default ([docs/MULTI_GPU.md](docs/MULTI_GPU.md)), and a volume
set up for one card switches to the pair on its first start there. Pin one card with `-e GPU=0`,
or name them with `-e GPUS=0,2` and where the later card's layers start with `-e LAYER_SPLIT=18`.
A memory limit needs `-e LOW_RAM=on`, which maps the model's experts from the pack instead of
keeping them in RAM: setup.py measures the host's RAM, not the container's limit, so it cannot
see a cap. LOW_RAM runs on one card.
The server listens on `0.0.0.0:8080` by default; set `-e API_KEY=<secret>` before exposing the port
to a network. The image has a `HEALTHCHECK` on `/health`, so `docker ps` shows the container
healthy once the model is loaded, and `GET /v1/status` says what it is running.
Sizes, downloads and what fits where: [docs/MODELS.md](docs/MODELS.md). You can add another model later with
`SETUP.bat` (Linux: `./setup.sh --setup`).
## Using it
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="The Strata app's Monitor tab next to a coding agent"><br>
<sub>The Strata app's <b>Monitor</b> (left) while a coding agent writes the pagoda garden from the video (right)</sub></p>
- **In the browser:** `http://127.0.0.1:8080` - the Strata app (it opens by itself when the model starts): **Chat**, a
live **Monitor** of the model and your GPU/CPU/RAM, and **About** with the settings and addresses.
- **Chat in the terminal:** `.venv\Scripts\python chat.py`
- **Your apps and coding agents:** add it as an "OpenAI-compatible" provider with base URL
**`http://127.0.0.1:8080/v1`**, any API key and any model name. Apps that use Anthropic's API: `http://127.0.0.1:8080/v1/messages`.
- **Thinking:** the model thinks before it answers. Choose **off, low, medium or high** - in the chat page menu, with
`/think low` in `chat.py`, or with your app's "reasoning effort" setting. Off is fastest; high is best for hard questions.
- **Pictures:** in the chat page click **Picture**; in `chat.py` type `/image <path>`; in apps just attach them.
- **From your phone or another PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`, then open the
address the server window prints; see the [details](docs/DETAILS.md#using-it).
- **Experimental speed projection (off by default):** an experimental control vector that setup can turn on; it
changes how the model answers - read [what it does](docs/DETAILS.md#experimental-speed-projection-experimental-off-by-default) first.
- **In the browser:** `http://127.0.0.1:8080` - **Chat**, a live **Monitor** of the model and your GPU/CPU/RAM, and
**About** with the settings and addresses.
- **Your apps and coding agents:** add an "OpenAI-compatible" provider with base URL **`http://127.0.0.1:8080/v1`**,
any API key and any model name. Apps that use Anthropic's API: `http://127.0.0.1:8080/v1/messages` (Claude Code:
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
- **Thinking:** choose **off, low, medium or high** in the chat menu or your app's "reasoning effort". Off is
fastest; high is best for hard questions.
- **Pictures:** say yes to "Images?" in setup, then click **Picture** in the chat, or attach them in your app
(AMD cards: on Linux through the processor, not on Windows yet).
- **From your phone or another PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>` - always with a key.
- **Good to know:** it answers one request at a time. The first message of a chat is read in full (about 1 minute
per 30,000 tokens); follow-ups start in seconds.
**Good to know:** it answers one request at a time. The first message of a chat is read in full (about 1 minute per
30,000 tokens); after that it keeps the conversation and reads only what is new, so follow-ups start in seconds.
### Where things are stored
- **Your chats: only in your browser.** The Chat tab keeps the conversation, its settings and the API key you typed
in the browser's local storage (`strata.*` keys) - not on the server and not in the Strata folder. Pictures are not
kept, only their names. Another browser or a private window starts empty; clearing the site's data deletes them.
- **How the model starts:** `strata-<model>.json` in the Strata folder (context, GPUs, host, API key, ...), written
by setup; next to it `run-<model>.bat` / `.sh`, the log `strata-<model>.log` and, when you use "Use for other
apps too", `strata-<model>.shared-settings.json`.
- **The model files** (`models/`, `packs/`, `mtp/`, 70-120 GB): in **`Strata-data` next to the Strata folder**, or
wherever `--data-dir` put them.
- **Where that data folder is:** `%APPDATA%\Strata\settings.json` on Windows, `~/.config/strata/settings.json` on
Linux ([details](docs/DETAILS.md)).
More: [where your chats are stored](docs/INSTALL.md#where-things-are-stored), [the API](docs/DETAILS.md#using-it).
## Something went wrong?
**My PC froze, or got very slow, the first time Strata started.**
That's normal while it starts, most of all the first time. Strata loads 35-55 GB into your RAM, locks part of it for
the graphics card, and works out how much of the model fits on your GPU. The mouse can freeze for a few minutes. **Wait, and don't close the
window.** The next starts are much faster. Still frozen after 10 minutes? Restart the PC, close other programs
(browsers use a lot of RAM) and try again. If it keeps happening, pick a smaller size (Q2_0 or IQ2_XS).
- **My PC froze the first time Strata started.** Normal while it loads the model: wait, don't close the window.
Still frozen after 10 minutes? Restart the PC, close other programs and try again, or pick a smaller size.
- **It stopped while downloading or installing.** Run `START-HERE.bat` (or `./setup.sh`) again: it continues where
it stopped.
- **It's very slow and the disk light keeps blinking, or "the engine stopped unexpectedly".** Not enough free RAM:
close other programs (browsers use a lot), or pick a smaller size (Q2_0 or IQ2_XS).
- **It says port 8080 is already in use.** Strata is already running - look for its window.
**It stopped while downloading or installing.**
Run `START-HERE.bat` again. It continues where it stopped.
**It says the NVIDIA driver is too old.**
Update it (NVIDIA App or [nvidia.com/drivers](https://www.nvidia.com/drivers)), restart the PC, and run
`START-HERE.bat` again.
**It says port 8080 is already in use.**
Strata is already running. Look for its window.
**It's very slow and the disk light keeps blinking.**
Your PC is out of free RAM. Close other programs, or pick a smaller size (Q2_0 or IQ2_XS).
**An answer stopped with "the engine stopped unexpectedly".**
Usually not enough RAM (on Linux the system then stops the engine). Just send your message again: Strata starts the
engine by itself. If it keeps happening, close other programs or pick a smaller size.
**It says the prompt exceeds the context.**
The conversation is longer than the context you chose. Start a new chat, or run `SETUP.bat` and pick more
context.
**Still stuck?** Look in the [full troubleshooting table](docs/DETAILS.md#troubleshooting), or open an issue and
attach `strata-<model>.log` from the Strata folder.
More problems and their fixes: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Still stuck? Open an
[issue](https://github.com/Niko1221/Strata/issues) and attach `strata-<model>.log` from the Strata folder.
## How does it work?
Models like this one normally run on servers with hundreds of gigabytes of graphics memory. Your graphics card has
12-24 GB. Strata makes it fit by **sharing the work across your whole PC** - the same idea as a kitchen, where the
things you use all the time stay on the counter and the rest waits in the pantry.
12-24 GB. Strata makes it fit by **sharing the work across your whole PC** - like a kitchen, where the things you use
all the time stay on the counter and the rest waits in the pantry.
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
- **The model is a team of 24,576 small specialists ("experts"),** and each word it writes needs only 10 of them.
So it doesn't have to have all of them on the graphics card at once.
- **Your graphics card** does the part of the work needed for every word, and keeps the few thousand experts that
are asked most often. It keeps learning which ones those are while you use it.
- **Your RAM** holds every expert. When a word needs one the card doesn't have, **your processor** works on it -
at the same time as the graphics card, so neither waits for the other.
- **Your SSD** holds a big lookup table; the model only reads a few small rows of it per word.
- **The model is a team of 24,576 small specialists ("experts"),** and each word needs only 10 of them.
- **Your graphics card** keeps the few thousand experts that are asked most often; **your RAM** holds all of them,
and **your processor** works on the rest at the same time. **Your SSD** holds a big lookup table.
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
- **Guess, then check.** A small, fast helper built into the model guesses the next few words, and the big model
checks all the guesses in one go. It keeps the ones it agrees with and writes the next word itself - so one step
often produces several words. The helper only guesses - the big model decides every word - so you get the same
quality answer, 1.6-1.8x sooner.
- **Long texts are read in big pieces** (up to 8,192 tokens - pieces of words - at a time), which is why a long
document or code base is read at over 1,000 tokens per second.
- **Guess, then check:** a small helper guesses the next few words and the big model checks them all at once, so
you get the same answer, 1.6-1.8x sooner.
- **Long texts are read in big pieces** (up to 8,192 tokens at a time): over 1,000 tokens per second.
Want the full picture? The [details](docs/DETAILS.md#how-it-works) explain every part and its numbers, and the
[paper](docs/paper/Strata-Paper.pdf) tells the whole story, with the measurements behind it.
The longer explanation: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Every part and its numbers: [the
details](docs/DETAILS.md#how-it-works) and the [paper](docs/paper/Strata-Paper.pdf).
## Credits
## Credits and license
- Model: [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team; compressed versions by
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF);
[Swift 1.5](https://huggingface.co/ukisai/Swift-1.5-Qwen3.8-Flash-Next-GSQ-RCO-GGUF) by UkisAI; the experimental
[UD-Q4_K_XL](https://huggingface.co/unsloth/Qwen3.8-Flash-Next-GGUF) by Unsloth (its support follows
[eddoursul/Strata](https://github.com/eddoursul/Strata)). Their licenses apply to the model files.
- Built with parts of [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) (MIT). Ideas from
[Splash](https://github.com/incoai/splash), [ninfer](https://github.com/Neroued/ninfer) and
[HyperQwen](https://github.com/syv-ai/HyperQwen). More in the [details](docs/DETAILS.md#credits-and-licenses).
The model is [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team, compressed by
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5) and Unsloth;
Strata is built with parts of [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). All credits:
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata is open source under the [MIT License](LICENSE); a few
parts and every model carry their own licenses ([which ones](docs/HOW_IT_WORKS.md#license)).
## License
## Support Strata
Strata is open source under the [MIT License](LICENSE). A few parts carry their own licenses: `third_party/ggml`
(MIT, llama.cpp / ggml), the web app's font (SIL Open Font License 1.1) and the experimental speed projection's
vector in `data/experimental-speed-projection` (Qwen Community License 1.0, from the model's activations). The
models are not part of this repository; each model's own license applies to its files.
Strata is free and open source. If it is useful to you, you can support its development:
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
+1 -1
View File
@@ -1,6 +1,6 @@
@echo off
rem Strata for Windows: the first run installs everything and starts the model; later runs just start it.
rem Needs only an NVIDIA driver. Python is installed for your user account if it is missing (no admin needed).
rem Needs only an NVIDIA or AMD graphics driver. Python is installed for your user account if it is missing (no admin needed).
setlocal
title Strata
cd /d "%~dp0"
+19 -5
View File
@@ -12,8 +12,14 @@ endif()
set(_strata_hip_validated gfx1100 gfx1201)
set(_strata_hip_community gfx1101 gfx1200)
set(_strata_hip_unvalidated gfx1102 gfx1030)
# CMake hands HIP a ';' list, but a -DCMAKE_HIP_ARCHITECTURES typed by hand (or ROCm's own Windows tooling) may use
# spaces, which foreach(IN LISTS) would otherwise treat as one element.
string(REPLACE " " ";" _strata_hip_norm "${CMAKE_HIP_ARCHITECTURES}")
set(STRATA_HIP_ARCH_LIST "")
foreach(_arch IN LISTS CMAKE_HIP_ARCHITECTURES)
foreach(_arch IN LISTS _strata_hip_norm)
if(_arch STREQUAL "")
continue()
endif()
string(REGEX REPLACE ":.*$" "" _base "${_arch}") # gfx1100:xnack- -> gfx1100
if(_base IN_LIST _strata_hip_validated)
elseif(_base IN_LIST _strata_hip_community)
@@ -62,11 +68,19 @@ target_include_directories(strata_hip_runtime BEFORE INTERFACE
"${STRATA_HIP_COMPAT_INCLUDE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_compile_definitions(strata_hip_runtime INTERFACE STRATA_USE_HIP=1 "STRATA_HIP_ARCHS=\"${STRATA_HIP_ARCHS}\"")
target_link_libraries(strata_hip_runtime INTERFACE hip::host)
foreach(_language IN ITEMS CXX HIP)
# The shim renames the CUDA runtime to HIP, force-included into every host and device source. On Windows the host
# compiler is ROCm's clang++ too (tools/hip/build_windows.bat: CMake refuses to mix cl.exe with Clang HIP), which takes
# -include like it does on Linux; an MSVC-style front end (cl / clang-cl) takes /FI instead. Forward slashes, so /FI
# does not read the path's backslashes as escapes.
file(TO_CMAKE_PATH "${STRATA_HIP_COMPAT_INCLUDE_DIR}/cuda_runtime.h" _strata_hip_force)
if(MSVC)
target_compile_options(strata_hip_runtime INTERFACE "$<$<COMPILE_LANGUAGE:CXX>:/FI${_strata_hip_force}>")
else()
target_compile_options(strata_hip_runtime INTERFACE
"$<$<COMPILE_LANGUAGE:${_language}>:-include>"
"$<$<COMPILE_LANGUAGE:${_language}>:${STRATA_HIP_COMPAT_INCLUDE_DIR}/cuda_runtime.h>")
endforeach()
"$<$<COMPILE_LANGUAGE:CXX>:-include>" "$<$<COMPILE_LANGUAGE:CXX>:${_strata_hip_force}>")
endif()
target_compile_options(strata_hip_runtime INTERFACE
"$<$<COMPILE_LANGUAGE:HIP>:-include>" "$<$<COMPILE_LANGUAGE:HIP>:${_strata_hip_force}>")
# CMake does not infer HIP from Strata's existing CUDA-shaped .cu suffixes.
file(GLOB_RECURSE _strata_hip_sources CONFIGURE_DEPENDS
+196
View File
@@ -0,0 +1,196 @@
# Installing Strata for a user (instructions for AI agents)
This page is written for an AI coding assistant (Claude Code, Cursor, Codex, GitHub Copilot, ...) that a user asked to
set up Strata on their PC. Strata runs the Qwen3.8-Flash-Next model locally on one NVIDIA or AMD graphics card plus
system RAM, and serves an OpenAI- and Anthropic-compatible API on `http://127.0.0.1:8080`. The human-oriented
version of everything below is [INSTALL.md](INSTALL.md).
Work through the steps in order. Tell the user what you are doing in plain words; they may not be technical.
## 0. Ground rules
- **Never expose the server without an API key.** Keep the default `--host 127.0.0.1`. Only if the user asks for
access from other devices, use `--host 0.0.0.0` **together with** `--api-key <a long random secret>`, and give the
user the key. Never put a tunnel or port forward in front of a server without a key.
- Do not change the user's system beyond what setup does (setup installs Python for the user account if needed,
and everything else inside the Strata folder and its `Strata-data` folder). Ask before installing drivers.
- The model download is ~70 GB (up to 111 GB for Unsloth's 4-bit). Confirm the user is fine with that before you
start, especially on a metered connection.
- Setup is long-running (an hour or more on a slow connection). Run it in the background or with a long timeout and
poll its output; do not kill it because it is quiet for a while. It is resumable: running the same command again
continues where it stopped.
## 1. Check the PC
The quickest way is setup's own check, which prints the GPU(s), driver, RAM, CPU, free disk and whether this PC can run
Strata, and installs nothing except Python and the `.venv` it needs to run (step 2 gets the repository first):
```
Windows: START-HERE.bat --check
Linux: ./setup.sh --check
```
To check by hand:
| What | Windows (PowerShell) | Linux |
| --- | --- | --- |
| OS | `[Environment]::OSVersion` | `cat /etc/os-release` |
| NVIDIA GPU, VRAM, driver | `nvidia-smi --query-gpu=index,name,memory.total,driver_version --format=csv` | the same |
| AMD GPU | `Get-CimInstance Win32_VideoController \| Select-Object Name, AdapterRAM` (AdapterRAM is capped at 4 GB; trust the model name) | `lspci \| grep -i -E 'vga\|display'`; VRAM: `cat /sys/class/drm/card*/device/mem_info_vram_total` (bytes); `rocm-smi` only if ROCm is installed |
| RAM | `(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB` | `free -g` |
| Free disk | `Get-PSDrive -PSProvider FileSystem` | `df -h .` |
Requirements (details: [INSTALL.md](INSTALL.md#what-you-need)):
- **GPU:** NVIDIA RTX 20, 30, 40 or 50 series, or AMD Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT,
RX 9070 / 9070 XT, Radeon AI PRO R9700, RX 6800 / 6900 series; 12 GB of VRAM or more (an NVIDIA card with 8 GB runs,
slowly). GTX 10 series and older, and integrated GPUs, are not supported.
- **Driver:** NVIDIA 580 or newer. AMD on Linux: the kernel's amdgpu driver; on Windows: a current AMD Adrenalin
driver. If the driver is missing or too old, tell the user to update it (NVIDIA App / nvidia.com/drivers, or AMD
Software) and restart; do not install drivers yourself unless they ask.
- **RAM:** 32 GB or more (see step 3). **Disk:** ~80 GB free, ideally on an NVMe SSD. **CPU:** x86-64 with AVX2.
- **OS:** Windows 10/11 or Linux (Ubuntu 22.04/24.04 are fully automatic).
If the PC does not meet them, say which part is missing and stop.
## 2. Get Strata
```
git clone https://github.com/Niko1221/Strata.git
```
Without git: download https://github.com/Niko1221/Strata/archive/refs/heads/main.zip and unzip it. Pick a drive with
enough free space: the model files go to a `Strata-data` folder **next to** the Strata folder (or `--data-dir <path>`
on another drive).
## 3. Pick the model
By the PC's RAM (ask the user whether they mainly want it for code - then the Coder is a good pick at any RAM size):
| RAM | `--family` / `--model` | Notes |
| --- | --- | --- |
| 32 GB | `--family coder` (size IQ1_M) | for code; with a 24 GB card Q2_0 and IQ2_XS also run (setup picks the low-RAM mode) |
| 48 GB | `--family qwen --model IQ2_XS` | or `Q2_0` (fastest) |
| 64 GB | `--family qwen --model IQ2_XS` (recommended) | `IQ3_XXS` / `IQ3_S` are slower and a bit better |
| 96 GB+ | `--family qwen --model IQ3_S` | `--family unsloth --model UD-Q4_K_XL` is experimental: NVIDIA only, NVMe SSD, 7-8.5 tokens/s on 64 GB |
`--family swift` (Swift 1.5, a fine-tune that thinks shorter; sizes Q2_0, IQ2_XS, IQ3_XXS) is the alternative to
`qwen`. With `--yes` and no `--model`, setup picks the recommended size for the RAM itself. More: [MODELS.md](MODELS.md).
## 4. Run setup without questions
From the Strata folder (Windows: in `cmd`, or `cmd /c START-HERE.bat ...` from PowerShell):
```
Windows: START-HERE.bat --yes --family qwen --model IQ2_XS --no-start
Linux: ./setup.sh --yes --family qwen --model IQ2_XS --no-start
```
The flags (all of them: `START-HERE.bat --help`):
| Flag | Meaning |
| --- | --- |
| `--yes` | take the recommended answer to every question (no prompts) |
| `--family qwen\|swift\|coder\|unsloth` | the model version |
| `--model Q2_0\|IQ2_XS\|IQ3_XXS\|IQ3_S\|IQ1_M\|UD-Q4_K_XL` | the size (the Coder is IQ1_M, Unsloth UD-Q4_K_XL) |
| `--context N` | context in tokens; default by VRAM: 32768 under 14 GB, 65536 under 20 GB, else 131072 |
| `--vision yes\|no\|gpu\|cpu` | read pictures; `--yes` leaves images off. AMD cards: `cpu` |
| `--gpu N` / `--gpus 0,1` / `--gpus all` | one card, or several sharing the model (default: the card with the most VRAM) |
| `--backend cuda\|hip` | NVIDIA or AMD engine; chosen by itself on a PC with only one kind of card |
| `--data-dir PATH` | where the 70-120 GB of model files go |
| `--port N` | the server port (default 8080) |
| `--host H --api-key K` | listen beyond this PC; **only together with a key** |
| `--no-start` | install only, do not start the server |
| `--setup` | install another model or change settings of an installed one |
| `--check` | only check the PC |
`--no-start` is recommended for agents: the server runs in the foreground until its window is closed, which would
block your shell. Start it separately in step 6.
Notes:
- **Linux:** setup uses `sudo apt` (or dnf/pacman) to install Python with venv if it is missing, and on AMD may need
`build-essential` and `git`. You cannot type the user's password: if a `sudo` step is needed, ask the user to run it
(e.g. `sudo apt install python3-venv build-essential git`) and then rerun setup.
- **AMD on Linux:** setup installs ROCm into `.venv` (~10 GB, no sudo) unless a system ROCm 7 exists, and compiles the
engine for the card (10-20 minutes, once).
- **AMD on Windows (new in 0.1.34):** setup downloads the ready-made AMD engine (~550 MB, ROCm included) - nothing
is compiled and only the AMD driver is needed. Before the model download it runs `engine\strata-device.exe
--list-devices`; if that does not list the card, the driver is the problem (tell the user to update AMD Software).
One card per model and no images on Windows for now. It is new: ask the user to report how it runs (docs/AMD_HIP.md).
- **NVIDIA with no ready-made engine for the card:** setup offers to install build tools and compile (20-40 minutes);
`--yes` accepts.
## 5. While it downloads, tell the user
- It downloads about 70 GB from Hugging Face; how long depends on their connection (at 100 Mbit/s roughly 1.5-2
hours). It can be stopped and continues where it left off.
- Then it prepares the model for their PC (a few minutes) and, on AMD on Linux, compiles the engine.
- **When the model starts, the PC can be slow or stop responding for 1-3 minutes** (longest the first time): Strata
loads 35-55 GB into RAM and locks part of it for the graphics card. That is normal; they should not close the window.
- Nothing leaves their PC: the model runs locally.
## 6. Start the server
Setup prints the start script it wrote (`start script: run-<model>.bat`). The name is the family tag plus the size,
lower case: `run-iq2_xs`, `run-swift-iq2_xs`, `run-coder-iq1_m`, `run-unsloth-ud-q4_k_xl`.
```
Windows (PowerShell): Start-Process -FilePath ".\run-iq2_xs.bat" (opens its own window)
Linux: nohup ./run-iq2_xs.sh > strata-server.out 2>&1 &
```
Or `START-HERE.bat` / `./setup.sh` without flags, which starts the installed model (it asks which one when several are
installed; add `--yes` to take the first). The server opens the user's browser on `http://127.0.0.1:8080` when it is
ready. Closing its window (or stopping the process) stops the model.
## 7. Verify
The HTTP server answers once the model is loaded (30-90 s on later starts, a few minutes the first time). Poll:
```
curl http://127.0.0.1:8080/health
```
It returns `{"status": "ok", "model": ..., "max_context": ..., "images": ..., "api_key": ..., "loaded": true, ...}`.
Then:
```
curl http://127.0.0.1:8080/v1/models
curl http://127.0.0.1:8080/v1/chat/completions -H "Content-Type: application/json" -d "{\"model\": \"strata\", \"messages\": [{\"role\": \"user\", \"content\": \"Say hello in five words.\"}], \"max_tokens\": 64, \"reasoning_effort\": \"none\"}"
```
With an API key, add `-H "Authorization: Bearer <key>"`. The server window prints a `ready: http://127.0.0.1:8080/v1`
line when it is ready; the engine log is `strata-<model>.log` in the Strata folder.
## 8. Connect the user's apps
- **Browser:** `http://127.0.0.1:8080` - Chat, a live Monitor, and About (settings and addresses).
- **Any OpenAI-compatible app or agent:** base URL `http://127.0.0.1:8080/v1`, any API key (or the configured one),
any model name.
- **Anthropic-compatible apps:** `http://127.0.0.1:8080/v1/messages`.
- **Claude Code:** `ANTHROPIC_BASE_URL=http://127.0.0.1:8080`, `ANTHROPIC_MODEL` set to a Claude model name it knows
(Strata ignores the name), and any `ANTHROPIC_AUTH_TOKEN` (or the configured key).
- **Thinking level:** `"reasoning_effort": "none" | "low" | "medium" | "high"` (default high).
- Strata answers one request at a time. API details: [DETAILS.md](DETAILS.md#using-it).
## 9. When something fails
| Symptom | What to do |
| --- | --- |
| `the NVIDIA driver is too old` | The user updates the driver (NVIDIA App or nvidia.com/drivers) and restarts; rerun setup. |
| Download or install stopped | Rerun the same setup command; it continues. |
| `port 8080 is already in use` | Strata is already running (check `/health`), or use `--port 8081`. |
| Very slow, disk busy, or "the engine stopped unexpectedly" | Not enough free RAM: close programs, or set up a smaller size (`--setup --model Q2_0` or IQ2_XS). |
| `ExpertCache: cudaMalloc(...) failed: out of memory` with free VRAM (Windows) | The page file is off or tiny: set it to "System managed" and restart. |
| `prompt ... exceeds the context` | Rerun setup with `--setup --context <bigger>`. |
| No AMD GPU found (Linux) | The amdgpu driver lists no GPU; check the card and driver. Integrated GPUs are not supported. |
| Python or build tools could not be installed | Install what the message names (links are printed), then rerun. |
More: [TROUBLESHOOTING.md](TROUBLESHOOTING.md) and the [full table](DETAILS.md#troubleshooting). If it still fails,
collect `strata-<model>.log` and the setup output, and suggest an issue at https://github.com/Niko1221/Strata/issues.
## Alternative: the MCP server
Strata also has an MCP server, so an AI tool can check, install, start and stop Strata through tool calls
(`strata_status`, `strata_models`, `strata_install`, `strata_start`, `strata_stop`, `strata_logs`) instead of the
shell commands above. See [MCP_SERVER.md](MCP_SERVER.md).
+70 -8
View File
@@ -1,10 +1,12 @@
# Experimental AMD HIP backend (gfx1100, gfx1101, gfx1200, gfx1201, gfx1030)
# AMD Radeon: the HIP backend (gfx1100, gfx1101, gfx1200, gfx1201, gfx1030)
This is a Linux source build for the RX 7900 XT / XTX (RDNA3, gfx1100) and the
Strata runs on AMD Radeon cards through its HIP backend, the same engine as on NVIDIA compiled for AMD. This page
covers the build on Linux (on Windows a ready-made engine, see [Windows](#windows)) for the RX 7900 XT / XTX (RDNA3, gfx1100) and the
RX 9070 / 9070 XT / Radeon AI PRO R9700 (RDNA4, gfx1201; see [RDNA4](#rdna4-gfx1201)). The RX 7800 XT / 7700 XT
(gfx1101) and the RX 9060 XT (gfx1200) were validated by their owners (see [Community-validated
cards](#community-validated-cards)); the RX 6800 / 6900 series (RDNA2, gfx1030) builds and runs too, reported by a community machine and not yet validated by the maintainers (see [RDNA2](#rdna2-gfx1030)). It is opt-in; the NVIDIA installer and CUDA build remain the default. Other AMD
architectures, wave64, Windows HIP, and mixed AMD/NVIDIA execution are outside this contribution.
cards](#community-validated-cards)); the RX 6800 / 6900 series (RDNA2, gfx1030) builds and runs too, reported by a community machine and not yet validated by the maintainers (see [RDNA2](#rdna2-gfx1030)). Setup chooses it by itself on a PC with no NVIDIA card Strata can use (`--backend hip` on a PC with both); the
install steps for users are in [INSTALL.md](INSTALL.md#amd-cards). Other AMD architectures, wave64, and mixed
AMD/NVIDIA execution in one run are not supported.
The backend maps the CUDA-shaped runtime and BLAS calls to HIP/hipBLAS, uses
RDNA2/RDNA3/RDNA4's signed integer dot instruction for quantized kernels, and supplies
@@ -49,6 +51,64 @@ the kernel's amdgpu driver (no ROCm install needed):
The rest of setup is the same as on NVIDIA: the model download, the start script, the server.
## Windows
Since 0.1.34 an AMD card on Windows is set up like an NVIDIA one: download Strata, double-click `START-HERE.bat`.
On a PC with no NVIDIA card Strata can use, the AMD card is chosen by itself; with both, setup asks
(`START-HERE.bat --backend hip` picks AMD directly).
- **You need:** Windows 10 or 11 (64-bit), one of the cards above, and a current AMD driver ([AMD Software:
Adrenalin Edition](https://www.amd.com/en/support/download/drivers.html)). Nothing else: no ROCm or HIP SDK
install, no compiler, no admin rights.
- **Detection:** setup reads the display adapters Windows lists (their PCI ids; the VRAM size from the display
driver's registry entry). An integrated Radeon is listed as not supported.
- **Engine:** the ready-made `strata-windows-x64-hip.zip` from the release (built by `tools\hip\build_windows.bat`
for gfx1100, gfx1101, gfx1102, gfx1200, gfx1201 and gfx1030) goes into `engine\`. It carries the ROCm libraries the
engine loads (`engine\rocm\bin`: the HIP runtime, hipBLAS / rocBLAS / hipBLASLt with their kernels for these cards,
amd_comgr and the Microsoft C++ runtime; ROCm 10.2.0a20260930 from AMD's TheRock builds, licenses in
`engine\rocm\licenses`). The HIP runtime works through the AMD driver's own components, so the driver is the one
thing it needs from the PC.
- **The HIP runtime next to `strata.exe` (0.1.35, #468 #461):** `amdhip64_7.dll` and `amd_comgr.dll` are also put in
`engine\` (setup copies them there on every start). Windows looks in the program's folder before System32, where
some AMD drivers install their own `amdhip64_7.dll`; with that one, the bundled libraries crashed on the first
prompt (an access violation, or `hipErrorInvalidDeviceFunction`). The engine's log names the runtime it loaded
(`strata generate: HIP runtime ...`).
- **Before the ~60 GB model download** setup runs `engine\strata-device.exe --list-devices` (with `engine\rocm\bin` on
the PATH): if the HIP runtime does not see the card, setup stops there and points to the driver. It also gives the
card's HIP number: with an integrated Radeon that is device 1, not 0 (#325). From then on setup lists the AMD cards
as HIP numbers them, so `--gpu N` and the config's `"gpu"` are HIP numbers.
- **Differences from Windows-on-NVIDIA and Linux-on-AMD:** no images yet (the CPU image encoder is Linux-only for
now), one card per model (`--gpus` is Linux-only for now), no calibration.
- Two Windows-only engine details (#247, #325): hipBLAS can return success and still leave `hipErrorInvalidValue`
set after some BF16/FP16 GEMMs (seen on gfx1201); the engine clears that one stale error after a GEMM that
succeeded, on Windows only. `hipHostGetDevicePointer` returns the host pointer itself on Windows: kernels read
mapped memory through it correctly, but a device-to-device copy into it does not land, which is why
`tests/hip/handoff` times out there (the engine does not use that copy; `tests/hip/mapped_alias` reports it).
**What is validated (0.1.34):** #325's author ran the engine of this port on an RX 9070 XT (Windows 11, ROCm
10.2.0a20260930 in `.venv`, compiled on the PC): Coder IQ1_M at 32K, 29.2 tok/s decode, ~181 tok/s prefill,
correct answers; ctest 42 of 46. The maintainers have no Windows AMD card: the release zip was built on an NVIDIA PC,
and checked there on the Ryzen CPU's integrated Radeon (gfx1036, a test build of the same tree): with only the zip's
libraries on the PATH, `strata-device` lists the card and a hipBLAS BF16 GEMM matches the CPU; the HIP ctest passes
52 of 56, the 4 failures the same as on the RX 9070 XT (`hip_handoff`, and three tests that need a pack fixture).
The ready-made zip itself has not run a model on a discrete card yet - please report.
**Reporting a Windows AMD run** (an issue, or on #325): your card and driver version (AMD Software > System), then
```bat
engine\strata-device.exe --list-devices
engine\strata-device.exe --selftest
```
(from the Strata folder, after `set PATH=%CD%\engine\rocm\bin;%PATH%`), the end of `strata-<model>.log`, and the
speed lines the server window prints for a first answer.
**Building it yourself:** `tools\hip\build_windows.bat` (Visual Studio 2022 Build Tools with the C++ workload, Python,
git; no admin, no AMD GPU) installs ROCm from AMD's TheRock wheels into `.rocm-win`, builds, and packages
`dist\strata-windows-x64-hip.zip`; `START-HERE.bat --backend hip --prebuilt dist\` installs that one.
`tools\hip\build_windows.bat tests` also builds the HIP tests (`ctest` in `build-hip-win`, with
`.rocm-win\Lib\site-packages\_rocm_sdk_devel\bin` on the PATH). `STRATA_HIP_ARCHS` picks other architectures.
## Build
Requirements: a working ROCm driver/runtime, HIP development headers and
@@ -123,7 +183,7 @@ The worker count above was used on a 16-core CPU; measure it for your CPU.
The 4K context is a smoke-test starting point, not a model limit. The expert cache
sizes itself automatically and leaves 1 GiB of VRAM headroom.
The installer supports this backend (see "Install with setup" above). The vision helper is NVIDIA-only for now.
The installer supports this backend (see "Install with setup" above). Images run through the CPU encoder for now (`--vision cpu`).
Setup installs one AMD card, or several with `--gpus` (the engine's layer split; see RDNA4 below).
## RDNA4 (gfx1201)
@@ -183,13 +243,15 @@ an RX 9070 XT 16 GB and a Radeon AI PRO R9700 32 GB (both gfx1201), a Ryzen 9 39
(split: ~51), the tokens of the R9700 alone (docs/MULTI_GPU.md).
- **Speed switches (engine 0.1.32, measured on the R9700 / 9070 XT with the Coder IQ1_M pack):**
- the MoE router (`router_top10`) runs a HIP kernel without its serial FP64 sum and block barriers by default: the
same ids and weights bit for bit (`hip_router_fast` checks 65,536 rows), 39 -> 9-12 us per call, decode
62.4 -> 70.0 tok/s on the R9700 and +4% on the 9070 XT, the same greedy tokens (5 + 5 starts).
same ids and weights bit for bit (`hip_router_fast` checks 65,536 rows), 39 -> 9-12 us per call, the same greedy
tokens (5 + 5 starts). Decode: 62.4 -> 70.0 tok/s on the R9700 and +4% on the 9070 XT in one A/B here (ROCm 10.2
nightly); a user's repeated A/B/A/B with setup's TheRock 7.10 wheels and an i5-12600K measured +1-4%, inside a
12-20% run-to-run spread (#432) - how much it gains depends on how much of decode the router is on that setup.
`STRATA_HIP_ROUTER_OLD=1` runs the portable kernel.
- `STRATA_HIP_WMMA=1` (opt-in, gfx12 only, int8 KV): the prompt path's QSA attention on RDNA4 matrix cores
(`v_wmma_f32_16x16x16_f16`, FP16 hi + lo halves like the CUDA tensor-core kernel; `hip_prompt_attn_wmma` bounds it
against the FP32 kernel and FP64). 7.2-7.5x the portable kernel; R9700 prompts 4K 1,784 -> 2,427 tok/s, 16K
1,797 -> 2,700. Not bitwise: greedy text differs from token ~50 on, as with the CUDA tensor-core attention. With it
1,797 -> 2,700 (a user with TheRock 7.10: +29-34%, #432). Not bitwise: greedy text differs from token ~50 on, as with the CUDA tensor-core attention. With it
the prompt path's expert ring is 96 slots (as STRATA_PREFILL_RING=96): with the default 384 the 9070 XT's 4K
prompts fell to 718 tok/s; with 96 they gain (1,017 -> 1,211; 16K 1,518 -> 2,032). PR #329
(bsorensen110) contributed an equivalent gfx12 WMMA kernel of the same speed (within 1%); this one also masks KV
+61 -11
View File
@@ -1,11 +1,13 @@
# Strata - the details
The technical side of Strata: every measured number, the API, images, all settings and how the engine works.
New here? Start with the [README](../README.md) - it has everything you need to install and use it.
New here? Start with the [README](../README.md); installing step by step is in [INSTALL.md](INSTALL.md), the models in
[MODELS.md](MODELS.md), common problems in [TROUBLESHOOTING.md](TROUBLESHOOTING.md).
> **On this page:** [Speed](#speed-measured) · [Other GPUs](#other-gpus-estimated) · [Which model?](#which-model) ·
> [Requirements](#before-you-start) · [Windows](#windows) · [Linux](#linux) · [API](#using-it) ·
> [MCP tools](#tools-from-mcp-servers) · [Images](#images-vision) ·
> [MCP tools](#tools-from-mcp-servers) · [MCP server](#manage-strata-from-your-ai-assistant-mcp-server) ·
> [Images](#images-vision) ·
> [Troubleshooting](#troubleshooting) · [How it works](#how-it-works)
---
@@ -50,14 +52,17 @@ accepted, so a different answer to the same prompt moves it by several percent.
[`bench/results/2026-09-28-speed-0114`](../bench/results/2026-09-28-speed-0114/README.md).
IQ3_XXS and IQ3_S at 262K are not measured: with their 43 / 50 GB of experts, a 260K-token context brings a 64 GB PC
to its memory limit. Use up to 128K with them on 64 GB (setup caps it). IQ3_S (engine 0.1.4 or newer) is only published
for the original model, not for Swift 1.5.
to its memory limit by setup's estimate (the experts + the context's KV cache + 24 GB), so setup recommends up to 128K
with them on 64 GB. A longer context you choose (`--context 262144`, or a pick in its list) is kept, with a note: users
ran IQ3_S at 256K on 64 GB with RAM to spare (#406). In the low-RAM mode the KV cache stays in VRAM and the context
does not count against RAM. IQ3_S (engine 0.1.4 or newer) is only published for the original model, not for Swift 1.5.
**KV streaming (engine 0.1.5):** at 64K and more, setup keeps the context's KV cache in RAM and only the part the
attention reads in VRAM (`--kv-resident 32768`), so more experts fit on the GPU. Q2_0 at 262K: 50.9 -> 62.6 tokens/s
(1,589 -> 3,872 experts in VRAM); at 128K about +6%. The attention reads exactly the same values (only where the KV lives changes); it
costs ~13.7 KB of RAM per context token (1.7 GB at 128K). Existing installs: run `START-HERE.bat --setup` once to turn
it on.
it on. Setup turns it on when the RAM has room for it; `--kv-streaming on|off` overrides that (on past the RAM test with a
note; never with `--kv k8v4` or under WSL, which cannot stream).
**4-bit KV cache (engine 0.1.8, optional):** `START-HERE.bat --setup` asks above 8K context (or pass `--kv q4_0`). It
halves the KV cache's memory with a Hadamard rotation before 4-bit rounding (PR #21), about 4% faster at 128K, but it
@@ -76,7 +81,14 @@ slightly differently. How many tokens share an expert depends on the drafts in a
at temperature 0 can end in a different (equally good) answer when the drafting, the cache state or a resumed
conversation differ (issue #152). `STRATA_IQ_MT_MIN=1` (in the config's `env`) uses the multi-token kernels for
every group: the answer then no longer depends on the drafting. Measured on a Ryzen 7600 (AVX-512): IQ3_S decode
-1..-3%, the other models the same; the default stays the fastest rule.
-1..-3%, the other models the same; the default stays the fastest rule. Through the server, two more things carry
over from one request to the next (#410): the adaptive tier moves experts between RAM and VRAM (the GPU and the CPU
round an expert differently), and the prompt cache resumes a repeated prompt and reads only its tail through the
decode path. For byte-identical repeats add `--prompt-cache 0 --adapt-swaps 0 --pcie-frac 0` to the engine's args
as well (#410): the PCIe share of the missed experts (computed on the GPU instead of the CPU) still made the first
answer after a start differ from the next ones. Measured here (IQ3_XXS, a 3.6K-token prompt, 4 repeats): with all
three switches 1 answer of 4, without `--pcie-frac 0` 2 of 4 (the first one differs), with the defaults 2 of 4.
`--pcie-frac 0` costs decode speed (the missed experts all run on the CPU), so keep it for A/B runs.
**The draft layer's tokens (0.1.27, `--draft-vocab`):** the MTP draft layer can only propose tokens from a subset
of the vocabulary (`mtp/rt/draft_vocab.bin`). Since 0.1.27 the subset includes every Chinese, Japanese and Korean
@@ -114,6 +126,12 @@ other ~18 GB), a 32 GB PC with a 12-16 GB GPU the Coder; IQ3_XXS on a 32 GB PC s
not hold do not fit, it says so and runs the plain mapped mode. The server log shows, per request, how many expert
reads went to the file (`resident RAM: ... blob reads from the file`: 0 in steady use).
- `--low-ram resident|mmap` forces one variant (also on a PC with enough RAM, e.g. to try it).
- Several GPUs (#364, #384): setup recommends one GPU in the low-RAM mode (the resident variant has no layer split
yet), and asks; `--gpus 0,1` (or answering 2) shares the model across them with the mapped variant
(`--mmap-experts`): the cards together hold more of the experts, and two users measured it 1.3-1.6x faster than
one card, but the OS file cache can fill the RAM to 0 free during long prompts. `--yes` keeps one GPU. A config
with `--resident-experts` started with `--gpus` switches to `--mmap-experts` with a note, and the engine runs that
pair as `--mmap-experts` with a warning instead of refusing it.
**Low-RAM mode without `experts.bin` (engine 0.1.31):** for the native packs (IQ2_XS, IQ3_XXS, IQ3_S, the Coder, Swift,
Q2_0 packed by `tools/iq_pack.py`; not the canonical Q2_0 pack setup makes for AVX-512 CPUs) the mapped mode no longer
@@ -128,7 +146,9 @@ engine fetches a layer's missing experts on 8 threads (`STRATA_FETCH_THREADS`) w
**A RAM budget (engine 0.1.31, `--resident-budget-gib N`):** the resident variant for a model whose experts do not all
fit: the N GiB of experts the GPU cache does not hold that the expert profile ranks hottest are copied into RAM at
start (locked; page-locked when the driver allows the whole budget), and the rest are read from the files through the
OS file cache. It implies `--mmap-experts` and leaves 4 GB of free RAM (a larger N is clamped, with a message). With
OS file cache. It implies `--mmap-experts` and leaves 4 GB of free RAM (a larger N is clamped to that less 256 MiB,
with a message; #403: a clamped budget no longer fails the safety check that follows, and a budget that cannot be
kept at all is a warning, with every expert read from the files). Setup sets N with `--resident-budget-gib N`. With
the GGUF read in place it also warms the next layer's likely experts: while the CPU works on a layer, a thread applies
the next layer's router to this layer's input and asks the OS for the pages of the predicted experts that neither the
GPU nor the RAM budget holds (only pages - the experts computed are the same; `STRATA_LOOKAHEAD=0` turns it off). This
@@ -139,7 +159,8 @@ RTX 5070, against ~3 tokens/s before these changes.
RAM copy, blobs and MB from the files, the time spent reading them; `routing prefetch`: how many of the file reads had
been warmed). The server log has the same per request (`expert tiers: GPU ... hits ...; RAM ... blobs, files ...
blobs ... MB read`), and `GET /metrics` lists `ram_blobs`, `file_blobs` and `file_mb` for each recent request (with
engine 0.1.31 or newer).
engine 0.1.31 or newer). It also lists each request's speculative drafts, `drafts_offered` and `drafts_accepted`
(`null` when the engine did not report them), and their sums since the server started in `totals` (#457).
Time to first token is prompt length / prompt speed: with Q2_0 about 4 s at 4K, 25 s at 32K, under 2 minutes at 128K
and 4.5 minutes at 262K (engine 0.1.13 made long prompts about twice as fast, below).
@@ -235,12 +256,13 @@ the positions of short greedy answers, 90-91% after a 16K prompt, differing most
## Before you start
You need **only an NVIDIA driver** (version 580 or newer; update it with the NVIDIA App or from
[nvidia.com/drivers](https://www.nvidia.com/drivers)). Everything else is installed for you the first time.
You need **only a graphics driver**: NVIDIA 580 or newer (update it with the NVIDIA App or from
[nvidia.com/drivers](https://www.nvidia.com/drivers)), or for AMD the one in [INSTALL.md](INSTALL.md#what-you-need).
Everything else is installed for you the first time.
| | |
| --- | --- |
| GPU | NVIDIA **RTX 20, 30, 40 or 50 series**, **12 GB VRAM or more** (8 GB runs, slowly). Measured on an RTX 5070 and an RTX 3090; RTX 20 (Turing, since 0.1.27) was tested by a contributor on an RTX 2070. |
| GPU | NVIDIA **RTX 20, 30, 40 or 50 series**, **12 GB VRAM or more** (8 GB runs, slowly). Measured on an RTX 5070 and an RTX 3090; RTX 20 (Turing, since 0.1.27) was tested by a contributor on an RTX 2070. Or AMD **Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700, RX 6800 / 6900 series**: [AMD_HIP.md](AMD_HIP.md). |
| RAM | **64 GB** recommended (see the table above). |
| CPU | x86-64 with AVX2 (any Intel/AMD desktop CPU from the last ~8 years). AVX-512 (Ryzen 7000/9000) is a bit faster. |
| Disk | ~70-80 GB free for the model, ~6 GB for the MTP layer (+1 GB with images). **Q2_0 on an AVX-512 CPU** also writes a one-time ~40 GB copy of its experts for the fast CPU kernel. An NVMe SSD is strongly recommended. |
@@ -609,6 +631,29 @@ unmeasured: all the runs above are text.
---
## Manage Strata from your AI assistant (MCP server)
`tools/strata_mcp.py` is an MCP server for Claude Code, Claude Desktop, Cursor, VS Code, Codex and other assistants.
Once it is added, you can ask your assistant "install Strata for this PC", "start Strata" or "is Strata running?".
In Claude Code, add it with:
```bash
claude mcp add strata -- python C:\Users\you\Strata\tools\strata_mcp.py
```
It has eight tools: status (the running model, what is installed, the hardware, a recommended size), the model
list, install, start, stop, logs, a speed test, and connection settings for other apps.
Install runs `setup.py` with `--yes` in the background. Before it downloads anything, it shows the plan and waits
for your OK. Start and stop work like the run scripts and the server's own unload. The MCP server only ends
processes it started itself. It uses only Python's standard library, so it works before `.venv` exists.
The config snippets for every client, the tool arguments and the safety rules are in
[docs/MCP_SERVER.md](MCP_SERVER.md). This is the opposite direction from
[Tools from MCP servers](#tools-from-mcp-servers) above, where the Strata model calls *your* MCP tools.
---
## Images (vision)
The model has a vision encoder: [`mmproj-Qwen3.8-Flash-Next-BF16.gguf`](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)
@@ -624,6 +669,11 @@ helper (`strata-vision`, from llama.cpp's `mtmd` library) and adds it to your st
A picture becomes up to 1,024 tokens of the context (a 640x480 photo: 300). The same picture sent again, as chat apps
do on every turn, is encoded only once.
**A spare GPU for the encoder (0.1.33, #408):** with a card the engine doesn't use, add `"cuda_device": 2` (numbered
like `nvidia-smi`) to the `"vision"` section of `strata-<model>.json`: the encoder then runs on that card alone. Lower
`--vram-reserve-mib` in `"args"` to 700 as well, so the engine's cards keep that VRAM for the expert cache. The
encoder's card needs code in the ready-made encoder (RTX 20/30/40/50).
### Sending a picture
**Terminal chat:** type `/image <path to a picture>`, press Enter, then type your question.
+77
View File
@@ -0,0 +1,77 @@
# How does Strata work?
Models like Qwen3.8-Flash-Next normally run on servers with hundreds of gigabytes of graphics memory. Your graphics
card has 12-24 GB. Strata makes it fit by **sharing the work across your whole PC** - the same idea as a kitchen, where
the things you use all the time stay on the counter and the rest waits in the pantry. Back to the
[README](../README.md#how-does-it-work).
## Who does what
<p align="center"><img src="media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
- **The model is a team of 24,576 small specialists ("experts"),** and each word it writes needs only 10 of them.
So it doesn't have to have all of them on the graphics card at once.
- **Your graphics card** does the part of the work needed for every word, and keeps the few thousand experts that
are asked most often. It keeps learning which ones those are while you use it.
- **Your RAM** holds every expert. When a word needs one the card doesn't have, **your processor** works on it -
at the same time as the graphics card, so neither waits for the other.
- **Your SSD** holds a big lookup table; the model only reads a few small rows of it per word.
The same in technical terms:
- **GPU (VRAM):** attention and DeltaNet mixers, the gated-residual weights, routers, shared experts, output head, the
MTP draft layer, the KV cache (from 64K: only its most-read part, the rest streams from RAM), and an **expert
cache** that fills the rest of VRAM with the most-used experts (it adapts to the conversation while you chat).
More VRAM matters more than a faster GPU: every extra GB holds ~700 more experts, and every expert on the GPU is one
the CPU does not have to compute.
- **RAM:** all 24,576 experts, pinned. The CPU computes the experts that are not on the GPU **in place**, at the same
time as the GPU works on the cached ones (AVX-512 / AVX2 kernels, ggml's for the i-quants). On a PC with little RAM
and a big card, the [low-RAM mode](MODELS.md#a-big-graphics-card-and-little-ram) keeps in RAM only what the GPU does
not hold.
- **SSD:** the 28.8 GB n-gram table, read a few rows per token through the OS cache.
This works the same on NVIDIA (CUDA) and AMD (HIP) cards: the same engine, compiled for each.
## Guess, then check
<p align="center"><img src="media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
- **Guess, then check.** A small, fast helper built into the model guesses the next few words, and the big model
checks all the guesses in one go. It keeps the ones it agrees with and writes the next word itself - so one step
often produces several words. The helper only guesses - the big model decides every word - so you get the same
quality answer, 1.6-1.8x sooner.
- In technical terms: the model's own MTP layer drafts up to 3 tokens; one pass over all 48 layers checks them, 2.4-3.2
tokens per pass on average. When the reply repeats the context (code edits, quoted text), **prompt lookup** drafts
up to 5 tokens from the earlier copy, but only where its measured acceptance and cost say it pays: code edits 6-11%
faster, other text unchanged. The drafts are checked like the MTP's, so the output is the same.
## Reading long texts
**Long texts are read in big pieces** (up to 8,192 tokens - pieces of words - at a time), which is why a long
document or code base is read at over 1,000 tokens per second. The experts of the next layer stream to the GPU over
PCIe while the current layer's attention runs. After the first message, Strata keeps the conversation and reads only
what is new, so follow-ups start in seconds.
## More
The [details](DETAILS.md#how-it-works) explain every part and its numbers, and the
[paper](paper/Strata-Paper.pdf) tells the whole story, with the measurements behind it.
## Credits
- Model: [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team; compressed versions by
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF);
[Swift 1.5](https://huggingface.co/ukisai/Swift-1.5-Qwen3.8-Flash-Next-GSQ-RCO-GGUF) by UkisAI; the experimental
[UD-Q4_K_XL](https://huggingface.co/unsloth/Qwen3.8-Flash-Next-GGUF) by Unsloth (its support follows
[eddoursul/Strata](https://github.com/eddoursul/Strata)). Their licenses apply to the model files.
- Built with parts of [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) (MIT). Ideas from
[Splash](https://github.com/incoai/splash), [ninfer](https://github.com/Neroued/ninfer) and
[HyperQwen](https://github.com/syv-ai/HyperQwen). More in the [details](DETAILS.md#credits-and-licenses).
- The AMD (HIP) backend: [AMD_HIP.md](AMD_HIP.md) names the contributors and the machines it was validated on.
## License
Strata is open source under the [MIT License](../LICENSE). A few parts carry their own licenses: `third_party/ggml`
(MIT, llama.cpp / ggml), the web app's font (SIL Open Font License 1.1) and the experimental speed projection's
vector in `data/experimental-speed-projection` (Qwen Community License 1.0, from the model's activations). The
models are not part of this repository; each model's own license applies to its files.
+187
View File
@@ -0,0 +1,187 @@
# Installing Strata
Everything about installing, starting, updating and configuring Strata, on Windows and Linux, with an NVIDIA or an
AMD graphics card. The short version is in the [README](../README.md#install); an AI coding assistant can do all of
this for you with [AI_SETUP.md](AI_SETUP.md).
> **On this page:** [What you need](#what-you-need) · [Windows](#windows) · [Linux](#linux) ·
> [AMD cards](#amd-cards) · [Several cards](#two-or-three-cards) · [Docker](#docker-linux) · [Updating](#updating) ·
> [Where things are stored](#where-things-are-stored) · [Setup's questions](#setups-questions) ·
> [Tuning](#tuning-for-your-pc) · [All options](#options-without-questions)
## What you need
| | |
| --- | --- |
| **GPU** | **NVIDIA** RTX 20, 30, 40 or 50 series, **12 GB VRAM or more** (8 GB runs, slowly). Measured on an RTX 5070 and an RTX 3090; RTX 20 (Turing, since 0.1.27) was tested by a contributor on an RTX 2070. **AMD** Radeon RX 7900 XT / XTX, RX 9070 / 9070 XT and Radeon AI PRO R9700 (validated), RX 7800 XT / 7700 XT and RX 9060 XT (validated by their owners), RX 6800 / 6900 series (community-reported), with 12 GB of VRAM or more. See [AMD cards](#amd-cards). |
| **RAM** | Enough for the size you pick ([which model](MODELS.md#pick-by-ram)); **64 GB** runs every size. A big GPU makes up for less RAM - the [low-RAM mode](MODELS.md#a-big-graphics-card-and-little-ram). |
| **CPU** | x86-64 with AVX2 (any Intel/AMD desktop CPU from the last ~8 years). AVX-512 (Ryzen 7000/9000) is a bit faster. |
| **Disk** | ~70-80 GB free for the model, ~6 GB for the MTP layer (+1 GB with images). **Q2_0 on an AVX-512 CPU** also writes a one-time ~40 GB copy of its experts for the fast CPU kernel. On Linux with an AMD card, ROCm takes ~10 GB more when setup installs it. An NVMe SSD is strongly recommended: it makes the first start much faster. |
| **OS** | Windows 10/11, or Linux (Ubuntu 22.04/24.04 get everything installed automatically). |
| **Driver** | **NVIDIA:** a current driver, version 580 or newer ([nvidia.com/drivers](https://www.nvidia.com/drivers) or the NVIDIA App). **AMD:** on Linux the kernel's amdgpu driver (no ROCm install needed); on Windows a current AMD Software: Adrenalin Edition driver ([amd.com/support](https://www.amd.com/en/support)). |
The driver is the only thing you install yourself. Everything else - Python, the engine, the model - is set up for
you the first time: Python 3.12 if you have none (for your user account, no admin), a private Python environment in
`.venv/`, the Strata engine, the model and the MTP draft layer. On NVIDIA it uses the ready-made engine for RTX
20/30/40/50 and NVIDIA's CUDA libraries from pip (~0.4 GB); if no ready-made engine fits your PC, it offers to install
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).
## Windows
1. [Download this project](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it (or
`git clone` it).
2. Double-click **`START-HERE.bat`**.
3. Answer a few questions - or just press Enter each time for the recommended choice ([the questions](#setups-questions)).
Then it downloads everything (the model is ~70 GB, so the first time takes a while - you can stop and it picks up
where it left off) and **starts the model**. Your browser opens the Strata app at `http://127.0.0.1:8080`.
> **While the model starts, your PC can be slow or stop responding for 1-3 minutes** (longest the first time): Strata
> loads 35-55 GB into your RAM and locks part of it for the graphics card. That's normal - wait, and don't close the
> window. The window tells you what it is doing.
**Next time**, just double-click `START-HERE.bat` again: it starts right away (30-90 s to load the model), nothing is
downloaded twice. Close its window to stop the model. `SETUP.bat` (the same as `START-HERE.bat --setup`) installs
another model or changes the settings. Starting Strata from Task Scheduler at logon needs two task settings, or the
start is 24x slower: [Running it at startup](DETAILS.md#running-it-at-startup-task-scheduler).
## Linux
```bash
./setup.sh
```
The same questions, the same automatic install (it uses `sudo apt` for Python and, only if it has to compile, for
the build tools), and the same start: `http://127.0.0.1:8080`. Later runs of `./setup.sh` (or `./run-<model>.sh`)
start the model directly; `./setup.sh --setup` installs another model or changes the settings. Other distributions,
WSL and compiling: [details](DETAILS.md#linux).
## AMD cards
The steps are the same as with NVIDIA: `START-HERE.bat` on Windows, `./setup.sh` on Linux. Setup finds the Radeon
card and chooses the AMD (HIP) engine by itself on a PC with no NVIDIA card Strata can use; `--backend hip` chooses
it on a PC that has both. Supported cards: RX 7900 XT / XTX (gfx1100), RX 7800 XT / 7700 XT (gfx1101), RX 9060 XT
(gfx1200), RX 9070 / 9070 XT and Radeon AI PRO R9700 (gfx1201), and the RX 6800 / 6900 series (gfx1030). Integrated
Radeon GPUs are listed as not supported.
On Linux setup uses a system ROCm 7 when there is one, or installs ROCm into `.venv` from AMD's wheels (~10 GB, no
sudo), and compiles the engine on your PC for the card (10-20 minutes, once; it needs a C++ compiler and git:
`sudo apt install build-essential git`). Several AMD cards share the model with `--gpus`, as on NVIDIA.
What differs from NVIDIA for now: pictures are read by the image encoder on the CPU (`--vision cpu`, 10-30 s per
picture), `--calibrate` is NVIDIA-only, and Unsloth's 4-bit model needs an NVIDIA card. Measurements per card, the
build by hand and the tuning tables: [AMD_HIP.md](AMD_HIP.md).
## Two or three cards
**Two or three NVIDIA cards?** Just run `START-HERE.bat`: it lists your cards, says which ones Strata can use, and
asks whether to share the model across them (recommended when two can). An install made on one card asks once at
its next start. Or choose yourself: `START-HERE.bat --gpus 0,2` (both, remembered), `--gpus all`, or `--gpu 0` (one
card, this start only). Each card keeps the experts of its own layers, and prompts flow through the cards in a
pipeline: on an RTX 5080 + RTX 3090 prompts were read 18-20% faster than on the 5080 alone, decoding on par.
Every card must be an RTX 20 series or newer with 8 GB or more. See [MULTI_GPU.md](MULTI_GPU.md).
**Several AMD cards:** setup takes one card (the one with the most VRAM, or `--gpu N`) unless you name more:
`--gpus 1,0` splits the model's layers across them, the first one the main card (numbers as setup lists them). A
split pays only when no single card holds the model's experts ([AMD_HIP.md](AMD_HIP.md#rdna4-gfx1201)).
## Docker (Linux)
The same idea, in a container (NVIDIA cards).
1. Host: Docker with the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
and a driver **580 or newer** (CUDA 13.0).
2. Build (this compiles the engine into the image, so the container never compiles):
`docker build -t strata .`
`docker build -t strata --build-arg CUDA_ARCHITECTURES=89 .` builds for one card only (faster).
The default covers RTX 30 (86), RTX 40 (89), RTX 50 (120) and A-series (80); a card outside that
set needs a rebuild with its own arch. Add `--build-arg BUILD_VISION=0` to skip the image encoder.
3. Run (the first start downloads the ~70 GB model, then starts; later starts go straight to serving):
`docker run --rm --gpus all -p 8080:8080 --ulimit memlock=-1 -v strata-data:/data strata`
The setup choices are env vars: `-e MODEL=IQ2_XS -e FAMILY=qwen -e CONTEXT=32768 -e VISION=no`
(or `MODEL=Q2_0|IQ3_XXS|IQ3_S`, `FAMILY=swift|coder`; the defaults above are the recommended ones).
`-e VISION=cpu` keeps the image encoder on the CPU. `-e KV=int8|q4_0|k8v4` picks the KV cache
precision; `k8v4` is INT8 K with 4-bit V and keeps its KV in VRAM from 64K up.
Only the model files, the prepared pack, the MTP layer and the install config live in the
`strata-data` volume; the engine is part of the image. Switching between models already on the
volume needs no setup pass: `-e MODEL=Q2_0 -e FAMILY=coder` picks that model's config. Add
`-e REINSTALL=1` only to change settings for a model already set up (context, vision, KV, host,
api_key, LOW_RAM), since those are recorded in its config.
Strata loads 32-62 GB into RAM. `--gpus all` on a host with two usable cards takes both: the
layer split is setup's recommended default ([MULTI_GPU.md](MULTI_GPU.md)), and a volume
set up for one card switches to the pair on its first start there. Pin one card with `-e GPU=0`,
or name them with `-e GPUS=0,2` and where the later card's layers start with `-e LAYER_SPLIT=18`.
A memory limit needs `-e LOW_RAM=on`, which maps the model's experts from the pack instead of
keeping them in RAM: setup.py measures the host's RAM, not the container's limit, so it cannot
see a cap. LOW_RAM runs on one card unless `GPUS` names several (then the experts the cards do not hold are
read through the OS file cache, which can fill the RAM during long prompts).
The server listens on `0.0.0.0:8080` by default; set `-e API_KEY=<secret>` before exposing the port
to a network. The image has a `HEALTHCHECK` on `/health`, so `docker ps` shows the container
healthy once the model is loaded, and `GET /v1/status` says what it is running.
## Updating
Download the new version and unzip it anywhere (or `git pull`), then run `START-HERE.bat` (Linux: `./setup.sh`) in
it. The model files are kept in a `Strata-data` folder next to your Strata folder, so a new copy finds them and sets
itself up the same way - nothing big is downloaded again. On Linux after a `git pull`, setup compiles the engine
again when its source changed (a minute or two for the changed files); if that compile fails, it says so and starts
the engine you had.
## Where things are stored
- **Your chats: only in your browser.** The Chat tab keeps the conversation, its settings and the API key you typed
in the browser's local storage (`strata.*` keys) - not on the server and not in the Strata folder. Pictures are not
kept, only their names. Another browser or a private window starts empty; clearing the site's data deletes them.
- **How the model starts:** `strata-<model>.json` in the Strata folder (context, GPUs, host, API key, ...), written
by setup; next to it `run-<model>.bat` / `.sh`, the log `strata-<model>.log` and, when you use "Use for other
apps too", `strata-<model>.shared-settings.json`.
- **The model files** (`models/`, `packs/`, `mtp/`, 70-120 GB): in **`Strata-data` next to the Strata folder**, or
wherever `--data-dir` put them.
- **Where that data folder is:** `%APPDATA%\Strata\settings.json` on Windows, `~/.config/strata/settings.json` on
Linux ([details](DETAILS.md#before-you-start)).
- **The program itself:** `.venv/`, `engine/` and `third_party/` in the Strata folder.
## Setup's questions
- **Which model and size?** The original, Swift 1.5, the Coder or Unsloth's 4-bit, and Q2_0, IQ2_XS, IQ3_XXS or
IQ3_S - see [which model](MODELS.md).
- **How much context?** How much text it can keep in mind at once (it suggests one for your card). 384K and
512K (experimental) extend the model past its trained 262K by rope scaling - the setup turns it on itself (yarn and a
covering factor; `--rope-scaling`/`--rope-scale` override) ([details](DETAILS.md): "Context extension past
262K").
- **Images?** Whether it should also read pictures. The image encoder (0.9 GB) runs on the GPU (0.1-0.5 s per
picture, ~1.4 GB of VRAM kept free for it) or on the CPU (10-30 s per picture, nothing on the GPU); with AMD cards
on the CPU for now. [Details](DETAILS.md#images-vision).
- **Experimental speed projection?** Off unless you say yes - [read what it does](DETAILS.md#experimental-speed-projection-experimental-off-by-default)
first. It changes how the model answers, and only the original model offers it.
With `--yes` setup takes the recommended answer to every question.
## Tuning for your PC
Every PC is different: `START-HERE.bat --calibrate` (Linux: `./setup.sh --calibrate`) measures a few engine settings
on yours and keeps the fastest (about 5-10 minutes; on an RTX 5070 with a Ryzen 5 7600 it made the Coder 7% faster).
It keeps a setting only when it is more than 3% faster, and the result is remembered per PC and model, so updates
keep it. NVIDIA cards for now. [What it measures](DETAILS.md#double-click-start-herebat).
## Options without questions
```
START-HERE.bat --setup install another model, or change context / images
SETUP.bat the same (double-click it)
START-HERE.bat --model IQ2_XS --context 32768 --vision yes --yes no questions
START-HERE.bat --gguf-dir D:\models\IQ2_XS use GGUF files you already have
START-HERE.bat --data-dir E:\Strata-data keep the model files somewhere else
START-HERE.bat --port 8081 another port
START-HERE.bat --gpu 1 another GPU (setup picks the one with the most VRAM)
START-HERE.bat --gpus 0,2 several GPUs sharing the model
START-HERE.bat --setup --backend hip the AMD engine on a PC that also has an NVIDIA card
START-HERE.bat --setup --host 0.0.0.0 --api-key <secret> reachable from other devices, with a key
START-HERE.bat --calibrate tune the engine for this PC (about 5-10 minutes), then start
START-HERE.bat --check only check this PC
```
On Linux the same options go to `./setup.sh`. `START-HERE.bat --help` lists them all. The server's own settings
(sharing the GPU with games, MCP tools, CORS, API keys, the API itself) are in the [details](DETAILS.md#using-it).
+205
View File
@@ -0,0 +1,205 @@
# Strata's MCP server: let your AI assistant install and run Strata
`tools/strata_mcp.py` is an [MCP](https://modelcontextprotocol.io) server for the AI assistant you already use
(Claude Code, Claude Desktop, Cursor, VS Code, Codex ...). Add it once, then ask in plain words:
- "Install Strata for this PC."
- "Start Strata." / "Stop Strata."
- "Is Strata running? How much VRAM does it use?"
- "Which Strata model fits my PC?"
- "Strata didn't start - what does the log say?"
- "How fast is Strata on this PC?" (a short speed test)
- "How do I connect Open WebUI / the OpenAI SDK / Claude Code to Strata?"
The assistant calls the server's tools. The tools run Strata's own setup and start the model the way the start
scripts do. It is one Python file that uses only the standard library, so it works before setup has created
`.venv`. It needs Python 3.10 or newer, 64-bit on Windows.
It is not the same thing as [Tools from MCP servers](DETAILS.md#tools-from-mcp-servers). That feature
(`serve/mcp.py`) lets the *Strata model* call tools from your MCP servers in its chat page. This page is the other
direction: *your assistant* manages Strata.
## Add it
You need a copy of Strata: the zip from GitHub, or `git clone https://github.com/Niko1221/Strata`. Below,
`C:\Users\you\Strata` (Windows) and `/home/you/Strata` (Linux) stand for that folder. Use `python3` on Linux. On
Windows, use `py` if `python` is not on your PATH.
### Claude Code
```bash
claude mcp add strata -- python C:\Users\you\Strata\tools\strata_mcp.py # Windows
claude mcp add strata -- python3 /home/you/Strata/tools/strata_mcp.py # Linux
```
Add `--scope user` to have it in every project. `/mcp` in Claude Code shows that it is connected.
### Claude Desktop
Open Settings > Developer > Edit Config (`claude_desktop_config.json`) and add:
```json
{
"mcpServers": {
"strata": {
"command": "python",
"args": ["C:\\Users\\you\\Strata\\tools\\strata_mcp.py"]
}
}
}
```
On Linux, use `"command": "python3"` and `"args": ["/home/you/Strata/tools/strata_mcp.py"]`. Restart Claude
Desktop after the change.
### Cursor
Use `~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` in one project. The format is the same as
Claude Desktop's:
```json
{
"mcpServers": {
"strata": {
"command": "python",
"args": ["C:\\Users\\you\\Strata\\tools\\strata_mcp.py"]
}
}
}
```
### VS Code (GitHub Copilot agent mode)
Use `.vscode/mcp.json` in a workspace, or run "MCP: Open User Configuration" from the command palette:
```json
{
"servers": {
"strata": {
"type": "stdio",
"command": "python",
"args": ["C:\\Users\\you\\Strata\\tools\\strata_mcp.py"]
}
}
}
```
On Linux, use `"command": "python3"` and `"args": ["/home/you/Strata/tools/strata_mcp.py"]`.
### Codex CLI
In `~/.codex/config.toml`:
```toml
[mcp_servers.strata]
command = "python"
args = ['C:\Users\you\Strata\tools\strata_mcp.py'] # Linux: command = "python3", args = ["/home/you/Strata/tools/strata_mcp.py"]
tool_timeout_sec = 180 # strata_stop / strata_benchmark can take longer than 60 s
```
### Any other MCP client
The command is `python <Strata folder>/tools/strata_mcp.py`. The transport is stdio. It speaks MCP revision
2025-06-18, and also 2025-03-26 and 2024-11-05.
`--root <folder>` manages a Strata folder other than the one the file is in. For example:
`python D:\tools\strata_mcp.py --root D:\Strata`.
## The tools
| Tool | What it does | Arguments (all optional) |
| --- | --- | --- |
| `strata_status` | Whether a model is running: the model, engine version, context, busy or idle, VRAM and RAM in use. What is installed: models, start scripts, engine version, data folder, downloads. An install in progress. This PC's hardware: OS, CPU, RAM, GPUs, page file, free disk. The model and context setup recommends for this PC. | `port`, `hardware` (true/false) |
| `strata_models` | Every family and size setup offers, with download size, RAM needed, whether it fits this PC, and whether it is installed. Also the context lengths and the recommendation. | - |
| `strata_install` | Runs setup without questions, in the background (see below). Without `confirm: true` it only returns the plan. A second call shows the progress. `cancel: true` stops the install. | `family` (qwen, swift, coder, unsloth), `model` (Q2_0, IQ2_XS, IQ3_XXS, IQ3_S, IQ1_M, UD-Q4_K_XL), `context` (8192 ... 524288), `vision` (no, yes, gpu, cpu), `kv` (int8, q4_0, k8v4), `gpu` (a number) or `gpus` ("0,2", "all"), `backend` (auto, cuda, hip), `low_ram`, `port`, `data_dir`, `confirm`, `cancel` |
| `strata_start` | Starts an installed model in the background and waits until it answers. If it is not ready after `wait_seconds` (default 50), it returns "loading". | `model` (an id from `strata_status`, e.g. `q2_0`, `coder-iq1_m`; default: the most recently used), `port`, `gpu`, `wait_seconds` (0-600) |
| `strata_stop` | Unloads the model through the server's own API, then ends the server it started. | `port`, `force` |
| `strata_logs` | The last lines of the server log, the engine log or the setup log. | `source` (server, engine, setup), `model`, `lines` (1-500) |
| `strata_benchmark` | Sends a short fixed request (greedy, thinking off) and reports output and prompt tokens/s. | `port`, `max_tokens` (16-512) |
| `strata_connect_info` | The OpenAI and Anthropic base URLs, the model id, whether a key is needed, and settings for Claude Code, Codex, the OpenAI SDK, curl, Cursor, Continue and Open WebUI. | `port` |
Every result is JSON with a one-line `summary`. It is sent both as text and as MCP structured content.
## How it works
**Install.** `strata_install` uses setup's own logic and tables (it imports `setup.py`'s `MODELS`, `FAMILIES`,
`CONTEXTS` and RAM checks) to fill in what you left out:
- the size setup itself would pick: IQ3_XXS from 60 GB of RAM, Q2_0 from about 44 GB, the Coder from about 28 GB,
and the Coder in the low-RAM mode below that when the GPU makes up for the RAM;
- the context setup recommends for the GPU's VRAM;
- images off.
The first call returns the plan: the download size (58-111 GB), the free disk space, the RAM check, and the exact
setup command. The assistant should show you the plan and call again with `confirm: true` once you agree.
It then runs the same steps `START-HERE.bat` / `setup.sh` run:
1. It creates `.venv` with the Python it runs on, if `.venv` is missing.
2. It runs `setup.py --yes --no-start --family ... --model ... --context ... --vision ...` as a background process.
The output goes to `.strata-mcp/setup.log` in the Strata folder.
The tool returns at once. Call it again to see the step (1-7), the download percentage, or setup's error message.
Everything setup has finished is skipped on the next run, and downloads resume. If the AI app closes, the install
keeps running. The install is refused while a model this server started is running, because setup may replace the
engine files that model uses.
**Start.** `strata_start` runs the same command as the `run-<model>` script, `serve/server.py --engine strata
--config strata-<model>.json --port N`, with the venv's Python, without `--open` (no browser) and without a console
window. The server's output goes to `.strata-mcp/server.log`. The tool then polls `GET /health` until the model
answers. Loading takes 1-3 minutes. If the server ends while loading, the tool returns the end of its log. The model
keeps running after the AI app closes, until `strata_stop`. If the AI app runs its tools inside a Windows job that
does not allow processes to leave it, the model ends together with the app.
Two cases are refused:
- a model is already running: the tool returns it and starts nothing;
- another program uses the port: the tool says so.
**Stop.** `strata_stop` first sends `POST /unload`. The server sends the engine `QUIT`, and the engine frees its
VRAM and pinned RAM itself, as when you close the window. Then the server ends:
- Linux: SIGTERM, which runs the server's own Ctrl+C path.
- Windows: the process ends. Its job object ends the vision encoder and the MCP servers it started.
While a request is running, the tool refuses unless you pass `force: true`.
**Status.** The tool reads `GET /health`, `/v1/status`, `/status` and, on older servers, `/metrics` on 127.0.0.1.
If a config of the folder sets an `api_key`, the tool sends that key with its own requests. It never shows the key.
## Safety
- **No shell, no free paths.** Every argument is checked against a fixed schema and against setup's own choices. An
unknown argument, a model or family setup does not offer, or a `gpus` value other than digits and commas is
refused before anything runs. Commands are passed as argument lists; nothing goes through a shell.
- **Paths.** The only path argument is `data_dir`. It must be one of:
- the current Strata data folder;
- a folder inside the Strata folder;
- a new absolute folder whose name starts with `Strata` (e.g. `D:\Strata-data`), whose parent exists, and which is
not in a system folder.
`..`, relative paths and network (UNC) paths are refused. Logs are read only from `.strata-mcp/` and from the
engine log a config names, if that log is inside the Strata folder or the data folder.
- **Only its own processes.** The server records the process id *and start time* of every process it starts, in
`.strata-mcp/*.json`. It only ends a process when both match, so a process id the OS has given to another program
is never touched. A Strata started some other way (its window, a run script, a service) is only asked over its
API to unload the model. To end that server, close its window.
- **Big downloads need a yes.** `strata_install` does nothing until it is called with `confirm: true`. The plan
shows the download size and the free space first, and the install is refused if the disk is too small.
- **Local only.** The server checks `127.0.0.1` and never changes the network settings: it does not install with
`--host 0.0.0.0` or an API key. To share Strata on your network, run setup yourself (see
[Using it](DETAILS.md#using-it)). Experimental options are left off too: the speed projection, rope scaling past
262K and `--build`. Run setup by hand for those.
## Troubleshooting
- **The assistant does not see the tools.** Run the command by hand:
`python C:\Users\you\Strata\tools\strata_mcp.py`. It should print `[strata-mcp] ready (Strata folder ...)` to
stderr and wait for input (Ctrl+C ends it). If it reports `not a Strata folder`, the path is wrong or `--root` is
needed. Python older than 3.10 is refused.
- **The install failed.** Ask the assistant for the setup log (`strata_logs` with `source: setup`), or open
`.strata-mcp/setup.log`. Setup's `[X]` line says what to fix. Then install again: every finished step is skipped.
- **The model does not start.** Get the server log with `strata_logs`, and the engine log with `source: engine`.
[Troubleshooting](DETAILS.md#troubleshooting) has the usual causes.
- **Tests.** `python -m unittest tools/test_strata_mcp.py` runs the tests with a fake setup and server. They do not
download anything or use the GPU.
+139
View File
@@ -0,0 +1,139 @@
# Which model? Sizes, versions and what fits
Strata runs one model, [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), in several **sizes**
(the same model, compressed more or less) and several **versions** (the original, a coding version, a fine-tune).
The installer recommends one for your PC; this page explains the choice. Back to the [README](../README.md).
> **On this page:** [Pick by RAM](#pick-by-ram) · [Speed](#how-fast-is-each-size) · [The sizes](#the-sizes) ·
> [Will it fit?](#will-it-fit) · [The versions](#the-versions) · [Adding another model](#adding-or-switching-models)
## Pick by RAM
| Your RAM | Take | Why |
| --- | --- | --- |
| **32 GB** | **Coder** | it fits 32 GB, and it is made for code (with a 24 GB card, Q2_0 and IQ2_XS run too: [low-RAM mode](#a-big-graphics-card-and-little-ram)) |
| **48 GB** | **IQ2_XS** (or Q2_0, the fastest) | the larger sizes do not fit |
| **64 GB** | **IQ2_XS** (recommended), or IQ3_XXS / IQ3_S | every size fits (IQ3_S with little else open) |
| **96 GB or more** | **IQ3_S**, or [Unsloth's 4-bit](#unsloth-ud-q4_k_xl-experimental) (experimental) | room for the largest sizes |
Not sure? Take **IQ2_XS** - or the **Coder** if you mainly write code, or have 32-48 GB of RAM.
## How fast is each size
Measured on an RTX 5070 (12 GB), a Ryzen 5 7600 and 64 GB of RAM:
| Size | Writes answers (short chat) | Writes answers (128K context) | Reads your prompt |
| --- | ---: | ---: | ---: |
| **Q2_0** | 93 tokens/s | 74 tokens/s | 2,170 tokens/s |
| **IQ2_XS** | 79 tokens/s | 63 tokens/s | 2,090 tokens/s |
| **IQ3_XXS** | 62 tokens/s | 49 tokens/s | 1,750 tokens/s |
| **IQ3_S** | 53 tokens/s | 46 tokens/s | 1,620 tokens/s |
| **Coder** (IQ1_M) | 55 tokens/s | 43 tokens/s | 2,180 tokens/s |
Measured on an AMD RX 9070 XT (16 GB), a Ryzen 9 3900X and 47 GB of RAM (Linux, setup's own install; Q2_0 is
above setup's RAM estimate for 47 GB and was installed with `--model Q2_0 --yes`):
| Size | Writes answers (short chat) | Writes answers (128K context) | Reads your prompt |
| --- | ---: | ---: | ---: |
| **Q2_0** | 60 tokens/s | 48 tokens/s | 1,160 tokens/s |
| **IQ2_XS** | 52 tokens/s | 36 tokens/s | 1,110 tokens/s |
| **Coder** (IQ1_M) | 44 tokens/s | 33 tokens/s | 1,420 tokens/s |
- **Writes answers** = how fast the reply appears (tokens per second; a token is about ¾ of a word).
- **Reads your prompt** = how fast it takes in what you send (long documents, code, chat history), measured on a
32K-token prompt; a 4K prompt reads at 910-1,580 tokens/s. A 32K prompt takes about 15 seconds with Q2_0.
A card with more VRAM is faster, because more of the model fits on the GPU: an RTX 3090 (24 GB) should do roughly
100-140 tokens per second. All measurements, long-context numbers and estimates for other cards are in the
[details](DETAILS.md#speed-measured). The AMD measurements per card (RX 9070 XT, Radeon AI PRO R9700, RX 7800 XT,
RX 9060 XT, RX 6900 XT) are in [AMD_HIP.md](AMD_HIP.md#rdna4-gfx1201).
Every PC is different: `START-HERE.bat --calibrate` measures a few engine settings on yours and keeps the fastest
(about 5-10 minutes; on the PC above it made the Coder 7% faster; NVIDIA cards for now). Measured Strata on your own
PC? See [Community benchmark results](COMMUNITY_BENCHMARKS.md) for a report template and how to share your results
in a pull request.
## The sizes
| Model | RAM+VRAM Requirements | Speed | Quality |
| --- | ---: | --- | --- |
| **Q2_0** | 37.6 GB | fastest | good |
| **IQ2_XS** | 39.2 GB | fast | better (**recommended**) |
| **IQ3_XXS** | 47.0 GB | slower | great |
| **IQ3_S** | 54.8 GB | slowest | best: matches the full model on the published tests (original model only) |
The download is 66-76 GB for the three smaller sizes ([details](DETAILS.md#which-model)); the first start also
fetches the MTP draft layer (~6 GB, +1 GB with images).
## Will it fit?
Shard 1 is the part of the model that gets loaded when it starts: its experts go into your **RAM**, the rest onto
your graphics card (the second shard, a 29 GB lookup table, stays on the SSD). So it fits when your **RAM is at least
shard 1 + about 10 GB** for Windows and your other programs. With 64 GB of RAM every size fits (IQ3_S with little
else open); with 48 GB, Q2_0 and IQ2_XS. A bigger graphics card makes it faster, but it doesn't lower the RAM needed
- except in the low-RAM mode below.
### A big graphics card and little RAM
On a PC whose RAM cannot hold the experts beside the system, setup maps them from the model's files instead and keeps
in RAM only what the graphics card does not hold (the [low-RAM mode](DETAILS.md#speed-measured), chosen by setup). For
example, a 32 GB PC with a 24 GB GPU runs Q2_0, IQ2_XS and the Coder this way, and a 32 GB PC with a 12-16 GB GPU the
Coder. With a small card most experts then come from the SSD and it is much slower (setup says so).
`START-HERE.bat --setup --low-ram on|off` overrides the choice.
## The versions
### Qwen3.8-Flash-Next
The original, in all four sizes. With images, and with the
[experimental speed projection](DETAILS.md#experimental-speed-projection-experimental-off-by-default) as an option.
### Coder
**[Coder](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF)** - ISTA-DASLab's coding version:
half of the experts removed, keeping the ones that code, tool use and images need (91% of the full model's SWE-bench
Verified score, 99% of LiveCodeBench, by its authors). One size (IQ1_M: its experts stored like IQ3_S): shard 1 is
**29.6 GB**, so it fits a PC with **32 GB of RAM**, runs 262K context on 64 GB, and reads long prompts the fastest of
all. It is weaker outside code, and that includes Chinese and other CJK text (#438: Chinese answers came out wrong
or looping where English was fine). For general chat or CJK text, take a size that keeps every expert: Q2_0,
IQ2_XS or IQ3_S.
More: [details](DETAILS.md#or-the-coder-half-the-experts-for-code).
```
START-HERE.bat --setup --family coder
```
### Swift 1.5
**[Swift 1.5](https://huggingface.co/ukisai/Swift-1.5-Qwen3.8-Flash-Next-GSQ-RCO-GGUF)** - a fine-tune by UkisAI that
thinks much shorter before answering, so you get the answer sooner, with about the same quality. Same speed per
token, and about the same RAM as the same size of the original (no IQ3_S). Its own license applies (see its page).
More: [details](DETAILS.md#or-swift-15-a-fine-tune-that-thinks-shorter).
```
START-HERE.bat --setup --family swift --model IQ2_XS
```
### Unsloth UD-Q4_K_XL (experimental)
**Unsloth's 4-bit UD-Q4_K_XL** (experimental) is the fourth version in setup's menu (`--family unsloth`): the closest
to the full model, but a 111 GB download whose 77 GB of experts do not fit in RAM. Strata keeps your RAM minus 24 GB
of them in RAM and reads the rest from the SSD while it answers: 7-8.5 tokens/s on a 64 GB PC with a 12 GB GPU, several
times slower than the sizes above, and long prompts are slow. It needs 48 GB of RAM or more, an NVMe SSD and one
NVIDIA GPU (no images yet). Details and measurements: [UD-Q4_K_XL](UNSLOTH_Q4.md).
```
START-HERE.bat --setup --family unsloth --model UD-Q4_K_XL
```
### OrcaRouter Uncensored IQ3_XXS
For **OrcaRouter's Flash-Next Uncensored IQ3_XXS**, see the [manual compatibility setup](ORCA.md). It needs an
explicit packing conversion and is not an installer menu option.
## Adding or switching models
You can add another model any time with `SETUP.bat` (the same as `START-HERE.bat --setup`; on Linux
`./setup.sh --setup`). Files another model shares are not downloaded again (the Coder uses the original's shard 2 and
vision encoder). With more than one model installed, `START-HERE.bat` asks which one to start; `run-<model>.bat`
(Linux: `run-<model>.sh`) starts one directly.
+2 -1
View File
@@ -43,7 +43,8 @@ now on; the answer is kept.
**Not supported** (setup says so and names the cards that can be used instead):
- a card older than the RTX 20 series (compute capability below 7.5: GTX 10 and older);
- a card with less than 8 GB of VRAM, together with others (each card holds a copy of the dense weights and its
own prompt buffers);
own prompt buffers) - unless you name it with `--gpus`: then setup says the risk and asks (`--yes` with the named
cards goes ahead);
- Intel GPUs, and a mix of NVIDIA and AMD cards. (AMD cards share a model among themselves: `./setup.sh --backend
hip --gpus 1,0`, see [AMD_HIP.md](AMD_HIP.md).)
+70
View File
@@ -0,0 +1,70 @@
# Something went wrong?
The common problems and what to do. Back to the [README](../README.md#something-went-wrong). The full table of
error messages, with the older engine fixes, is in the [details](DETAILS.md#troubleshooting).
## While installing or starting
**My PC froze, or got very slow, the first time Strata started.**
That's normal while it starts, most of all the first time. Strata loads 35-55 GB into your RAM, locks part of it for
the graphics card, and works out how much of the model fits on your GPU. The mouse can freeze for a few minutes.
**Wait, and don't close the window.** The next starts are much faster. Still frozen after 10 minutes? Restart the
PC, close other programs (browsers use a lot of RAM) and try again. If it keeps happening, pick a smaller size (Q2_0
or IQ2_XS).
**It stopped while downloading or installing.**
Run `START-HERE.bat` (Linux: `./setup.sh`) again. It continues where it stopped.
**It says the NVIDIA driver is too old.**
Update it (NVIDIA App or [nvidia.com/drivers](https://www.nvidia.com/drivers)), restart the PC, and run
`START-HERE.bat` again.
**It says port 8080 is already in use.**
Strata is already running. Look for its window. Or another program uses the port: `START-HERE.bat --port 8081`.
**Python or the build tools could not be installed.**
Install what it names (links are printed), then run it again. Everything already done is kept.
**The first start takes minutes.**
It is reading 34-55 GB into RAM; the second start is faster while the files are in the OS cache. Started from Task
Scheduler, it can be 24x slower: see [Running it at startup](DETAILS.md#running-it-at-startup-task-scheduler).
## While it answers
**It's very slow and the disk light keeps blinking.**
Your PC is out of free RAM. Close other programs, or pick a smaller size (Q2_0 or IQ2_XS).
**An answer stopped with "the engine stopped unexpectedly".**
Usually not enough RAM (on Linux the system then stops the engine). Just send your message again: Strata starts the
engine by itself. If it keeps happening, close other programs or pick a smaller size.
**It says the prompt exceeds the context.**
The conversation is longer than the context you chose. Start a new chat, or run `SETUP.bat` and pick more
context.
**It is slower than the tables.**
The monitor plugged into the graphics card and other GPU programs take VRAM from the expert cache; RAM running below
its rated speed (enable EXPO/XMP in the BIOS) slows the CPU half.
**Pictures are refused, or slow.**
"this server was started without the vision encoder": the model was set up for text only - run setup again with
`--vision gpu` (or `--vision cpu`). Pictures that take 10-30 s are read by the encoder on the CPU; `--vision gpu`
(NVIDIA, ~1.4 GB of VRAM) makes it 0.1-0.5 s.
## AMD cards
**"No AMD GPU found (the amdgpu driver's KFD topology is empty)" on Linux.**
The kernel's amdgpu driver is not loaded for the card. Integrated Radeon GPUs are listed as not supported; setup
lists every card it found and whether Strata can use it.
**The engine stops at start with the card's name, its architecture and the build's list.**
The engine was compiled for another card (for example after moving the Strata folder to another PC). Run
`./setup.sh --setup --backend hip`: it compiles the engine for this card's architecture.
**Large pinned host allocations fail on ROCm although RAM is free.**
See [AMD_HIP.md](AMD_HIP.md#model-and-serving-configuration): the mapped expert mode avoids the full pinned arena.
## Still stuck?
Look in the [full troubleshooting table](DETAILS.md#troubleshooting), or open an
[issue](https://github.com/Niko1221/Strata/issues) and attach `strata-<model>.log` from the Strata folder.
+7 -4
View File
@@ -22,15 +22,18 @@ START-HERE.bat --setup --family unsloth --model UD-Q4_K_XL
What setup does differently for this model:
- It needs 48 GB of RAM or more and engine 0.1.32 or newer (checked before anything is downloaded), and an NVIDIA
GPU. One GPU only: with `--gpus` it uses the first one and says so. No images (the vision encoder is not wired to
- It needs 48 GB of RAM or more (with less it asks, default no; `--model UD-Q4_K_XL --yes` installs it anyway) and
engine 0.1.32 or newer (checked before anything is downloaded), and an NVIDIA
GPU: it has not been run on AMD cards (its prompt kernels for the Q4_K / Q5_K experts are NVIDIA-only), so with
`--backend hip` setup says so and asks before the download (#429; `--model UD-Q4_K_XL --yes` tries it). One GPU only: with `--gpus` it uses the first one and says so. No images (the vision encoder is not wired to
this file yet) and no experimental speed projection (not tested with it).
- It downloads the four shards below from the pinned revision `38bb39e` (resumable, like the other models), then
checks each one's size and SHA-256 against the table below; the check takes a few minutes once and is remembered
in the file's finish mark. A file with the wrong hash is deleted, so the next run downloads it again.
- It packs with `--compat-bf16` and never writes `experts.bin` (`--low-ram` does not apply).
- The RAM budget is the PC's RAM less 24 GB: `--resident-budget-gib 40` on 64 GB, at most all 71 GiB of experts on
96 GB or more. From a 64K context up, where the KV cache moves to RAM, its size comes out of the budget.
96 GB or more. From a 64K context up, where the KV cache moves to RAM, its size comes out of the budget. Setup's
`--resident-budget-gib N` sets another one (a bigger one is kept, with a note on what it risks).
- It recommends an 8K context on a GPU under 14 GB (every GB of KV cache is a GB less of cached experts).
## The files
@@ -130,7 +133,7 @@ budget take first.
**Choosing the RAM budget N:** your RAM minus 20-24 GB (the OS, the engine itself, the 126 MB router copy, and room for
the OS file cache that serves the rest). On 64 GB, 40. The engine clamps a budget larger than the RAM it finds free
(minus 4 GB) and says so. Everything above N comes from the SSD for every token, so N is the setting that matters
(minus 4 GB and a 256 MiB margin) and says so; before #403's fix such a clamped budget could then fail the start. Everything above N comes from the SSD for every token, so N is the setting that matters
most; a bigger budget was faster in every measurement (24 / 32 / 40 GiB). When the driver page-locks the whole budget
(24 GiB did on this PC, 32 and 40 did not), the GPU also computes a share of the misses over PCIe, as in the resident
low-RAM mode; `STRATA_RESIDENT_PIN=0` keeps the budget locked only (the CPU then computes every miss).
+10
View File
@@ -38,6 +38,16 @@ std::string gpu_arch_problem(int ordinal);
// The GPU architectures this binary was compiled for ("gfx1100,gfx1201"); "" on CUDA builds.
const char* compiled_gpu_archs();
// How many devices the runtime enumerates, numbered as HIP_VISIBLE_DEVICES / CUDA_VISIBLE_DEVICES number them; 0 when
// there is none (or no usable runtime). `device_info` throws on a card this binary cannot run, so a caller that LISTS
// every card - the ones it has no code for included (`strata-device --list-devices`) - counts them with this first.
int device_count();
// One line per device for `strata-device --list-devices`, without the arch check that `device_info` applies:
// "arch gfx1201, 15.9 GiB" (HIP) or "compute capability 12.0, 11.9 GiB" (CUDA), plus the device's name. false when
// the runtime cannot describe it.
bool device_summary(int ordinal, std::string& name, std::string& detail);
// Throws when there is no CUDA device. The engine targets sm_120 specifically and must say so rather than
// run slowly on something else: `CMakeLists.txt` already refuses to COMPILE for another architecture, and
// this is the matching check at run time (a binary can be carried to a different machine).
+4
View File
@@ -421,6 +421,10 @@ public:
/// CS-T, `budget_bytes` > 0 (`--resident-budget-gib`): only as many of those experts as fit `budget_bytes`, taken
/// in `rank` order (the expert profile: the hottest after the GPU cache's), are copied; the rest stay on the
/// mapped files (the SSD tier). No lend region then (a lent slot's expert is read from the files).
/// #467: `budget_bytes` = `kResidentWhatFits` is that path sized by the RAM alone (available minus the headroom
/// and the #403 margin) - the soft --resident-experts mode's second try when the whole complement does not fit;
/// false when not even one expert fits. On Windows the mapped experts leave the working set before any reading.
static constexpr uint64_t kResidentWhatFits = ~0ull;
bool pin_cache_complement(
const ExpertCache& cache, std::string& err, bool pin = true,
const std::vector<std::pair<int32_t, int32_t>>& additional_gpu_pairs = {}, int64_t lend_from_slot = -1,
+10
View File
@@ -1,5 +1,14 @@
#pragma once
// Included only by STRATA_ENABLE_HIP builds. CUDA builds use NVIDIA headers.
#if defined(_WIN32)
// hip_runtime.h pulls in <windows.h> on Windows: keep its min/max macros and the rarely used APIs out of the engine.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#endif
#include <hip/hip_runtime.h>
// Do not let HIP's legacy macro corrupt libstdc++ attribute names.
#ifdef __noinline__
@@ -9,6 +18,7 @@
#define cudaDevAttrMultiProcessorCount hipDeviceAttributeMultiprocessorCount
#define cudaDevAttrClockRate hipDeviceAttributeClockRate
#define cudaDevAttrComputeCapabilityMajor hipDeviceAttributeComputeCapabilityMajor
#define cudaDevAttrComputeCapabilityMinor hipDeviceAttributeComputeCapabilityMinor
#define cudaDeviceGetAttribute hipDeviceGetAttribute
#define cudaDeviceProp hipDeviceProp_t
#define cudaDeviceSynchronize hipDeviceSynchronize
+4
View File
@@ -15,6 +15,10 @@ bool built();
/// MMQ covers this ggml type (the i-quants and Q2_0 the packs use, Q8_0, and in a CUDA build with STRATA_MMQ_KQUANTS
/// the Q4_K / Q5_K / Q5_1 of Unsloth's UD-Q4_K_XL; IQ1_M is not covered).
bool supported(int ggml_type);
/// #420: `supported`, and on every visible GPU llama.cpp's MMQ has a tile for this type and a weight matrix of
/// `w_rows` rows that fits the card's shared memory - the same test its tile choice makes, which aborts the process
/// ("J_best=0") when nothing fits. false (said once per type) keeps that product on the non-MMQ path.
bool fits(int ggml_type, int64_t w_rows);
/// Bytes of one expert's gate+up ([2*n_ff, n_embd]) or down ([n_embd, n_ff]) weights in `ggml_type`.
size_t matrix_bytes(int ggml_type, int64_t rows, int64_t cols);
/// Bytes of `rows` activation rows of `cols` values quantized for MMQ (the row padded to 512 values).
+28 -4
View File
@@ -25,12 +25,17 @@ from jinja2.sandbox import ImmutableSandboxedEnvironment
# ------------------------------------------------------------------------------------------------ template
class TemplateRequestError(jinja2.exceptions.TemplateError, ValueError):
"""The template refused the request's messages (e.g. "No user query found in messages."): a ValueError, so the
client gets a 400 with the template's message instead of a dropped connection (#365)."""
class ChatTemplate:
"""The model's chat template, rendered with the same Jinja settings as transformers' apply_chat_template."""
def __init__(self, path: str | Path):
def raise_exception(message):
raise jinja2.exceptions.TemplateError(message)
raise TemplateRequestError(message)
def tojson(x, ensure_ascii=False, indent=None, separators=None, sort_keys=False):
return json.dumps(x, ensure_ascii=ensure_ascii, indent=indent, separators=separators, sort_keys=sort_keys)
@@ -138,10 +143,27 @@ def _late_system_to_user(messages: list[dict]) -> list[dict]:
return [dict(m, role="user") if m.get("role") == "system" and i > 0 else m for i, m in enumerate(messages)]
def _object_list(value, name: str) -> list[dict]:
"""#460: a request's "messages" (or a message's "tool_calls") as a list of objects. Some clients send the array
double-encoded, as a JSON string, which used to be iterated character by character and crashed on m.get: such a
string is decoded. Anything that is still not a list of objects is a ValueError, which the server answers with
a 400 naming the field. None (or no field) is an empty list, as a missing field always was."""
if value is None:
return []
if isinstance(value, str):
try:
value = json.loads(value)
except ValueError:
raise ValueError(f"{name} must be a list of objects (a string was sent that is not JSON)") from None
if not isinstance(value, list) or not all(isinstance(m, dict) for m in value):
raise ValueError(f"{name} must be a list of objects")
return value
def openai_to_messages(req: dict) -> tuple[list[dict], list[dict] | None, dict]:
"""OpenAI Chat Completions -> (template messages, template tools, template kwargs)."""
messages = []
for m in req.get("messages", []):
for m in _object_list(req.get("messages"), "messages"):
role = m.get("role")
if role == "developer":
role = "system"
@@ -150,8 +172,10 @@ def openai_to_messages(req: dict) -> tuple[list[dict], list[dict] | None, dict]:
out["reasoning_content"] = m["reasoning_content"]
if m.get("tool_calls"):
calls = []
for c in m["tool_calls"]:
for c in _object_list(m["tool_calls"], "tool_calls"):
fn = c.get("function", c)
if not isinstance(fn, dict):
raise ValueError("tool_calls must be a list of objects (each with a \"function\" object)")
args = fn.get("arguments")
if isinstance(args, str): # the template requires a mapping, not a JSON string
args = json.loads(args) if args.strip() else {}
@@ -181,7 +205,7 @@ def anthropic_to_messages(req: dict, think_unasked: bool = True) -> tuple[list[d
system = req.get("system")
if system:
messages.append({"role": "system", "content": _text_of(system)})
for m in req.get("messages", []):
for m in _object_list(req.get("messages"), "messages"):
content = m.get("content")
if isinstance(content, str):
messages.append({"role": m["role"], "content": content})
+82 -9
View File
@@ -31,7 +31,9 @@ import json
import os
import queue
import re
import select
import signal
import socket
import subprocess
import sys
import tempfile
@@ -585,10 +587,12 @@ class Vision:
return self.cache[key]
img, out = self.dir / f"{key}.img", self.dir / f"{key}.sve"
img.write_bytes(data)
self.proc.stdin.write(f"ENC {img} {out}\n")
self.proc.stdin.flush()
line = self.proc.stdout.readline().strip()
img.unlink(missing_ok=True)
try:
self.proc.stdin.write(f"ENC {img} {out}\n")
self.proc.stdin.flush()
line = self.proc.stdout.readline().strip()
finally: # #352: also when the encoder's pipe is gone
img.unlink(missing_ok=True)
if not line.startswith("OK"):
raise ValueError("the image could not be read: " + (line[4:] if line.startswith("ERR") else
"the vision encoder stopped"))
@@ -629,12 +633,28 @@ def engine_args(cfg: dict) -> list[str]:
return args
def hip_visible(cfg: dict) -> list[int]:
"""AMD: the devices the engine should see, as the HIP runtime numbers them (HIP_VISIBLE_DEVICES).
On Linux setup's KFD order is HIP's order, so the config's "gpu" is it. On Windows setup finds the cards in the
display-adapter order, and an integrated Radeon that HIP also enumerates takes ordinal 0 and pushes the discrete
card to 1 (#325): setup records the ordinal `strata-device --list-devices` gave the card as "hip_ordinal", which
wins for a one-card config. Without it (a config from before), the config's "gpu"."""
ordinal = cfg.get("hip_ordinal")
if ordinal is not None and str(ordinal).strip() != "" and len(gpu_list(cfg)) <= 1:
try:
return [int(str(ordinal).strip())]
except ValueError:
pass
return gpu_list(cfg)
def child_env(cfg: dict) -> dict:
"""The engine's environment: the CUDA libraries setup installed (pip's nvidia packages, or the toolkit that
compiled it) first on the library search path."""
env = dict(os.environ)
if gpu_list(cfg) and cfg.get("backend") == "hip": # AMD: numbered as HIP numbers them (setup's KFD order)
env["HIP_VISIBLE_DEVICES"] = ",".join(str(i) for i in gpu_list(cfg))
if hip_visible(cfg) and cfg.get("backend") == "hip": # AMD: numbered as HIP numbers them (hip_visible)
env["HIP_VISIBLE_DEVICES"] = ",".join(str(i) for i in hip_visible(cfg))
elif gpu_list(cfg): # issue #51: the GPU(s) to run on, numbered as nvidia-smi does; CUDA's
env["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" # own default order (fastest first) can number the cards otherwise
env["CUDA_VISIBLE_DEVICES"] = ",".join(str(i) for i in gpu_list(cfg))
@@ -647,6 +667,22 @@ def child_env(cfg: dict) -> dict:
return env
def vision_env(cfg: dict, env: dict) -> dict:
"""The image encoder's environment: the engine's, unless the config's vision section names its own "cuda_device"
(numbered like nvidia-smi) - then the encoder runs on that card alone, so a spare GPU can hold it while the engine
keeps all of its own cards' VRAM (#408, Efs-O). Without it, nothing changes."""
dev = (cfg.get("vision") or {}).get("cuda_device")
if dev is None:
return env
env = dict(env)
if cfg.get("backend") == "hip":
env["HIP_VISIBLE_DEVICES"] = str(dev)
else:
env["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
env["CUDA_VISIBLE_DEVICES"] = str(dev)
return env
class ByteTokenizer:
"""Tiny stand-in tokenizer for tests without the pack: one id per UTF-8 byte, specials as ids >= 256."""
SPECIALS = ["<|im_start|>", "<|im_end|>", "<|endoftext|>", "<|vision_start|>", "<|image_pad|>", "<|vision_end|>"]
@@ -729,7 +765,8 @@ class Service:
self.history = collections.deque(maxlen=500) # the last finished requests, newest last (GET /metrics)
# since the server started (the Monitor's totals, issue #35)
self.totals = {"since": time.time(), "requests": 0, "prompt_tokens": 0, "reused": 0, "output_tokens": 0,
"prompt_ms": 0.0, "decode_ms": 0.0}
"prompt_ms": 0.0, "decode_ms": 0.0,
"drafts_offered": 0, "drafts_accepted": 0} # #457: the MTP drafts, summed where reported
self.last_timings = None # the last finished request's, llama.cpp's names (/v1/status)
self.last_request_at = None # when a request last started or finished
self.started_at = time.time()
@@ -1302,7 +1339,10 @@ class Service:
"decode_tok_s": round(last["generated"] / (last["decode_ms"] / 1000), 1)
if n and last.get("generated") and last.get("decode_ms") else None,
"hit_rate": hit_rate, "ram_blobs": last.get("ram_blobs"),
"file_blobs": last.get("file_blobs"), "file_mb": last.get("file_mb")})
"file_blobs": last.get("file_blobs"), "file_mb": last.get("file_mb"),
# #457: the speculative drafts from the DONE line (None: the engine did not say)
"drafts_offered": last.get("drafts_offered"),
"drafts_accepted": last.get("drafts_accepted")})
t = self.totals
t["requests"] += 1
t["prompt_tokens"] += len(ids)
@@ -1310,6 +1350,8 @@ class Service:
t["output_tokens"] += n
t["prompt_ms"] += last.get("prompt_ms") or 0.0
t["decode_ms"] += last.get("decode_ms") or 0.0
t["drafts_offered"] += last.get("drafts_offered") or 0
t["drafts_accepted"] += last.get("drafts_accepted") or 0
fresh = getattr(self.engine, "last", None)
if fresh is not None and fresh is not before: # the engine's clock for THIS request
timings = request_timings(len(ids), n, last)
@@ -1688,10 +1730,36 @@ def make_handler(svc: Service):
protocol_version = "HTTP/1.0" # SSE ends by closing the connection
record = None # #332: this request's monitor record, if kept
watch_done = None # #430 #431: stops this request's disconnect watcher
def log_message(self, fmt, *args):
pass
def _watch_client(self, cancel: threading.Event) -> None:
"""#430 #431: cancel the request as soon as its client hangs up. A non-streamed request writes nothing
until it ends, and a streamed one only a keep-alive per prompt chunk (and the first write after a hang-up
usually still succeeds), so a dropped request kept the engine busy until its answer or the whole prompt
was done. Every 0.5 s: the socket readable with nothing to read (EOF) means the client closed it. A
request is HTTP/1.0 and fully read here, so no later bytes are expected - data is not a hang-up."""
done = self.watch_done = threading.Event()
sock = self.connection
def watch():
while not done.wait(0.5) and not cancel.is_set():
try:
readable, _, _ = select.select([sock], [], [], 0)
gone = bool(readable) and sock.recv(1, socket.MSG_PEEK) == b""
except (ConnectionError, TimeoutError):
gone = True
except (OSError, ValueError): # the socket was closed here: the request has ended
return
if gone:
self._note(outcome="disconnected")
cancel.set()
return
threading.Thread(target=watch, daemon=True, name="strata-client-watch").start()
def _note(self, **values):
"""#332: what the monitor shows about this request (nothing when the monitor is off)."""
if self.record is not None:
@@ -1944,6 +2012,8 @@ def make_handler(svc: Service):
self._note(outcome="disconnected")
raise # as before #332: the server's own handling
finally:
if self.watch_done is not None:
self.watch_done.set()
record = self.record
if record is not None:
with svc.status_lock:
@@ -2073,6 +2143,7 @@ def make_handler(svc: Service):
ids, thinking, max_new = svc.prepare(messages, tools, kw, max_new)
_debug_req("openai", req, messages, tools, max_new, thinking, len(ids))
cancel = threading.Event()
self._watch_client(cancel) # #430 #431
run = run_with_mcp(svc, svc.mcp, messages, tools, kw, ids, thinking, max_new, max_req, req, cancel,
{t["name"] for t in extra}) if use_mcp else None
chunks = openai_chunks(svc, req, ids, thinking, tools, max_new, cancel, run=run)
@@ -2124,6 +2195,7 @@ def make_handler(svc: Service):
ids, thinking, max_new = svc.prepare(messages, tools, kw, max_new)
_debug_req("anthropic", req, messages, tools, max_new, thinking, len(ids))
cancel = threading.Event()
self._watch_client(cancel) # #430 #431
events = anthropic_events(svc, req, ids, thinking, tools, max_new, cancel)
events = self._capture(events, "anthropic")
if not req.get("stream"):
@@ -2412,7 +2484,8 @@ def main() -> int:
vcfg = {k: (os.path.abspath(os.path.join(cfg.get("cwd") or ".", v))
if k in ("exe", "mmproj", "model") and isinstance(v, str) and not os.path.isabs(v) else v)
for k, v in cfg["vision"].items()}
vision = Vision(vcfg, log=open(cfg["log"], "a", encoding="utf-8") if cfg.get("log") else None, env=env)
vision = Vision(vcfg, log=open(cfg["log"], "a", encoding="utf-8") if cfg.get("log") else None,
env=vision_env(cfg, env))
print("model unloaded; the first request loads it ..." if lazy else
"loading the model (the first start takes a minute or two) ...", flush=True)
if len(gpu_list(cfg)) > 1:
+197
View File
@@ -455,6 +455,55 @@ class ClientShapes(unittest.TestCase):
for part in ("Be brief.", "Now use digits.", "Also this.", "1+1?"):
self.assertIn(part, text)
def test_no_user_turn_is_a_400(self):
# #365: the template's own refusal (Qwen's "No user query found in messages." when no turn is a user's query)
# answers 400, not a dropped connection
with tempfile.TemporaryDirectory() as d:
tpl = Path(d) / "chat_template.jinja"
tpl.write_text("{% if messages[-1].role != 'user' %}{{ raise_exception('No user query found in messages.') }}"
"{% endif %}{{ messages[-1].content }}", encoding="utf-8")
template, self.svc.template = self.svc.template, ChatTemplate(tpl)
try:
status, b = self.post("/v1/chat/completions", {
"model": "x", "max_tokens": 20, "messages": [{"role": "system", "content": "Only a system."}]})
self.assertEqual(status, 400, b)
self.assertIn("No user query found", b["error"]["message"])
finally:
self.svc.template = template
status, b = self.post("/v1/chat/completions", {"model": "x", "max_tokens": 20,
"messages": [{"role": "user", "content": "hi"}]})
self.assertEqual(status, 200, b) # the server goes on
def test_messages_as_a_json_string_over_http(self):
# #460: a double-encoded "messages" is answered; one that is not a list of objects is a 400, not a 500
encoded = json.dumps([{"role": "user", "content": "1+1?"}])
for path in ("/v1/chat/completions", "/v1/messages"):
with self.subTest(path=path):
status, b = self.post(path, {"model": "x", "max_tokens": 20, "messages": encoded})
self.assertEqual(status, 200, b)
self.assertIn("1+1?", self.prompt_text())
status, b = self.post(path, {"model": "x", "max_tokens": 20, "messages": ["hi"]})
self.assertEqual(status, 400, b)
self.assertIn("messages must be a list of objects", b["error"]["message"])
def test_vision_temp_image_removed_when_the_pipe_fails(self):
# #352: the temporary image goes even when the encoder's pipe raises
from serve.server import Vision
class Gone:
def write(self, _):
raise BrokenPipeError("the encoder is gone")
v = Vision.__new__(Vision)
v.dir, v.lock, v.cache = Path(tempfile.mkdtemp(prefix="strata-vision-test-")), threading.Lock(), {}
v.proc = mock.Mock(stdin=Gone())
with mock.patch.object(Vision, "load", return_value=b""), mock.patch.object(Vision, "normalize",
return_value=b"png"):
with self.assertRaises(BrokenPipeError):
v.encode("x")
self.assertEqual(list(v.dir.iterdir()), [])
v.dir.rmdir()
def test_leading_system_unchanged(self):
from serve.frontend import anthropic_to_messages, openai_to_messages
msgs, _, _ = openai_to_messages({"messages": [{"role": "developer", "content": "D"}, {"role": "user", "content": "u"}]})
@@ -462,6 +511,32 @@ class ClientShapes(unittest.TestCase):
msgs, _, _ = anthropic_to_messages({"system": "S", "messages": [{"role": "user", "content": "u"}]})
self.assertEqual([m["role"] for m in msgs], ["system", "user"])
def test_messages_sent_as_a_json_string(self):
# #460: a client that double-encodes "messages" (and "tool_calls") as a JSON string gets them decoded; what is
# still not a list of objects is a ValueError (the server's 400), not an AttributeError on m.get
from serve.frontend import anthropic_to_messages, openai_to_messages
call = [{"id": "c1", "type": "function", "function": {"name": "f", "arguments": "{\"x\": 1}"}}]
listed = [{"role": "user", "content": "u"}, {"role": "assistant", "content": "", "tool_calls": call}]
encoded = [listed[0], dict(listed[1], tool_calls=json.dumps(call))]
want = openai_to_messages({"messages": listed})[0]
self.assertEqual(want[1]["tool_calls"], [{"function": {"name": "f", "arguments": {"x": 1}}}])
self.assertEqual(openai_to_messages({"messages": json.dumps(listed)})[0], want)
self.assertEqual(openai_to_messages({"messages": json.dumps(encoded)})[0], want)
anth = [{"role": "user", "content": "u"}]
self.assertEqual(anthropic_to_messages({"messages": json.dumps(anth)})[0],
anthropic_to_messages({"messages": anth})[0])
self.assertEqual(openai_to_messages({})[0], []) # no field: nothing, as before
self.assertEqual(openai_to_messages({"messages": None})[0], [])
for bad in ("not json", "\"a string\"", json.dumps({"role": "user"}), ["hi"], [{"role": "user"}, 3], 5,
{"role": "user", "content": "u"}):
for fn in (openai_to_messages, anthropic_to_messages):
with self.subTest(bad=bad, fn=fn.__name__):
with self.assertRaisesRegex(ValueError, "messages must be a list of objects"):
fn({"messages": bad})
for calls in (["f"], "[1]", [{"function": "f"}], "{"):
with self.subTest(calls=calls), self.assertRaisesRegex(ValueError, "tool_calls must be a list of objects"):
openai_to_messages({"messages": [{"role": "assistant", "content": "", "tool_calls": calls}]})
class SamplingKeys(unittest.TestCase):
"""The GEN line's sampling keys: top_k 0 ("off") or wider than the engine's 64 get the widest list, 64 (they used
@@ -503,6 +578,30 @@ class GpuChoice(unittest.TestCase):
self.assertEqual(plain.get("CUDA_VISIBLE_DEVICES"), os.environ.get("CUDA_VISIBLE_DEVICES"))
self.assertEqual(plain.get("CUDA_DEVICE_ORDER"), os.environ.get("CUDA_DEVICE_ORDER"))
def test_vision_device(self):
# #408: the image encoder on its own card; the engine's environment stays as it was
from serve.server import child_env, vision_env
cfg = {"gpu": [0, 1], "vision": {"exe": "v", "cuda_device": 2}}
env = child_env(cfg)
venv = vision_env(cfg, env)
self.assertEqual(venv["CUDA_VISIBLE_DEVICES"], "2")
self.assertEqual(venv["CUDA_DEVICE_ORDER"], "PCI_BUS_ID")
self.assertEqual(env["CUDA_VISIBLE_DEVICES"], "0,1")
plain = {"gpu": [0, 1], "vision": {"exe": "v"}}
self.assertIs(vision_env(plain, env), env) # no cuda_device: the engine's environment, unchanged
def test_hip_ordinal(self):
"""#325: on Windows the HIP ordinal setup resolved wins over the config's "gpu" (an iGPU takes HIP's 0)."""
from serve.server import child_env
self.assertEqual(child_env({"backend": "hip", "gpu": 1})["HIP_VISIBLE_DEVICES"], "1") # Linux: KFD order
self.assertEqual(child_env({"backend": "hip", "hip_ordinal": 1})["HIP_VISIBLE_DEVICES"], "1")
self.assertEqual(child_env({"backend": "hip", "gpu": 0, "hip_ordinal": 1})["HIP_VISIBLE_DEVICES"], "1")
self.assertEqual(child_env({"backend": "hip", "gpu": [1, 0], "hip_ordinal": 2})["HIP_VISIBLE_DEVICES"],
"1,0") # a layer split keeps its list
self.assertEqual(child_env({"backend": "hip", "gpu": 0, "hip_ordinal": "x"})["HIP_VISIBLE_DEVICES"], "0")
plain = child_env({"backend": "hip"})
self.assertEqual(plain.get("HIP_VISIBLE_DEVICES"), os.environ.get("HIP_VISIBLE_DEVICES"))
class RecordingPrompt(MockEngine):
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
@@ -532,6 +631,59 @@ class DyingEngine(MockEngine):
yield t
class SlowPromptEngine(MockEngine):
"""Reads a "long prompt" for up to 20 s without a token (the engine sends nothing then), stopping on cancel."""
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
self.cancelled_after = None
t0 = time.monotonic()
while time.monotonic() - t0 < 20:
if cancel.is_set():
self.cancelled_after = time.monotonic() - t0
return
time.sleep(0.05)
yield from super().generate(ids, max_new, sampling, cancel, embeddings)
class ClientHangUp(unittest.TestCase):
"""#430 #431: a client that hangs up during a long prompt read cancels the request within about a second -
non-streamed (which writes nothing until the end) and streamed (one keep-alive per prompt chunk) alike."""
@classmethod
def setUpClass(cls):
tok = ByteTokenizer()
cls.engine = SlowPromptEngine(tok, "</think>\n\nOK", max_context=CTX)
cls.svc = Service(cls.engine, tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
cls.httpd = serve(cls.svc, port=0)
cls.port = cls.httpd.server_address[1]
@classmethod
def tearDownClass(cls):
cls.httpd.shutdown()
cls.httpd.server_close()
def hang_up(self, stream):
import socket as so
body = json.dumps({"model": "x", "max_tokens": 20, "stream": stream,
"messages": [{"role": "user", "content": "a long prompt"}]}).encode()
c = so.create_connection(("127.0.0.1", self.port))
c.sendall(b"POST /v1/chat/completions HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\n"
b"Content-Length: " + str(len(body)).encode() + b"\r\n\r\n" + body)
time.sleep(1.0)
c.close()
t0 = time.monotonic()
while self.engine.cancelled_after is None and time.monotonic() - t0 < 10:
time.sleep(0.05)
self.assertIsNotNone(self.engine.cancelled_after, "the request was not cancelled")
self.assertLess(self.engine.cancelled_after, 3.0)
def test_non_streamed(self):
self.hang_up(False)
def test_streamed(self):
self.hang_up(True)
class EngineDeath(unittest.TestCase):
"""Issue #27: a dead engine is an error (not "length"), and the next request starts it again."""
@@ -595,6 +747,51 @@ class EngineDeath(unittest.TestCase):
httpd.server_close()
class DoneLineEngine(MockEngine):
"""The mock engine whose `last` comes from a DONE line, parsed as StrataEngine parses it."""
def __init__(self, *a, done_lines=(), **kw):
super().__init__(*a, **kw)
self.done_lines = list(done_lines)
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
try:
yield from super().generate(ids, max_new, sampling, cancel, embeddings)
finally:
StrataEngine._parse_done(self, self.done_lines.pop(0))
class DraftCounts(unittest.TestCase):
"""#457: GET /metrics gives each request's speculative draft counts (offered / accepted, from the engine's DONE
line; None when the line has no such fields) and their running sums in the totals."""
def test_drafts_in_history_and_totals(self):
tok = ByteTokenizer()
engine = DoneLineEngine(tok, "</think>\n\nok", max_context=CTX, done_lines=[
"DONE 4 20 40.0 30.0 stop 7 12 0", # 7 of 12 drafts accepted
"DONE 4 20 40.0 30.0 stop", # an engine that reports no drafts
"DONE 4 20 40.0 30.0 stop 3 5 0 9 10"])
svc = Service(engine, tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
self.assertEqual((svc.totals["drafts_offered"], svc.totals["drafts_accepted"]), (0, 0))
httpd = serve(svc, port=0)
base = f"http://127.0.0.1:{httpd.server_address[1]}"
try:
for _ in range(3):
body = json.dumps({"model": "m", "max_tokens": 10, "messages": [{"role": "user", "content": "hi"}]})
with urllib.request.urlopen(urllib.request.Request(base + "/v1/chat/completions", data=body.encode(),
headers={"Content-Type": "application/json"}),
timeout=30) as r:
self.assertEqual(r.status, 200)
with urllib.request.urlopen(base + "/metrics", timeout=10) as r:
m = json.loads(r.read())
finally:
httpd.shutdown()
httpd.server_close()
rows = m["requests"] # newest first
self.assertEqual([(r["drafts_offered"], r["drafts_accepted"]) for r in rows], [(5, 3), (None, None), (12, 7)])
self.assertEqual((m["totals"]["drafts_offered"], m["totals"]["drafts_accepted"]), (17, 10))
class LiveRate(unittest.TestCase):
"""The Monitor's Speed readout: live.tok_s is a rate, and a request that never got a DONE keeps no counters.
+778 -108
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/sh
# Strata for Linux: the first run installs everything and starts the model; later runs just start it.
# Needs only an NVIDIA driver (or, experimental: an AMD RX 7900 XT/XTX with the amdgpu driver, see docs/AMD_HIP.md).
# Needs only an NVIDIA driver (or, for an AMD Radeon card, the kernel's amdgpu driver: see docs/AMD_HIP.md).
# Python (with venv) is installed through apt/dnf if it is missing (asks for sudo).
cd "$(dirname "$0")" || exit 1
# Python 3.10+ that can make a venv WITH pip: Debian/Ubuntu ship `venv` without `ensurepip` (that is the separate
+28
View File
@@ -73,6 +73,34 @@ const char* compiled_gpu_archs() {
#endif
}
int device_count() {
int count = 0;
if (cudaGetDeviceCount(&count) != cudaSuccess) { // HIP without a usable device reports an error, not 0
cudaGetLastError();
return 0;
}
return count < 0 ? 0 : count;
}
bool device_summary(int ordinal, std::string& name, std::string& detail) {
cudaDeviceProp p{};
if (ordinal < 0 || ordinal >= device_count() || cudaGetDeviceProperties(&p, ordinal) != cudaSuccess) {
cudaGetLastError();
return false;
}
char buf[160];
#if defined(STRATA_USE_HIP)
std::snprintf(buf, sizeof(buf), "arch %s, %.1f GiB, wave%d", base_arch(p.gcnArchName).c_str(),
(double) p.totalGlobalMem / (1024.0 * 1024 * 1024), p.warpSize);
#else
std::snprintf(buf, sizeof(buf), "compute capability %d.%d, %.1f GiB", p.major, p.minor,
(double) p.totalGlobalMem / (1024.0 * 1024 * 1024));
#endif
name = p.name;
detail = buf;
return true;
}
std::string gpu_arch_problem(int ordinal) {
#if defined(STRATA_USE_HIP)
int count = 0;
+40 -1
View File
@@ -9,6 +9,12 @@
#include <cstdio>
#include <cstring>
#include <string>
#if defined(STRATA_USE_HIP) && defined(_WIN32)
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
static std::string human(uint64_t b) {
char buf[64];
@@ -19,10 +25,14 @@ static std::string human(uint64_t b) {
int main(int argc, char** argv) {
bool selftest = false;
bool list_devices = false;
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--selftest") == 0) selftest = true;
else if (std::strcmp(argv[i], "--list-devices") == 0) list_devices = true;
else if (std::strcmp(argv[i], "--help") == 0 || std::strcmp(argv[i], "-h") == 0) {
std::printf("usage: strata-device [--selftest]\n");
std::printf("usage: strata-device [--selftest] [--list-devices]\n"
" --list-devices every GPU the runtime enumerates, numbered as HIP_VISIBLE_DEVICES /\n"
" CUDA_VISIBLE_DEVICES number them, and whether this binary can run it\n");
return 0;
} else {
std::fprintf(stderr, "unknown argument: %s\n", argv[i]);
@@ -30,6 +40,28 @@ int main(int argc, char** argv) {
}
}
// The runtime's numbering, which setup needs on Windows: there an integrated Radeon is HIP device 0 and pushes the
// discrete card to 1, while setup finds the cards in the display-adapter order (#325). No arch check here - the
// cards this binary has no code for are part of the answer. Format (setup.py's hip_devices parses it):
// device N: <name>
// arch gfx1201, 15.9 GiB, wave32 (CUDA: compute capability 12.0, 11.9 GiB)
// cannot run: <why> (only for a card this binary cannot run)
if (list_devices) {
const int count = strata::core::device_count();
if (count == 0) std::printf("(no GPU device)\n");
for (int ordinal = 0; ordinal < count; ++ordinal) {
std::string name, detail;
if (!strata::core::device_summary(ordinal, name, detail)) {
std::printf("device %d: (the runtime cannot describe it)\n", ordinal);
continue;
}
std::printf("device %d: %s\n %s\n", ordinal, name.c_str(), detail.c_str());
if (const std::string why = strata::core::gpu_arch_problem(ordinal); !why.empty())
std::printf(" cannot run: %s\n", why.c_str());
}
return 0;
}
try {
const strata::core::DeviceInfo d = strata::core::device_info(0);
std::printf("device %d: %s\n", d.ordinal, d.name.c_str());
@@ -42,6 +74,13 @@ int main(int argc, char** argv) {
std::printf(" multiprocessors %d\n", d.multi_processor_count);
std::printf(" VRAM total / free %s / %s\n", human(d.total_bytes).c_str(), human(d.free_bytes).c_str());
std::printf(" driver / runtime %d / %d\n", d.driver_version, d.runtime_version);
#if defined(STRATA_USE_HIP) && defined(_WIN32)
// #468 #461: which HIP runtime this process loaded - the one beside the exe, or an AMD driver's System32 copy
if (HMODULE h = GetModuleHandleA("amdhip64_7.dll")) {
char path[MAX_PATH] = {};
if (GetModuleFileNameA(h, path, MAX_PATH) > 0) std::printf(" HIP runtime %s\n", path);
}
#endif
// The planner's view against the card's. A plan that does not fit in what is actually FREE is the
// failure this print exists to make visible at startup rather than at token 4000.
+7
View File
@@ -182,7 +182,14 @@ bool ExpertCache::open_sized(const std::vector<int64_t>& slot_bytes, int64_t n_l
return false;
}
#endif
// #369: each layer's cursor at the bottom of its own range, as open() seeds it - open() above ran on byte-sized
// "slots", so its seeds are not slot indices
layer_next_.assign((size_t) (n_layers > 0 ? n_layers : 0), 0);
for (int64_t l = 0; l < n_layers; ++l) {
int64_t lo = 0, hi = 0;
layer_slot_range(l, lo, hi);
layer_next_[(size_t) l] = (int32_t) lo;
}
return true;
}
+42 -7
View File
@@ -1024,6 +1024,23 @@ bool FileExpertSource::pin_cache_complement(
uint64_t bytes = 0;
if (!detail::make_cache_complement_plan(n_layers_, n_expert_, layer_blob_bytes_, primary_gpu_pairs,
additional_gpu_pairs, offsets, bytes, err)) return false;
#if defined(_WIN32)
// #467: the GPU cache's pre-fill touched its experts through the mapping (~19 GiB on a 24 GB card), and Windows
// counts those file pages in this process's working set, not as available: a 32 GB PC read 0.44 GiB here
// (20.7 GiB before the start). Trimmed, they move to the standby list (still cached, counted as available).
// Resident mode only: nothing else calls this function. Locked/pinned pages stay; the rest fault back softly.
{
uint64_t before = 0, after = 0;
const bool read_before = available_memory_bytes(before);
(void) SetProcessWorkingSetSize(GetCurrentProcess(), (SIZE_T) -1, (SIZE_T) -1);
if (read_before && available_memory_bytes(after))
std::fprintf(stderr, "FileExpertSource: available RAM %.2f GiB, %.2f GiB after the mapped experts left the "
"process working set (#467)\n",
(double) before / 1073741824.0, (double) after / 1073741824.0);
}
#endif
const bool what_fits = budget_bytes == kResidentWhatFits; // #467: the soft mode's second try
uint64_t budget_physical = 0; // #403: the RAM reading a budget was sized from (0: no budget)
if (budget_bytes > 0) {
// CS-T: a RAM budget. The complement's experts in `rank` order (the expert profile, hottest first) while
// they fit, the rest left on the mapped files; clamped to what the RAM has room for.
@@ -1032,13 +1049,25 @@ bool FileExpertSource::pin_cache_complement(
err = "FileExpertSource: cannot determine available RAM for --resident-budget-gib";
return false;
}
budget_physical = physical;
const uint64_t room = physical > headroom_bytes ? physical - headroom_bytes : 0;
if (budget_bytes > room) {
std::fprintf(stderr, "FileExpertSource: --resident-budget-gib %.2f is more than the RAM has room for "
"(%.2f GiB available minus %.0f GiB headroom): %.2f GiB\n",
(double) budget_bytes / 1073741824.0, (double) physical / 1073741824.0,
(double) headroom_bytes / 1073741824.0, (double) room / 1073741824.0);
budget_bytes = room;
// #403: 256 MiB under the room, so the engine's own allocations after this reading still leave the
// headroom (a budget clamped to exactly the room failed the safety check below on a reading a few MB
// lower). An unclamped budget is unchanged.
const uint64_t margin = 256ull << 20;
const uint64_t clamped = room > margin ? room - margin : 0;
if (what_fits)
std::fprintf(stderr, "FileExpertSource: RAM room for the complement: %.2f GiB (%.2f GiB available "
"minus %.0f GiB headroom and a 0.25 GiB margin)\n",
(double) clamped / 1073741824.0, (double) physical / 1073741824.0,
(double) headroom_bytes / 1073741824.0);
else
std::fprintf(stderr, "FileExpertSource: --resident-budget-gib %.2f is more than the RAM has room for "
"(%.2f GiB available minus %.0f GiB headroom and a 0.25 GiB margin): %.2f GiB\n",
(double) budget_bytes / 1073741824.0, (double) physical / 1073741824.0,
(double) headroom_bytes / 1073741824.0, (double) clamped / 1073741824.0);
budget_bytes = clamped;
}
std::vector<uint64_t> ranked(offsets.size(), kNoComplement);
uint64_t at = 0;
@@ -1054,6 +1083,10 @@ bool FileExpertSource::pin_cache_complement(
at += b;
++held;
}
if (what_fits && held == 0) { // #467: nothing to keep - the caller's plain mmap fallback, not an empty copy
err = "FileExpertSource: the RAM has no room for any expert of the complement";
return false;
}
std::fprintf(stderr, "FileExpertSource: RAM budget %.2f GiB: %lld of the %.2f GiB of experts the GPU cache does "
"not hold, by profile rank; the rest are read from the files\n",
(double) budget_bytes / 1073741824.0, (long long) held, (double) bytes / 1073741824.0);
@@ -1065,8 +1098,10 @@ bool FileExpertSource::pin_cache_complement(
const bool lend = lend_from_slot >= 0 && lend_from_slot < n_slots && additional_gpu_pairs.empty();
uint64_t budget = std::numeric_limits<uint64_t>::max();
if (bytes > 0 || lend) {
uint64_t physical = 0;
if (!available_memory_bytes(physical)) {
// #403: with a budget, the reading it was sized from - a second reading a few MB lower (the engine's own
// allocations, the file cache) failed a budget the first one had clamped. (A budget turns `lend` off.)
uint64_t physical = budget_physical;
if (physical == 0 && !available_memory_bytes(physical)) {
err = "FileExpertSource: cannot determine available RAM for the resident-memory safety check";
return false;
}
+21
View File
@@ -150,10 +150,31 @@ bool NativeEmbed::load(const std::vector<std::string>& shards, int64_t n_embd, i
} else {
std::memcpy(host_, gguf.tensor_data(*t), bytes_);
void* d = nullptr;
#if defined(STRATA_USE_HIP)
// #325: the Windows HIP stack can refuse the device alias of a mapped allocation (and, when it gives
// one, it is the host address itself - unified addressing; kernels read it correctly there, a
// device-to-device copy into it does not land: tests/hip/mapped_alias). The table is only gathered from,
// so without an alias it goes into VRAM like the unpinnable case above, instead of failing the start.
if (cudaHostGetDevicePointer(&d, host_, 0) != cudaSuccess || d == nullptr) {
cudaGetLastError();
d = nullptr;
if (cudaMalloc(&d, bytes_) != cudaSuccess ||
cudaMemcpy(d, gguf.tensor_data(*t), bytes_, cudaMemcpyHostToDevice) != cudaSuccess) {
if (d) cudaFree(d);
cudaGetLastError();
err = "native embedding: no device alias for the mapped table, and no VRAM to copy it into";
return false;
}
cudaFreeHost(host_); // the destructor frees dev_ when host_ is null
host_ = nullptr;
std::fprintf(stderr, "strata: native embedding: no device alias for the mapped table, kept in VRAM\n");
}
#else
if (cudaHostGetDevicePointer(&d, host_, 0) != cudaSuccess) {
err = "native embedding: no device alias for the mapped table";
return false;
}
#endif
dev_ = d;
}
type_ = (int) t->type;
+5 -3
View File
@@ -835,9 +835,11 @@ void fused_gr_read_multi(const FusedGrArgs* a, int n_tok, float* xn_scratch, voi
if (need1 <= limit &&
cudaFuncSetAttribute(gr_down_v3_kernel<1>, cudaFuncAttributeMaxDynamicSharedMemorySize, need1) == cudaSuccess)
split = 1;
else if (need2 <= limit &&
cudaFuncSetAttribute(gr_down_v3_kernel<2>, cudaFuncAttributeMaxDynamicSharedMemorySize, need2) == cudaSuccess)
split = 2;
else if (need2 <= limit)
// #375 (kenh0u): the S = 2 split (a 64 KB opt-in card: Turing) disagrees with itself in gr_parity
// (graph replay vs direct call) - such a card keeps the default read until that split is fixed
std::fprintf(stderr, "strata: STRATA_GR_V3=1 needs the two-half split on this card, which fails its "
"checks (#375): the default read is used\n");
cudaGetLastError(); // drop any error the attempts left behind
split3[dev3] = split;
}
+10 -7
View File
@@ -979,21 +979,24 @@ bool qsa_prompt_attn_batch(const float* q, const QsaAttnPools& pools, const int3
{ // sm_75 or newer: the MMA above compiles for both. sm_80+ runs the cp.async kernel (launch_i8); Turing has
// no cp.async, so it runs the v1 kernel (launch<1>, same accuracy, another summation order). An older card
// keeps the old kernel.
static int cc_major[64] = {};
// #371: the compute capability with its minor - sm_70 (V100) has no m16n8k8 (the kernels trap below sm_75)
static int cc[64] = {};
int dev = 0;
if (cudaGetDevice(&dev) != cudaSuccess || dev < 0 || dev >= 64) { cudaGetLastError(); return false; }
if (cc_major[dev] == 0) {
int major = 0;
if (cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, dev) != cudaSuccess) {
if (cc[dev] == 0) {
int major = 0, minor = 0;
if (cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, dev) != cudaSuccess ||
cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor, dev) != cudaSuccess) {
cudaGetLastError();
return false;
}
// STRATA_QSA_WARP=1|attn (an A/B arm): the pre-sm_80 kernels on any card, as RTX 20 runs them
const char* w = std::getenv("STRATA_QSA_WARP");
cc_major[dev] = w && (!std::strcmp(w, "1") || !std::strcmp(w, "attn")) ? 7 : strata::cc_major_of(major);
cc[dev] = w && (!std::strcmp(w, "1") || !std::strcmp(w, "attn")) ? 75
: 10 * strata::cc_major_of(major) + strata::cc_minor_of(minor);
}
if (cc_major[dev] < 7) return false;
turing = cc_major[dev] < 8;
if (cc[dev] < 75) return false;
turing = cc[dev] < 80;
}
#if defined(__HIPCC__)
// the tensor-core kernels are compiled out on AMD (its major version is not a CUDA sm); RDNA4 has its own int8-KV
+17 -1
View File
@@ -26,6 +26,7 @@
#include <cmath>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <random>
@@ -331,7 +332,22 @@ int fused_multi_lds_parity(const float* d_norm, const uint16_t* d_down, const ui
check(cudaMemcpy(s.mixed.data(), d_mixed, s.mixed.size() * sizeof(float), cudaMemcpyDeviceToHost), "multi read mixed");
return s;
};
auto same = [](const Snapshot& a, const Snapshot& b) {
// the split read (STRATA_GR_V3=1) sums in another order than the single-token kernel: equal within float
// rounding, not to the bit, so it is compared with a relative tolerance; the default kernels bit for bit
static const bool v3 = [] { const char* v = std::getenv("STRATA_GR_V3"); return v != nullptr && std::atoi(v) != 0; }();
auto close = [](const std::vector<float>& x, const std::vector<float>& y) {
double worst = 0.0, mag = 1e-30;
for (size_t i = 0; i < x.size(); ++i) {
worst = std::max(worst, (double) std::fabs(x[i] - y[i]));
mag = std::max(mag, (double) std::fabs(y[i]));
}
if (worst > 2e-6 * mag)
std::printf(" tolerance: worst %.3e of max |ref| %.3e (rel %.3e), n %zu\n", worst, mag, worst / mag, x.size());
return worst <= 2e-6 * mag;
};
auto same = [&](const Snapshot& a, const Snapshot& b) {
if (v3) // `lo` is the default kernels' workspace between down and up; the split read keeps it in shared memory
return close(a.r_out, b.r_out) && close(a.rs, b.rs) && close(a.inject, b.inject) && close(a.mixed, b.mixed);
return std::memcmp(a.r_out.data(), b.r_out.data(), a.r_out.size() * sizeof(float)) == 0 &&
std::memcmp(a.lo.data(), b.lo.data(), a.lo.size() * sizeof(float)) == 0 &&
std::memcmp(a.rs.data(), b.rs.data(), a.rs.size() * sizeof(float)) == 0 &&
+21
View File
@@ -42,6 +42,23 @@ void ck(cublasStatus_t s, const char* what) {
}
}
// #247/#325: on Windows (seen on gfx1201), hipBLAS can return success with the correct BF16/FP16 product for some
// shapes (hc up once T >= 96, the router) and still leave hipErrorInvalidValue set, which the next kernel's error
// check turns into an exit. The multiply has finished, so that one stale error is cleared after a GEMM that succeeded;
// any other error still stops the engine. Windows only: on Linux a stale hipErrorInvalidValue is a real error from
// an earlier call and keeps being reported. A no-op everywhere else (CUDA compiles none of it).
#if defined(__HIPCC__) && defined(_WIN32)
void absorb_hipblas_sticky(const char* what) {
const hipError_t sticky = hipGetLastError();
if (sticky == hipSuccess || sticky == hipErrorInvalidValue) return;
std::fprintf(stderr, "prefill gemm: %s left %s\n", what, hipGetErrorString(sticky));
std::exit(1);
}
#define STRATA_ABSORB_HIPBLAS_STICKY(what) absorb_hipblas_sticky(what)
#else
#define STRATA_ABSORB_HIPBLAS_STICKY(what) ((void) 0)
#endif
// A setup call whose failure the engine survives (the handle keeps its defaults), as before #240 - but said.
void note(cublasStatus_t s, const char* what) {
if (s != CUBLAS_STATUS_SUCCESS) std::fprintf(stderr, "prefill gemm: %s: cuBLAS status %d (continuing)\n", what, (int) s);
@@ -363,6 +380,7 @@ void Gemm::bf16(const uint16_t* X, const uint16_t* W, float* Y, int64_t T, int64
#if defined(__HIPCC__) && defined(STRATA_HIPBLASLT_AVAILABLE)
if (try_hipblaslt(hipblaslt_state_, strata::prefill::hipblaslt::InputType::bf16, X, W, Y, T, N, K, ldy,
beta, stream_)) {
STRATA_ABSORB_HIPBLAS_STICKY("hipBLASLt bf16");
return;
}
#endif
@@ -371,6 +389,7 @@ void Gemm::bf16(const uint16_t* X, const uint16_t* W, float* Y, int64_t T, int64
CUDA_R_16BF, (int) K, X, CUDA_R_16BF, (int) K, &beta, Y, CUDA_R_32F, (int) ldy,
CUBLAS_COMPUTE_32F, CUBLAS_GEMM_DEFAULT),
"cublasGemmEx");
STRATA_ABSORB_HIPBLAS_STICKY("cublasGemmEx");
}
void Gemm::f16(const uint16_t* X, const uint16_t* W, float* Y, int64_t T, int64_t N, int64_t K, int64_t ldy,
@@ -381,6 +400,7 @@ void Gemm::f16(const uint16_t* X, const uint16_t* W, float* Y, int64_t T, int64_
#if defined(__HIPCC__) && defined(STRATA_HIPBLASLT_AVAILABLE)
if (try_hipblaslt(hipblaslt_state_, strata::prefill::hipblaslt::InputType::f16, X, W, Y, T, N, K, ldy,
beta, stream_)) {
STRATA_ABSORB_HIPBLAS_STICKY("hipBLASLt f16");
return;
}
#endif
@@ -388,6 +408,7 @@ void Gemm::f16(const uint16_t* X, const uint16_t* W, float* Y, int64_t T, int64_
CUDA_R_16F, (int) K, X, CUDA_R_16F, (int) K, &beta, Y, CUDA_R_32F, (int) ldy,
CUBLAS_COMPUTE_32F, CUBLAS_GEMM_DEFAULT),
"cublasGemmEx f16");
STRATA_ABSORB_HIPBLAS_STICKY("cublasGemmEx f16");
}
void Gemm::native(const uint16_t* X, int ggml_type, const void* W_blocks, float* Y, int64_t T, int64_t N, int64_t K,
+28
View File
@@ -107,6 +107,34 @@ bool supported(int t) {
}
}
bool fits(int t, int64_t w_rows) {
if (!supported(t)) return false;
// mul_mat_q_case's choice: the "fallback" configs when the rows are not a multiple of 128; then
// mul_mat_q_switch_J's loop - a tile size whose config exists for this card and fits its shared memory
const bool fallback = w_rows % 128 != 0;
const ggml_cuda_device_info& info = ggml_cuda_info();
for (int id = 0; id < info.device_count; ++id) {
const int cc = info.devices[id].cc;
const size_t smpbo = info.devices[id].smpbo;
bool any = false;
for (int J = 8; J <= 128 && !any; J += 8) {
const ggml_cuda_mmq_config c = ggml_cuda_mmq_get_config((ggml_type) t, J, fallback, cc);
any = c.type != GGML_TYPE_COUNT && mmq_get_nbytes_shared(c, cc) <= smpbo;
}
if (!any) {
static bool said[GGML_TYPE_COUNT] = {};
if (t >= 0 && t < GGML_TYPE_COUNT && !said[t]) {
said[t] = true;
std::fprintf(stderr, "strata: prompt kernels: llama.cpp's MMQ has no tile for %s (%lld rows) on GPU %d "
"(cc %d, %zu bytes of shared memory per block): that product takes the non-MMQ path "
"(#420)\n", ggml_type_name((ggml_type) t), (long long) w_rows, id, cc, smpbo);
}
return false;
}
}
return true;
}
size_t matrix_bytes(int t, int64_t rows, int64_t cols) {
return (size_t) rows * (size_t) (cols / ggml_blck_size((ggml_type) t)) * ggml_type_size((ggml_type) t);
}
+4 -2
View File
@@ -46,6 +46,7 @@
namespace strata::prefill::mmq {
bool built() { return false; }
bool supported(int) { return false; }
bool fits(int, int64_t) { return false; }
size_t matrix_bytes(int, int64_t, int64_t) { return 0; }
size_t q8_bytes(int64_t, int64_t) { return 0; }
void quantize(const float*, const int32_t*, void*, int, int64_t, int64_t, int64_t, void*) {}
@@ -479,7 +480,8 @@ const MmqPlan& mmq_plan() {
p.fallback = !on || layers <= 0;
for (int64_t l = 0; on && l < layers; ++l) {
const int gt = lay.native ? lay.fmt[(size_t) l].gu_type : 42, dt = lay.native ? lay.fmt[(size_t) l].d_type : 42;
if (!mmq::supported(gt) || !mmq::supported(dt)) { p.fallback = true; continue; }
// #420: a tile on every GPU for these shapes (gate+up: 1280 rows, down: N rows), else the FP16 path
if (!mmq::fits(gt, 1280) || !mmq::fits(dt, N)) { p.fallback = true; continue; }
p.layer[(size_t) l] = 1;
p.any = true;
p.gu_max = std::max(p.gu_max, mmq::matrix_bytes(gt, 1280, N));
@@ -768,7 +770,7 @@ bool Prefill::draft_kv(core::MtpDrafter& mtp, const float* R_rows, const int32_t
// E-9: the drafter's Q8_0 matrices through Q8_1 x Q8_0 MMQ - its own pass's integer dot products (mmvq), so
// its K/V stay close to what the drafter computes itself; STRATA_MTP_BATCH_F16=1: FP16 GEMMs (the A/B)
static const bool f16_only = [] { const char* v = std::getenv("STRATA_MTP_BATCH_F16"); return v && v[0] == '1'; }();
const bool q8 = !f16_only && mmq::built() && mmq::supported(kQ8_0);
const bool q8 = !f16_only && mmq::built() && mmq::fits(kQ8_0, Nn) && mmq::fits(kQ8_0, KV); // #420
const uint64_t per_row = 4 * (2 * Nn + 4 * HCN + LR + HC + Nn + 2 * KV + 1) + 2 * (Nn + 2 * HCN + LR + Nn) + 64 +
(q8 ? (uint64_t) mmq::q8_bytes(g.hc, Nn) + 4 * g.hc : 0);
const int64_t B = std::min<int64_t>(n - r0, (int64_t) (m.region_bytes / per_row) & ~(int64_t) 63);
+120 -27
View File
@@ -272,6 +272,7 @@ struct Options {
bool resident_pin = false;
uint64_t resident_headroom = 8ull << 30;
bool resident_soft = false;
bool resident_cpu_explicit = false; ///< #384: --resident-cpu-experts given by itself (not only implied)
/// CS-T `--resident-budget-gib N`: the resident mode with a RAM budget - the N GiB of experts the GPU cache does
/// not hold that the expert profile ranks hottest are copied into RAM, the rest are read from the files in place
/// (the GGUF shards when the pack has no experts.bin). 0 = the whole complement (--resident-experts).
@@ -1212,7 +1213,7 @@ int main(int argc, char** argv) {
else if (a == "--gpu-stages") o.gpu_stages = true;
else if (a == "--mmap-experts") o.mmap_experts = true;
else if (a == "--shared-expert-arena") o.shared_expert_arena = next("--shared-expert-arena");
else if (a == "--resident-cpu-experts") o.resident_cpu_experts = true;
else if (a == "--resident-cpu-experts") o.resident_cpu_experts = o.resident_cpu_explicit = true;
else if (a == "--resident-experts") {
o.mmap_experts = o.resident_cpu_experts = o.resident_pin = o.resident_soft = true;
o.resident_headroom = 4ull << 30;
@@ -1320,9 +1321,20 @@ int main(int argc, char** argv) {
std::fprintf(stderr, "strata generate: --resident-cpu-experts requires --mmap-experts and a static --expert-profile\n");
return 2;
}
if (o.resident_cpu_experts &&
(!o.layer_split.empty() || o.expert_cache_remote[0] > 0 || o.expert_cache_remote[1] > 0 ||
o.expert_cache_remote[2] > 0)) {
const bool remote_caches = o.expert_cache_remote[0] > 0 || o.expert_cache_remote[1] > 0 ||
o.expert_cache_remote[2] > 0;
if (o.resident_cpu_experts && !o.layer_split.empty() && !remote_caches && o.resident_soft &&
!o.resident_cpu_explicit && o.resident_budget == 0) {
// #364 #384: setup's --resident-experts with a layer split (--gpus at start, or a config edited by hand) runs
// as the plain mmap mode - the placement those users measured 1.3-1.6x faster than one GPU - instead of
// refusing. Exactly --mmap-experts: nothing else reads these flags (the headroom only sizes the copy).
std::fprintf(stderr, "strata generate: WARNING: the resident RAM mode (--resident-experts) does not support a "
"layer split yet: the experts the GPUs do not hold are read through the OS file cache "
"(--mmap-experts), and RAM may fill up during long prompts\n");
o.resident_cpu_experts = o.resident_pin = o.resident_soft = false;
o.resident_headroom = 8ull << 30;
}
if (o.resident_cpu_experts && (!o.layer_split.empty() || remote_caches)) {
std::fprintf(stderr, "strata generate: --resident-cpu-experts does not support layer splits or remote expert caches\n");
return 2;
}
@@ -2541,6 +2553,14 @@ int main(int argc, char** argv) {
const char* name = named && p.name[0] ? p.name : "(an unnamed GPU)";
#if defined(STRATA_USE_HIP)
std::fprintf(stderr, "strata generate: GPU %d: %s (%s)\n", dev, name, named ? p.gcnArchName : "?");
#if defined(_WIN32)
// #468 #461: which HIP runtime was loaded - the bundled one beside the exe, or an AMD driver's System32 copy
if (HMODULE h = GetModuleHandleA("amdhip64_7.dll")) {
char path[MAX_PATH] = {};
if (GetModuleFileNameA(h, path, MAX_PATH) > 0)
std::fprintf(stderr, "strata generate: HIP runtime %s\n", path);
}
#endif
#else
std::fprintf(stderr, "strata generate: GPU %d: %s, compute capability %d.%d%s\n", dev, name,
strata::cc_major_of(p.major), strata::cc_minor_of(p.minor),
@@ -2682,16 +2702,20 @@ int main(int argc, char** argv) {
const int64_t reserve = ((int64_t) o.vram_reserve_mib + prefill_mib) << 20;
const int64_t fit = std::max<int64_t>(((int64_t) free_b - reserve) / (int64_t) strata::kernels::cpu::expert_layout().max_blob, 0);
if (o.expert_cache > fit) {
std::fprintf(stderr, "strata generate: layer split: --expert-cache %d leaves no room for the prompt path's "
"buffers (%lld MiB) on CUDA0: %lld slots\n", o.expert_cache, (long long) prefill_mib,
(long long) fit);
// a WARNING that names the knob: the user asked for this size, and gets fewer slots
std::fprintf(stderr, "strata generate: WARNING: layer split: --expert-cache %d leaves no room for the "
"prompt path's buffers (%lld MiB) and the %d MiB reserve on CUDA0: %lld slots instead "
"(a smaller --vram-reserve-mib leaves more of them)\n", o.expert_cache,
(long long) prefill_mib, o.vram_reserve_mib, (long long) fit);
o.expert_cache = (int) fit;
}
}
// plan v0.3 P6: a native pack's blobs differ per layer, so with a profile its slots are sized per pair: the
// same VRAM holds ~30% more IQ3_XXS experts than slots of the largest blob would
// #369: not with --expert-cache-per-layer - its per-layer slot ranges ignore the profile rank a sized slot was cut
// for, so a layer's larger blob could land in a smaller slot: that mode keeps slots of the largest blob
std::vector<int64_t> sized_slots;
if (native_pack && o.expert_cache > 0 && !profile.empty()) {
if (native_pack && o.expert_cache > 0 && !profile.empty() && !o.expert_cache_per_layer) {
size_t free_b = 0, total_b = 0;
cudaMemGetInfo(&free_b, &total_b);
const auto& lay = strata::kernels::cpu::expert_layout();
@@ -2830,10 +2854,17 @@ int main(int argc, char** argv) {
// the policy rather than a hint.
int64_t prefilled = 0;
if (!profile.empty() && srcp != nullptr) {
const int64_t want = std::min<int64_t>((int64_t) profile.size(), xcache.slots());
// #369 (dag08): per layer, a full layer skips only its own pairs - each layer takes its hottest experts until
// its range is full (one full layer used to end the whole fill, leaving most layers empty)
const bool per_layer = xcache.per_layer_admission();
const int64_t want = per_layer ? (int64_t) profile.size()
: std::min<int64_t>((int64_t) profile.size(), xcache.slots());
for (int64_t i = 0; i < want; ++i) {
const int32_t slot = xcache.admit(profile[(size_t) i].first, profile[(size_t) i].second);
if (slot == strata::core::kNotResident) break;
if (slot == strata::core::kNotResident) {
if (per_layer) continue;
break;
}
const uint8_t* b = srcp->blob(profile[(size_t) i].first, profile[(size_t) i].second);
if (b == nullptr || !xcache.fill_slot_blocking(slot, b, err,
(int64_t) strata::kernels::cpu::expert_layout().blob_bytes(profile[(size_t) i].first))) {
@@ -2854,7 +2885,7 @@ int main(int argc, char** argv) {
}
mem_mark("the profile fill");
std::fprintf(stderr, "strata generate: pre-filled %lld of %lld slots from the profile; slot 0 verified\n",
(long long) prefilled, (long long) want);
(long long) prefilled, (long long) (per_layer ? xcache.slots() : want));
}
for (auto& stp : stages) {
@@ -3757,8 +3788,26 @@ int main(int argc, char** argv) {
const int64_t k = plan_lend(chunk);
if (k > 0) lend_from = xcache.slots() - k;
}
if (src.pin_cache_complement(xcache, err, o.resident_pin, {}, lend_from, o.resident_headroom,
o.resident_budget, &profile)) {
bool resident_ok = src.pin_cache_complement(xcache, err, o.resident_pin, {}, lend_from, o.resident_headroom,
o.resident_budget, &profile);
std::string whole_err;
if (!resident_ok && o.resident_soft) {
// #467: the whole complement does not fit - keep what does, the hottest by the profile, through the #403
// budget path (sized by the RAM alone) instead of none: the misses outside it read the same file bytes
// the mmap fallback reads, so the answers are unchanged. Nothing pinned: the old fallback below.
whole_err = err;
resident_ok = src.pin_cache_complement(xcache, err, o.resident_pin, {}, -1, o.resident_headroom,
strata::core::FileExpertSource::kResidentWhatFits, &profile);
if (resident_ok)
std::fprintf(stderr, "strata generate: WARNING: the whole resident RAM mode does not fit (%s); %.2f "
"GiB of the experts the GPU does not hold, the hottest by the expert profile, are "
"kept in RAM and the rest are read from the model folder through the OS file "
"cache\n",
whole_err.c_str(), (double) src.resident_bytes() / 1073741824.0);
else
err = whole_err + "; " + err;
}
if (resident_ok) {
if (o.adapt_every > 0 && o.adapt_swaps > 0 &&
!src.reserve_exchanges(std::min<int64_t>(o.adapt_swaps, 96), err)) {
std::fprintf(stderr, "strata generate: CPU expert residency: %s\n", err.c_str());
@@ -3775,6 +3824,12 @@ int main(int argc, char** argv) {
std::fprintf(stderr, "strata generate: WARNING: the resident RAM mode does not fit (%s); the experts the "
"GPU does not hold are read from the model folder through the OS file cache "
"(--mmap-experts), which is slower when the RAM cannot keep them\n", err.c_str());
} else if (o.resident_budget > 0) {
// #403: a RAM budget that cannot be kept is not a reason to stop - the experts it would have held are
// read from the files like the ones outside it (pin_cache_complement leaves nothing half-built)
std::fprintf(stderr, "strata generate: WARNING: the RAM budget (--resident-budget-gib) cannot be kept (%s); "
"every expert the GPU does not hold is read from the model files through the OS file "
"cache (--mmap-experts), which is slower\n", err.c_str());
} else {
std::fprintf(stderr, "strata generate: CPU expert residency: %s\n", err.c_str());
return 1;
@@ -3858,24 +3913,62 @@ int main(int argc, char** argv) {
// 128-slot floor. The percentage cap is an AUTO-chunk rule and only the auto scan applies it - an
// explicit --prefill is the operator's number, and a loan of it only has to fit. With one participant
// (no split) this reduces to plan_lend exactly, so the single-GPU loan is unchanged from main.
auto fits = [&](int64_t c, bool cap) -> bool {
for (const PfPart& p : pf_parts) {
const int64_t k = part_slots(p, c);
if (k <= 0 || k + 128 > p.cache->slots()) return false;
if (cap && k * 100 > kAutoLendPct * p.cache->slots()) return false;
}
auto fits_one = [&](const PfPart& p, int64_t c, bool cap) -> bool {
const int64_t k = part_slots(p, c);
if (k <= 0 || k + 128 > p.cache->slots()) return false;
return !(cap && k * 100 > kAutoLendPct * p.cache->slots());
};
// `only`: CUDA0's cache alone (#448: what one GPU would choose, for the log below); null: every one
auto fits = [&](int64_t c, bool cap, const PfPart* only = nullptr) -> bool {
if (only != nullptr) return fits_one(*only, c, cap);
for (const PfPart& p : pf_parts)
if (!fits_one(p, c, cap)) return false;
return true;
};
static constexpr int64_t kAutoChunks[] = {32768, 16384, 8192, 6144, 4096, 3072, 2048, 1024, 512, 256};
int64_t chunk = 0;
if (o.prefill_auto) {
for (const int64_t c : kAutoChunks) {
if (c > 8192 && (c > o.prefill_auto_max || c > o.max_context)) continue; // #282, as plan_lend
if (fits(c, true)) { chunk = c; break; }
auto pick = [&](const PfPart* only) -> int64_t {
if (o.prefill_auto) {
for (const int64_t c : kAutoChunks) {
if (c > 8192 && (c > o.prefill_auto_max || c > o.max_context)) continue; // #282, as plan_lend
if (fits(c, true, only)) return c;
}
} else {
for (int64_t c = o.prefill_chunk; c >= 256; c /= 2)
if (fits(c, false, only)) return c;
}
return 0;
};
const int64_t chunk = pick(nullptr);
// #448: a small card in a layer split caps every stage's chunk (an RTX 3080's 512-slot cache held a
// 32 GB card's split to 512 tokens: prompts 6.2x slower, decode the same). Named when it bites, so the
// regression is one log line: each stage that cannot fund the chunk CUDA0 alone would read in.
if (pf_parts.size() > 1) {
const int64_t alone = pick(&pf_parts[0]);
if (alone > chunk) {
for (size_t i = 1; i < pf_parts.size(); ++i) {
const PfPart& p = pf_parts[i];
if (fits_one(p, alone, o.prefill_auto)) continue;
const int dev = p.dev < 0 ? 0 : p.dev;
cudaDeviceProp prop{};
if (cudaGetDeviceProperties(&prop, dev) != cudaSuccess) {
(void) cudaGetLastError();
prop.name[0] = 0;
}
const std::string pct =
o.prefill_auto ? ", and lend at most " + std::to_string(kAutoLendPct) + "%" : "";
std::fprintf(stderr, "strata serve: WARNING: prompt chunk %lld tokens, not %lld: CUDA%d (%s) "
"has %lld expert-cache slots, and a %lld-token chunk borrows %lld of them "
"(it must keep 128%s) - prompts read slower than on CUDA0 alone (#448)\n",
(long long) chunk, (long long) alone, dev, prop.name,
(long long) p.cache->slots(), (long long) alone,
(long long) part_slots(p, alone), pct.c_str());
// the helper tiers start at CUDA1 without a split (and are enabled in order)
std::fprintf(stderr, "strata serve: a card this small can serve as a helper expert cache "
"instead of a split stage: without --layer-split, with %s "
"(docs/SECOND_GPU.md)\n",
dev == 1 ? "--expert-cache-device1 N" : "--expert-cache-device1..3 N, in order");
}
}
} else {
for (int64_t c = o.prefill_chunk; c >= 256; c /= 2)
if (fits(c, false)) { chunk = c; break; }
}
if (chunk > 0) {
if (o.prefill_auto)
@@ -0,0 +1,44 @@
// #369: per-layer admission on a sized cache (open_sized, every native pack) - each layer admits into its own slot
// range, as on the uniform cache, so no two layers write the same slot. Needs a CUDA device (exits 77 without one).
#include "strata/core/expert_cache.hpp"
#include <cuda_runtime.h>
#include <cstdio>
#include <string>
#include <vector>
int main() {
int n = 0;
if (cudaGetDeviceCount(&n) != cudaSuccess || n == 0) {
std::puts("no CUDA device: skipped");
return 77;
}
std::string err;
for (int sized = 0; sized < 2; ++sized) {
strata::core::ExpertCache cache;
const bool ok = sized ? cache.open_sized(std::vector<int64_t>(6, 1024), 3, 4, err)
: cache.open(6, 3, 4, 1024, err);
if (!ok) {
std::fprintf(stderr, "open: %s\n", err.c_str());
return 1;
}
cache.set_per_layer_admission(true);
for (int64_t layer = 0; layer < 3; ++layer) {
int64_t lo = 0, hi = 0;
cache.layer_slot_range(layer, lo, hi);
for (int64_t e = 0; e < 3; ++e) {
const int32_t slot = cache.admit(layer, e);
const bool want_room = lo + e < hi;
if (want_room ? slot != (int32_t) (lo + e) : slot != strata::core::kNotResident) {
std::fprintf(stderr, "%s cache: layer %lld expert %lld got slot %d (range %lld..%lld)\n",
sized ? "sized" : "uniform", (long long) layer, (long long) e, slot, (long long) lo,
(long long) hi - 1);
return 1;
}
}
}
}
std::puts("expert cache per-layer admission: uniform and sized caches passed");
return 0;
}
+108
View File
@@ -0,0 +1,108 @@
// Is a hipHostGetDevicePointer alias usable on this stack? (#325)
//
// tests/hip/handoff.cpp fails on gfx1201/Windows with "separate copy/ring timeout": it does a
// hipMemcpyAsync(..., hipMemcpyDeviceToDevice) into a hipHostGetDevicePointer alias, then waits for
// a doorbell that copy should have published. This test pins down the properties that decide it.
//
// On Linux the alias is a device address of the pinned host memory, a D2D copy lands, the host sees
// it, and a kernel reads it. On Windows (#325, RX 9070 XT) the runtime hands back the HOST pointer
// itself (unified addressing; this test prints both addresses), and a D2D copy into it does not
// publish - hip_handoff's timeout there. The engine itself runs on Windows (the reporter's serving
// run gave correct answers), so kernels reading mapped memory through that address work; only the
// copy-engine path does not. src/core/native_head.cpp keeps the token embedding in VRAM when the
// alias is refused outright.
//
// Reporting, not gating: a Windows host is EXPECTED to fail some checks below, so this test exits 0
// whenever it ran at all, and 77 (skipped) without a device. When the alias IS the host pointer the
// D2D copy is not attempted (it may fault rather than report); the kernel read is, since that is
// what the engine relies on.
#include "strata/kernels/elementwise.hpp"
#include <hip/hip_runtime.h>
#include <cstdio>
#include <vector>
namespace {
int not_held = 0;
void report(const char* what, bool holds) {
std::printf(" %-44s %s\n", what, holds ? "holds" : "DOES NOT HOLD");
if (!holds) ++not_held;
}
// A kernel reads `n` floats through `alias` into VRAM; the result is copied back and compared.
bool kernel_reads(const float* alias, const std::vector<float>& want) {
const int64_t n = (int64_t) want.size();
float* out = nullptr;
if (hipMalloc(reinterpret_cast<void**>(&out), want.size() * sizeof(float)) != hipSuccess) return false;
strata::kernels::copy_from_mapped(out, alias, n, nullptr);
std::vector<float> got(want.size(), -1.0f);
const bool ran = hipDeviceSynchronize() == hipSuccess &&
hipMemcpy(got.data(), out, want.size() * sizeof(float), hipMemcpyDeviceToHost) == hipSuccess;
hipFree(out);
hipGetLastError();
return ran && got == want;
}
} // namespace
int main() {
int runtime = 0;
if (hipRuntimeGetVersion(&runtime) != hipSuccess) return 1;
int count = 0;
if (hipGetDeviceCount(&count) != hipSuccess || count < 1) {
std::printf("no HIP device\n");
return 77; // ctest "skipped"
}
std::printf("HIP runtime %d, %d device(s)\n", runtime, count);
const int64_t n = 4096; // enough elements for copy_from_mapped's float4 path
const size_t bytes = size_t(n) * sizeof(float);
std::vector<float> pattern(size_t(n), 0.0f);
for (int64_t i = 0; i < n; ++i) pattern[size_t(i)] = float(i + 1);
float* host = nullptr;
if (hipHostMalloc(reinterpret_cast<void**>(&host), bytes, hipHostMallocMapped) != hipSuccess) return 1;
void* alias = nullptr;
if (hipHostGetDevicePointer(&alias, host, 0) != hipSuccess || alias == nullptr) {
std::printf("hipHostGetDevicePointer failed: native_head.cpp keeps the embedding in VRAM here\n");
hipGetLastError();
hipHostFree(host);
return 0;
}
const bool distinct = alias != reinterpret_cast<void*>(host);
std::printf("\n host %p\n alias %p\n -> %s\n\n", static_cast<void*>(host), alias,
distinct ? "a distinct device address" : "the host pointer itself (unified addressing)");
report("the alias is a distinct device address", distinct);
// What the engine relies on: a kernel reading mapped host memory through the alias.
for (int64_t i = 0; i < n; ++i) host[i] = pattern[size_t(i)];
report("a kernel reads the alias correctly", kernel_reads(reinterpret_cast<const float*>(alias), pattern));
// What tests/hip/handoff.cpp attempts: a DeviceToDevice copy INTO the alias, seen by the host.
if (distinct) {
float* device = nullptr;
std::vector<float> twice(pattern);
for (float& v : twice) v *= 2.0f;
if (hipMalloc(reinterpret_cast<void**>(&device), bytes) == hipSuccess &&
hipMemcpy(device, twice.data(), bytes, hipMemcpyHostToDevice) == hipSuccess) {
const hipError_t copy = hipMemcpy(alias, device, bytes, hipMemcpyDeviceToDevice);
report("a D2D copy into the alias succeeds", copy == hipSuccess);
bool seen = hipDeviceSynchronize() == hipSuccess;
for (int64_t i = 0; seen && i < n; ++i) seen = host[i] == twice[size_t(i)];
report("the host sees a D2D copy into the alias", seen);
}
if (device) hipFree(device);
hipGetLastError();
} else {
std::printf(" the D2D copy into the alias is not attempted: with alias == host it may fault rather than\n"
" report (tests/hip/handoff's timeout on Windows)\n");
}
std::printf("\n%d check(s) did not hold (a Windows host is expected to fail some)\n", not_held);
hipHostFree(host);
return 0;
}
+24 -4
View File
@@ -119,10 +119,30 @@ def run(cfg: dict, say=print, start_engine=None) -> dict:
tok = ST.Tokenizer(toks, (tpath / "merges.txt").read_text(encoding="utf-8").split("\n"),
json.loads((tpath / "token_type.json").read_text()))
ids_list = [chat_ids(tok, p) for p in PROMPTS]
args = list(cfg["args"])
if isinstance(cfg.get("gpu"), list) and "--layer-split" not in args: # several cards: measured as it runs
args += ["--layer-split", str(cfg.get("layer_split") or "auto")]
return measure(args, ids_list, start_engine, say)
return measure(engine_args(cfg), ids_list, start_engine, say)
def engine_args(cfg: dict) -> list[str]:
"""The arguments the server starts this config's engine with (serve.server.engine_args), so the tuning measures
the engine as it runs. #447: this used to read any "gpu" list as a layer split and add --layer-split auto, which
broke a one-card config with a helper card for the expert tier ("gpu": [0] + --expert-cache-device1: the engine
refuses a split that leaves no card without a stage), missed the "0,2" spelling, and ignored split_skip_if_fits."""
from serve.server import engine_args as server_args
return server_args(cfg)
def engine_error(log: str | None, since: int = 0) -> str | None:
"""#447: the engine's own reason for a failed start - the last line of its log written after byte `since` that is
its own ("strata ..." or "ERR ..."), else the last line there; None without a log or a new line."""
if not log:
return None
try:
with open(log, "rb") as f:
f.seek(since)
lines = [x.strip() for x in f.read()[-16384:].decode("utf-8", "replace").splitlines() if x.strip()]
except OSError:
return None
return next((x for x in reversed(lines) if x.startswith(("strata", "ERR"))), lines[-1] if lines else None)
def measure(base_args: list[str], ids_list, start_engine, say=print) -> dict:
+86
View File
@@ -0,0 +1,86 @@
@echo off
rem Strata's HIP (AMD) engine for Windows, and the ready-made zip setup.py downloads (strata-windows-x64-hip.zip).
rem
rem tools\hip\build_windows.bat build strata.exe + strata-device.exe and package dist\strata-windows-x64-hip.zip
rem tools\hip\build_windows.bat tests also build the HIP unit tests (ctest in build-hip-win; they need an AMD GPU)
rem
rem Needs: Visual Studio 2022 (or 2019) Build Tools with the C++ workload (the linker, the CRT and the Windows SDK),
rem Python 3.10+ and git. No admin rights and no AMD GPU: ROCm comes from AMD's TheRock Python wheels (the same
rem ROCm 10.2 nightly #325 ran on an RX 9070 XT), installed into a private venv (ROCM_VENV, ~7 GB). CMake refuses to
rem mix cl.exe with Clang for HIP, so ROCm's clang compiles the host code too (targeting the MSVC ABI, with MSVC's
rem headers and linker from vcvars); cmake/hip_backend.cmake force-includes the CUDA->HIP shim.
rem
rem Settings (environment variables, all optional):
rem STRATA_HIP_ARCHS gfx1100;gfx1101;gfx1102;gfx1200;gfx1201;gfx1030 (the cards setup supports, + gfx1102)
rem STRATA_ROCM_VERSION 10.2.0a20260930 STRATA_ROCM_INDEX https://nightly.repo.amd.com/rocm/whl-next/
rem ROCM_VENV <repo>\.rocm-win BUILD_DIR <repo>\build-hip-win DIST_DIR <repo>\dist
rem STRATA_GGML_DIR a llama.cpp checkout at the pinned commit (default: CMake fetches it)
setlocal EnableDelayedExpansion
for %%I in ("%~dp0..\..") do set "SRC=%%~fI"
if not defined STRATA_HIP_ARCHS set "STRATA_HIP_ARCHS=gfx1100;gfx1101;gfx1102;gfx1200;gfx1201;gfx1030"
if not defined STRATA_ROCM_VERSION set "STRATA_ROCM_VERSION=10.2.0a20260930"
if not defined STRATA_ROCM_INDEX set "STRATA_ROCM_INDEX=https://nightly.repo.amd.com/rocm/whl-next/"
if not defined ROCM_VENV set "ROCM_VENV=%SRC%\.rocm-win"
if not defined BUILD_DIR set "BUILD_DIR=%SRC%\build-hip-win"
if not defined DIST_DIR set "DIST_DIR=%SRC%\dist"
set "TESTS=OFF"
if /i "%~1"=="tests" set "TESTS=ON"
rem ---- 1. ROCm (TheRock wheels: the compiler, the HIP runtime, hipBLAS/hipBLASLt/rocBLAS, a device package per arch)
set "PY="
py -3 -c "import sys" >nul 2>nul && set "PY=py -3"
if not defined PY python -c "import sys" >nul 2>nul && set "PY=python"
if not defined PY (echo Python 3.10+ is needed & exit /b 1)
if not exist "%ROCM_VENV%\Scripts\python.exe" %PY% -m venv "%ROCM_VENV%" || exit /b 1
set "EXTRAS=libraries,devel"
for %%A in (%STRATA_HIP_ARCHS:;= %) do set "EXTRAS=!EXTRAS!,device-%%A"
set "STAMP=%ROCM_VENV%\strata-rocm.txt"
set "WANT=%STRATA_ROCM_VERSION% %EXTRAS%"
set "HAVE="
if exist "%STAMP%" set /p HAVE=<"%STAMP%"
if not "!HAVE!"=="!WANT!" (
echo Installing ROCm %STRATA_ROCM_VERSION% [%EXTRAS%] into %ROCM_VENV% ...
"%ROCM_VENV%\Scripts\python.exe" -m pip install --disable-pip-version-check --index-url "%STRATA_ROCM_INDEX%" "rocm[%EXTRAS%]==%STRATA_ROCM_VERSION%" || exit /b 1
"%ROCM_VENV%\Scripts\rocm-sdk.exe" init || exit /b 1
>"%STAMP%" echo !WANT!
)
for /f "delims=" %%R in ('"%ROCM_VENV%\Scripts\rocm-sdk.exe" path --root') do set "ROCM=%%R"
if not exist "%ROCM%\lib\llvm\bin\clang++.exe" (echo ROCm has no compiler in "%ROCM%" & exit /b 1)
set "ROCM_F=%ROCM:\=/%"
set "BITCODE=%ROCM_F%/lib/llvm/amdgcn/bitcode"
rem ---- 2. MSVC: the linker, the C runtime and the Windows SDK (ROCm's clang uses them)
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
set "VS="
if exist "%VSWHERE%" for /f "delims=" %%V in ('"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath') do set "VS=%%V"
if not defined VS (echo Visual Studio Build Tools with the C++ workload are needed & exit /b 1)
call "%VS%\VC\Auxiliary\Build\vcvars64.bat" >nul || exit /b 1
set "HIP_PLATFORM=amd"
set "HIP_PATH=%ROCM%"
set "ROCM_PATH=%ROCM%"
set "PATH=%ROCM%\bin;%ROCM%\lib\llvm\bin;%PATH%"
rem ---- 3. configure + build (STRATA_PORTABLE: the CPU kernels for an AVX2 baseline, as in the NVIDIA zip)
set "GGML="
if defined STRATA_GGML_DIR set "GGML=-DSTRATA_GGML_DIR=%STRATA_GGML_DIR:\=/%"
if not exist "%BUILD_DIR%\build.ninja" (
cmake -G Ninja -S "%SRC%" -B "%BUILD_DIR%" -DCMAKE_BUILD_TYPE=Release ^
-DSTRATA_ENABLE_HIP=ON -DSTRATA_ENABLE_CUDA=OFF -DSTRATA_BUILD_TESTS=%TESTS% -DSTRATA_PREFILL_MMQ=ON ^
-DSTRATA_NATIVE_EXPERTS=ON -DSTRATA_PORTABLE=ON "-DCMAKE_HIP_ARCHITECTURES=%STRATA_HIP_ARCHS%" ^
"-DCMAKE_C_COMPILER=%ROCM_F%/lib/llvm/bin/clang.exe" "-DCMAKE_CXX_COMPILER=%ROCM_F%/lib/llvm/bin/clang++.exe" ^
"-DCMAKE_HIP_COMPILER=%ROCM_F%/lib/llvm/bin/clang++.exe" "-DCMAKE_HIP_COMPILER_ROCM_ROOT=%ROCM_F%" ^
"-DCMAKE_PREFIX_PATH=%ROCM_F%" "-DCMAKE_HIP_FLAGS=--rocm-path=%ROCM_F% --rocm-device-lib-path=%BITCODE%" ^
%GGML% || exit /b 1
)
if "%TESTS%"=="ON" (
cmake --build "%BUILD_DIR%" || exit /b 1
) else (
cmake --build "%BUILD_DIR%" --target strata strata-device || exit /b 1
)
rem ---- 4. the zip: the two programs, the ROCm DLLs they load (+ rocBLAS/hipBLASLt kernels for these archs), licenses
"%ROCM_VENV%\Scripts\python.exe" "%SRC%\tools\hip\package_windows.py" --build "%BUILD_DIR%" --rocm "%ROCM%" ^
--archs "%STRATA_HIP_ARCHS%" --rocm-version "%STRATA_ROCM_VERSION%" --out "%DIST_DIR%" || exit /b 1
echo.
echo Done: %DIST_DIR%\strata-windows-x64-hip.zip
endlocal
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""Package the Windows HIP engine as the ready-made zip setup.py downloads (strata-windows-x64-hip.zip).
python tools/hip/package_windows.py --build build-hip-win --rocm <rocm-sdk path --root> \\
--archs "gfx1100;gfx1201" --rocm-version 10.2.0a20260930 --out dist
tools/hip/build_windows.bat runs it after the build. The zip holds:
strata.exe, strata-device.exe, BUILD.json (backend "hip", the archs, the ROCm and hipBLASLt versions)
amdhip64_7.dll, amd_comgr.dll the HIP runtime beside the exes too (#468 #461: before System32)
rocm/bin/*.dll the ROCm DLLs the two programs load (their import tables, followed
through the ROCm DLLs, + amd_comgr.dll, which the HIP runtime loads
by name) and the Microsoft C++ runtime they import
rocm/bin/rocblas/library, rocm/bin/hipblaslt/library the GEMM kernels, for these archs only
rocm/.kpack/ rocBLAS's own device code for these archs (rocblas.dll finds it at
../.kpack from its folder)
rocm/licenses/ the shipped components' licenses, and NOTICE.txt
setup.py puts rocm/bin first on the engine's PATH (the config's lib_dirs). The AMD display driver (AMD Software:
Adrenalin Edition) is the only thing the user installs: TheRock's HIP runtime runs on top of it.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
ASSET = "strata-windows-x64-hip.zip"
PROGRAMS = ("strata.exe", "strata-device.exe")
DYNAMIC = ("amd_comgr.dll",) # LoadLibrary'd by amdhip64_7.dll: not in any import table
CRT = ("msvcp140.dll", "msvcp140_1.dll", "msvcp140_2.dll", "vcruntime140.dll", "vcruntime140_1.dll")
KPACKS = ("blas_lib",) # rocblas.dll's own device code; rocSOLVER's (27 MB per arch) is never launched by Strata
LICENSES = ("rocblas", "rocsolver", "hipblas", "hipblas-common", "hipblaslt", "amd_comgr", "rocm-core")
NOTICE = """Strata's Windows HIP engine ships these AMD ROCm components unmodified, from AMD's TheRock Python wheels
(ROCm {rocm}, https://nightly.repo.amd.com/rocm/whl-next/, built from https://github.com/ROCm/TheRock):
{dlls}
Licenses: rocBLAS, hipBLAS, hipBLAS-common, hipBLASLt (and its TensileLite host and origami libraries), rocm-core: MIT;
rocSOLVER: BSD 2-clause; amd_comgr: Apache 2.0 with LLVM exceptions (files in this folder). The HIP runtime
(amdhip64_7.dll, https://github.com/ROCm/rocm-systems/tree/develop/projects/clr) and rocm_kpack.dll (TheRock) are MIT
licensed; their wheels carry no license file. The Microsoft Visual C++ runtime DLLs (msvcp140*.dll,
vcruntime140*.dll) are redistributable under the Visual Studio license (Distributable Code).
"""
def imports(objdump: Path, dll: Path) -> list[str]:
text = subprocess.run([str(objdump), "-p", str(dll)], capture_output=True, text=True, check=True).stdout
return [m.group(1) for m in re.finditer(r"DLL Name:\s*(\S+)", text)]
def crt_dir() -> Path | None:
"""The MSVC redist folder with the C++ runtime DLLs (vcvars sets VCToolsRedistDir)."""
base = os.environ.get("VCToolsRedistDir")
if not base:
return None
hits = sorted(Path(base, "x64").glob("Microsoft.VC*.CRT"))
return hits[-1] if hits else None
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--build", required=True, type=Path)
ap.add_argument("--rocm", required=True, type=Path, help="the ROCm root (rocm-sdk path --root)")
ap.add_argument("--archs", required=True, help="the archs the engine was compiled for, ';'-separated")
ap.add_argument("--rocm-version", required=True)
ap.add_argument("--out", required=True, type=Path)
a = ap.parse_args()
archs = [x for x in re.split(r"[;, ]+", a.archs) if x]
rbin = a.rocm / "bin"
objdump = a.rocm / "lib" / "llvm" / "bin" / "llvm-objdump.exe"
stage = a.out / "strata-windows-x64-hip"
shutil.rmtree(stage, ignore_errors=True)
(stage / "rocm" / "bin").mkdir(parents=True)
for p in PROGRAMS:
shutil.copy2(a.build / p, stage / p)
# the ROCm DLLs: the programs' imports, followed through the ROCm DLLs themselves
rocm_dlls = {p.name.lower(): p for p in rbin.glob("*.dll")}
need, todo, crt = [], [stage / p for p in PROGRAMS] + [rbin / d for d in DYNAMIC], set()
while todo:
f = todo.pop()
for name in imports(objdump, f):
low = name.lower()
if low in CRT:
crt.add(low)
elif low in rocm_dlls and low not in need:
need.append(low)
todo.append(rocm_dlls[low])
for d in DYNAMIC:
if d not in need:
need.append(d)
for d in sorted(need):
shutil.copy2(rocm_dlls[d], stage / "rocm" / "bin" / rocm_dlls[d].name)
cdir = crt_dir()
if cdir is None:
sys.exit("VCToolsRedistDir is not set: run this from a Visual Studio x64 developer prompt (vcvars64.bat)")
for d in sorted(crt):
shutil.copy2(cdir / d, stage / "rocm" / "bin" / d)
# #468 #461: the HIP runtime (and the compiler it loads by name) next to the exes as well - Windows searches the
# exe's folder before System32, where an AMD driver may install its own amdhip64_7.dll (found before PATH's
# rocm/bin); rocBLAS/hipBLASLt stay in rocm/bin, which they resolve their kernel libraries and ../.kpack from
for pat in ("amdhip64_*.dll", "amd_comgr*.dll"):
for f in (stage / "rocm" / "bin").glob(pat):
shutil.copy2(f, stage / f.name)
# the GEMM kernels and the libraries' own device code, for these archs
lib = stage / "rocm" / "bin" / "rocblas" / "library"
lib.mkdir(parents=True)
for f in (rbin / "rocblas" / "library").iterdir():
m = re.search(r"gfx[0-9a-f]+", f.name)
if m is None or m.group() in archs:
shutil.copy2(f, lib / f.name)
for arch in archs:
src = rbin / "hipblaslt" / "library" / arch
if src.is_dir(): # hipBLASLt has no RDNA2 (gfx1030) kernels
shutil.copytree(src, stage / "rocm" / "bin" / "hipblaslt" / "library" / arch)
kp = stage / "rocm" / ".kpack"
kp.mkdir()
kpack_src = a.rocm / ".kpack"
for arch in archs:
for k in KPACKS:
f = kpack_src / f"{k}_{arch}.kpack"
if not f.exists():
sys.exit(f"missing {f}: install the rocm-sdk-device-{arch} wheel and run rocm-sdk init")
shutil.copy2(f, kp / f.name)
lic = stage / "rocm" / "licenses"
lic.mkdir()
for comp in LICENSES:
for f in (a.rocm / "share" / "doc" / comp).glob("LICENSE*"):
shutil.copy2(f, lic / f"{comp}-{f.name}")
shipped = sorted(p.name for p in (stage / "rocm" / "bin").glob("*.dll"))
(lic / "NOTICE.txt").write_text(NOTICE.format(rocm=a.rocm_version, dlls="\n".join(" " + d for d in shipped)),
encoding="utf-8")
version = re.search(r"project\(strata VERSION ([\d.]+)", (ROOT / "CMakeLists.txt").read_text()).group(1)
hl = (a.rocm / "include" / "hipblaslt" / "hipblaslt-version.h").read_text()
hlv = [int(re.search(rf"#define\s+HIPBLASLT_VERSION_{k}\s+(\d+)", hl).group(1)) for k in ("MAJOR", "MINOR", "PATCH")]
meta = {"source": "prebuilt", "backend": "hip", "platform": "windows-x64", "version": version, "archs": archs,
"rocm": a.rocm_version, "hipblaslt_version": hlv[0] * 100000 + hlv[1] * 100 + hlv[2], "vision": "none",
"lib_dirs": ["rocm/bin"]}
(stage / "BUILD.json").write_text(json.dumps(meta, indent=1), encoding="utf-8")
z = a.out / ASSET
z.unlink(missing_ok=True)
with zipfile.ZipFile(z, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as f:
for p in sorted(stage.rglob("*")):
if p.is_file():
f.write(p, p.relative_to(stage).as_posix())
size = sum(p.stat().st_size for p in stage.rglob("*") if p.is_file())
print(f"{z}: {z.stat().st_size / 2**20:.0f} MiB ({size / 2**20:.0f} MiB unpacked), engine {version}, "
f"{', '.join(archs)}, ROCm {a.rocm_version}, DLLs: {', '.join(shipped)}")
return 0
if __name__ == "__main__":
sys.exit(main())
+1725
View File
File diff suppressed because it is too large. Load diff
+77
View File
@@ -11,6 +11,7 @@ import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "tools"))
@@ -91,6 +92,53 @@ class Calibrate(unittest.TestCase):
self.assertIsNone(CAL.arg_value(b, "--pool-workers"))
self.assertEqual(b.count("--spec-min-p"), 1)
def test_engine_args_are_the_servers(self):
# #447: a "gpu" list is a layer split only with two or more cards; "0,2" is one too; split_skip_if_fits counts
helper = {"args": BASE + ["--expert-cache-device1", "1800"], "gpu": [0], "env": {"CUDA_VISIBLE_DEVICES": "0,1"}}
self.assertEqual(CAL.engine_args(helper), helper["args"]) # one stage + a helper card: no split
self.assertEqual(CAL.engine_args({"args": BASE, "gpu": 1}), BASE)
self.assertEqual(CAL.engine_args({"args": BASE, "gpu": [0, 2]}), BASE + ["--layer-split", "auto"])
self.assertEqual(CAL.engine_args({"args": BASE, "gpu": "0,2", "layer_split": "18"}),
BASE + ["--layer-split", "18"])
self.assertEqual(CAL.engine_args({"args": BASE, "gpu": [0, 1], "split_skip_if_fits": True}),
BASE + ["--layer-split", "auto", "--split-skip-if-fits"])
own = BASE + ["--layer-split", ""] # the config's own value wins
self.assertEqual(CAL.engine_args({"args": own, "gpu": [0, 1]}), own)
def test_run_measures_with_the_servers_args(self):
with tempfile.TemporaryDirectory() as d:
tok = Path(d)
(tok / "vocab.json").write_text(json.dumps({"a": 0, "b": 1}))
(tok / "merges.txt").write_text("")
(tok / "token_type.json").write_text(json.dumps([1, 1]))
seen = []
saved = CAL.measure
CAL.measure = lambda args, ids_list, start_engine, say=print: seen.append(args) or {}
fake = type("ST", (), {"Tokenizer": lambda *a: type("T", (), {"encode": lambda s, t, **k: [0]})()})
try:
with mock.patch.dict(sys.modules, {"strata_tokenizer": fake}):
CAL.run({"tokenizer": d, "args": list(BASE), "gpu": [0]}, start_engine=lambda a: None)
finally:
CAL.measure = saved
self.assertEqual(seen, [BASE])
def test_engine_error(self):
with tempfile.TemporaryDirectory() as d:
log = Path(d) / "strata-x.log"
log.write_text("strata generate: an old error\n", encoding="utf-8")
since = log.stat().st_size
with open(log, "a", encoding="utf-8") as f:
f.write("loading ...\nstrata generate: --expert-cache-remote with a layer split needs a GPU that runs "
"no stage (2 visible, 2 used by the split)\n\n")
self.assertEqual(CAL.engine_error(str(log), since), "strata generate: --expert-cache-remote with a layer "
"split needs a GPU that runs no stage (2 visible, 2 used by the split)")
with open(log, "a", encoding="utf-8") as f:
f.write("Segmentation fault\n")
self.assertIn("--expert-cache-remote", CAL.engine_error(str(log), since)) # the engine's own line first
self.assertEqual(CAL.engine_error(str(log), log.stat().st_size), None) # nothing new since
self.assertIsNone(CAL.engine_error(None))
self.assertIsNone(CAL.engine_error(str(Path(d) / "missing.log")))
def test_worker_candidates(self):
self.assertEqual(CAL.worker_candidates(6), [6, 4, 3])
self.assertEqual(CAL.worker_candidates(23), [23, 15, 12])
@@ -152,6 +200,35 @@ class SetupIntegration(unittest.TestCase):
self.assertEqual(json.loads(cfg_path.read_text())["args"], BASE)
self.assertIsNone(self.S.saved_calibration({"args": BASE, "model_name": "m"}))
def test_failed_calibration_says_the_engines_reason(self):
# #447: the engine's last error line is printed, and the --calibrate run repeats the failure before it starts
import contextlib
import io
log = Path(self.tmp.name) / "strata-q2_0.log"
log.write_text("strata generate: an error of an earlier start\n", encoding="utf-8")
cfg_path = Path(self.tmp.name) / "strata-q2_0.json"
cfg_path.write_text(json.dumps({"exe": "x", "args": list(BASE), "model_name": "m", "log": str(log)}))
def boom(*a, **k):
with open(log, "a", encoding="utf-8") as f:
f.write("strata generate: --expert-cache-remote with a layer split needs a GPU that runs no stage\n")
raise RuntimeError(f"the engine exited before it was ready (see {log})")
CAL.run = boom
out = io.StringIO()
with contextlib.redirect_stdout(out):
self.assertFalse(self.S.calibrate_config(cfg_path))
self.assertIn("the engine said: strata generate: --expert-cache-remote with a layer split", out.getvalue())
self.assertNotIn("an earlier start", out.getvalue())
out = io.StringIO()
with mock.patch.object(self.S, "ROOT", Path(self.tmp.name)), \
mock.patch.object(self.S, "data_folder", lambda d: (Path(self.tmp.name) / "data", [])), \
mock.patch.object(self.S, "update_installed_engine", lambda *a: None), \
mock.patch.object(sys, "argv", ["setup.py", "--calibrate", "--no-start"]), \
contextlib.redirect_stdout(out):
self.assertEqual(self.S.main(), 0)
self.assertIn("this PC is NOT tuned: the tuning failed (the reason is above); the model keeps the default "
"settings", out.getvalue())
if __name__ == "__main__":
unittest.main()
+220
View File
@@ -9,6 +9,7 @@ from __future__ import annotations
import sys
import tempfile
import unittest
from unittest import mock
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
@@ -116,6 +117,40 @@ class GpuLists(unittest.TestCase):
else:
setup.os.environ["ROCM_PATH"] = old
def test_runtime_only_system_rocm_falls_back_to_the_wheels(self):
"""#446: a system ROCm 7 with hipcc and libhipblas but no HIP development files (no hip-lang CMake package, no
hip_runtime.h) is not used for the build: a warning says what is missing and the wheels path follows (here
it stops at the two-family check, which only the wheels path makes); with the files it is used as before."""
said = []
setup.say = lambda msg="": said.append(msg) # tearDown puts the real one back
with tempfile.TemporaryDirectory() as d:
sysroot = Path(d)
for rel, text in (("bin/hipcc", ""), ("lib/libhipblas.so.3", ""),
("include/rocm-core/rocm_version.h",
"#define ROCM_VERSION_MAJOR 7\n#define ROCM_VERSION_MINOR 14\n")):
(sysroot / rel).parent.mkdir(parents=True, exist_ok=True)
(sysroot / rel).write_text(text)
with mock.patch.dict(setup.os.environ, {"ROCM_PATH": d}):
with self.assertRaises(SystemExit):
setup.rocm_root(["gfx1100", "gfx1201"])
self.assertIn(f"the ROCm in {sysroot} has no HIP development files (lib/cmake/hip-lang/hip-lang-"
"config.cmake, include/hip/hip_runtime.h): using AMD's wheels", "\n".join(said))
self.assertIn("two GPU families", "\n".join(said))
for lib in ("lib64", "lib"): # either place CMake looks
with self.subTest(lib=lib):
cfg = sysroot / lib / "cmake/hip-lang/hip-lang-config.cmake"
cfg.parent.mkdir(parents=True, exist_ok=True)
cfg.write_text("")
said.clear()
with self.assertRaises(SystemExit):
setup.rocm_root(["gfx1100", "gfx1201"])
self.assertIn("(include/hip/hip_runtime.h)", "\n".join(said))
(sysroot / "include/hip").mkdir(parents=True, exist_ok=True)
(sysroot / "include/hip/hip_runtime.h").write_text("")
self.assertEqual(setup.rocm_root(["gfx1100", "gfx1201"]), (sysroot, [str(sysroot / "lib")]))
cfg.unlink()
(sysroot / "include/hip/hip_runtime.h").unlink()
def test_build_for_every_arch(self):
"""build_engine_hip compiles for the set of the chosen cards' archs and records it in BUILD.json."""
calls = {}
@@ -152,5 +187,190 @@ class GpuLists(unittest.TestCase):
setattr(setup, k, v)
class WindowsDetection(unittest.TestCase):
"""Windows: the AMD cards from the display adapters (mocked Win32_VideoController rows and display-class registry
values), the HIP runtime's numbering from `strata-device --list-devices`, and the card setup chose matched to its
HIP ordinal (#325: an integrated Radeon is HIP's device 0)."""
ADAPTERS = [ # Win32_VideoController: name, PNPDeviceID, AdapterRAM (32-bit: at most 4 GB)
{"name": "NVIDIA GeForce RTX 5070", "pnp": r"PCI\VEN_10DE&DEV_2F04&SUBSYS_1234&REV_A1\4&1", "ram": 4293918720},
{"name": "AMD Radeon RX 9070 XT", "pnp": r"PCI\VEN_1002&DEV_7550&SUBSYS_0E3B1002&REV_C0\6&2", "ram": 4293918720},
{"name": "AMD Radeon(TM) Graphics", "pnp": r"PCI\VEN_1002&DEV_164E&SUBSYS_88771043&REV_C1\4&3", "ram": 536870912},
{"name": "AMD Radeon RX 7800 XT", "pnp": r"PCI\VEN_1002&DEV_7499&SUBSYS_0000&REV_C8\6&4", "ram": 4293918720},
]
REGISTRY = [ # the display class's driver instances: 64-bit VRAM size (as int, or as bytes)
{"DriverDesc": "NVIDIA GeForce RTX 5070", "MatchingDeviceId": r"PCI\VEN_10DE&DEV_2F04",
"HardwareInformation.qwMemorySize": 12 << 30},
{"DriverDesc": "AMD Radeon RX 6600", "MatchingDeviceId": r"PCI\VEN_1002&DEV_73FF", # a removed card
"HardwareInformation.qwMemorySize": 8 << 30},
{"DriverDesc": "AMD Radeon RX 9070 XT", "MatchingDeviceId": r"PCI\VEN_1002&DEV_7550&REV_C0",
"HardwareInformation.qwMemorySize": (16 << 30).to_bytes(8, "little"), "DriverVersion": "32.0.21013.1000"},
{"DriverDesc": "AMD Radeon(TM) Graphics", "MatchingDeviceId": r"PCI\VEN_1002&DEV_164E&REV_C1",
"HardwareInformation.qwMemorySize": 512 << 20},
{"DriverDesc": "AMD Radeon RX 7800 XT", "MatchingDeviceId": r"PCI\VEN_1002&DEV_7499",
"HardwareInformation.qwMemorySize": 16 << 30},
]
LIST = ("device 0: AMD Radeon(TM) Graphics\n arch gfx1036, 28.1 GiB, wave32\n"
" cannot run: GPU 0 (AMD Radeon(TM) Graphics, gfx1036) is not an architecture this Strata engine was "
"compiled for (gfx1100,gfx1101,gfx1102,gfx1200,gfx1201,gfx1030); ...\n"
"device 1: AMD Radeon RX 9070 XT\n arch gfx1201, 15.9 GiB, wave32\n"
"device 2: AMD Radeon RX 7800 XT\n arch gfx1101, 16.0 GiB, wave32\n")
def test_adapters(self):
g = setup.amd_gpus_windows(self.ADAPTERS, self.REGISTRY)
self.assertEqual([x["name"] for x in g], ["AMD Radeon RX 9070 XT", "AMD Radeon(TM) Graphics",
"AMD Radeon RX 7800 XT"]) # AMD only, present only, in order
self.assertEqual([x["index"] for x in g], [0, 1, 2])
self.assertEqual(g[0]["arch"], "gfx1201") # by PCI device id
self.assertEqual(g[2]["arch"], "gfx1101") # an unlisted id: by its name
self.assertTrue(g[1]["arch"].startswith("unknown")) # the iGPU: listed, not supported
self.assertAlmostEqual(g[0]["vram_gb"], 16.0) # the registry's 64-bit size
self.assertEqual(g[0]["driver"], "32.0.21013.1000")
self.assertEqual([setup.amd_problem(x) is None for x in g], [True, False, True])
def test_registry_alone(self):
"""No WMI answer: the registry's own list (which can hold a removed card)."""
g = setup.amd_gpus_windows([], self.REGISTRY)
self.assertEqual([x["arch"] for x in g if not x["arch"].startswith("unknown")], ["gfx1201", "gfx1101"])
def test_arch_names(self):
for name, arch in (("AMD Radeon RX 9070 GRE", "gfx1201"), ("AMD Radeon AI PRO R9700", "gfx1201"),
("AMD Radeon RX 9060 XT", "gfx1200"), ("AMD Radeon RX 7900 GRE", "gfx1100"),
("AMD Radeon PRO W7800", "gfx1100"), ("AMD Radeon RX 7700 XT", "gfx1101"),
("AMD Radeon RX 7600", "gfx1102"), ("AMD Radeon RX 6950 XT", "gfx1030"),
("AMD Radeon RX 6800M", ""), ("AMD Radeon 780M Graphics", ""), ("AMD Radeon RX 7700S", "")):
self.assertEqual(setup.win_amd_arch(None, name), arch, name)
self.assertEqual(setup.win_amd_arch(0x744C, "whatever"), "gfx1100")
def test_list_devices(self):
h = setup.hip_devices(text=self.LIST)
self.assertEqual([(x["index"], x["arch"]) for x in h], [(0, "gfx1036"), (1, "gfx1201"), (2, "gfx1101")])
self.assertAlmostEqual(h[1]["vram_gb"], 15.9)
self.assertIn("not an architecture", h[0]["cannot_run"])
self.assertIsNotNone(setup.amd_problem(h[0]))
self.assertIsNone(setup.amd_problem(h[1]))
cannot = setup.hip_devices(text="device 0: AMD Radeon RX 9070 XT\n arch gfx1201, 15.9 GiB, wave32\n"
" cannot run: GPU 0 runs wave64\n")
self.assertEqual(setup.amd_problem(cannot[0]), "GPU 0 runs wave64") # the engine's own check counts
self.assertEqual(setup.hip_devices(text="(no GPU device)\n"), [])
def test_the_card_by_its_hip_ordinal(self):
"""#325: setup's GPU 0 (the 9070 XT, display order) is HIP's device 1 behind the iGPU."""
listed = setup.amd_gpus_windows(self.ADAPTERS, self.REGISTRY)
hip = setup.hip_devices(text=self.LIST)
self.assertEqual(setup.hip_match(listed[0], listed, hip)["index"], 1)
self.assertEqual(setup.hip_match(listed[2], listed, hip)["index"], 2)
two = [{"index": 0, "arch": "gfx1201"}, {"index": 1, "arch": "gfx1201"}] # two of a kind: by rank
self.assertEqual(setup.hip_match(two[1], two, [{"index": 0, "arch": "gfx1036"}, {"index": 1, "arch": "gfx1201"},
{"index": 2, "arch": "gfx1201"}])["index"], 2)
self.assertIsNone(setup.hip_match({"index": 0, "arch": "gfx1100"}, [{"index": 0, "arch": "gfx1100"}], hip))
def test_hip_card(self):
listed = setup.amd_gpus_windows(self.ADAPTERS, self.REGISTRY)
with mock.patch.object(setup, "hip_devices", lambda probe=None, text=None: setup_hip(self.LIST)), \
mock.patch.object(setup, "ok", lambda *a: None):
g = setup.hip_card(Path("engine"), listed[0], listed)
self.assertEqual((g["index"], g["count"], g["arch"], g["vram_gb"]), (1, 3, "gfx1201", 15.9))
with mock.patch.object(setup, "hip_devices", lambda probe=None, text=None: []), \
mock.patch.object(setup, "say", lambda *a, **k: None), self.assertRaises(SystemExit):
setup.hip_card(Path("engine"), listed[0], listed) # no AMD driver: stops before the download
with mock.patch.object(setup, "hip_devices", lambda probe=None, text=None: setup_hip(self.LIST)[:1]), \
mock.patch.object(setup, "say", lambda *a, **k: None), self.assertRaises(SystemExit):
setup.hip_card(Path("engine"), listed[0], listed) # HIP does not list the card
def test_windows_dispatch(self):
"""amd_gpus() on Windows: HIP's numbering once the HIP engine answers, the display adapters before."""
with mock.patch.object(setup, "WIN", True), \
mock.patch.object(setup, "hip_devices", lambda probe=None, text=None: None), \
mock.patch.object(setup, "amd_gpus_windows", lambda adapters=None, registry=None: ["display"]):
self.assertEqual(setup.amd_gpus(), ["display"])
with mock.patch.object(setup, "WIN", True), \
mock.patch.object(setup, "hip_devices", lambda probe=None, text=None: ["hip"]), \
mock.patch.object(setup, "amd_gpus_windows", lambda adapters=None, registry=None: ["display"]):
self.assertEqual(setup.amd_gpus(), ["hip"])
def test_no_hip_engine_no_probe(self):
with tempfile.TemporaryDirectory() as d:
eng = Path(d)
(eng / "strata-device.exe").write_text("x")
(eng / "BUILD.json").write_text('{"backend": "cuda"}') # an NVIDIA engine is never asked
self.assertIsNone(setup.hip_devices(eng / "strata-device.exe"))
def test_prebuilt_hip_zip(self):
"""get_prebuilt_hip: a published zip (here a local folder) is unpacked into engine/; one without code for the
card, or older than the first Windows HIP release, is refused."""
import json
import zipfile
with tempfile.TemporaryDirectory() as d:
root = Path(d)
pub = root / "pub"
pub.mkdir()
def publish(meta):
with zipfile.ZipFile(pub / setup.WIN_HIP_ASSET, "w") as z:
z.writestr("strata.exe", "engine")
z.writestr("strata-device.exe", "probe")
z.writestr("rocm/bin/amdhip64_7.dll", "dll")
z.writestr("BUILD.json", json.dumps(meta))
ver = ".".join(map(str, setup.WIN_HIP_MIN_ENGINE))
good = {"source": "prebuilt", "backend": "hip", "version": ver, "archs": ["gfx1100", "gfx1201"],
"lib_dirs": ["rocm/bin"]}
with mock.patch.object(setup, "ROOT", root), mock.patch.object(setup, "say", lambda *a, **k: None), \
mock.patch.object(setup, "ok", lambda *a: None), mock.patch.object(setup, "warn", lambda *a: None):
publish({**good, "archs": ["gfx1100"]})
self.assertIsNone(setup.get_prebuilt_hip(str(pub) + "/", {"arch": "gfx1201"}))
publish({**good, "version": "0.1.30"})
self.assertIsNone(setup.get_prebuilt_hip(str(pub) + "/", {"arch": "gfx1201"}))
publish(good)
eng = setup.get_prebuilt_hip(str(pub) + "/", {"arch": "gfx1201"})
self.assertEqual(eng, root / "engine")
self.assertTrue((eng / "rocm" / "bin" / "amdhip64_7.dll").exists())
self.assertEqual(setup.hip_lib_dirs(eng), [eng / "rocm" / "bin"])
(pub / setup.WIN_HIP_ASSET).unlink() # installed: kept, nothing downloaded again
self.assertEqual(setup.get_prebuilt_hip(str(pub) + "/", {"arch": "gfx1201"}), eng)
_HIP_DEVICES = setup.hip_devices # the real parser, for the tests that mock setup.hip_devices
def setup_hip(text):
return _HIP_DEVICES(text=text)
class WindowsHipVision(unittest.TestCase):
def test_no_cpu_encoder_on_windows(self):
with mock.patch.object(setup, "WIN", True), mock.patch.object(setup, "warn", lambda *a: None):
self.assertEqual(setup.hip_vision("cpu"), "none")
self.assertEqual(setup.hip_vision("yes"), "none")
with mock.patch.object(setup, "WIN", False), mock.patch.object(setup, "warn", lambda *a: None):
self.assertEqual(setup.hip_vision("cpu"), "cpu")
class HipRuntimeBesideExe(unittest.TestCase):
"""#468 #461: the bundled HIP runtime (and amd_comgr) goes next to strata.exe, so an AMD driver's System32 copy is
not found first; rocBLAS and the rest stay in rocm/bin."""
def test_copies_only_the_runtime(self):
import json
with tempfile.TemporaryDirectory() as d:
eng = Path(d)
rb = eng / "rocm" / "bin"
rb.mkdir(parents=True)
(eng / "BUILD.json").write_text(json.dumps({"backend": "hip", "lib_dirs": ["rocm/bin"]}))
for n, data in (("amdhip64_7.dll", b"hip-3686"), ("amd_comgr.dll", b"comgr"), ("rocblas.dll", b"blas")):
(rb / n).write_bytes(data)
setup.hip_runtime_beside_exe(eng)
self.assertEqual((eng / "amdhip64_7.dll").read_bytes(), b"hip-3686")
self.assertEqual((eng / "amd_comgr.dll").read_bytes(), b"comgr")
self.assertFalse((eng / "rocblas.dll").exists()) # finds its kernels relative to rocm/bin
(rb / "amdhip64_7.dll").write_bytes(b"hip-3690-newer") # a newer zip: replaced on the next start
setup.hip_runtime_beside_exe(eng)
self.assertEqual((eng / "amdhip64_7.dll").read_bytes(), b"hip-3690-newer")
def test_no_rocm_bin_is_a_no_op(self):
with tempfile.TemporaryDirectory() as d:
setup.hip_runtime_beside_exe(Path(d)) # no BUILD.json (a CUDA or Linux engine)
self.assertEqual(list(Path(d).iterdir()), [])
if __name__ == "__main__":
unittest.main()
+60 -1
View File
@@ -92,6 +92,64 @@ class GgufDirShards(unittest.TestCase):
self.assertEqual(self.shards(["a-00001-of-00002.gguf", "b-00001-of-00002.gguf"]), want) # ambiguous
class GgufDirUnsupported(unittest.TestCase):
"""#444: --gguf-dir with GGUFs Strata cannot run (Unsloth's UD-IQ3_XXS, UD-Q2_K_XL) says so, names the files it
runs (ISTA-DASLab's GSQ-RCO, the Coder's, Unsloth's UD-Q4_K_XL) and the --family/--model of the usable ones."""
UD = ["Qwen3.8-Flash-Next-UD-IQ3_XXS-%05d-of-00003.gguf" % i for i in range(1, 4)] + \
["Qwen3.8-Flash-Next-UD-Q2_K_XL-%05d-of-00003.gguf" % i for i in range(1, 4)]
def problem(self, names, family="qwen", model="IQ3_XXS"):
with tempfile.TemporaryDirectory() as d:
for n in names:
(Path(d) / n).write_bytes(b"")
fam = setup.FAMILIES[family]
first = setup.gguf_dir_shards(Path(d), fam, model)[0]
p = setup.gguf_dir_problem(Path(d), first, fam, model)
return p and (p[0].replace(d, "<D>"), p[1])
def test_quant_names(self):
for name, want in (("Qwen3.8-Flash-Next-UD-IQ3_XXS-00001-of-00003.gguf", "UD-IQ3_XXS"),
("Qwen3.8-Flash-Next-UD-Q2_K_XL-00001-of-00003.gguf", "UD-Q2_K_XL"),
("model-Q4_K_M.gguf", "Q4_K_M"),
("Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00001-of-00002.gguf", None),
("Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf", None),
("my-IQ3_XXS-00001-of-00003.gguf", None), ("mmproj-F16.gguf", None),
("a-00001-of-00002.gguf", None)):
self.assertEqual(setup.gguf_unsupported(name), want, name)
def test_an_unsloth_ud_file_is_not_taken_for_a_gsq_rco_size(self):
msg, hint = self.problem(self.UD) # IQ3_XXS: UD-IQ3_XXS has the size in its name
self.assertEqual(msg, "Qwen3.8-Flash-Next-UD-IQ3_XXS-00001-of-00003.gguf is UD-IQ3_XXS, a GGUF Strata "
"cannot run")
self.assertIn("ISTA-DASLab's GSQ-RCO files", hint)
self.assertIn("Unsloth's UD-Q4_K_XL only", hint)
def test_a_folder_without_the_choice_names_what_is_there(self):
gsq = ["Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-%05d-of-00002.gguf" % i for i in (1, 2)]
msg, hint = self.problem(self.UD + gsq, "unsloth", "UD-Q4_K_XL")
self.assertEqual(msg, "<D> has no Qwen3.8-Flash-Next (Unsloth) UD-Q4_K_XL file")
self.assertIn("Not usable here: Qwen3.8-Flash-Next-UD-IQ3_XXS-00001-of-00003.gguf, "
"Qwen3.8-Flash-Next-UD-Q2_K_XL-00001-of-00003.gguf", hint)
self.assertIn("Usable here: --family coder --model IQ1_M", hint)
def test_usable_folders_are_left_alone(self):
good = ["Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-%05d-of-00002.gguf" % i for i in (1, 2)]
self.assertIsNone(self.problem(good + self.UD))
self.assertIsNone(self.problem(["my-IQ3_XXS-%05d-of-00003.gguf" % i for i in (1, 2, 3)]))
self.assertIsNone(self.problem([])) # nothing there: check_shards' "missing", as before
def test_the_size_of_another_family(self):
from test_setup_golden import PROFILES, install
ram, found = PROFILES["64GB-1x32GB"]
with tempfile.TemporaryDirectory() as d:
code, out, _, _ = install(ram, found, ["--gguf-dir", d, "--family", "unsloth", "--model", "IQ3_XXS"])
self.assertEqual(code, 1)
self.assertIn("has no IQ3_XXS model file", out)
self.assertIn("choose one of: UD-Q4_K_XL (or IQ3_XXS: --family qwen --model IQ3_XXS, --family swift --model "
"IQ3_XXS)", out)
self.assertIn("Strata runs ISTA-DASLab's GSQ-RCO files", out)
class ExperimentalSm60(unittest.TestCase):
"""#295: Pascal (6.x) and Volta (7.0) only with STRATA_EXPERIMENTAL_SM60=1, built with -DSTRATA_EXPERIMENTAL_SM60=ON
and a CUDA 12.x toolkit; nothing changes without the variable."""
@@ -128,7 +186,7 @@ class ExperimentalSm60(unittest.TestCase):
str(old): "Cuda compilation tools, release 12.9, V12.9.86"}
with mock.patch.object(setup, "WIN", False), mock.patch.object(setup.shutil, "which", lambda n: str(new)), \
mock.patch.dict(os.environ, {"CUDA_PATH": str(Path(d) / "12")}), \
mock.patch.object(setup, "out", lambda cmd: versions[cmd[0]]):
mock.patch.object(setup, "out", lambda cmd: versions.get(cmd[0], "")): # #414
self.assertEqual(setup.find_nvcc(), (str(new), (13, 0)))
self.assertEqual(setup.find_nvcc(below=(13, 0)), (str(old), (12, 9)))
@@ -160,6 +218,7 @@ class ExperimentalSm60(unittest.TestCase):
class HipVision(unittest.TestCase):
"""#304: --vision cpu with --backend hip builds the CPU image encoder beside the HIP engine."""
@mock.patch.object(setup, "WIN", False) # Linux: compiled here (Windows has no AMD image encoder yet)
def test_the_choice(self):
self.assertEqual(quiet(setup.hip_vision, "cpu"), ("cpu", ""))
for asked in (None, "no", "none"):
+966
View File
@@ -0,0 +1,966 @@
{
"128GB-1x24GB qwen IQ3_S": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/iq3_s",
"--native",
"<T>/models/IQ3_S/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/IQ3_S/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"131072",
"--kv",
"int8",
"--kv-resident",
"32768"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-iq3_s.log",
"model_name": "qwen3.8-flash-next-iq3_s",
"port": 8080,
"tokenizer": "<T>/data/packs/iq3_s/tokenizer"
}
},
"128GB-1x24GB qwen IQ3_XXS": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/iq3_xxs",
"--native",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"131072",
"--kv",
"int8",
"--kv-resident",
"32768"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-iq3_xxs.log",
"model_name": "qwen3.8-flash-next-iq3_xxs",
"port": 8080,
"tokenizer": "<T>/data/packs/iq3_xxs/tokenizer"
}
},
"128GB-1x24GB qwen Q2_0": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/q2_0",
"--native",
"<T>/models/Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"131072",
"--kv",
"int8",
"--kv-resident",
"32768"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-q2_0.log",
"model_name": "qwen3.8-flash-next-q2_0",
"port": 8080,
"tokenizer": "<T>/data/packs/q2_0/tokenizer"
}
},
"128GB-1x24GB qwen recommended": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/iq3_xxs",
"--native",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"131072",
"--kv",
"int8",
"--kv-resident",
"32768"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-iq3_xxs.log",
"model_name": "qwen3.8-flash-next-iq3_xxs",
"port": 8080,
"tokenizer": "<T>/data/packs/iq3_xxs/tokenizer"
}
},
"128GB-1x24GB unsloth UD-Q4_K_XL": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/unsloth-ud-q4_k_xl",
"--native",
"<T>/models/unsloth-UD-Q4_K_XL/Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"32768",
"--kv",
"int8",
"--resident-budget-gib",
"71"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-unsloth-ud-q4_k_xl.log",
"model_name": "qwen3.8-flash-next-unsloth-ud-q4_k_xl",
"port": 8080,
"tokenizer": "<T>/data/packs/unsloth-ud-q4_k_xl/tokenizer"
}
},
"32GB-2x24GB qwen IQ3_S": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/iq3_s",
"--native",
"<T>/models/IQ3_S/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/IQ3_S/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"131072",
"--kv",
"int8",
"--mmap-experts"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-iq3_s.log",
"model_name": "qwen3.8-flash-next-iq3_s",
"port": 8080,
"tokenizer": "<T>/data/packs/iq3_s/tokenizer"
}
},
"32GB-2x24GB qwen IQ3_XXS": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/iq3_xxs",
"--native",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"131072",
"--kv",
"int8",
"--mmap-experts"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-iq3_xxs.log",
"model_name": "qwen3.8-flash-next-iq3_xxs",
"port": 8080,
"tokenizer": "<T>/data/packs/iq3_xxs/tokenizer"
}
},
"32GB-2x24GB qwen Q2_0": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/q2_0",
"--native",
"<T>/models/Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"131072",
"--kv",
"int8",
"--resident-experts"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-q2_0.log",
"model_name": "qwen3.8-flash-next-q2_0",
"port": 8080,
"tokenizer": "<T>/data/packs/q2_0/tokenizer"
}
},
"32GB-2x24GB qwen recommended": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/q2_0",
"--native",
"<T>/models/Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"131072",
"--kv",
"int8",
"--resident-experts"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-q2_0.log",
"model_name": "qwen3.8-flash-next-q2_0",
"port": 8080,
"tokenizer": "<T>/data/packs/q2_0/tokenizer"
}
},
"32GB-2x24GB unsloth UD-Q4_K_XL": {
"code": 1,
"config": null
},
"47GB-2x16GB qwen IQ3_S": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/iq3_s",
"--native",
"<T>/models/IQ3_S/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/IQ3_S/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"65536",
"--kv",
"int8",
"--mmap-experts"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-iq3_s.log",
"model_name": "qwen3.8-flash-next-iq3_s",
"port": 8080,
"tokenizer": "<T>/data/packs/iq3_s/tokenizer"
}
},
"47GB-2x16GB qwen IQ3_XXS": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/iq3_xxs",
"--native",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"65536",
"--kv",
"int8",
"--resident-experts"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-iq3_xxs.log",
"model_name": "qwen3.8-flash-next-iq3_xxs",
"port": 8080,
"tokenizer": "<T>/data/packs/iq3_xxs/tokenizer"
}
},
"47GB-2x16GB qwen Q2_0": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/q2_0",
"--native",
"<T>/models/Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"65536",
"--kv",
"int8"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": [
0,
1
],
"gpus_asked": true,
"layer_split": "auto",
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-q2_0.log",
"model_name": "qwen3.8-flash-next-q2_0",
"port": 8080,
"tokenizer": "<T>/data/packs/q2_0/tokenizer"
}
},
"47GB-2x16GB qwen recommended": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/q2_0",
"--native",
"<T>/models/Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"65536",
"--kv",
"int8"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": [
0,
1
],
"gpus_asked": true,
"layer_split": "auto",
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-q2_0.log",
"model_name": "qwen3.8-flash-next-q2_0",
"port": 8080,
"tokenizer": "<T>/data/packs/q2_0/tokenizer"
}
},
"47GB-2x16GB unsloth UD-Q4_K_XL": {
"code": 1,
"config": null
},
"64GB-1x32GB qwen IQ3_S": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/iq3_s",
"--native",
"<T>/models/IQ3_S/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/IQ3_S/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"131072",
"--kv",
"int8"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-iq3_s.log",
"model_name": "qwen3.8-flash-next-iq3_s",
"port": 8080,
"tokenizer": "<T>/data/packs/iq3_s/tokenizer"
}
},
"64GB-1x32GB qwen IQ3_XXS": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/iq3_xxs",
"--native",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"131072",
"--kv",
"int8",
"--kv-resident",
"32768"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-iq3_xxs.log",
"model_name": "qwen3.8-flash-next-iq3_xxs",
"port": 8080,
"tokenizer": "<T>/data/packs/iq3_xxs/tokenizer"
}
},
"64GB-1x32GB qwen Q2_0": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/q2_0",
"--native",
"<T>/models/Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"131072",
"--kv",
"int8",
"--kv-resident",
"32768"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-q2_0.log",
"model_name": "qwen3.8-flash-next-q2_0",
"port": 8080,
"tokenizer": "<T>/data/packs/q2_0/tokenizer"
}
},
"64GB-1x32GB qwen recommended": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/iq3_xxs",
"--native",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"131072",
"--kv",
"int8",
"--kv-resident",
"32768"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-iq3_xxs.log",
"model_name": "qwen3.8-flash-next-iq3_xxs",
"port": 8080,
"tokenizer": "<T>/data/packs/iq3_xxs/tokenizer"
}
},
"64GB-1x32GB unsloth UD-Q4_K_XL": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/unsloth-ud-q4_k_xl",
"--native",
"<T>/models/unsloth-UD-Q4_K_XL/Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"32768",
"--kv",
"int8",
"--resident-budget-gib",
"40"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-unsloth-ud-q4_k_xl.log",
"model_name": "qwen3.8-flash-next-unsloth-ud-q4_k_xl",
"port": 8080,
"tokenizer": "<T>/data/packs/unsloth-ud-q4_k_xl/tokenizer"
}
},
"96GB-1x16GB qwen IQ3_S": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/iq3_s",
"--native",
"<T>/models/IQ3_S/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/IQ3_S/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"65536",
"--kv",
"int8",
"--kv-resident",
"32768"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-iq3_s.log",
"model_name": "qwen3.8-flash-next-iq3_s",
"port": 8080,
"tokenizer": "<T>/data/packs/iq3_s/tokenizer"
}
},
"96GB-1x16GB qwen IQ3_XXS": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/iq3_xxs",
"--native",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"65536",
"--kv",
"int8",
"--kv-resident",
"32768"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-iq3_xxs.log",
"model_name": "qwen3.8-flash-next-iq3_xxs",
"port": 8080,
"tokenizer": "<T>/data/packs/iq3_xxs/tokenizer"
}
},
"96GB-1x16GB qwen Q2_0": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/q2_0",
"--native",
"<T>/models/Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"65536",
"--kv",
"int8",
"--kv-resident",
"32768"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-q2_0.log",
"model_name": "qwen3.8-flash-next-q2_0",
"port": 8080,
"tokenizer": "<T>/data/packs/q2_0/tokenizer"
}
},
"96GB-1x16GB qwen recommended": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/iq3_xxs",
"--native",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00001-of-00002.gguf",
"--ple-gguf",
"<T>/models/IQ3_XXS/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00002-of-00002.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"65536",
"--kv",
"int8",
"--kv-resident",
"32768"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-iq3_xxs.log",
"model_name": "qwen3.8-flash-next-iq3_xxs",
"port": 8080,
"tokenizer": "<T>/data/packs/iq3_xxs/tokenizer"
}
},
"96GB-1x16GB unsloth UD-Q4_K_XL": {
"code": 0,
"config": {
"args": [
"--pack",
"<T>/data/packs/unsloth-ud-q4_k_xl",
"--native",
"<T>/models/unsloth-UD-Q4_K_XL/Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf",
"--expert-profile",
"<T>/data/expert-profile.bin",
"--expert-cache",
"auto",
"--prefill",
"auto",
"--spec",
"4",
"--spec-min-p",
"0.5",
"--mtp",
"<T>/data/mtp/rt",
"--max-context",
"32768",
"--kv",
"int8",
"--resident-budget-gib",
"71"
],
"cwd": "<T>",
"exe": "<T>/engine/<EXE>",
"gpu": 0,
"gpus_asked": true,
"lib_dirs": [
"<cuda>"
],
"log": "<T>/strata-unsloth-ud-q4_k_xl.log",
"model_name": "qwen3.8-flash-next-unsloth-ud-q4_k_xl",
"port": 8080,
"tokenizer": "<T>/data/packs/unsloth-ud-q4_k_xl/tokenizer"
}
}
}
+208
View File
@@ -0,0 +1,208 @@
"""The owner's rule for setup (#403 #406 #364 #384): setup RECOMMENDS, it never forces - and the recommended answers
stay exactly what they were. setup.main() with --yes (and with Enter for every question) on a few PCs writes the same
run config, byte for byte, as 0.1.32 plus the fixes on top of it did (tools/test_setup_golden.json, written by that
setup). Every outside effect is mocked: no GPU, no downloads, nothing written outside a temp folder.
python -m unittest tools.test_setup_golden
python tools/test_setup_golden.py --write (only to record a new baseline on purpose)
"""
from __future__ import annotations
import contextlib
import io
import json
import sys
import tempfile
import types
import unittest
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
import setup # noqa: E402
GOLDEN = Path(__file__).with_name("test_setup_golden.json")
def card(i, name, vram, arch):
return {"index": i, "name": name, "vram_gb": vram, "arch": arch, "driver": "580.97"}
# the PCs: total RAM as ram_gb() reads it (a little under the label), the cards as nvidia-smi lists them
PROFILES = {
"64GB-1x32GB": (63.7, [card(0, "NVIDIA GeForce RTX 5090", 31.8, "120")]), # #406
"32GB-2x24GB": (31.9, [card(i, "NVIDIA GeForce RTX 3090", 24.0, "86") for i in range(2)]), # #364
"47GB-2x16GB": (47.0, [card(i, "NVIDIA GeForce RTX 5060 Ti", 15.9, "120") for i in range(2)]), # #384
"96GB-1x16GB": (95.8, [card(0, "NVIDIA GeForce RTX 5080", 15.9, "120")]),
"128GB-1x24GB": (127.8, [card(0, "NVIDIA GeForce RTX 4090", 24.0, "89")]),
}
# (family, model): None = the recommended size
MODELS = [("qwen", None), ("qwen", "Q2_0"), ("qwen", "IQ3_XXS"), ("qwen", "IQ3_S"), ("unsloth", "UD-Q4_K_XL")]
class FakeGGUF:
"""gguf_reader.GGUFFile: shard 2 holds the PLE table."""
def __init__(self, path):
names = ["per_layer_token_embd.weight"] if "00002-of" in str(path) else ["blk.0.ffn_up_exps.weight"]
self.tensors = [types.SimpleNamespace(name=n) for n in names]
def normalize(v, t: Path):
"""The config with the temp folder and the platform's names taken out (it compares on Windows and Linux)."""
if isinstance(v, dict):
return {k: normalize(x, t) for k, x in v.items()}
if isinstance(v, list):
return [normalize(x, t) for x in v]
if isinstance(v, str):
return v.replace(str(t), "<T>").replace("\\", "/").replace(setup.EXE, "<EXE>")
return v
def install(ram, found, argv, answers=None, extra=(), avx512=False, configs=()):
"""setup.main() on a mocked PC -> (exit code, printed text, the written config or None, the questions asked).
answers: None = --yes (a question fails the run), "" = Enter for every question, a list of answers in order, or
{words of a question: its answer} (Enter for the others).
extra: more patches (mock.patch objects). configs: (name, dict) run configs already in the Strata folder."""
with tempfile.TemporaryDirectory() as tmp:
t = Path(tmp)
(t / "data" / "mtp" / "rt").mkdir(parents=True)
(t / "data" / "mtp" / "rt" / "experts.bin").write_bytes(b"")
eng = t / "engine"
eng.mkdir()
(eng / "BUILD.json").write_text(json.dumps({"version": "0.1.32", "source": "local", "cuda_dirs": ["<cuda>"]}))
for name, c in configs:
(t / name).write_text(json.dumps(c))
asked = []
def fake_input(prompt=""):
asked.append(prompt)
if answers is None:
raise AssertionError(f"asked {prompt!r} with --yes")
if isinstance(answers, list):
return answers.pop(0) if answers else ""
if isinstance(answers, dict): # {words of the question: answer}, else Enter
return next((v for k, v in answers.items() if k in prompt), "")
return answers
def fake_download(url, dst, what=None):
dst.parent.mkdir(parents=True, exist_ok=True)
dst.write_bytes(b"")
setup.mark(dst)
patches = [
mock.patch.object(setup, "ROOT", t),
mock.patch.object(setup, "GPU_PICK", None),
mock.patch.object(setup, "data_folder", lambda d: (t / "data", [])),
mock.patch.object(setup, "load_settings", lambda: {}),
mock.patch.object(setup, "save_settings", lambda s: None),
mock.patch.object(setup, "gpus", lambda: found),
mock.patch.object(setup, "amd_gpus", lambda: []),
mock.patch.object(setup, "ram_gb", lambda: ram),
mock.patch.object(setup, "cpu_info", lambda: ("Test CPU", True, avx512)),
mock.patch.object(setup, "page_file_gb", lambda: 16.0),
mock.patch.object(setup, "is_wsl", lambda: False),
mock.patch.object(setup, "free_gb", lambda p: 900.0),
mock.patch.object(setup, "pip_install", lambda *a, **k: None),
mock.patch.object(setup, "get_llama_cpp", lambda: t / "llama.cpp"),
mock.patch.object(setup, "get_prebuilt", lambda *a, **k: eng),
mock.patch.object(setup, "update_installed_engine", lambda *a, **k: None),
mock.patch.object(setup, "download", fake_download),
mock.patch.object(setup, "check_shards", lambda shards: None),
mock.patch.object(setup, "verify_sha256", lambda *a: None),
mock.patch.object(setup, "run", lambda *a, **k: None),
mock.patch.object(setup, "mtp_corrupt", lambda *a, **k: False),
mock.patch.object(setup, "refresh_draft_vocab", lambda *a, **k: None),
mock.patch.object(setup, "write_run_script", lambda tag, cfg, port: t / f"run-{tag}.bat"),
mock.patch.object(setup, "saved_calibration", lambda cfg: None),
mock.patch.object(setup, "calibrate_config", mock.Mock(side_effect=AssertionError("calibrated"))),
mock.patch.dict(sys.modules, {"gguf_reader": types.SimpleNamespace(GGUFFile=FakeGGUF)}),
mock.patch.object(sys, "argv", ["setup.py", "--models-dir", str(t / "models"), *argv]
+ (["--yes"] if answers is None else [])),
mock.patch("builtins.input", fake_input),
*extra,
]
out = io.StringIO()
code = None
with contextlib.ExitStack() as st:
for p in patches:
st.enter_context(p)
with contextlib.redirect_stdout(out):
try:
code = setup.main()
except SystemExit as e:
code = e.code
written = sorted(t.glob("strata-*.json"), key=lambda p: p.stat().st_mtime)
cfg = None
if written and code == 0:
cfg = normalize(json.loads(written[-1].read_text(encoding="utf-8")), t)
return code, out.getvalue(), cfg, asked
def argv_for(family, model):
return ["--family", family, "--no-start"] + (["--model", model] if model else [])
def record() -> dict:
"""Every profile x model with --yes: the exit code and the config (the baseline this test compares against)."""
res = {}
for prof, (ram, found) in PROFILES.items():
for family, model in MODELS:
code, out, cfg, _ = install(ram, found, argv_for(family, model))
res[f"{prof} {family} {model or 'recommended'}"] = {"code": code, "config": cfg}
return res
class Golden(unittest.TestCase):
"""--yes and Enter: the same config as the baseline wherever the baseline wrote one."""
@classmethod
def setUpClass(cls):
cls.golden = json.loads(GOLDEN.read_text(encoding="utf-8"))
def cases(self):
for prof, (ram, found) in PROFILES.items():
for family, model in MODELS:
key = f"{prof} {family} {model or 'recommended'}"
if self.golden[key]["code"] == 0:
yield key, ram, found, family, model
def test_the_baseline_covers_every_kind_of_pc(self):
written = [k for k, v in self.golden.items() if v["code"] == 0]
self.assertGreaterEqual(len(written), 20)
lowram_multi = self.golden["32GB-2x24GB qwen IQ3_XXS"]["config"] # #364: low-RAM, two cards -> one
self.assertIn("--resident-experts" if "--resident-experts" in lowram_multi["args"] else "--mmap-experts",
lowram_multi["args"])
self.assertNotIsInstance(lowram_multi.get("gpu"), list)
def test_yes(self):
for key, ram, found, family, model in self.cases():
with self.subTest(key):
code, out, cfg, asked = install(ram, found, argv_for(family, model))
self.assertEqual(code, 0, out[-3000:])
self.assertEqual(cfg, self.golden[key]["config"], out[-3000:])
self.assertEqual(asked, [])
def test_enter_for_every_question(self):
for key, ram, found, family, model in self.cases():
with self.subTest(key):
code, out, cfg, asked = install(ram, found, argv_for(family, model), answers="")
self.assertEqual(code, 0, out[-3000:])
self.assertEqual(cfg, self.golden[key]["config"], out[-3000:])
def test_the_refusals_with_yes_alone_stay(self):
"""A size the baseline refused with --yes and no explicit --model still stops (--yes alone is no consent)."""
for prof, (ram, found) in PROFILES.items():
for family, model in MODELS:
key = f"{prof} {family} {model or 'recommended'}"
if self.golden[key]["code"] != 0 and model is None:
with self.subTest(key):
self.assertNotEqual(install(ram, found, argv_for(family, model))[0], 0)
if __name__ == "__main__":
if "--write" in sys.argv:
GOLDEN.write_text(json.dumps(record(), indent=1, sort_keys=True) + "\n", encoding="utf-8")
print(f"wrote {GOLDEN}")
else:
unittest.main()
+4 -2
View File
@@ -46,8 +46,10 @@ class OneGpuWhy(unittest.TestCase):
need = arena + setup.LOW_RAM_HEADROOM_GB
self.assertIn(f"{arena:.0f} + {setup.LOW_RAM_HEADROOM_GB} = {need:.0f} GB", text)
self.assertIn("this PC has 46 GB", text)
self.assertIn("layer split", text) # why one GPU
self.assertIn("--low-ram off", text) # and the way to keep them all
self.assertIn("layer split", text) # why one GPU is recommended
self.assertIn("--gpus 0,1", text) # #364 #384: and the way to use them all
self.assertIn("OS file cache", text)
self.assertNotIn("--low-ram off", text) # (that pages the experts: not the way)
def test_an_explicit_choice_says_so(self):
text = "\n".join(setup.low_ram_one_gpu_why("Q2_0", 64, "resident"))
+89
View File
@@ -0,0 +1,89 @@
"""Setup prompts require input or --yes; EOF must not authorize system installs (#393).
No GPU, downloads or installers are used.
python -m unittest tools.test_setup_prompts
"""
from __future__ import annotations
import contextlib
import io
import sys
import unittest
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
import setup # noqa: E402
class Prompts(unittest.TestCase):
def test_eof_stops_with_guidance(self):
out = io.StringIO()
with mock.patch("sys.stdin", io.StringIO()), contextlib.redirect_stdout(out):
with self.assertRaises(SystemExit) as cm:
setup.ask("Install them now?", ["y", "n"], "y", False)
self.assertEqual(cm.exception.code, 1)
self.assertIn("Install them now?", out.getvalue())
self.assertIn("--yes", out.getvalue())
self.assertIn("Setup stopped", out.getvalue())
def test_yes_accepts_the_default_without_reading_input(self):
with mock.patch("builtins.input", side_effect=AssertionError("unexpected prompt")):
for default in ("y", "n"):
with self.subTest(default=default):
self.assertEqual(setup.ask("Install?", ["y", "n"], default, True), default)
def test_enter_and_explicit_answers(self):
for answer, expected in (("\n", "y"), (" \n", "y"), ("y\n", "y"), (" N \n", "n")):
with self.subTest(answer=answer), mock.patch("sys.stdin", io.StringIO(answer)), \
contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(setup.ask("Install?", ["y", "n"], "y", False), expected)
def test_invalid_answer_reprompts_and_preserves_choice_case(self):
out = io.StringIO()
with mock.patch("sys.stdin", io.StringIO("invalid\nyArN\n")), contextlib.redirect_stdout(out):
self.assertEqual(setup.ask("Method?", ["Yarn", "Linear"], "Linear", False), "Yarn")
self.assertIn("please answer one of: Yarn, Linear", out.getvalue())
def test_eof_after_invalid_answer_stops(self):
with mock.patch("sys.stdin", io.StringIO("invalid\n")), contextlib.redirect_stdout(io.StringIO()):
with self.assertRaises(SystemExit) as cm:
setup.ask("Install?", ["y", "n"], "y", False)
self.assertEqual(cm.exception.code, 1)
class BuildTools(unittest.TestCase):
def test_eof_never_starts_an_installer(self):
for win in (True, False):
with self.subTest(win=win), mock.patch.object(setup, "WIN", win), \
mock.patch.object(setup, "find_nvcc", return_value=(None, (0, 0))), \
mock.patch.object(setup, "find_vcvars", return_value=None), \
mock.patch.object(setup.shutil, "which", side_effect=lambda name: None if name == "g++" else name), \
mock.patch.object(setup, "run", side_effect=AssertionError("installer started")) as run, \
mock.patch.object(setup, "download", side_effect=AssertionError("download started")) as download, \
mock.patch("sys.stdin", io.StringIO()), contextlib.redirect_stdout(io.StringIO()):
with self.assertRaises(SystemExit) as cm:
setup.install_build_tools({"arch": "120"}, False)
self.assertEqual(cm.exception.code, 1)
run.assert_not_called()
download.assert_not_called()
def test_yes_still_installs_missing_windows_tools(self):
nvcc, vcvars = Path("nvcc"), Path("vcvars64.bat")
with mock.patch.object(setup, "WIN", True), \
mock.patch.object(setup, "find_nvcc", side_effect=[(None, (0, 0)), (nvcc, (13, 0))]), \
mock.patch.object(setup, "find_vcvars", side_effect=[None, vcvars, vcvars, vcvars]), \
mock.patch.object(setup.shutil, "which", return_value="winget"), \
mock.patch.object(setup, "run") as run, \
mock.patch("builtins.input", side_effect=AssertionError("unexpected prompt")), \
contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(setup.install_build_tools({"arch": "120"}, True), (nvcc, vcvars))
commands = [call.args[0] for call in run.call_args_list]
self.assertEqual(len(commands), 2)
self.assertIn("Microsoft.VisualStudio.2022.BuildTools", commands[0])
self.assertIn("Nvidia.CUDA", commands[1])
if __name__ == "__main__":
unittest.main()
+414
View File
@@ -0,0 +1,414 @@
"""Tests for the owner's rule in setup.py - it recommends, it never forces: a choice bigger than the recommendation is
kept and its risk said (#406's 256K on 64 GB, #364/#384's two GPUs in the low-RAM mode, #403's RAM budget), and the
stops that stay are the ones a user can confirm (confirm_risk: --yes with an explicit flag, or y). The defaults are
tools/test_setup_golden.py's. Mocked - no GPU, no downloads, nothing written outside a temp folder.
python -m unittest tools.test_setup_risk
"""
from __future__ import annotations
import contextlib
import io
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "tools"))
import setup # noqa: E402
from test_setup_golden import PROFILES, card, install # noqa: E402
def arg(cfg, flag):
a = cfg["args"]
return a[a.index(flag) + 1] if flag in a else None
def run(fn, *args, stdin=None):
"""-> (exit code or None, printed text, questions asked); stdin answers every input() (None: never asked)."""
out, asked, code = io.StringIO(), [], None
def fake_input(prompt=""):
asked.append(prompt)
if stdin is None:
raise AssertionError(f"asked {prompt!r} with --yes")
return stdin
with contextlib.redirect_stdout(out), mock.patch("builtins.input", fake_input):
try:
fn(*args)
except SystemExit as e:
code = e.code
return code, out.getvalue(), asked
class ConfirmRisk(unittest.TestCase):
"""S6: one helper for the stops a user may take on."""
def go(self, explicit, yes, stdin=None):
return run(setup.confirm_risk, "it may not fit", explicit, yes, "it does not fit", "a smaller size",
stdin=stdin)
def test_yes_with_an_explicit_choice_is_consent(self):
code, out, asked = self.go(True, True)
self.assertIsNone(code)
self.assertIn("it may not fit", out)
self.assertEqual(asked, [])
def test_yes_alone_keeps_the_stop(self):
code, out, asked = self.go(False, True)
self.assertEqual(code, 1)
self.assertIn("it does not fit", out)
self.assertIn("a smaller size", out)
def test_interactive_default_is_no(self):
for explicit in (True, False):
code, out, asked = self.go(explicit, False, stdin="")
self.assertEqual(code, 1)
self.assertIn("[n]", asked[0])
self.assertIsNone(self.go(explicit, False, stdin="y")[0])
def test_paging_with_an_explicit_model(self):
code, out, asked = run(setup.confirm_paging, "IQ3_XXS", 54, "auto", True, True)
self.assertIsNone(code, out)
self.assertIn("installing IQ3_XXS with 54 GB of RAM, as you chose (--model)", out)
class Context(unittest.TestCase):
"""S1 (#406 #364): the RAM rule recommends, an explicit or picked context is kept with a note."""
RAM64, GPU32 = PROFILES["64GB-1x32GB"]
def test_the_ram_rule(self):
self.assertEqual(setup.ram_ctx("IQ3_S", 63.7), 131072)
self.assertEqual(setup.ram_ctx("IQ3_XXS", 63.7), 131072)
self.assertEqual(setup.ram_ctx("IQ3_XXS", 95.8), 524288) # 43 + 7.2 + 24 GB fits 96
self.assertEqual(setup.ram_ctx("IQ3_S", 76.5), 131072)
self.assertEqual(setup.ram_ctx("IQ3_S", 78.5), 262144)
self.assertEqual(setup.ram_ctx("Q2_0", 31.9), 524288) # other sizes: no RAM rule
self.assertEqual(setup.ram_ctx("IQ3_XXS", 31.9, low_ram=True), 524288) # the KV cache is in VRAM there
self.assertIsNone(setup.ctx_ram_need("IQ3_XXS", 262144, low_ram=True))
self.assertAlmostEqual(setup.ctx_ram_need("IQ3_S", 262144), 77.9, places=1)
def test_an_explicit_256k_is_kept(self):
code, out, cfg, _ = install(self.RAM64, self.GPU32, ["--family", "qwen", "--model", "IQ3_S", "--no-start",
"--context", "262144"])
self.assertEqual(code, 0, out)
self.assertEqual(arg(cfg, "--max-context"), "262144")
self.assertNotIn("--rope-scaling", cfg["args"])
self.assertIn("256K with IQ3_S needs ~78 GB of RAM by setup's estimate", out)
self.assertIn("this PC has 64. Kept as you chose: it may be slower or run out of RAM under load. 128K is the "
"recommended size.", out)
def test_a_picked_256k_is_kept_and_the_menu_says_the_risk(self):
code, out, cfg, asked = install(self.RAM64, self.GPU32, ["--family", "qwen", "--model", "IQ3_S", "--no-start"],
answers={"Context?": "5"})
self.assertEqual(code, 0, out)
self.assertEqual(arg(cfg, "--max-context"), "262144")
self.assertIn("256K tokens (needs ~78 GB RAM, this PC has 64: may run out of memory)", out)
self.assertIn("128K tokens (recommended for your GPU)\n", out)
self.assertIn("Kept as you chose", out)
def test_low_ram_mode_keeps_262k_without_a_ram_note(self):
ram, found = PROFILES["32GB-2x24GB"] # #364
code, out, cfg, _ = install(ram, found, ["--family", "qwen", "--model", "IQ3_XXS", "--no-start",
"--context", "262144"])
self.assertEqual(code, 0, out)
self.assertEqual(arg(cfg, "--max-context"), "262144")
self.assertNotIn("Kept as you chose: it may be slower", out)
def test_the_earlier_install_keeps_its_context(self):
"""An update (a new copy of Strata) set up like the earlier install keeps its 256K instead of capping it."""
with tempfile.TemporaryDirectory() as d:
prev = Path(d) / "strata-iq3_s.json"
prev.write_text(json.dumps({"args": ["--max-context", "262144", "--kv", "int8"], "port": 8080}))
started = mock.Mock(return_value=0)
code, out, cfg, _ = install(self.RAM64, self.GPU32, [], extra=[
mock.patch.object(setup, "previous_config", lambda *a: prev),
mock.patch.object(setup, "start", started)])
self.assertEqual(code, 0, out)
self.assertEqual(arg(cfg, "--max-context"), "262144")
self.assertIn("Kept as you chose", out)
self.assertTrue(started.called)
class BrokenEarlierConfig(unittest.TestCase):
"""#459: a new copy of Strata set up like an earlier install skips an earlier config that does not parse (an
empty strata-*.json crashed START-HERE with JSONDecodeError) and goes on as a fresh install; configs are written
whole (a temporary file moved over the old one)."""
RAM64, GPU32 = PROFILES["64GB-1x32GB"]
def setup_with(self, files):
with tempfile.TemporaryDirectory() as d:
for i, (name, text) in enumerate(files): # in order, oldest first
(Path(d) / name).write_text(text, encoding="utf-8")
os.utime(Path(d) / name, (1_700_000_000 + i, 1_700_000_000 + i))
return install(self.RAM64, self.GPU32, [], extra=[
mock.patch.object(setup, "other_installs", lambda settings: [Path(d)]),
mock.patch.object(setup, "start", mock.Mock(return_value=0))])
def test_an_empty_config_is_skipped(self):
for text, why in (("", "the file is empty"), ("{\"args\": [", "not valid JSON"), ("[1]", "not a JSON object")):
with self.subTest(text=text):
code, out, cfg, _ = self.setup_with([("strata-iq3_s.json", text)])
self.assertEqual(code, 0, out)
self.assertIn("skipped the earlier config ", out)
self.assertIn(f"strata-iq3_s.json ({why}): setting this copy up without it", out)
self.assertNotIn("Found your earlier install", out)
self.assertIsNotNone(cfg) # the fresh install's config
def test_the_newest_readable_config_is_used(self):
good = json.dumps({"args": ["--max-context", "262144", "--kv", "int8"], "port": 8080})
code, out, cfg, _ = self.setup_with([("strata-iq3_s.json", good), ("strata-q2_0.json", "")])
self.assertEqual(code, 0, out)
self.assertIn("strata-q2_0.json (the file is empty)", out)
self.assertIn("Found your earlier install", out)
self.assertIn("(iq3_s)", out)
self.assertEqual(arg(cfg, "--max-context"), "262144")
def test_write_config_leaves_no_partial_file(self):
with tempfile.TemporaryDirectory() as d:
p = Path(d) / "strata-iq3_s.json"
p.write_text("old", encoding="utf-8")
setup.write_config(p, {"args": ["--kv", "int8"]})
self.assertEqual(p.read_text(encoding="utf-8"), json.dumps({"args": ["--kv", "int8"]}, indent=1))
self.assertEqual([f.name for f in Path(d).iterdir()], ["strata-iq3_s.json"]) # no .tmp left
with mock.patch.object(Path, "write_text", side_effect=OSError(28, "No space left on device")):
with self.assertRaises(OSError):
setup.write_config(p, {"args": []})
self.assertEqual(json.loads(p.read_text(encoding="utf-8")), {"args": ["--kv", "int8"]}) # kept whole
class LowRamGpus(unittest.TestCase):
"""S2/S3 (#364 #384): one GPU recommended in the low-RAM mode, all of them when asked for."""
def test_explicit_gpus_are_kept_with_the_mapped_variant(self):
for prof, model in (("32GB-2x24GB", "IQ3_XXS"), ("32GB-2x24GB", "Q2_0"), ("47GB-2x16GB", "IQ3_XXS")):
with self.subTest(prof=prof, model=model):
ram, found = PROFILES[prof]
code, out, cfg, _ = install(ram, found, ["--family", "qwen", "--model", model, "--no-start",
"--gpus", "0,1"])
self.assertEqual(code, 0, out)
self.assertEqual(cfg["gpu"], [0, 1])
self.assertEqual(cfg["layer_split"], "auto")
self.assertIn("--mmap-experts", cfg["args"])
self.assertNotIn("--resident-experts", cfg["args"])
self.assertIn("as you chose (--gpus)", out)
self.assertIn("RAM can fill up to 0 free", out)
self.assertIn("low-RAM mode on 2 GPUs", out)
def test_the_share_counts_every_card(self):
ram, found = PROFILES["47GB-2x16GB"] # #384: 2 x 16 GB, 47 GB, IQ3_XXS at 64K
code, out, cfg, _ = install(ram, found, ["--family", "qwen", "--model", "IQ3_XXS", "--no-start",
"--gpus", "0,1"])
one = setup.low_ram_gpu_gb("IQ3_XXS", 15.9, 65536, "int8")
self.assertIn(f"the GPUs hold ~{100 * 2 * one / setup.MODELS['IQ3_XXS']['arena_gb']:.0f}% of them", out)
def test_asked_and_answered_two(self):
ram, found = PROFILES["32GB-2x24GB"]
code, out, cfg, asked = install(ram, found, ["--family", "qwen", "--model", "Q2_0", "--no-start"],
answers={"Low-RAM mode: which GPUs?": "2"})
self.assertEqual(code, 0, out)
self.assertTrue(any("Low-RAM mode: which GPUs? [1]" in q for q in asked), asked)
self.assertEqual(cfg["gpu"], [0, 1])
self.assertIn("--mmap-experts", cfg["args"])
self.assertIn("1) GPU 0 (NVIDIA GeForce RTX 3090, 24 GB) only", out)
def test_yes_and_enter_keep_one_gpu(self):
ram, found = PROFILES["32GB-2x24GB"]
for answers in (None, ""):
code, out, cfg, _ = install(ram, found, ["--family", "qwen", "--model", "Q2_0", "--no-start"],
answers=answers)
self.assertEqual(code, 0, out)
self.assertEqual(cfg["gpu"], 0)
self.assertIn("--resident-experts", cfg["args"])
def test_low_ram_resident_with_gpus(self):
ram, found = PROFILES["32GB-2x24GB"]
code, out, cfg, _ = install(ram, found, ["--family", "qwen", "--model", "Q2_0", "--no-start", "--gpus", "0,1",
"--low-ram", "resident"])
self.assertEqual(code, 0, out)
self.assertEqual(cfg["gpu"], [0, 1])
self.assertIn("--mmap-experts", cfg["args"])
self.assertIn("--low-ram resident has no layer split yet", out)
def test_low_ram_resident_alone_keeps_one_gpu_without_asking(self):
ram, found = PROFILES["32GB-2x24GB"]
code, out, cfg, asked = install(ram, found, ["--family", "qwen", "--model", "Q2_0", "--no-start",
"--low-ram", "resident"], answers="")
self.assertEqual(code, 0, out)
self.assertEqual(cfg["gpu"], 0)
self.assertFalse(any("Low-RAM mode" in q for q in asked), asked)
class StartOnSeveralGpus(unittest.TestCase):
"""A resident low-RAM config started on several GPUs reads the experts through the file cache (#364 #384)."""
def test_split_mmap(self):
cfg = {"args": ["--pack", "p", "--resident-experts", "--kv", "int8"]}
out = io.StringIO()
with contextlib.redirect_stdout(out):
self.assertTrue(setup.split_mmap(cfg))
self.assertFalse(setup.split_mmap(cfg))
self.assertFalse(setup.split_mmap({"args": ["--mmap-experts"]}))
self.assertEqual(cfg["args"], ["--pack", "p", "--mmap-experts", "--kv", "int8"])
self.assertIn("no layer split yet", out.getvalue())
def offer(self, args, stdin):
found = PROFILES["32GB-2x24GB"][1]
with tempfile.TemporaryDirectory() as d:
p = Path(d) / "strata-q2_0.json"
cfg = {"args": list(args)}
with mock.patch.object(setup, "gpus", lambda: found), \
mock.patch.object(setup, "engine_runs_on", lambda g: True):
code, out, asked = run(setup.offer_together, p, cfg, stdin is None, stdin=stdin)
return code, out, asked, json.loads(p.read_text())
def test_offer_together_recommends_one_gpu_for_a_resident_config(self):
code, out, asked, cfg = self.offer(["--resident-experts"], None) # --yes: the recommendation
self.assertIsNone(code, out)
self.assertNotIn("gpu", cfg)
self.assertEqual(cfg["args"], ["--resident-experts"])
code, out, asked, cfg = self.offer(["--resident-experts"], "y")
self.assertIn("[n]", asked[0])
self.assertEqual(cfg["gpu"], [0, 1])
self.assertEqual(cfg["args"], ["--mmap-experts"])
code, out, asked, cfg = self.offer(["--mmap-experts"], None) # other configs: as before
self.assertEqual(cfg["gpu"], [0, 1])
def test_start_with_gpus(self):
found = PROFILES["32GB-2x24GB"][1]
with tempfile.TemporaryDirectory() as d:
exe = Path(d) / "strata.exe"
exe.write_bytes(b"")
p = Path(d) / "strata-q2_0.json"
p.write_text(json.dumps({"exe": str(exe), "args": ["--resident-experts"], "gpu": 0, "gpus_asked": True}))
call = mock.Mock(return_value=0)
with mock.patch.object(setup, "gpus", lambda: found), \
mock.patch.object(setup, "ensure_engine_for", lambda cards, path, cfg, yes: cfg), \
mock.patch.object(setup, "is_wsl", lambda: False), \
mock.patch.object(setup.subprocess, "call", call):
code, out, _ = run(setup.start, p, None, [0, 1], False, True)
cfg = json.loads(p.read_text())
self.assertIsNone(code, out)
self.assertEqual(cfg["gpu"], [0, 1])
self.assertEqual(cfg["args"], ["--mmap-experts"])
self.assertIn("no layer split yet", out)
self.assertTrue(call.called)
class SmallCard(unittest.TestCase):
"""S9: a card under 8 GB named with --gpus is a risk to confirm, not a stop."""
FOUND = [card(0, "NVIDIA GeForce RTX 3090", 24.0, "86"), card(1, "NVIDIA GeForce RTX 2060", 6.0, "75")]
def test_named_with_yes(self):
code, out, cfg, _ = install(63.7, self.FOUND, ["--family", "qwen", "--model", "Q2_0", "--no-start",
"--gpus", "0,1"])
self.assertEqual(code, 0, out)
self.assertEqual(cfg["gpu"], [0, 1])
self.assertIn("GPU 1 (NVIDIA GeForce RTX 2060) has 6 GB of VRAM", out)
self.assertIn("is used together with the others, as you chose", out)
def test_named_and_enter_stops(self):
code, out, cfg, asked = install(63.7, self.FOUND, ["--family", "qwen", "--model", "Q2_0", "--no-start",
"--gpus", "0,1"], answers="")
self.assertEqual(code, 1)
self.assertTrue(any("Use it anyway?" in q for q in asked), asked)
def test_not_named_is_not_offered(self):
code, out, cfg, _ = install(63.7, self.FOUND, ["--family", "qwen", "--model", "Q2_0", "--no-start"])
self.assertEqual(code, 0, out)
self.assertEqual(cfg["gpu"], 0) # one card: as before
class SplitShortCard(unittest.TestCase):
"""#448: a second card too small to lend a split's prompt chunk: the first card alone recommended, the pair kept
when named."""
FOUND = [card(0, "NVIDIA RTX PRO 4500 Blackwell", 31.8, "120"), card(1, "NVIDIA GeForce RTX 3080", 10.0, "86")]
def test_rule(self):
self.assertEqual([g["index"] for g in setup.split_short(self.FOUND)], [1])
self.assertEqual(setup.split_short(PROFILES["32GB-2x24GB"][1]), [])
self.assertEqual(setup.split_short(PROFILES["47GB-2x16GB"][1]), [])
eights = [card(i, "NVIDIA GeForce RTX 3070", 8.0, "86") for i in range(2)]
self.assertEqual(setup.split_short(eights), []) # neither card alone reads more
def test_yes_takes_the_first_card(self):
code, out, cfg, _ = install(127.8, self.FOUND, ["--family", "qwen", "--model", "Q2_0", "--no-start"])
self.assertEqual(code, 0, out)
self.assertEqual(cfg["gpu"], 0)
self.assertIn("too small to lend a split its prompt buffers", out)
def test_named_pair_is_kept(self):
code, out, cfg, _ = install(127.8, self.FOUND, ["--family", "qwen", "--model", "Q2_0", "--no-start",
"--gpus", "0,1"])
self.assertEqual(code, 0, out)
self.assertEqual(cfg["gpu"], [0, 1])
def test_offer_together_defaults_to_one(self):
with tempfile.TemporaryDirectory() as d:
p = Path(d) / "strata-q2_0.json"
with mock.patch.object(setup, "gpus", lambda: self.FOUND), \
mock.patch.object(setup, "engine_runs_on", lambda g: True):
code, out, asked = run(setup.offer_together, p, {"args": ["--mmap-experts"]}, True)
cfg = json.loads(p.read_text())
self.assertIsNone(code, out)
self.assertNotIn("gpu", cfg)
self.assertIn("#448", out)
class RamFloor(unittest.TestCase):
"""S7: less RAM than the smallest model needs: a stop with --yes alone, a risk with --model."""
FOUND = [card(0, "NVIDIA GeForce RTX 3060", 8.0, "86")]
def test_yes_alone_stops(self):
code, out, cfg, _ = install(19.9, self.FOUND, ["--family", "qwen", "--no-start"])
self.assertEqual(code, 1)
self.assertIn("--model NAME --yes", out)
def test_an_explicit_model_goes_on(self):
code, out, cfg, _ = install(19.9, self.FOUND, ["--family", "coder", "--model", "IQ1_M", "--no-start"])
self.assertEqual(code, 0, out)
self.assertIn("going on with 20 GB of RAM, as you chose", out)
class KvStreaming(unittest.TestCase):
"""8: --kv-streaming on|off|auto (auto: the RAM test, as before)."""
RAM, FOUND = PROFILES["64GB-1x32GB"]
def go(self, model, *flags):
return install(self.RAM, self.FOUND, ["--family", "qwen", "--model", model, "--no-start", *flags])
def test_off(self):
code, out, cfg, _ = self.go("IQ3_XXS", "--kv-streaming", "off")
self.assertEqual(code, 0, out)
self.assertNotIn("--kv-resident", cfg["args"])
self.assertIn("KV streaming off, as you chose", out)
def test_on_past_the_ram_test(self):
code, out, cfg, _ = self.go("IQ3_S", "--kv-streaming", "on") # auto leaves it off on 64 GB
self.assertEqual(code, 0, out)
self.assertEqual(arg(cfg, "--kv-resident"), "32768")
self.assertIn("Kept as you chose (--kv-streaming on)", out)
def test_on_where_it_cannot(self):
code, out, cfg, _ = self.go("Q2_0", "--kv-streaming", "on", "--kv", "k8v4")
self.assertNotIn("--kv-resident", cfg["args"])
self.assertIn("it refuses the pair", out)
code, out, cfg, _ = self.go("Q2_0", "--kv-streaming", "on", "--context", "32768")
self.assertNotIn("--kv-resident", cfg["args"])
self.assertIn("a context under 64K is not streamed", out)
def test_budget_flag_on_another_size(self):
code, out, cfg, _ = self.go("Q2_0", "--resident-budget-gib", "30")
self.assertEqual(code, 0, out)
self.assertNotIn("--resident-budget-gib", cfg["args"])
self.assertIn("--resident-budget-gib is for UD-Q4_K_XL", out)
if __name__ == "__main__":
unittest.main()
+69 -5
View File
@@ -139,7 +139,7 @@ class Main(unittest.TestCase):
def tearDown(self):
self.tmp.cleanup()
def main(self, argv, ram=63.7, version="0.1.32", n_gpus=1):
def main(self, argv, ram=63.7, version="0.1.32", n_gpus=1, model=True, amd=(), free=500.0):
eng = self.t / "engine"
eng.mkdir(exist_ok=True)
(eng / "BUILD.json").write_text(json.dumps({"version": version, "source": "local"}))
@@ -161,11 +161,11 @@ class Main(unittest.TestCase):
mock.patch.object(setup, "data_folder", lambda d: (self.t / "data", [])),
mock.patch.object(setup, "installed_configs", lambda: []),
mock.patch.object(setup, "gpus", lambda: found),
mock.patch.object(setup, "amd_gpus", lambda: []),
mock.patch.object(setup, "amd_gpus", lambda *a: list(amd)),
mock.patch.object(setup, "ram_gb", lambda: ram),
mock.patch.object(setup, "cpu_info", lambda: ("Test CPU", True, True)),
mock.patch.object(setup, "page_file_gb", lambda: 16.0),
mock.patch.object(setup, "free_gb", lambda p: 500.0),
mock.patch.object(setup, "free_gb", lambda p: free),
mock.patch.object(setup, "pip_install", lambda *a, **k: None),
mock.patch.object(setup, "get_llama_cpp", lambda: self.t / "llama.cpp"),
mock.patch.object(setup, "get_prebuilt", lambda *a, **k: eng),
@@ -178,7 +178,8 @@ class Main(unittest.TestCase):
mock.patch.object(setup, "saved_calibration", lambda cfg: None),
mock.patch.object(setup, "start", mock.Mock(side_effect=AssertionError("started"))),
mock.patch.dict(sys.modules, {"gguf_reader": types.SimpleNamespace(GGUFFile=FakeGGUF)}),
mock.patch.object(sys, "argv", ["setup.py", "--family", "unsloth", "--model", M, "--yes", "--no-start",
mock.patch.object(sys, "argv", ["setup.py", "--family", "unsloth", *(["--model", M] if model else []),
"--yes", "--no-start",
"--models-dir", str(self.t / "models"), *argv]),
mock.patch("builtins.input", mock.Mock(side_effect=AssertionError("asked"))),
]
@@ -238,11 +239,74 @@ class Main(unittest.TestCase):
self.assertIsNone(cfg)
def test_too_little_ram(self):
code, out, cfg = self.main(["--context", "8192"], ram=31.9)
code, out, cfg = self.main(["--context", "8192"], ram=31.9, model=False) # --yes alone: still a stop
self.assertEqual(code, 1)
self.assertIn("needs 48 GB of RAM or more", out)
self.assertIn("--model UD-Q4_K_XL --yes", out) # the way to insist
self.assertEqual(self.downloads, [])
def test_amd_is_asked_before_the_download(self):
"""#429: UD-Q4_K_XL has not run on AMD (CUDA-only prompt kernels): --yes alone stops before the 111 GB
download, saying why; it is not refused outright (the owner's rule: --model with --yes goes on)."""
r9700 = [{"index": 0, "name": "AMD Radeon AI PRO R9700", "vram_gb": 31.9, "arch": "gfx1201",
"driver": "amdgpu"}]
code, out, cfg = self.main(["--context", "8192", "--backend", "hip"], model=False, amd=r9700)
self.assertEqual(code, 1, out)
self.assertIn("has not been run on AMD cards yet", out)
self.assertIn("--model UD-Q4_K_XL --yes", out)
self.assertEqual(self.downloads, [])
def test_resumed_download_needs_room_for_the_rest_only(self):
"""#425: the disk check counts the finished shards and .part files already there (sizes scaled down: a
2 MB "model" with 1.5 MB on the disk, 8 GB of other room needed, 8.001 GB free)."""
d = self.t / "models" / "unsloth-UD-Q4_K_XL"
d.mkdir(parents=True)
(d / (list(setup.UNSLOTH_SHARDS)[0] + ".part")).write_bytes(b"x" * 1_500_000)
with mock.patch.dict(setup.MODELS[M], {"download_gb": 0.002}):
code, out, cfg = self.main(["--context", "8192"], free=8.001)
self.assertNotIn("not enough free disk space", out)
self.assertEqual(code, 0, out)
for f in d.iterdir(): # the first run "downloaded" them
f.unlink()
(d / (list(setup.UNSLOTH_SHARDS)[0] + ".part")).write_bytes(b"x" * 1_500_000)
with mock.patch.dict(setup.MODELS[M], {"download_gb": 0.003}): # 1.5 MB still missing: no room
code, out, cfg = self.main(["--context", "8192"], free=8.001)
self.assertEqual(code, 1)
self.assertIn("not enough free disk space", out)
def test_too_little_ram_with_an_explicit_model_installs(self):
"""S6 (the owner's rule): --model with --yes is the consent; the risk is said."""
code, out, cfg = self.main(["--context", "8192"], ram=31.9)
self.assertEqual(code, 0, out)
self.assertIn("needs 48 GB of RAM or more; this PC has 32 GB", out)
self.assertIn("installing UD-Q4_K_XL with 32 GB of RAM, as you chose", out)
self.assertEqual(cfg["args"][cfg["args"].index("--resident-budget-gib") + 1], "8")
def test_budget_flag(self):
"""S4: --resident-budget-gib N is kept; more than the recommendation says what it risks."""
code, out, cfg = self.main(["--context", "8192", "--resident-budget-gib", "48"])
self.assertEqual(code, 0, out)
self.assertEqual(cfg["args"][cfg["args"].index("--resident-budget-gib") + 1], "48")
self.assertIn("more than setup recommends for this PC (40 GiB", out)
self.assertIn("Kept as you chose", out)
code, out, cfg = self.main(["--context", "8192", "--resident-budget-gib", "32.5"])
self.assertEqual(cfg["args"][cfg["args"].index("--resident-budget-gib") + 1], "32.5")
self.assertNotIn("more than setup recommends", out)
def test_budget_flag_with_kv_streaming_is_kept(self):
code, out, cfg = self.main(["--context", "131072", "--resident-budget-gib", "40"])
self.assertEqual(code, 0, out)
args = cfg["args"]
self.assertIn("--kv-resident", args)
self.assertEqual(args[args.index("--resident-budget-gib") + 1], "40") # not 38: the user's number
self.assertIn("come on top of your 40 GiB RAM budget", out)
def test_budget_flag_needs_a_positive_number(self):
with contextlib.redirect_stderr(io.StringIO()):
code, out, cfg = self.main(["--context", "8192", "--resident-budget-gib", "0"])
self.assertEqual(code, 2) # argparse: N > 0
self.assertIsNone(cfg)
def test_one_gpu_and_no_images(self):
code, out, cfg = self.main(["--context", "8192", "--gpus", "0,1", "--vision", "yes", "--low-ram", "on"],
n_gpus=2)
+570
View File
@@ -0,0 +1,570 @@
"""tools/test_strata_mcp.py - tests for tools/strata_mcp.py (Strata's MCP server), standard library only.
python -m unittest tools/test_strata_mcp.py -v
Setup and the model server are fakes (a temporary Strata folder with a stand-in setup.py and serve/server.py), so
nothing is downloaded and no GPU is used: the JSON-RPC handshake, argument checks, install / start / stop with real
background processes, status against a fake HTTP server, and that the built-in model table still matches setup.py.
"""
from __future__ import annotations
import io
import json
import os
import socket
import subprocess
import sys
import tempfile
import threading
import time
import unittest
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import strata_mcp as M # noqa: E402
FAKE_SETUP = r'''
import json, sys, time
from pathlib import Path
def main():
root = Path(__file__).resolve().parent
(root / "setup-args.json").write_text(json.dumps(sys.argv[1:]))
print("Strata - fake setup", flush=True)
print("\n=== Step 1: checking your PC ===", flush=True)
print(" [ok] GPU: fake", flush=True)
print("\n=== Step 5: downloading Qwen3.8-Flash-Next Q2_0 ===", flush=True)
for pct in (10, 20, 30):
print(f"\r model.gguf: {pct * 0.5:.2f} / 50.00 GB ({pct}%) ", end="", flush=True)
print(flush=True)
if (root / "slow").exists():
end = time.time() + 30
while not (root / "go").exists() and time.time() < end:
time.sleep(0.1)
if (root / "fail").exists():
print("\n [X] not enough free disk space", flush=True)
print(" use --models-dir on a bigger drive", flush=True)
sys.exit(1)
a = sys.argv
model = a[a.index("--model") + 1].lower()
cfg = {"exe": sys.executable, "args": ["--max-context", a[a.index("--context") + 1]], "port": 8080,
"model_name": "qwen3.8-flash-next-" + model, "log": str(root / f"strata-{model}.log")}
(root / f"strata-{model}.json").write_text(json.dumps(cfg))
print("All set.", flush=True)
if __name__ == "__main__":
main()
'''
FAKE_SERVER = r'''
import json, os, signal, sys, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
a = sys.argv
port = int(a[a.index("--port") + 1])
cfg = json.loads(Path(a[a.index("--config") + 1]).read_text())
root = Path(__file__).resolve().parent.parent
print("loading the model (fake) ...", flush=True)
if cfg.get("crash"):
print("engine failed: out of memory (fake)", flush=True)
sys.exit(3)
time.sleep(cfg.get("delay", 0))
state = {"loaded": True}
class H(BaseHTTPRequestHandler):
def log_message(self, *x):
pass
def js(self, code, obj):
b = json.dumps(obj).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(b)))
self.end_headers()
self.wfile.write(b)
def do_GET(self):
p = self.path.split("?")[0]
if p == "/health":
self.js(200, {"status": "ok", "service": "strata", "model": cfg["model_name"], "max_context": 32768,
"images": False, "api_key": False, "loaded": state["loaded"]})
elif p == "/status":
self.js(200, {"busy": bool(cfg.get("busy")), "queued": 0})
elif p == "/v1/status":
self.js(200, {"service": "strata", "engine": "0.1.32", "uptime_s": 5, "activity": {"requests": 0},
"machine": {"gpu": {"name": "Fake GPU", "used_mib": 11000, "total_mib": 12227},
"ram": {"used_gib": 40.0, "total_gib": 63.1}}})
else:
self.js(404, {"error": {"message": "not found"}})
def do_POST(self):
n = int(self.headers.get("Content-Length") or 0)
body = json.loads(self.rfile.read(n) or b"{}")
p = self.path.split("?")[0]
if p == "/unload":
state["loaded"] = False
with open(root / "unloaded.txt", "a") as f:
f.write(str(port) + "\n")
self.js(200, {"status": "unloaded"})
elif p == "/v1/chat/completions":
with open(root / "bench-request.json", "w") as f:
json.dump(body, f)
self.js(200, {"model": cfg["model_name"], "choices": [{"message": {"content": "x"}, "finish_reason": "length"}],
"usage": {"prompt_tokens": 30, "completion_tokens": body.get("max_tokens", 1)},
"timings": {"predicted_per_second": 61.5, "prompt_per_second": 400.0}})
else:
self.js(404, {})
httpd = ThreadingHTTPServer(("127.0.0.1", port), H)
def term(*_):
print("[strata] stopped (fake)", flush=True)
os._exit(0)
signal.signal(signal.SIGTERM, term)
print(f"ready: http://127.0.0.1:{port}/v1", flush=True)
httpd.serve_forever()
'''
def free_port() -> int:
s = socket.socket()
s.bind(("127.0.0.1", 0))
p = s.getsockname()[1]
s.close()
return p
def wait_for(cond, seconds=30.0):
end = time.time() + seconds
while time.time() < end:
if cond():
return True
time.sleep(0.1)
return cond()
FAKE_HW = {"os": "TestOS 1", "ram_gb": 64.0, "cpu": {"name": "Fake CPU", "cores": 12, "avx2": True, "avx512": True},
"gpus": [{"index": 0, "name": "NVIDIA GeForce RTX 5070", "vram_gb": 11.9, "arch": "120", "vendor": "nvidia",
"problem": None, "usable": True}]}
class FakeRoot(unittest.TestCase):
"""A temporary Strata folder: fake setup.py and serve/server.py, its own settings file (data folder)."""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
base = Path(self.tmp.name)
self.root = base / "Strata"
(self.root / "serve").mkdir(parents=True)
(self.root / "setup.py").write_text(FAKE_SETUP)
(self.root / "serve" / "server.py").write_text(FAKE_SERVER)
self.env_backup = {k: os.environ.get(k) for k in ("APPDATA", "XDG_CONFIG_HOME")}
os.environ["APPDATA"] = os.environ["XDG_CONFIG_HOME"] = str(base / "config")
self.strata = M.Strata(self.root)
self.strata.python_override = sys.executable
self.strata.health_timeout = 1.0
self.strata._hardware = lambda: dict(FAKE_HW)
self.strata.disk_free = lambda p: 1000.0
self.tools = M.Tools(self.strata)
self.extra = []
def tearDown(self):
for p in self.extra:
if p.poll() is None:
p.kill()
p.wait(10)
srv = self.strata.state("server")
if srv and M.proc_alive(srv.get("pid"), srv.get("ident")):
M.proc_kill_tree(srv["pid"], srv["ident"])
M.wait_gone(srv["pid"], srv["ident"], 10)
job = self.strata.state("install")
if job and M.proc_alive(job.get("pid"), job.get("ident")):
M.proc_kill_tree(job["pid"], job["ident"])
M.wait_gone(job["pid"], job["ident"], 10)
for p in self.strata.children:
try:
p.wait(5)
except subprocess.TimeoutExpired:
pass
for k, v in self.env_backup.items():
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
for _ in range(20): # Windows: a log may still be closing
try:
self.tmp.cleanup()
break
except OSError:
time.sleep(0.25)
def call(self, name, args=None):
"""A tools/call through the JSON-RPC layer: (result dict, isError)."""
srv = M.McpServer(self.strata, io.BytesIO())
r = srv.handle({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": name, "arguments": args or {}}})
res = r["result"]
return json.loads(res["content"][0]["text"]), res["isError"]
def write_config(self, tag="q2_0", port=8080, **extra):
cfg = {"exe": sys.executable, "args": ["--max-context", "32768"], "port": port,
"model_name": f"qwen3.8-flash-next-{tag}", "log": str(self.root / f"strata-{tag}.log"), **extra}
(self.root / f"strata-{tag}.json").write_text(json.dumps(cfg))
(self.root / f"strata-{tag}.log").write_text("engine line 1\nengine line 2\n")
return cfg
# ------------------------------------------------------------------------------------------------ the protocol
class Protocol(FakeRoot):
def run_lines(self, msgs):
inp = io.BytesIO(("\n".join(m if isinstance(m, str) else json.dumps(m) for m in msgs) + "\n").encode())
out = io.BytesIO()
M.McpServer(self.strata, out).serve(inp)
return {m.get("id"): m for m in (json.loads(x) for x in out.getvalue().decode().splitlines())}
def test_handshake_list_call(self):
r = self.run_lines([
{"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "t", "version": "1"}}},
{"jsonrpc": "2.0", "method": "notifications/initialized"},
{"jsonrpc": "2.0", "id": 2, "method": "tools/list"},
{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "strata_models", "arguments": {}}},
{"jsonrpc": "2.0", "id": 4, "method": "ping"},
])
init = r[1]["result"]
self.assertEqual(init["protocolVersion"], "2025-06-18")
self.assertIn("tools", init["capabilities"])
self.assertEqual(init["serverInfo"]["name"], "strata")
names = [t["name"] for t in r[2]["result"]["tools"]]
self.assertEqual(names, ["strata_status", "strata_models", "strata_install", "strata_start", "strata_stop",
"strata_logs", "strata_benchmark", "strata_connect_info"])
for t in r[2]["result"]["tools"]:
self.assertEqual(t["inputSchema"]["type"], "object")
self.assertFalse(t["inputSchema"]["additionalProperties"])
self.assertGreater(len(t["description"]), 60)
call = r[3]["result"]
self.assertFalse(call["isError"])
self.assertIn("families", call["structuredContent"])
self.assertEqual(json.loads(call["content"][0]["text"])["recommendation"]["model"], "IQ3_XXS")
self.assertEqual(r[4]["result"], {})
self.assertNotIn(None, r) # the notification got no answer
def test_older_protocol_and_errors(self):
r = self.run_lines([
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05"}},
"{not json",
{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "rm_rf", "arguments": {}}},
{"jsonrpc": "2.0", "id": 3, "method": "no/such"},
{"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "strata_logs", "arguments": []}},
])
self.assertEqual(r[1]["result"]["protocolVersion"], "2024-11-05")
self.assertEqual(r[None]["error"]["code"], -32700)
self.assertEqual(r[2]["error"]["code"], -32602)
self.assertEqual(r[3]["error"]["code"], -32601)
self.assertTrue(r[4]["result"]["isError"])
self.assertNotIn("structuredContent", r[4]["result"]) # 2024-11-05 has no structured results
def test_real_stdio_process(self):
"""The script itself over a pipe: stdout carries only JSON-RPC lines."""
msgs = [{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18"}},
{"jsonrpc": "2.0", "id": 2, "method": "tools/list"},
{"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": "strata_logs", "arguments": {"source": "setup"}}}]
p = subprocess.run([sys.executable, str(HERE / "strata_mcp.py"), "--root", str(self.root)],
input="\n".join(json.dumps(m) for m in msgs) + "\n", capture_output=True, text=True,
timeout=60, env=dict(os.environ))
lines = [json.loads(x) for x in p.stdout.splitlines()]
self.assertEqual(sorted(m["id"] for m in lines), [1, 2, 3])
self.assertEqual(len(next(m for m in lines if m["id"] == 2)["result"]["tools"]), 8)
self.assertIn("ready", p.stderr)
def test_not_a_strata_folder(self):
p = subprocess.run([sys.executable, str(HERE / "strata_mcp.py"), "--root", self.tmp.name], input="",
capture_output=True, text=True, timeout=60)
self.assertEqual(p.returncode, 2)
# ------------------------------------------------------------------------------------------------ validation
class Validation(FakeRoot):
def assertRejected(self, tool, args, text):
res, err = self.call(tool, args)
self.assertTrue(err, f"{tool} {args} was accepted: {res}")
self.assertIn(text, res["error"])
def test_install_arguments(self):
self.assertRejected("strata_install", {"model": "Q9_X"}, "model must be one of")
self.assertRejected("strata_install", {"family": "llama"}, "family must be one of")
self.assertRejected("strata_install", {"context": 12345}, "context must be one of")
self.assertRejected("strata_install", {"gpus": "0;rm -rf /"}, "gpus=")
self.assertRejected("strata_install", {"gpus": "0 && calc"}, "gpus=")
self.assertRejected("strata_install", {"gpu": -1}, "between")
self.assertRejected("strata_install", {"gpu": "1; calc"}, "whole number")
self.assertRejected("strata_install", {"gpu": 0, "gpus": "0,1"}, "not both")
self.assertRejected("strata_install", {"port": 80}, "between")
self.assertRejected("strata_install", {"backend": "rocm"}, "backend must be one of")
self.assertRejected("strata_install", {"command": "setup.py --build"}, "unknown argument")
self.assertRejected("strata_install", {"family": "swift", "model": "IQ3_S"}, "has no IQ3_S")
self.assertRejected("strata_install", {"family": "unsloth", "vision": "yes"}, "images are not available")
self.assertRejected("strata_install", {"vision": "yes\nrm"}, "control characters")
def test_data_dir_paths(self):
self.assertRejected("strata_install", {"data_dir": "models"}, "absolute")
self.assertRejected("strata_install", {"data_dir": str(self.root.parent / "x" / ".." / "Strata-x")}, "'..'")
self.assertRejected("strata_install", {"data_dir": str(self.root.parent / "Documents")}, "named Strata")
self.assertRejected("strata_install", {"data_dir": str(self.root.parent / "nope" / "Strata-data")},
"does not exist")
self.assertRejected("strata_install", {"data_dir": "\\\\server\\share\\Strata-data"}, "network")
sysdir = (os.environ.get("SystemRoot", "C:\\Windows") + "\\Strata-data") if M.WIN else "/etc/Strata-data"
self.assertRejected("strata_install", {"data_dir": sysdir}, "system folder")
ok = [self.root.parent / "Strata-data", self.root / "models-here", self.root.parent / "Strata-big"]
for d in ok:
self.assertEqual(M.norm(self.tools.check_data_dir(str(d))), M.norm(d))
def test_other_tools_arguments(self):
self.assertRejected("strata_start", {"model": "../../etc/passwd"}, "not valid")
self.assertRejected("strata_start", {"model": "q2_0"}, "no model is installed")
self.write_config("q2_0")
self.assertRejected("strata_start", {"model": "iq3_s"}, "no installed model 'iq3_s'")
self.assertRejected("strata_start", {"wait_seconds": 100000}, "between")
self.assertRejected("strata_logs", {"lines": 0}, "between")
self.assertRejected("strata_logs", {"source": "/etc/passwd"}, "source must be one of")
self.assertRejected("strata_benchmark", {"max_tokens": 100000}, "between")
self.assertRejected("strata_stop", {"force": "yes"}, "true or false")
def test_engine_log_outside_is_not_read(self):
self.write_config("q2_0", log=str(Path(self.tmp.name).parent / "elsewhere.log"))
res, err = self.call("strata_logs", {"source": "engine", "model": "q2_0"})
self.assertTrue(err)
self.assertIn("outside the Strata folder", res["error"])
self.write_config("q2_0")
res, err = self.call("strata_logs", {"source": "engine"})
self.assertFalse(err)
self.assertEqual(res["lines"], ["engine line 1", "engine line 2"])
# ------------------------------------------------------------------------------------------------ install
class Install(FakeRoot):
def wait_job(self):
self.assertTrue(wait_for(lambda: not self.strata.install_job()["running"], 60))
def test_plan_then_install(self):
res, err = self.call("strata_install", {})
self.assertFalse(err)
self.assertIn("Nothing was done yet", res["summary"])
self.assertEqual((res["plan"]["family"], res["plan"]["model"], res["plan"]["context"]), ("qwen", "IQ3_XXS", 32768))
self.assertFalse((self.root / "setup-args.json").exists())
self.assertIsNone(self.strata.install_job())
res, err = self.call("strata_install", {"model": "Q2_0", "context": 65536, "vision": "no", "confirm": True})
self.assertFalse(err, res)
self.assertIn("started in the background", res["summary"])
self.wait_job()
args = json.loads((self.root / "setup-args.json").read_text())
self.assertEqual(args[:2], ["--yes", "--no-start"])
for pair in (["--family", "qwen"], ["--model", "Q2_0"], ["--context", "65536"], ["--vision", "no"]):
i = args.index(pair[0])
self.assertEqual(args[i:i + 2], pair)
res, err = self.call("strata_install", {})
self.assertEqual(res["install"]["result"], "installed")
self.assertEqual(res["install"]["exit_code"], 0)
self.assertEqual(res["next"], "strata_start")
st, _ = self.call("strata_status", {"hardware": False})
self.assertEqual([m["model"] for m in st["installed"]["models"]], ["q2_0"])
def test_progress_while_running_and_cancel(self):
(self.root / "slow").write_text("")
res, err = self.call("strata_install", {"family": "coder", "confirm": True})
self.assertFalse(err, res)
self.assertTrue(wait_for(lambda: (self.strata.install_job() or {}).get("download") is not None, 30))
res, err = self.call("strata_install", {"model": "IQ3_S", "confirm": True}) # a second call: progress only
self.assertFalse(err)
self.assertIn("an install is running", res["summary"])
self.assertEqual(res["install"]["step"], "5/7: downloading Qwen3.8-Flash-Next Q2_0")
self.assertEqual(res["install"]["download"]["percent"], 30)
res, err = self.call("strata_start", {})
self.assertTrue(err)
self.assertIn("install is still running", res["error"])
res, err = self.call("strata_install", {"cancel": True})
self.assertIn("stopped", res["summary"])
self.assertFalse(self.strata.install_job()["running"])
def test_failure_is_reported(self):
(self.root / "fail").write_text("")
self.call("strata_install", {"model": "Q2_0", "confirm": True})
self.wait_job()
res, _ = self.call("strata_install", {})
self.assertEqual(res["install"]["result"], "failed")
self.assertIn("not enough free disk space - use --models-dir on a bigger drive", res["install"]["error"])
def test_disk_space_is_checked(self):
self.strata.disk_free = lambda p: 10.0
res, err = self.call("strata_install", {"model": "Q2_0"})
self.assertIn("not enough free disk space", res["plan"]["disk_short"])
res, err = self.call("strata_install", {"model": "Q2_0", "confirm": True})
self.assertTrue(err)
self.assertIsNone(self.strata.install_job())
# ------------------------------------------------------------------------------------------------ start / stop / status
class StartStop(FakeRoot):
def test_start_status_bench_stop(self):
port = free_port()
self.write_config("q2_0", port=port, delay=1.5)
res, err = self.call("strata_start", {"wait_seconds": 30})
self.assertFalse(err, res)
self.assertIn("Strata is running", res["summary"])
pid = res["server"]["pid"]
st, _ = self.call("strata_status", {"hardware": False})
self.assertIn(f"running on port {port}", st["summary"])
srv = st["running"][0]
self.assertEqual(srv["engine_version"], "0.1.32")
self.assertEqual(srv["gpu"]["total_mib"], 12227)
self.assertIn("this MCP server", srv["started_by"])
res, err = self.call("strata_start", {}) # already running: nothing new
self.assertIn("already running", res["summary"])
b, err = self.call("strata_benchmark", {"max_tokens": 64})
self.assertFalse(err, b)
self.assertEqual(b["output_tok_s"], 61.5)
sent = json.loads((self.root / "bench-request.json").read_text())
self.assertEqual((sent["temperature"], sent["max_tokens"], sent["reasoning_effort"]), (0, 64, "none"))
c, _ = self.call("strata_connect_info", {})
self.assertEqual(c["openai_base_url"], f"http://127.0.0.1:{port}/v1")
logs, _ = self.call("strata_logs", {"source": "server"})
self.assertTrue(any(line.startswith("ready:") for line in logs["lines"]))
res, err = self.call("strata_stop", {})
self.assertFalse(err, res)
self.assertIn("Strata stopped", res["summary"])
self.assertEqual(res["engine_unloaded"], "unloaded")
self.assertEqual((self.root / "unloaded.txt").read_text().split(), [str(port)])
ident = M.proc_identity(pid)
self.assertIsNone(ident)
st, _ = self.call("strata_status", {"hardware": False})
self.assertEqual(st["running"], [])
res, _ = self.call("strata_stop", {})
self.assertEqual(res["summary"], "Strata is not running")
def test_start_reports_a_crash(self):
self.write_config("q2_0", port=free_port(), crash=True)
res, err = self.call("strata_start", {"wait_seconds": 30})
self.assertTrue(err)
self.assertIn("out of memory (fake)", res["error"])
self.assertEqual(self.strata.state("server"), {})
def test_loading_then_ready(self):
port = free_port()
self.write_config("q2_0", port=port, delay=3)
res, err = self.call("strata_start", {"wait_seconds": 0})
self.assertIn("loading", res["summary"])
st, _ = self.call("strata_status", {"hardware": False})
self.assertIn("loading", st["summary"])
self.assertTrue(wait_for(lambda: self.strata.probe(port) is not None, 30))
def test_stop_while_busy_needs_force(self):
port = free_port()
self.write_config("q2_0", port=port, busy=True)
self.call("strata_start", {"wait_seconds": 30})
res, err = self.call("strata_stop", {})
self.assertTrue(err)
self.assertIn("force=true", res["error"])
res, err = self.call("strata_stop", {"force": True})
self.assertFalse(err, res)
def test_external_server_is_only_unloaded(self):
port = free_port()
self.write_config("q2_0", port=port)
p = subprocess.Popen([sys.executable, str(self.root / "serve" / "server.py"), "--config",
str(self.root / "strata-q2_0.json"), "--port", str(port)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
self.extra.append(p)
self.assertTrue(wait_for(lambda: self.strata.probe(port) is not None, 30))
st, _ = self.call("strata_status", {"hardware": False})
self.assertIn("outside this MCP server", st["running"][0]["started_by"])
res, err = self.call("strata_start", {})
self.assertIn("started outside this MCP server", res["summary"])
res, err = self.call("strata_stop", {})
self.assertFalse(err, res)
self.assertIn("only its model was unloaded", res["summary"])
time.sleep(0.5)
self.assertIsNone(p.poll()) # the server itself was not touched
def test_never_kills_an_unrelated_process(self):
other = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"])
self.extra.append(other)
# a stale record: this process id, but another start time (a reused id)
self.strata.save_state("server", {"pid": other.pid, "ident": "win:1" if M.WIN else "linux:1",
"port": free_port(), "model": "q2_0"})
res, _ = self.call("strata_stop", {})
self.assertEqual(res["summary"], "Strata is not running")
self.assertFalse(M.proc_terminate(other.pid, "linux:1"))
time.sleep(0.3)
self.assertIsNone(other.poll())
def test_status_sees_other_programs_on_the_port(self):
class H(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(404)
self.end_headers()
def log_message(self, *a):
pass
httpd = HTTPServer(("127.0.0.1", 0), H)
port = httpd.server_address[1]
threading.Thread(target=httpd.serve_forever, daemon=True).start()
try:
self.write_config("q2_0", port=port)
st, _ = self.call("strata_status", {"hardware": False})
self.assertEqual(st["running"], [])
self.assertEqual(st["ports_used_by_other_programs"][0]["port"], port)
res, err = self.call("strata_start", {})
self.assertTrue(err)
self.assertIn("used by another program", res["error"])
finally:
httpd.shutdown()
httpd.server_close()
# ------------------------------------------------------------------------------------------------ helpers
class Helpers(unittest.TestCase):
def test_progress_from_setup_output(self):
with tempfile.TemporaryDirectory() as d:
f = Path(d) / "setup.log"
f.write_text("=== Step 4: the Strata engine ===\n [ok] engine\n=== Step 5: downloading X ===\n"
"\r a.gguf: 1.00 / 50.00 GB (2%) \r a.gguf: 25.00 / 50.00 GB (50%) \n")
p = M.install_progress(M.read_tail(f, 50))
self.assertEqual(p["step"], "5/7: downloading X")
self.assertEqual(p["download"], {"file": "a.gguf", "done_gb": 25.0, "total_gb": 50.0, "percent": 50})
self.assertNotIn("error", p)
def test_builtin_table_matches_setup(self):
s = M.Strata(HERE.parent)
S = s.setup_module()
self.assertIsNotNone(S, "the repository's setup.py did not import")
self.assertEqual(list(S.MODELS), list(M.FALLBACK_MODELS))
for m, d in S.MODELS.items():
for k in ("download_gb", "ram_gb", "arena_gb"):
self.assertEqual(d[k], M.FALLBACK_MODELS[m][k], f"{m} {k}")
self.assertEqual(d.get("families", ("qwen", "swift")), M.FALLBACK_MODELS[m].get("families", ("qwen", "swift")))
self.assertEqual(list(S.FAMILIES), list(M.FALLBACK_FAMILIES))
for f, d in S.FAMILIES.items():
self.assertEqual(d["tag"], M.FALLBACK_FAMILIES[f]["tag"])
self.assertEqual(list(S.CONTEXTS), M.FALLBACK_CONTEXTS)
def test_recommendation_follows_ram(self):
s = M.Strata(HERE.parent)
gpu = [{"index": 0, "name": "x", "vram_gb": 24.0, "usable": True, "vendor": "nvidia"}]
cases = {96: ("qwen", "IQ3_XXS", 131072), 48: ("qwen", "Q2_0", 131072), 32: ("coder", "IQ1_M", 131072)}
for ram, want in cases.items():
r = s.recommend({"ram_gb": ram, "gpus": gpu, "cpu": {"avx2": True}})
self.assertEqual((r["family"], r["model"], r["context"]), want, ram)
r = s.recommend({"ram_gb": 16, "gpus": [{**gpu[0], "vram_gb": 8.0}], "cpu": {"avx2": True}})
self.assertIsNone(r["model"])
r = s.recommend({"ram_gb": 64, "gpus": [], "cpu": {"avx2": True}})
self.assertIsNone(r["model"])
r = s.recommend({"ram_gb": 64, "gpus": [{**gpu[0], "vram_gb": 12.0}], "cpu": {"avx2": True}})
self.assertEqual(r["context"], 32768)
if __name__ == "__main__":
unittest.main()