mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 14:21:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b734e427c | ||
|
|
82d7e82926 | ||
|
|
772c9330e9 | ||
|
|
6f8536aed1 | ||
|
|
6cc16bee09 | ||
|
|
95f2860231 | ||
|
|
22ee8d0e9a | ||
|
|
0e4c3e8727 | ||
|
|
cd49a498c8 | ||
|
|
ce2d83b62d | ||
|
|
42bc3dd9ad | ||
|
|
a2edcb12f9 | ||
|
|
8896607268 | ||
|
|
eb8bb8bcd9 | ||
|
|
43685766ae | ||
|
|
3997ca29fd | ||
|
|
1867a8d6a1 | ||
|
|
dd23aa676c | ||
|
|
348ac7ef0e | ||
|
|
d5871c1af1 | ||
|
|
af53393b94 | ||
|
|
77668b2e3c | ||
|
|
14dbd1be5f | ||
|
|
3629d22c76 | ||
|
|
524b091daf | ||
|
|
6edbe9d6ed | ||
|
|
8554b6bc53 | ||
|
|
30a0f3269a | ||
|
|
decfc96c98 | ||
|
|
95ade2d5ee | ||
|
|
f0ee8ce54f | ||
|
|
7349d9112b | ||
|
|
152f7198b9 | ||
|
|
41e9ecb016 | ||
|
|
18a0ec2d47 | ||
|
|
17ebb94433 | ||
|
|
e878547256 | ||
|
|
950c1199dd | ||
|
|
37fe5b5328 | ||
|
|
6aa51c24d8 | ||
|
|
0c8b8dc4d3 | ||
|
|
36beee903b | ||
|
|
318d74d9d2 | ||
|
|
082d59640f | ||
|
|
26cc76347a | ||
|
|
b8349a8144 | ||
|
|
36c11637f5 | ||
|
|
51fdc43c80 | ||
|
|
eecb32abf6 | ||
|
|
885e52052b | ||
|
|
f938412499 | ||
|
|
0ad23b584d |
No files matched your search
@@ -0,0 +1 @@
|
||||
buy_me_a_coffee: strataengine
|
||||
@@ -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/
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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).
|
||||
@@ -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
@@ -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
@@ -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).)
|
||||
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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"):
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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"))
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user