mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 17:38:20 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
256485621e | ||
|
|
9ac549fd32 | ||
|
|
06f41f49a8 | ||
|
|
bef32ed6c3 | ||
|
|
dcb0c24422 | ||
|
|
235cf4bd09 | ||
|
|
b38d11761a | ||
|
|
fbbfb9ce60 | ||
|
|
5c52c0fd00 | ||
|
|
9253c92ad7 | ||
|
|
d7a66bd33e | ||
|
|
d1b30db332 | ||
|
|
0015035642 | ||
|
|
89c62e5361 | ||
|
|
97cf327c1e | ||
|
|
6c06571710 | ||
|
|
fb9b924e5c | ||
|
|
ffcdb437b8 | ||
|
|
2838cbae46 | ||
|
|
9ee557f781 | ||
|
|
7cf709e5b1 | ||
|
|
0f3ff4dece | ||
|
|
6255c8fd14 | ||
|
|
9ffd6c9461 | ||
|
|
1e3bded2f7 | ||
|
|
ae3dc218ea | ||
|
|
3cba59ea31 | ||
|
|
ecba7dfc10 | ||
|
|
1314ebfc7f | ||
|
|
7664149081 | ||
|
|
6abee7f397 | ||
|
|
868dc724d6 | ||
|
|
f1b35531c3 | ||
|
|
20351c6030 | ||
|
|
bbba5bf576 | ||
|
|
5f563ce3a3 | ||
|
|
6c431c60cc | ||
|
|
822a5c783f | ||
|
|
0d721cf87b | ||
|
|
40542e8bb3 | ||
|
|
e8b75ff2f6 | ||
|
|
7f21e2b401 |
No files matched your search
@@ -46,3 +46,4 @@ build-vision.bat
|
||||
/dist/
|
||||
*.done
|
||||
/third_party/*.zip
|
||||
|
||||
+52
-2
@@ -8,7 +8,14 @@
|
||||
# written against sm_120 (RTX 5000 / Blackwell) and silently building for an older 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.0 LANGUAGES CXX)
|
||||
project(strata VERSION 0.1.15 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
|
||||
if(MSVC AND CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:/Zi>)
|
||||
add_link_options(/DEBUG /OPT:REF /OPT:ICF)
|
||||
endif()
|
||||
|
||||
# The prebuilt release (setup.py downloads it, so users need no compiler): ggml's CPU backend for any AVX2 CPU
|
||||
# instead of this machine's (Strata's own AVX-512 kernels are per-file and chosen at run time either way), and
|
||||
@@ -178,7 +185,9 @@ if(STRATA_ENABLE_CUDA)
|
||||
src/kernels/cuda/ple.cu
|
||||
src/kernels/cuda/kv_q8.cu
|
||||
src/kernels/cuda/kv_stream.cu
|
||||
src/kernels/cuda/kv_q4.cu
|
||||
src/kernels/cuda/fused_gr.cu
|
||||
src/kernels/cuda/cvec.cu
|
||||
src/kernels/cuda/qsa_decode_attn.cu
|
||||
src/kernels/cuda/fused_gdn.cu
|
||||
src/kernels/cuda/dequant_bf16.cu
|
||||
@@ -363,6 +372,16 @@ if(STRATA_ENABLE_CUDA)
|
||||
target_link_libraries(kv_stream_parity PRIVATE strata_kernels CUDA::cudart)
|
||||
add_test(NAME kv_stream_parity COMMAND kv_stream_parity)
|
||||
|
||||
# PR #21: Q4_0 KV with Walsh-Hadamard rotation vs a host reference (GPU, synthetic, no model).
|
||||
add_executable(kv_q4_parity src/kernels/kv_q4_parity.cpp)
|
||||
target_include_directories(kv_q4_parity PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
|
||||
target_link_libraries(kv_q4_parity PRIVATE strata_kernels CUDA::cudart)
|
||||
add_test(NAME kv_q4_parity COMMAND kv_q4_parity)
|
||||
add_executable(cvec_parity src/kernels/cvec_parity.cpp)
|
||||
target_include_directories(cvec_parity PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
|
||||
target_link_libraries(cvec_parity PRIVATE strata_kernels CUDA::cudart)
|
||||
add_test(NAME cvec_parity COMMAND cvec_parity)
|
||||
|
||||
# Plan v0.3 P3: native MMVQ for 1..8 tokens (bitwise selftest + --bench timing); synthetic data, no model.
|
||||
if(STRATA_BUILD_TESTS)
|
||||
add_executable(native_mmvq_multi bench/micro/native_mmvq_multi.cpp)
|
||||
@@ -495,16 +514,43 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
endif()
|
||||
# part of strata_kernels_cpu (the pool calls these and they call its CPU probe: one library, so GNU ld's
|
||||
# one-pass static linking has no cycle to resolve)
|
||||
target_sources(strata_kernels_cpu PRIVATE src/kernels/cpu/native_expert.cpp src/kernels/cpu/iq_avx512.cpp)
|
||||
target_sources(strata_kernels_cpu PRIVATE src/kernels/cpu/native_expert.cpp src/kernels/cpu/iq_avx512.cpp
|
||||
src/kernels/cpu/iq_avx2.cpp)
|
||||
target_include_directories(strata_kernels_cpu PRIVATE third_party/ggml)
|
||||
if(MSVC)
|
||||
set_source_files_properties(src/kernels/cpu/iq_avx512.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX512")
|
||||
set_source_files_properties(src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
|
||||
else()
|
||||
set_source_files_properties(src/kernels/cpu/iq_avx512.cpp PROPERTIES COMPILE_OPTIONS
|
||||
"-mavx512f;-mavx512bw;-mavx512vl;-mavx512dq;-mavx512vnni;-mavx512vbmi;-mf16c;-mfma")
|
||||
set_source_files_properties(src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
|
||||
endif()
|
||||
target_link_libraries(strata_kernels_cpu PUBLIC ggml-cpu ggml-base)
|
||||
target_compile_definitions(strata_kernels_cpu PUBLIC STRATA_NATIVE_EXPERTS=1)
|
||||
if(STRATA_ENABLE_CUDA)
|
||||
# prompt-speed plan step 2b: llama.cpp's MMQ kernels (ggml-cuda, MIT) for the prompt path's experts, compiled
|
||||
# from the same checkout as ggml above - only mmq.cuh's instances for the packs' types and the quantizer, with
|
||||
# the few host symbols of ggml-cuda.cu they use in src/prefill/ggml_cuda_host.cu
|
||||
if(STRATA_GGML_DIR)
|
||||
set(_strata_gg ${STRATA_GGML_DIR}/ggml)
|
||||
else()
|
||||
set(_strata_gg ${strata_llamacpp_SOURCE_DIR}/ggml)
|
||||
endif()
|
||||
set(_strata_mmq ${_strata_gg}/src/ggml-cuda)
|
||||
add_library(strata_mmq STATIC src/prefill/moe_mmq.cu src/prefill/ggml_cuda_host.cu ${_strata_mmq}/quantize.cu)
|
||||
foreach(_t q2_0 iq2_xxs iq2_xs iq2_s iq3_xxs iq3_s iq4_nl iq4_xs)
|
||||
target_sources(strata_mmq PRIVATE ${_strata_mmq}/template-instances/mmq-instance-${_t}.cu)
|
||||
endforeach()
|
||||
target_include_directories(strata_mmq PRIVATE ${_strata_gg}/include ${_strata_gg}/src ${_strata_mmq}
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
target_compile_options(strata_mmq PRIVATE $<$<COMPILE_LANGUAGE:CUDA>:--extended-lambda -use_fast_math>)
|
||||
if(MSVC)
|
||||
target_compile_options(strata_mmq PRIVATE $<$<COMPILE_LANGUAGE:CUDA>:-Xcompiler=/bigobj>)
|
||||
endif()
|
||||
target_link_libraries(strata_mmq PUBLIC ggml-base CUDA::cudart)
|
||||
target_link_libraries(strata_prefill PUBLIC strata_mmq)
|
||||
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1)
|
||||
endif()
|
||||
if(STRATA_ENABLE_CUDA)
|
||||
add_executable(native_expert_parity src/kernels/native_expert_parity.cpp)
|
||||
target_include_directories(native_expert_parity PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
|
||||
@@ -538,6 +584,10 @@ if(STRATA_BUILD_TESTS)
|
||||
add_executable(pool_test src/kernels/cpu/pool_test.cpp)
|
||||
target_link_libraries(pool_test PRIVATE strata_kernels_cpu)
|
||||
add_test(NAME pool_test COMMAND pool_test --selftest WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
# issue #29: tiny batches with sleeping workers, as on a big-VRAM card; a hang fails it
|
||||
add_executable(pool_stress src/kernels/cpu/pool_stress.cpp)
|
||||
target_link_libraries(pool_stress PRIVATE strata_kernels_cpu)
|
||||
add_test(NAME pool_stress COMMAND pool_stress 20)
|
||||
endif()
|
||||
|
||||
if(STRATA_BUILD_TESTS)
|
||||
|
||||
@@ -3,67 +3,36 @@
|
||||
<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"><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.
|
||||
|
||||
- **Private:** everything runs on your PC. Nothing is sent anywhere.
|
||||
- **Works with your apps:** chat apps, coding agents and scripts that speak the OpenAI or Anthropic API just work.
|
||||
- **Sees pictures** too, if you want (screenshots, photos, scanned pages).
|
||||
- **Free and open source.**
|
||||
|
||||
> **Jump to:** [Is my PC enough?](#is-my-pc-enough) · [Install](#install-3-steps) · [How fast?](#how-fast-is-it) ·
|
||||
> [Which model?](#which-model-should-i-pick) · [Using it](#using-it) · [Problems?](#something-went-wrong) ·
|
||||
> [How it works](#how-does-it-work) · [All the details](docs/DETAILS.md)
|
||||
> **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)
|
||||
|
||||
---
|
||||
|
||||
## Is my PC enough?
|
||||
|
||||
| You need | |
|
||||
| --- | --- |
|
||||
| **Graphics card** | NVIDIA RTX 30, 40 or 50 series with **12 GB of VRAM or more** |
|
||||
| **Memory (RAM)** | **64 GB** |
|
||||
| **Free disk space** | ~80 GB (an SSD makes the first start much faster) |
|
||||
| **System** | Windows 10/11, or Linux |
|
||||
|
||||
That's it. 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.
|
||||
|
||||
## Install (3 steps)
|
||||
|
||||
**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 4 questions - or just press Enter each time for the recommended choice:
|
||||
- **Which model?** The original, or Swift 1.5 (a version that thinks shorter and answers sooner)
|
||||
- **Which size?** Q2_0, IQ2_XS, IQ3_XXS or IQ3_S - see [which model](#which-model-should-i-pick)
|
||||
- **How much context?** How much text it can keep in mind at once (it suggests one for your card)
|
||||
- **Images?** Whether it should also read pictures
|
||||
|
||||
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 a chat page at `http://127.0.0.1:8080`.
|
||||
|
||||
**Next time**, just double-click `START-HERE.bat` again: it starts right away, nothing is downloaded twice. Close its
|
||||
window to stop the model.
|
||||
|
||||
**Linux:** run `./setup.sh` - same questions, same result.
|
||||
|
||||
## How fast is it?
|
||||
|
||||
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** | 95 tokens/s | 65 tokens/s | 539 tokens/s |
|
||||
| **IQ2_XS** | 78 tokens/s | 52 tokens/s | 463 tokens/s |
|
||||
| **IQ3_XXS** | 66 tokens/s | 45 tokens/s | 410 tokens/s |
|
||||
| **IQ3_S** | 54 tokens/s | 42 tokens/s | 374 tokens/s |
|
||||
| **Q2_0** | 90 tokens/s | 67 tokens/s | 1,310 tokens/s |
|
||||
| **IQ2_XS** | 74 tokens/s | 60 tokens/s | 1,240 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 46 tokens/s | 1,110 tokens/s |
|
||||
| **IQ3_S** | 52 tokens/s | 41 tokens/s | 1,070 tokens/s |
|
||||
|
||||
- **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).
|
||||
- **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 740-1,000 tokens/s. A 32K prompt takes about 25 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
|
||||
@@ -73,49 +42,105 @@ A card with more VRAM is faster, because more of the model fits on the GPU: an R
|
||||
|
||||
**The size** (the same model, compressed more or less):
|
||||
|
||||
| Size | Download | Speed | Quality | Pick it if... |
|
||||
| --- | ---: | --- | --- | --- |
|
||||
| **Q2_0** | 66 GB | fastest | good | you want speed |
|
||||
| **IQ2_XS** | 68 GB | fast | better | you want a good all-rounder (**recommended**) |
|
||||
| **IQ3_XXS** | 76 GB | slower | great | you want better answers (uses 43 GB of your 64 GB RAM) |
|
||||
| **IQ3_S** | 84 GB | slowest | best: matches the full model on the published tests | you want the very best answers (original model only; uses 50 GB of your 64 GB RAM, so close other big programs) |
|
||||
| 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) |
|
||||
|
||||
**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.
|
||||
- **[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. Its own license applies (see its page).
|
||||
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**. You can add another one later with `START-HERE.bat --setup`.
|
||||
|
||||
## Install
|
||||
|
||||
**You need:** an NVIDIA RTX 30, 40 or 50 card with 12 GB of VRAM or more, enough RAM for the size you pick (above),
|
||||
~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.
|
||||
|
||||
**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:
|
||||
- **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)
|
||||
- **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
|
||||
|
||||
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`.
|
||||
|
||||
**Next time**, just double-click `START-HERE.bat` again: it starts right away, nothing is downloaded twice. Close its
|
||||
window to stop the model.
|
||||
|
||||
**Linux:** run `./setup.sh` - same questions, same result.
|
||||
|
||||
## Using it
|
||||
|
||||
- **Chat in the browser:** `http://127.0.0.1:8080` - a simple chat page (it opens by itself when the model starts).
|
||||
<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:** see the [details](docs/DETAILS.md#using-it) (set an API key first).
|
||||
- **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.
|
||||
|
||||
**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.
|
||||
|
||||
## Something went wrong?
|
||||
|
||||
| What you see | What to do |
|
||||
| --- | --- |
|
||||
| `the NVIDIA driver is too old` | Update the driver (NVIDIA App or nvidia.com/drivers), restart the PC, run `START-HERE.bat` again. |
|
||||
| It stopped during download or setup | Run `START-HERE.bat` again - it continues where it stopped. |
|
||||
| `port 8080 is already in use` | Strata is already running - look for its window. |
|
||||
| The first start takes minutes | Normal: it loads 35-43 GB into RAM. The next start is faster. |
|
||||
| Slow, and the disk light is busy | Not enough free RAM: close other programs (browsers use a lot), or pick Q2_0 / IQ2_XS. |
|
||||
| "prompt exceeds the context" | The conversation is longer than the context you chose: run `START-HERE.bat --setup` and pick more. |
|
||||
**My PC froze, or got very slow, the first time Strata started.**
|
||||
That's normal 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).
|
||||
|
||||
More in the [full troubleshooting table](docs/DETAILS.md#troubleshooting). Still stuck? Open an issue and attach
|
||||
`strata-<model>.log` from this folder.
|
||||
**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 `START-HERE.bat --setup` 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.
|
||||
|
||||
## How does it work?
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# 2026-09-27: expert cache on vs off (issue #23)
|
||||
|
||||
Question: the engine used to warn at start that "the GPU hit path is NOT CORRECT ... any OUTPUT from it is not". Does
|
||||
the expert cache make the output worse?
|
||||
|
||||
Method: teacher-forced. `strata --dump-logits` runs a text one token at a time and writes the logits at every
|
||||
position, so both runs see the same text and each position's next-token prediction can be compared:
|
||||
- cache off: no `--expert-cache`, every routed expert computed by the CPU pool;
|
||||
- cache on: `--expert-profile pack/profile-decode-8k.bin --expert-cache auto`, 5,130 of the most-used experts on the GPU.
|
||||
|
||||
Q2_0, RTX 5070, the first 512 or 1,024 tokens of three frozen prompts.
|
||||
|
||||
| Text | Same top-1 | Median KL | Perplexity off | Perplexity on | On - off (nats per token) |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| Code agent (512) | 97.7% | 3.0e-04 | 2.672 | 2.630 | -0.016 +- 0.019 |
|
||||
| Long document (1,024) | 96.2% | 4.5e-03 | 29.844 | 29.843 | -0.000 +- 0.005 |
|
||||
| Chat (1,024) | 95.0% | 4.4e-03 | 18.380 | 18.301 | -0.004 +- 0.006 |
|
||||
| **All 2,557 tokens** | | | | | **-0.005 +- 0.005** |
|
||||
|
||||
Reading:
|
||||
- The cache changes which of two nearly equal candidates wins at 2-5% of positions. Most of the flips have a top-2
|
||||
margin under 0.5 logits (`logits-compare.txt`).
|
||||
- Neither side is more accurate: the perplexity of the actual text is the same, within 1 standard error.
|
||||
- A GPU expert and the CPU compute the same quantized expert with a different floating-point order. Both are an
|
||||
equally good approximation of the model.
|
||||
|
||||
The start-up warning dates from round 328, when the GPU hit path had a real fault (divergence from token 0). That fault
|
||||
was fixed long ago and is covered by `expert_parity`, `native_expert_parity` and the grouped kernels' tests. Since
|
||||
engine 0.1.8 the warning is replaced by one accurate line.
|
||||
@@ -0,0 +1,36 @@
|
||||
# 2026-09-27: the experimental-speed-projection control vector
|
||||
|
||||
The package's `Qwen3.8-Flash-Next-experimental-speed-projection.gguf` (47 unit directions of 2,560, one per layer),
|
||||
applied as `--control-vector-scaled FILE:1.0 --control-vector-layer-range 4 44 --cvec-mode project --cvec-dir
|
||||
per-layer`: after layers 4-44, every hyper-connection stream `h -= (h . v) v`. The package describes it as a
|
||||
refusal-direction projection. Q2_0, RTX 5070, fixed experts (`--expert-cache 4000 --pcie-frac 0 --adapt-swaps 0`).
|
||||
|
||||
## Kernel (`cvec_parity`)
|
||||
|
||||
Project and add against a double-precision reference (max error 5e-7); with the switch off, the kernel's pending
|
||||
residual write is bitwise the fused hyper-connection read's own, so a loaded-but-off vector is the stock engine.
|
||||
|
||||
## Teacher-forced, stock vs projection (`esp_kl.py`, per-token path)
|
||||
|
||||
| Text | Tokens | Same top-1 | Mean KL | p99 KL | Perplexity stock | With the vector |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| Code | 511 | 92.8% | 0.098 | 1.69 | 2.594 | 2.982 |
|
||||
| Document | 1,023 | 89.8% | 0.058 | 0.84 | 30.299 | 30.984 |
|
||||
| Chat | 1,023 | 89.6% | 0.050 | 0.33 | 18.409 | 18.485 |
|
||||
| **All** | 2,557 | | **0.063** (max 4.10) | 1.00 | | |
|
||||
|
||||
The package reports a mean KL of 0.186 on its own neutral text with UD-Q2_K_XL in llama.cpp.
|
||||
|
||||
Cost on the same tokens: 44.34 -> 44.26, 46.74 -> 46.56, 47.02 -> 46.83 tok/s (0.2-0.4%).
|
||||
|
||||
## Serve (`esp_serve.py`)
|
||||
|
||||
- Loaded and switched off (`cvec=0`) vs an engine without it: 65 / 65 first tokens after prefixes (verify windows
|
||||
and the batched prompt path) and three 256-token generations **token-identical**.
|
||||
- On, off, on again: identical (the switch drops the conversation cache, so nothing computed one way is reused the
|
||||
other way).
|
||||
- The prompt paths steer like the per-token path: served-with-vector first tokens agree with the per-token path's
|
||||
teacher-forced argmax with the vector 62 / 65, and with the stock one 57 / 65 (stock served: 64 / 65 and 57 / 65).
|
||||
- Decode tok/s of 3 x 256-token generations: stock 66.4, on 76.7 / 81.2. **Not a like-for-like comparison:** the
|
||||
model writes different text with it on (on the code prompt it copied a long stretch of the source, which prompt
|
||||
lookup drafts at 91% acceptance, where stock stopped after 78 tokens). On the same tokens it is 0.2-0.4% slower.
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Teacher-forced logits, stock vs the experimental-speed-projection vector, on the same texts and fixed experts.
|
||||
|
||||
python esp_kl.py run # the dumps (per-token path)
|
||||
python esp_kl.py compare # same top-1, mean/median KL(stock || projection), perplexities
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
S = os.path.dirname(os.path.abspath(__file__))
|
||||
E = r"C:\Users\AI-Server\Desktop\Strata\Public\Engine"
|
||||
EXE = E + r"\build-dev\strata.exe"
|
||||
VEC = r"C:\Users\AI-Server\Desktop\Strata\experimental-speed-projection\Qwen3.8-Flash-Next-experimental-speed-projection.gguf"
|
||||
TEXTS = {"code": S + r"\p512.ids", "doc": S + r"\4k-02-long-doc-1k.ids", "chat": S + r"\4k-04-chat-1k.ids"}
|
||||
OUT = S + r"\esp-kl"
|
||||
ARMS = {"stock": [],
|
||||
"proj": ["--control-vector-scaled", VEC + ":1.0", "--control-vector-layer-range", "4", "44",
|
||||
"--cvec-mode", "project", "--cvec-dir", "per-layer"]}
|
||||
|
||||
|
||||
def base_args():
|
||||
c = json.load(open(S + r"\cfg-rel.json"))
|
||||
a = list(c["args"])
|
||||
i = a.index("--expert-cache")
|
||||
a[i + 1] = "4000"
|
||||
j = a.index("--prefill") # the prompt one token at a time: a logits row per position
|
||||
del a[j:j + 2]
|
||||
return a + ["--pcie-frac", "0", "--adapt-swaps", "0"], c
|
||||
|
||||
|
||||
def run():
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
args, c = base_args()
|
||||
env = dict(os.environ)
|
||||
env["PATH"] = os.pathsep.join(c["lib_dirs"]) + os.pathsep + env["PATH"]
|
||||
for name, path in TEXTS.items():
|
||||
for arm, extra in ARMS.items():
|
||||
out = f"{OUT}\\{name}-{arm}.bin"
|
||||
if os.path.exists(out):
|
||||
continue
|
||||
cmd = [EXE, *args, *extra, "--tokens-file", path, "--max-new", "1", "--dump-logits", out]
|
||||
with open(f"{OUT}\\{name}-{arm}.log", "w") as log:
|
||||
r = subprocess.run(cmd, cwd=c["cwd"], stdout=log, stderr=subprocess.STDOUT, env=env)
|
||||
print(name, arm, "exit", r.returncode, flush=True)
|
||||
|
||||
|
||||
def load(p):
|
||||
h = np.fromfile(p, dtype=np.int32, count=2)
|
||||
return np.memmap(p, dtype=np.float32, mode="r", offset=8, shape=(int(h[1]), int(h[0])))
|
||||
|
||||
|
||||
def lsm(x):
|
||||
x = x.astype(np.float64)
|
||||
m = x.max(axis=-1, keepdims=True)
|
||||
return x - (m + np.log(np.exp(x - m).sum(axis=-1, keepdims=True)))
|
||||
|
||||
|
||||
def compare():
|
||||
print(f"{'text':<6}{'tokens':>7}{'same top-1':>11}{'mean KL':>9}{'median KL':>11}{'p99 KL':>8}{'ppl stock':>10}{'ppl proj':>9}")
|
||||
allkl = []
|
||||
for name, path in TEXTS.items():
|
||||
ids = [int(x) for x in open(path).read().replace(",", " ").split()]
|
||||
a, b = load(f"{OUT}\\{name}-stock.bin"), load(f"{OUT}\\{name}-proj.bin")
|
||||
n = min(a.shape[0], b.shape[0], len(ids) - 1)
|
||||
kl, same, la, lb = [], 0, [], []
|
||||
for r in range(0, n, 64):
|
||||
pa, pb = lsm(a[r:min(n, r + 64)]), lsm(b[r:min(n, r + 64)])
|
||||
kl += list((np.exp(pa) * (pa - pb)).sum(axis=1))
|
||||
same += int((pa.argmax(1) == pb.argmax(1)).sum())
|
||||
nxt = np.array(ids[r + 1:min(n, r + 64) + 1])
|
||||
la += list(pa[np.arange(len(nxt)), nxt])
|
||||
lb += list(pb[np.arange(len(nxt)), nxt])
|
||||
kl = np.array(kl)
|
||||
allkl += list(kl)
|
||||
print(f"{name:<6}{n:>7}{100 * same / n:>10.1f}%{kl.mean():>9.3f}{np.median(kl):>11.4f}{np.percentile(kl, 99):>8.2f}"
|
||||
f"{np.exp(-np.mean(la)):>10.3f}{np.exp(-np.mean(lb)):>9.3f}")
|
||||
allkl = np.array(allkl)
|
||||
print(f"all {len(allkl)} tokens: mean KL {allkl.mean():.3f}, max {allkl.max():.2f}, p99 {np.percentile(allkl, 99):.2f}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
{"run": run, "compare": compare}[sys.argv[1]]()
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Serve-mode checks of the experimental-speed-projection vector (exact mode: fixed experts, no PCIe share).
|
||||
|
||||
python esp_serve.py run ARM # ARM = stock | vec; writes esp-serve/<arm>.json
|
||||
python esp_serve.py compare
|
||||
|
||||
Per arm: the first greedy token after many prefixes of three texts (short ones go through the verify windows, long
|
||||
ones through the batched prompt path; the conversation cache reuses the earlier prefix), and three 256-token greedy
|
||||
generations. The vec arm runs everything with cvec=1, then cvec=0, then cvec=1 again.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
|
||||
S = os.path.dirname(os.path.abspath(__file__))
|
||||
E = r"C:\Users\AI-Server\Desktop\Strata\Public\Engine"
|
||||
sys.path.insert(0, E)
|
||||
from serve.server import StrataEngine # noqa: E402
|
||||
|
||||
EXE = E + r"\build-dev\strata.exe"
|
||||
VEC = r"C:\Users\AI-Server\Desktop\Strata\experimental-speed-projection\Qwen3.8-Flash-Next-experimental-speed-projection.gguf"
|
||||
TEXTS = {"code": S + r"\p512.ids", "doc": S + r"\4k-02-long-doc-1k.ids", "chat": S + r"\4k-04-chat-1k.ids"}
|
||||
OUT = S + r"\esp-serve"
|
||||
VEC_ARGS = ["--control-vector-scaled", VEC + ":1.0", "--control-vector-layer-range", "4", "44",
|
||||
"--cvec-mode", "project", "--cvec-dir", "per-layer"]
|
||||
|
||||
|
||||
def ids_of(name):
|
||||
return [int(x) for x in open(TEXTS[name]).read().replace(",", " ").split()]
|
||||
|
||||
|
||||
def engine(extra):
|
||||
c = json.load(open(S + r"\cfg-rel.json"))
|
||||
a = list(c["args"])
|
||||
a[a.index("--expert-cache") + 1] = "4000"
|
||||
env = dict(os.environ)
|
||||
env["PATH"] = os.pathsep.join(c["lib_dirs"]) + os.pathsep + env["PATH"]
|
||||
return StrataEngine(EXE, a + ["--pcie-frac", "0", "--adapt-swaps", "0"] + extra, cwd=c["cwd"],
|
||||
log=OUT + r"\engine.log", env=env)
|
||||
|
||||
|
||||
def gen(eng, ids, n, sampling):
|
||||
toks = [t for t in eng.generate(ids, n, sampling, threading.Event()) if t is not None]
|
||||
return toks, dict(eng.last)
|
||||
|
||||
|
||||
def suite(eng, sampling):
|
||||
out = {"first": {}, "long": {}}
|
||||
for name in TEXTS:
|
||||
ids = ids_of(name)
|
||||
# short prefixes growing by 4 (each continues the live session through the verify windows), then long ones
|
||||
# SHRINKING (no cached prefix, so each is read from 0 by the batched prompt path)
|
||||
lens = list(range(32, 72, 4)) + list(range(min(len(ids) - 1, 700), 95, -48))
|
||||
out["first"][name] = {L: gen(eng, ids[:L], 1, sampling)[0][0] for L in lens}
|
||||
for name in TEXTS:
|
||||
toks, last = gen(eng, ids_of(name)[:300], 256, sampling)
|
||||
out["long"][name] = {"tokens": toks, "last": last}
|
||||
return out
|
||||
|
||||
|
||||
def run(arm):
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
eng = engine(VEC_ARGS if arm == "vec" else [])
|
||||
res = {"info": eng.info}
|
||||
if arm == "stock":
|
||||
res["stock"] = suite(eng, {})
|
||||
else:
|
||||
res["on"] = suite(eng, {"experimental_speed_projection": True})
|
||||
res["off"] = suite(eng, {"experimental_speed_projection": False})
|
||||
res["on2"] = suite(eng, {"experimental_speed_projection": True})
|
||||
eng.close()
|
||||
json.dump(res, open(f"{OUT}\\{arm}.json", "w"))
|
||||
print(arm, "done", eng.info, flush=True)
|
||||
|
||||
|
||||
def compare():
|
||||
import numpy as np
|
||||
sys.path.insert(0, S)
|
||||
from esp_kl import load, OUT as KL
|
||||
st = json.load(open(OUT + r"\stock.json"))
|
||||
ve = json.load(open(OUT + r"\vec.json"))
|
||||
print("engine INFO (vec):", ve["info"])
|
||||
|
||||
def eq_first(a, b):
|
||||
tot = same = 0
|
||||
for name in a:
|
||||
for L, t in a[name].items():
|
||||
tot += 1
|
||||
same += int(b[name][L] == t)
|
||||
return same, tot
|
||||
|
||||
def eq_long(a, b):
|
||||
return all(a[n]["tokens"] == b[n]["tokens"] for n in a), \
|
||||
[(n, next((i for i, (x, y) in enumerate(zip(a[n]["tokens"], b[n]["tokens"])) if x != y), None)) for n in a]
|
||||
|
||||
print("1. vector loaded, switched off == no vector")
|
||||
print(" first tokens: %d / %d identical" % eq_first(st["stock"]["first"], ve["off"]["first"]))
|
||||
print(" 256-token generations identical:", *eq_long(st["stock"]["long"], ve["off"]["long"]))
|
||||
print("2. repeatable: on, off, on again")
|
||||
print(" first tokens: %d / %d identical" % eq_first(ve["on"]["first"], ve["on2"]["first"]))
|
||||
print(" 256-token generations identical:", *eq_long(ve["on"]["long"], ve["on2"]["long"]))
|
||||
print("3. the prompt paths against the per-token path's teacher-forced argmax (row L-1 predicts token L)")
|
||||
for arm, first in (("stock", st["stock"]["first"]), ("on", ve["on"]["first"])):
|
||||
for ref in ("stock", "proj"):
|
||||
agree = tot = 0
|
||||
for name in first:
|
||||
d = load(f"{KL}\\{name}-{ref}.bin")
|
||||
for L, t in first[name].items():
|
||||
L = int(L)
|
||||
if L - 1 < d.shape[0]:
|
||||
tot += 1
|
||||
agree += int(int(np.argmax(d[L - 1])) == t)
|
||||
print(f" served {arm:<5} vs per-token {ref:<5}: {agree} / {tot}")
|
||||
print("4. how different the text is: 256-token generations, on vs stock, first difference")
|
||||
print(" ", eq_long(st["stock"]["long"], ve["on"]["long"])[1])
|
||||
print("5. decode speed (exact mode: fixed experts, no PCIe share - not the normal setup)")
|
||||
for label, r in (("stock", st["stock"]), ("vec off", ve["off"]), ("vec on", ve["on"]), ("vec on 2", ve["on2"])):
|
||||
g = sum(v["last"]["generated"] for v in r["long"].values())
|
||||
ms = sum(v["last"]["decode_ms"] for v in r["long"].values())
|
||||
acc = sum(v["last"].get("drafts_accepted", 0) for v in r["long"].values())
|
||||
off = sum(v["last"].get("drafts_offered", 0) for v in r["long"].values())
|
||||
print(f" {label:<9} {g / ms * 1000:6.1f} tok/s drafts accepted {acc}/{off}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.argv[1] == "run":
|
||||
run(sys.argv[2])
|
||||
else:
|
||||
compare()
|
||||
@@ -0,0 +1,20 @@
|
||||
1. vector loaded, switched off == no vector
|
||||
first tokens: 65 / 65 identical
|
||||
256-token generations identical: True [('code', None), ('doc', None), ('chat', None)]
|
||||
2. repeatable: on, off, on again
|
||||
first tokens: 65 / 65 identical
|
||||
256-token generations identical: True [('code', None), ('doc', None), ('chat', None)]
|
||||
3. the prompt paths against the per-token path's teacher-forced argmax (row L-1 predicts token L)
|
||||
served stock vs per-token stock: 64 / 65
|
||||
served stock vs per-token proj : 57 / 65
|
||||
served on vs per-token stock: 57 / 65
|
||||
served on vs per-token proj : 62 / 65
|
||||
4. how different the text is: 256-token generations, on vs stock, first difference
|
||||
[('code', 11), ('doc', 6), ('chat', 3)]
|
||||
5. decode speed (exact mode: fixed experts, no PCIe share - not the normal setup)
|
||||
stock 66.4 tok/s drafts accepted 256/309
|
||||
vec off 65.7 tok/s drafts accepted 251/318
|
||||
vec on 76.7 tok/s drafts accepted 366/425
|
||||
vec on 2 81.2 tok/s drafts accepted 366/423
|
||||
|
||||
[exited with code 0]
|
||||
@@ -0,0 +1,6 @@
|
||||
text tokens same top-1 mean KL median KL p99 KL ppl stock ppl proj
|
||||
code 511 92.8% 0.098 0.0031 1.69 2.594 2.982
|
||||
doc 1023 89.8% 0.058 0.0271 0.84 30.299 30.984
|
||||
chat 1023 89.6% 0.050 0.0265 0.33 18.409 18.485
|
||||
all 2557 tokens: mean KL 0.063, max 4.10, p99 1.00
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# 2026-09-27: Q4_0 KV with Hadamard rotation (PR #21, `--kv q4_0`)
|
||||
|
||||
PR #21 (code-martin) adds a 4-bit K/V format: an orthonormal 256-point Walsh-Hadamard rotation of K, V and Q, then
|
||||
ggml `q4_0` blocks (576 B per cell and layer, vs 1,056 B in int8).
|
||||
|
||||
## Integration fixes made on merge
|
||||
|
||||
- **Q4 + KV streaming (`--kv-resident`).** The PR had no host copy for Q4 and skipped the staging, so with streaming
|
||||
on, the prompt path wrote K/V through the residency map's -1 entries (out-of-bounds writes).
|
||||
- Q4 is now a third format in `kv_stream`: host copy, block residency, prompt-path staging, MTP ring and STATE_HASH.
|
||||
- `kv_stream_parity` checks it bitwise, streamed vs resident, with ring restore.
|
||||
- **Duplicate attention.** The per-token decode path ran attention twice, the first time before the selection's
|
||||
blocks were made resident; it now runs once.
|
||||
- **Tie-break.** The group maximum's tie-break is now deterministic across lanes: equal |x| with opposite signs
|
||||
could give one block two scales.
|
||||
- **Unrelated changes dropped:** duplicate path fixes (already in 0.1.7), personal `.gitignore` entries, and a
|
||||
re-encoded CMakeLists.
|
||||
|
||||
`kv_q4_parity` (the PR's test) and all 70 ctest tests pass.
|
||||
|
||||
## Quality (teacher-forced, `tools/kv_precision_compare.py`)
|
||||
|
||||
Same text, same fixed expert set (Q2_0, 3,000 slots, `--adapt-swaps 0`); only `--kv` differs. fp16 is the reference.
|
||||
|
||||
| Text | KV | Same top-1 as fp16 | Mean KL | Perplexity | vs fp16 (nats/token) |
|
||||
|---|---|---:|---:|---:|---:|
|
||||
| code agent (512) | int8 | 98.4% | 0.014 | 2.649 | -0.011 +- 0.006 |
|
||||
| | q4_0 | 95.7% | 0.030 | 2.598 | -0.031 +- 0.014 |
|
||||
| document (1,024) | int8 | 95.6% | 0.009 | 30.18 | +0.012 +- 0.005 |
|
||||
| | q4_0 | 91.3% | 0.039 | 32.26 | **+0.078 +- 0.011** |
|
||||
| chat (1,024) | int8 | 95.5% | 0.009 | 18.24 | -0.003 +- 0.005 |
|
||||
| | q4_0 | 90.7% | 0.028 | 18.52 | +0.012 +- 0.008 |
|
||||
| document, 8K (every 8th position) | int8 | 92.3% | 0.114 | 7.63 | -0.013 +- 0.020 |
|
||||
| | q4_0 | 88.0% | 0.297 | 8.65 | **+0.112 +- 0.037** |
|
||||
|
||||
- **int8 is indistinguishable from fp16** (within about 2 standard errors everywhere).
|
||||
- **q4_0 costs measurable precision, and more as the context grows:**
|
||||
- document perplexity +8% at 1K and +12% at 8K;
|
||||
- the KL from fp16 is 2.6-4.2x int8's;
|
||||
- code and chat are near-neutral.
|
||||
- **Needles with `--kv q4_0 --kv-resident 32768`: 5/5** (1K, 32K, 128K at 10% and 90% depth, 262K;
|
||||
`needles-q4.*`). Retrieval holds.
|
||||
|
||||
## Speed and memory (Q2_0, 128K, matrix bench, `--kv-resident 32768`)
|
||||
|
||||
| KV | Expert slots | Decode tok/s | Tokens/round | Rounds/s | K/V in RAM |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| int8 (`kv-stream/matrix-stream`) | 4,044 | 62.4 | 2.52 | 24.8 | 1.52 GiB |
|
||||
| q4_0 (`matrix-q4`) | 4,190 | 67.4 | 2.62 | 25.7 (+3.7%) | 0.83 GiB |
|
||||
|
||||
Without streaming (contexts up to 64K, or too little RAM for it) q4_0 halves the whole KV cache in VRAM instead.
|
||||
|
||||
## Decision
|
||||
|
||||
`--kv q4_0` is a user option:
|
||||
- `START-HERE.bat --kv q4_0`, or the question setup asks above 8K context;
|
||||
- or `--kv q4_0` in a `strata-*.json`.
|
||||
|
||||
int8 stays the default: every published quality number was measured with it, and q4_0's precision cost is real,
|
||||
though it passes the needles.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Faster prompt reading (engine 0.1.13)
|
||||
|
||||
RTX 5070 12 GB, Ryzen 5 7600, 64 GB DDR5. 32K-token prompt (`bench/prompts/long/32k-01-code-agent.ids`), one-shot
|
||||
engine, `--expert-cache auto`, greedy. Numbers are prompt tokens/s.
|
||||
|
||||
## What changed, step by step
|
||||
|
||||
| Step | What | Result |
|
||||
|---|---|---|
|
||||
| Buffers | The attention half and the MoE half of a layer share one scratch region (~260 KB less per prompt token) | bit-identical; chunk 4096 borrows 1.78 GiB instead of 2.84 |
|
||||
| PLE rows | The next chunk's PLE rows are read from the model file on a thread while the GPU works | bit-identical; 762 -> 790 at chunk 4096 |
|
||||
| Chunk size | `--prefill auto`: the largest of 8192 / 6144 / 4096 / ... whose buffers fit in the cache slots it may borrow; a request borrows only what its own prompt needs | Q2_0: 4096 791, 6144 878, 8192 973 |
|
||||
| Host copies | Experts the arena could not pin are copied to pinned buffers by helper threads instead of the launching thread | bit-identical; IQ3_S at 4096: 551 -> 652 |
|
||||
| PLE block | Computed for the whole chunk (two GEMMs) instead of token by token | Q2_0 981 -> 1053, IQ3_S 831 -> 940 |
|
||||
| Experts | llama.cpp's MMQ kernels (weights stay quantized, int8 tensor cores) instead of dequantize-to-FP16 + cuBLAS | Q2_0 1052 -> 1130, IQ3_S (8192) 932 -> 1014 |
|
||||
| Streaming | At chunks of 2048+ every non-resident expert streams in a fixed order through a ring, so the next layer's arrive while the current layer's attention runs | bit-identical; Q2_0 1130 -> 1290, IQ3_S -> 1208 |
|
||||
|
||||
Tried and dropped: cuBLAS grouped GEMMs for the experts (faster in isolation, slower inside the engine: 770 vs 790).
|
||||
|
||||
## Before and after
|
||||
|
||||
| | engine 0.1.12 (`--prefill 2048`) | engine 0.1.13 (`--prefill auto`) |
|
||||
|---|---:|---:|
|
||||
| Q2_0, 32K prompt | 572 | 1,290 |
|
||||
| IQ3_S, 32K prompt | 383 | 1,208 |
|
||||
| Server, Q2_0, 128K context, 999-token prompt | 353 | 438 |
|
||||
| Server, 6,927-token prompt | 529 | 1,077 |
|
||||
| Server, 28,584-token prompt | 584 | 1,249 |
|
||||
|
||||
Output speed is unchanged (the decode path is the same). Short prompts (a few hundred tokens) go through the verify
|
||||
windows as before.
|
||||
|
||||
## Quality
|
||||
|
||||
- "Bit-identical" steps were checked with the GDN state hash after the 32K prompt (all 36 recurrent layers).
|
||||
- The steps that change rounding (the batched PLE block, MMQ's 8-bit activations) were compared teacher-forced: the
|
||||
32K prompt is read by the prompt path up to its last 256 tokens, those go through the token path with their logits
|
||||
dumped. At 32K every rounding change moves the predictions about this much - even a different chunk size of the
|
||||
unchanged engine - so that is the yardstick:
|
||||
|
||||
| Against the 8192-chunk FP16 path | Same top-1 | Mean KL | Perplexity (reference 9.21) |
|
||||
|---|---:|---:|---:|
|
||||
| Chunk 6144, same code (the yardstick) | 89.8% | 0.33 | 9.18 |
|
||||
| MMQ experts | 85.9% | 0.38 | 8.63 |
|
||||
|
||||
- Needles (the answer hidden at a given depth): 5 of 5 at 1K, 32K, 128K (two depths) and 262K, Q2_0, KV streaming.
|
||||
- A rerun of the same configuration is bit-identical.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Speed with engine 0.1.14
|
||||
|
||||
RTX 5070 12 GB (PCIe 5.0 x16), Ryzen 5 7600, 64 GB DDR5-5200, Windows 10. The ready-made 0.1.14 engine, one-shot runs
|
||||
with what setup writes: `--prefill auto`, the shipped expert profile, `--expert-cache auto`, 8-bit KV above 4K, KV
|
||||
streaming (`--kv-resident 32768`) from 64K, MTP (`--spec 4 --spec-min-p 0.5`), greedy, 256 generated tokens. One
|
||||
code-agent prompt per length (`bench/prompts/*/…-code-agent.ids`, the same prompts as `2026-09-24-final`).
|
||||
|
||||
The tables: [`matrix.md`](matrix.md) (prompt and output speed, draft acceptance, experts in VRAM); every run in
|
||||
[`matrix.json`](matrix.json).
|
||||
|
||||
- **IQ2_XS** was measured with Swift 1.5's IQ2_XS (the original's file was not on the machine; the two run at the same
|
||||
speed: 4K 465 vs 467 prompt tokens/s in `2026-09-24`).
|
||||
- **IQ3_S at 128K** comes from a first pass during which a large download was running (the clean rerun of that row
|
||||
was stopped when Windows ran short of memory); the other rows are from runs on an otherwise idle machine.
|
||||
- **IQ3_XXS and IQ3_S at 262K** are not measured: with 43 / 50 GB of experts, a 260K context exceeds 64 GB of RAM.
|
||||
|
||||
## Against 0.1.12
|
||||
|
||||
Prompts are 1.2x (1K) to 2.4x (32K-128K) faster: the 0.1.13 prompt path (chunks of up to 8,192 tokens, MMQ experts,
|
||||
streamed expert copies; see `2026-09-28-prefill-speed`).
|
||||
|
||||
Output speed moves with the text: speculative decoding is faster when more drafts are accepted, and the new prompt
|
||||
path's rounding changes the answer, so the same prompt can land a few percent either way (Q2_0 32K: acceptance 0.865
|
||||
then, 0.709 now). The engine itself is not slower: on the 4K prompt, run back to back on the same machine state,
|
||||
0.1.14 writes 88.5 tokens/s and 0.1.12 85.7 (3 runs each, same free VRAM).
|
||||
|
||||
## Found on the way
|
||||
|
||||
IQ3_XXS at 64K and 128K ended with `out of memory: cudaFuncSetAttribute` in the MMQ launch: CUDA loads a kernel's code
|
||||
when it is first used, and mid-prompt there was no VRAM left for it. Loading every kernel at start
|
||||
(`CUDA_MODULE_LOADING=EAGER`, which the engine now sets itself) fixes it for ~30 MB of VRAM; the IQ3_XXS 64K/128K rows
|
||||
were measured with that fix.
|
||||
@@ -0,0 +1,322 @@
|
||||
[
|
||||
{
|
||||
"model": "Q2_0",
|
||||
"tier": "1k",
|
||||
"prompt_tokens": 1017,
|
||||
"exit": 0,
|
||||
"wall_s": 41.0,
|
||||
"prefill_tok_s": 494.43,
|
||||
"ttft_s": 3.55,
|
||||
"decode_tok_s": 84.26,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.709,
|
||||
"tokens_per_round": 2.72,
|
||||
"vram_slots": 4176
|
||||
},
|
||||
{
|
||||
"model": "Q2_0",
|
||||
"tier": "4k",
|
||||
"prompt_tokens": 3562,
|
||||
"exit": 0,
|
||||
"wall_s": 41.2,
|
||||
"prefill_tok_s": 1006.63,
|
||||
"ttft_s": 5.05,
|
||||
"decode_tok_s": 90.26,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.846,
|
||||
"tokens_per_round": 3.23,
|
||||
"vram_slots": 4123
|
||||
},
|
||||
{
|
||||
"model": "Q2_0",
|
||||
"tier": "32k",
|
||||
"prompt_tokens": 31566,
|
||||
"exit": 0,
|
||||
"wall_s": 63.1,
|
||||
"prefill_tok_s": 1307.6,
|
||||
"ttft_s": 25.63,
|
||||
"decode_tok_s": 73.61,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.709,
|
||||
"tokens_per_round": 2.68,
|
||||
"vram_slots": 3833
|
||||
},
|
||||
{
|
||||
"model": "Q2_0",
|
||||
"tier": "64k",
|
||||
"prompt_tokens": 64162,
|
||||
"exit": 0,
|
||||
"wall_s": 89.0,
|
||||
"prefill_tok_s": 1293.5,
|
||||
"ttft_s": 51.12,
|
||||
"decode_tok_s": 69.02,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.679,
|
||||
"tokens_per_round": 2.62,
|
||||
"vram_slots": 3780
|
||||
},
|
||||
{
|
||||
"model": "Q2_0",
|
||||
"tier": "128k",
|
||||
"prompt_tokens": 128478,
|
||||
"exit": 0,
|
||||
"wall_s": 147.4,
|
||||
"prefill_tok_s": 1208.46,
|
||||
"ttft_s": 107.81,
|
||||
"decode_tok_s": 67.21,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.691,
|
||||
"tokens_per_round": 2.64,
|
||||
"vram_slots": 3697
|
||||
},
|
||||
{
|
||||
"model": "Q2_0",
|
||||
"tier": "262k",
|
||||
"prompt_tokens": 259943,
|
||||
"exit": 0,
|
||||
"wall_s": 309.8,
|
||||
"prefill_tok_s": 966.84,
|
||||
"ttft_s": 270.35,
|
||||
"decode_tok_s": 60.3,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.623,
|
||||
"tokens_per_round": 2.43,
|
||||
"vram_slots": 3525
|
||||
},
|
||||
{
|
||||
"model": "IQ2_XS",
|
||||
"tier": "1k",
|
||||
"prompt_tokens": 1017,
|
||||
"exit": 0,
|
||||
"wall_s": 40.4,
|
||||
"prefill_tok_s": 461.28,
|
||||
"ttft_s": 2.28,
|
||||
"decode_tok_s": 74.38,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.772,
|
||||
"tokens_per_round": 2.84,
|
||||
"vram_slots": 4386,
|
||||
"note": "first pass"
|
||||
},
|
||||
{
|
||||
"model": "IQ2_XS",
|
||||
"tier": "4k",
|
||||
"prompt_tokens": 3562,
|
||||
"exit": 0,
|
||||
"wall_s": 41.8,
|
||||
"prefill_tok_s": 810.71,
|
||||
"ttft_s": 4.48,
|
||||
"decode_tok_s": 73.84,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.79,
|
||||
"tokens_per_round": 3.24,
|
||||
"vram_slots": 4336,
|
||||
"note": "first pass"
|
||||
},
|
||||
{
|
||||
"model": "IQ2_XS",
|
||||
"tier": "32k",
|
||||
"prompt_tokens": 31566,
|
||||
"exit": 0,
|
||||
"wall_s": 65.0,
|
||||
"prefill_tok_s": 1237.98,
|
||||
"ttft_s": 25.59,
|
||||
"decode_tok_s": 71.49,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.856,
|
||||
"tokens_per_round": 3.28,
|
||||
"vram_slots": 4057,
|
||||
"note": "first pass"
|
||||
},
|
||||
{
|
||||
"model": "IQ2_XS",
|
||||
"tier": "64k",
|
||||
"prompt_tokens": 64162,
|
||||
"exit": 0,
|
||||
"wall_s": 97.1,
|
||||
"prefill_tok_s": 1135.63,
|
||||
"ttft_s": 56.59,
|
||||
"decode_tok_s": 64.29,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.707,
|
||||
"tokens_per_round": 2.76,
|
||||
"vram_slots": 4005,
|
||||
"note": "first pass"
|
||||
},
|
||||
{
|
||||
"model": "IQ2_XS",
|
||||
"tier": "128k",
|
||||
"prompt_tokens": 128478,
|
||||
"exit": 0,
|
||||
"wall_s": 158.0,
|
||||
"prefill_tok_s": 1070.61,
|
||||
"ttft_s": 120.1,
|
||||
"decode_tok_s": 59.76,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.726,
|
||||
"tokens_per_round": 2.69,
|
||||
"vram_slots": 3923,
|
||||
"note": "first pass"
|
||||
},
|
||||
{
|
||||
"model": "IQ2_XS",
|
||||
"tier": "262k",
|
||||
"prompt_tokens": 259943,
|
||||
"exit": 0,
|
||||
"wall_s": 332.4,
|
||||
"prefill_tok_s": 886.2,
|
||||
"ttft_s": 293.43,
|
||||
"decode_tok_s": 52.83,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.628,
|
||||
"tokens_per_round": 2.53,
|
||||
"vram_slots": 3565,
|
||||
"note": "first pass"
|
||||
},
|
||||
{
|
||||
"model": "IQ3_XXS",
|
||||
"tier": "1k",
|
||||
"prompt_tokens": 1017,
|
||||
"exit": 0,
|
||||
"wall_s": 44.1,
|
||||
"prefill_tok_s": 414.55,
|
||||
"ttft_s": 2.54,
|
||||
"decode_tok_s": 60.26,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.864,
|
||||
"tokens_per_round": 3.52,
|
||||
"vram_slots": 3304,
|
||||
"note": "first pass"
|
||||
},
|
||||
{
|
||||
"model": "IQ3_XXS",
|
||||
"tier": "4k",
|
||||
"prompt_tokens": 3562,
|
||||
"exit": 0,
|
||||
"wall_s": 45.8,
|
||||
"prefill_tok_s": 769.57,
|
||||
"ttft_s": 4.73,
|
||||
"decode_tok_s": 62.07,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.855,
|
||||
"tokens_per_round": 3.24,
|
||||
"vram_slots": 3263,
|
||||
"note": "first pass"
|
||||
},
|
||||
{
|
||||
"model": "IQ3_XXS",
|
||||
"tier": "32k",
|
||||
"prompt_tokens": 31566,
|
||||
"exit": 0,
|
||||
"wall_s": 69.7,
|
||||
"prefill_tok_s": 1107.75,
|
||||
"ttft_s": 28.61,
|
||||
"decode_tok_s": 51.4,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.678,
|
||||
"tokens_per_round": 2.6,
|
||||
"vram_slots": 3031,
|
||||
"note": "first pass"
|
||||
},
|
||||
{
|
||||
"model": "IQ3_XXS",
|
||||
"tier": "64k",
|
||||
"prompt_tokens": 64162,
|
||||
"exit": 0,
|
||||
"wall_s": 104.9,
|
||||
"prefill_tok_s": 1065.31,
|
||||
"ttft_s": 61.77,
|
||||
"decode_tok_s": 49.95,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.741,
|
||||
"tokens_per_round": 2.94,
|
||||
"vram_slots": 2965,
|
||||
"note": "first pass"
|
||||
},
|
||||
{
|
||||
"model": "IQ3_XXS",
|
||||
"tier": "128k",
|
||||
"prompt_tokens": 128478,
|
||||
"exit": 0,
|
||||
"wall_s": 172.1,
|
||||
"prefill_tok_s": 1015.11,
|
||||
"ttft_s": 128.12,
|
||||
"decode_tok_s": 45.78,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.665,
|
||||
"tokens_per_round": 2.35,
|
||||
"vram_slots": 2901,
|
||||
"note": "first pass"
|
||||
},
|
||||
{
|
||||
"model": "IQ3_S",
|
||||
"tier": "1k",
|
||||
"prompt_tokens": 1017,
|
||||
"exit": 0,
|
||||
"wall_s": 56.2,
|
||||
"prefill_tok_s": 396.13,
|
||||
"ttft_s": 4.14,
|
||||
"decode_tok_s": 51.52,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.826,
|
||||
"tokens_per_round": 3.12,
|
||||
"vram_slots": 2617
|
||||
},
|
||||
{
|
||||
"model": "IQ3_S",
|
||||
"tier": "4k",
|
||||
"prompt_tokens": 3562,
|
||||
"exit": 0,
|
||||
"wall_s": 57.5,
|
||||
"prefill_tok_s": 736.9,
|
||||
"ttft_s": 6.35,
|
||||
"decode_tok_s": 51.6,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.747,
|
||||
"tokens_per_round": 2.89,
|
||||
"vram_slots": 2580
|
||||
},
|
||||
{
|
||||
"model": "IQ3_S",
|
||||
"tier": "32k",
|
||||
"prompt_tokens": 31566,
|
||||
"exit": 0,
|
||||
"wall_s": 82.5,
|
||||
"prefill_tok_s": 1069.83,
|
||||
"ttft_s": 31.33,
|
||||
"decode_tok_s": 48.19,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.772,
|
||||
"tokens_per_round": 2.94,
|
||||
"vram_slots": 2383
|
||||
},
|
||||
{
|
||||
"model": "IQ3_S",
|
||||
"tier": "64k",
|
||||
"prompt_tokens": 64162,
|
||||
"exit": 0,
|
||||
"wall_s": 115.0,
|
||||
"prefill_tok_s": 1069.8,
|
||||
"ttft_s": 61.47,
|
||||
"decode_tok_s": 48.75,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.786,
|
||||
"tokens_per_round": 2.9,
|
||||
"vram_slots": 2347
|
||||
},
|
||||
{
|
||||
"model": "IQ3_S",
|
||||
"tier": "128k",
|
||||
"prompt_tokens": 128478,
|
||||
"exit": 0,
|
||||
"wall_s": 191.8,
|
||||
"prefill_tok_s": 931.09,
|
||||
"ttft_s": 138.11,
|
||||
"decode_tok_s": 40.48,
|
||||
"decoded": 256,
|
||||
"spec_accept": 0.661,
|
||||
"tokens_per_round": 2.71,
|
||||
"vram_slots": 2310,
|
||||
"note": "first pass (a download was running)"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
### Prompt processing (tok/s)
|
||||
|
||||
| Model | 1K | 4K | 32K | 64K | 128K | 262K |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| Q2_0 | 494 | 1,007 | 1,308 | 1,294 | 1,208 | 967 |
|
||||
| IQ2_XS | 461 | 811 | 1,238 | 1,136 | 1,071 | 886 |
|
||||
| IQ3_XXS | 415 | 770 | 1,108 | 1,065 | 1,015 | - |
|
||||
| IQ3_S | 396 | 737 | 1,070 | 1,070 | 931 | - |
|
||||
|
||||
### Output (tok/s)
|
||||
|
||||
| Model | 1K | 4K | 32K | 64K | 128K | 262K |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| Q2_0 | 84.3 | 90.3 | 73.6 | 69.0 | 67.2 | 60.3 |
|
||||
| IQ2_XS | 74.4 | 73.8 | 71.5 | 64.3 | 59.8 | 52.8 |
|
||||
| IQ3_XXS | 60.3 | 62.1 | 51.4 | 50.0 | 45.8 | - |
|
||||
| IQ3_S | 51.5 | 51.6 | 48.2 | 48.8 | 40.5 | - |
|
||||
|
||||
### Draft acceptance
|
||||
|
||||
| Model | 1K | 4K | 32K | 64K | 128K | 262K |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| Q2_0 | 0.709 | 0.846 | 0.709 | 0.679 | 0.691 | 0.623 |
|
||||
| IQ2_XS | 0.772 | 0.790 | 0.856 | 0.707 | 0.726 | 0.628 |
|
||||
| IQ3_XXS | 0.864 | 0.855 | 0.678 | 0.741 | 0.665 | - |
|
||||
| IQ3_S | 0.826 | 0.747 | 0.772 | 0.786 | 0.661 | - |
|
||||
|
||||
### Experts in VRAM
|
||||
|
||||
| Model | 1K | 4K | 32K | 64K | 128K | 262K |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| Q2_0 | 4,176 | 4,123 | 3,833 | 3,780 | 3,697 | 3,525 |
|
||||
| IQ2_XS | 4,386 | 4,336 | 4,057 | 4,005 | 3,923 | 3,565 |
|
||||
| IQ3_XXS | 3,304 | 3,263 | 3,031 | 2,965 | 2,901 | - |
|
||||
| IQ3_S | 2,617 | 2,580 | 2,383 | 2,347 | 2,310 | - |
|
||||
BIN
Binary file not shown.
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"mode": "project",
|
||||
"dir": "per-layer",
|
||||
"scale": 1.0,
|
||||
"layer_range": [
|
||||
4,
|
||||
44
|
||||
],
|
||||
"refusal_harmful_thinkoff": 2.0,
|
||||
"refusal_harmful_thinkon": 2.0,
|
||||
"refusal_harmless_thinkoff": 4.0,
|
||||
"refusal_harmless_thinkon": 6.0,
|
||||
"broken": 0,
|
||||
"kl_mean": 0.18598,
|
||||
"kl_max": 18.26549,
|
||||
"kl_p99": 3.143571,
|
||||
"decode_tps": 51.19,
|
||||
"vector_file": "Qwen3.8-Flash-Next-experimental-speed-projection.gguf",
|
||||
"base_model_gguf": "unsloth/Qwen3.8-Flash-Next-GGUF UD-Q2_K_XL",
|
||||
"base_model_sha256_shard1": "a4f3b21e77353999829f2f767e9ac21ce9c71d29a74f2cc9eda48c9bf23c8b86",
|
||||
"worktree_commit": "37e026e3b",
|
||||
"criterion_status": "PASS",
|
||||
"dirty_direction": false,
|
||||
"note": "projection ablation h -= s*(h.v)*v, v unit-normalized per layer. dir=per-layer",
|
||||
"branch": "cvec-hooks (github.com/Cudila/llama.cpp)"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# experimental-speed-projection (EXPERIMENTAL, off by default)
|
||||
|
||||
`Qwen3.8-Flash-Next-experimental-speed-projection.gguf` is a 480 KB control vector for Qwen3.8-Flash-Next: one unit
|
||||
direction of 2,560 values per layer (`direction.1` .. `direction.47`, GGUF `controlvector` format). The JSON beside it
|
||||
is the configuration its authors selected and their scores.
|
||||
|
||||
**What it does.** Turned on, Strata removes each direction from the residual stream after layers 4-44
|
||||
(`h -= (h . v) v` on every hyper-connection stream), the same operation as llama.cpp's `--cvec-mode project` in the
|
||||
package's patches. The package's own documentation describes the vector as a **refusal-direction projection**: the
|
||||
model declines far fewer requests (it reports 1 of 50 vs 50 of 50 on its test set), and removing refusals removes a
|
||||
safety behaviour - you are responsible for what the model writes with it on. It also shifts the model's predictions
|
||||
slightly on ordinary text (measured here: `bench/results/2026-09-27-esp/`). It is not an optimization in the engine:
|
||||
on the same text it costs 0.2-0.4% per token; a chat's speed with it on depends on the text the model writes.
|
||||
|
||||
**Turning it on.** `START-HERE.bat --setup` asks (default: off), or pass `--experimental-speed-projection on`. With it
|
||||
loaded, the web app's Sampling drawer and the API field `"experimental_speed_projection": false` switch it off per
|
||||
request. See `docs/DETAILS.md`, "Experimental speed projection".
|
||||
|
||||
**Origin and license.** A local package of the vector (the published original uses filenames containing "refusal";
|
||||
the bytes are the same), made from Qwen3.8-Flash-Next activations, so the Qwen Community License 1.0 of the model
|
||||
applies to it. Made for the original Qwen3.8-Flash-Next (not Swift 1.5).
|
||||
Binary file not shown.
+121
-20
@@ -11,26 +11,34 @@ New here? Start with the [README](../README.md) - it has everything you need to
|
||||
|
||||
## Speed (measured)
|
||||
|
||||
RTX 5070 **12 GB**, Ryzen 5 7600 (6 cores), 64 GB DDR5-5200, Windows. One code-agent prompt per length, 256 generated
|
||||
tokens, MTP speculative decoding on. "262K" is the model's full context window (a 259,943-token prompt).
|
||||
RTX 5070 **12 GB**, Ryzen 5 7600 (6 cores), 64 GB DDR5-5200, Windows, engine 0.1.14 with the settings setup writes
|
||||
(`--prefill auto`, 8-bit KV above 4K, KV streaming from 64K). One code-agent prompt per length, 256 generated tokens,
|
||||
MTP speculative decoding on. "262K" is the model's full context window (a 259,943-token prompt). The IQ2_XS row was
|
||||
measured with Swift 1.5's IQ2_XS, which runs at the original's speed.
|
||||
|
||||
### Prompt processing (tokens/s)
|
||||
|
||||
| Model | 1K | 4K | 32K | 64K | 128K | 262K |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| **Q2_0** | 389 | 539 | 571 | 561 | 543 | 496 |
|
||||
| **IQ2_XS** | 332 | 463 | 495 | 486 | 472 | 437 |
|
||||
| **IQ3_XXS** | 285 | 410 | 435 | 427 | 414 | - |
|
||||
| **IQ3_S** | 260 | 374 | 397 | - | 378 | - |
|
||||
| **Q2_0** | 494 | 1,007 | 1,308 | 1,294 | 1,208 | 967 |
|
||||
| **IQ2_XS** | 461 | 811 | 1,238 | 1,136 | 1,071 | 886 |
|
||||
| **IQ3_XXS** | 415 | 770 | 1,108 | 1,065 | 1,015 | - |
|
||||
| **IQ3_S** | 396 | 737 | 1,070 | 1,070 | 931 | - |
|
||||
|
||||
### Output (tokens/s)
|
||||
|
||||
| Model | 1K | 4K | 32K | 64K | 128K | 262K |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| **Q2_0** | 88.7 | 94.6 | 87.5 | 76.4 | 65.1 | 56.3 |
|
||||
| **IQ2_XS** | 82.0 | 78.0 | 65.3 | 63.7 | 52.0 | 48.0 |
|
||||
| **IQ3_XXS** | 64.6 | 65.6 | 57.3 | 54.4 | 44.8 | - |
|
||||
| **IQ3_S** | 51.2 | 54.4 | 51.1 | - | 42.2 | - |
|
||||
| **Q2_0** | 84.3 | 90.3 | 73.6 | 69.0 | 67.2 | 60.3 |
|
||||
| **IQ2_XS** | 74.4 | 73.8 | 71.5 | 64.3 | 59.8 | 52.8 |
|
||||
| **IQ3_XXS** | 60.3 | 62.1 | 51.4 | 50.0 | 45.8 | - |
|
||||
| **IQ3_S** | 51.5 | 51.6 | 48.2 | 48.8 | 40.5 | - |
|
||||
|
||||
Output speed depends on the text as well: speculative decoding runs faster when more of the drafted tokens are
|
||||
accepted, so a different answer to the same prompt moves it by several percent. Run back to back on the 4K prompt,
|
||||
0.1.14 writes 88.5 tokens/s and 0.1.12 85.7. The numbers before 0.1.13 (prompts about half as fast):
|
||||
[`bench/results/2026-09-24-final`](../bench/results/2026-09-24-final/matrix.md); these:
|
||||
[`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
|
||||
@@ -42,13 +50,32 @@ attention reads in VRAM (`--kv-resident 32768`), so more experts fit on the GPU.
|
||||
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.
|
||||
|
||||
Time to first token is prompt length / prompt speed: about 7 s at 4K, 55 s at 32K, 4 minutes at 128K and 9 minutes at
|
||||
262K. The raw numbers: [`bench/results/`](../bench/results/). The [paper](paper/Strata-Paper.pdf) explains every number.
|
||||
**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
|
||||
is measurably less precise on long documents (perplexity +8-12%; needle tests still pass). 8-bit stays the default.
|
||||
Details: [`bench/results/2026-09-27-kv-q4`](../bench/results/2026-09-27-kv-q4/README.md).
|
||||
|
||||
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).
|
||||
|
||||
**Faster prompts (engine 0.1.13):** the prompt is read in chunks of up to 8,192 tokens instead of 2,048 (`--prefill
|
||||
auto`: the largest chunk whose buffers fit in the expert-cache slots it borrows, and a request borrows only what its
|
||||
prompt needs); the experts are multiplied by llama.cpp's quantized MMQ kernels instead of being expanded to FP16
|
||||
first; the next layer's experts stream over PCIe while the current layer's attention runs; the PLE block runs for the
|
||||
whole chunk at once; unpinned experts are copied by helper threads. Measured on the RTX 5070 12 GB, 64 GB RAM,
|
||||
32K-token prompt: Q2_0 572 -> 1,290 tokens/s, IQ3_S 383 -> 1,208. Through the server (Q2_0, 128K context): 999 tokens
|
||||
353 -> 438 tokens/s, 6,927 tokens 529 -> 1,077, 28,584 tokens 584 -> 1,249. Output speed is unchanged. Needles 5/5
|
||||
(1K-262K). Details and the quality check:
|
||||
[`bench/results/2026-09-28-prefill-speed`](../bench/results/2026-09-28-prefill-speed/README.md). Existing installs
|
||||
switch to `--prefill auto` the next time START-HERE / setup.sh starts them. The raw numbers:
|
||||
[`bench/results/`](../bench/results/). The [paper](paper/Strata-Paper.pdf) explains every number.
|
||||
|
||||
## Other GPUs (estimated)
|
||||
|
||||
Not measured - estimated from the runs above (same CPU and 64 GB RAM): the GPU part scaled by memory bandwidth, the CPU
|
||||
part by how many more experts the card's VRAM holds. Treat as **±20%**. Numbers are *prompt / output* tokens/s.
|
||||
The prompt figures predate engine 0.1.13, which about doubled prompt speed on the measured card; how much of that a
|
||||
card gains depends on its PCIe link (the experts stream over it), so they are still the older estimates.
|
||||
|
||||
| GPU | Model | 1K | 4K | 32K | 64K | 128K | 262K |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
@@ -60,7 +87,9 @@ part by how many more experts the card's VRAM holds. Treat as **±20%**. Numbers
|
||||
| | IQ3_XXS | ~260 / ~106 | ~374 / ~103 | ~396 / ~89 | ~390 / ~85 | ~378 / ~71 | - |
|
||||
|
||||
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. A 3090's 24 GB takes most of the CPU work away.
|
||||
CPU does not have to compute. A 3090's 24 GB takes most of the CPU work away. (Since 0.1.14 the expert profile ranks
|
||||
all 24,576 experts; before, the cache stopped at 8,000, about 10-14 GB. `tools/make_profile.py` builds a profile from
|
||||
your own prompts: run the engine once with `--dump-routing trace.bin`, see the tool's help.)
|
||||
|
||||
## Which model?
|
||||
|
||||
@@ -125,8 +154,16 @@ Windows, `build-essential` + CUDA on Ubuntu) and compiles the engine for your GP
|
||||
4. **Images?** yes / no (see [Images](#images-vision)).
|
||||
|
||||
Then it downloads and prepares everything (the model is 66-76 GB, so the first start takes a while; an interrupted
|
||||
download continues where it stopped) and **starts the model**: your browser opens `http://127.0.0.1:8080`, a small
|
||||
page that shows it is running and lets you chat. The API is at `http://127.0.0.1:8080/v1` for your apps.
|
||||
download continues where it stopped) and **starts the model**: your browser opens `http://127.0.0.1:8080`, the Strata
|
||||
app. It has three tabs:
|
||||
- **Chat:** streaming answers, the model's thinking (folded away once it answers), code with a copy button, pictures when
|
||||
images are on, and sampling and thinking-level settings. Chats stay in your browser.
|
||||
- **Monitor:** what the model is doing (reading the prompt, with progress, or writing, at how many tokens/s); GPU load,
|
||||
VRAM, temperature, power and PCIe traffic; CPU, RAM and disk; the context in use; the last requests.
|
||||
- **About:** the model and engine settings, and the addresses to connect other apps.
|
||||
|
||||
`http://127.0.0.1:8080/?q=your question` opens it with a new chat already asking. The API is at
|
||||
`http://127.0.0.1:8080/v1` for your apps.
|
||||
|
||||
**Every time after that**, `START-HERE.bat` just starts the model (30-90 s to load 34-43 GB into RAM). Nothing is
|
||||
downloaded again. Closing the window stops the model.
|
||||
@@ -159,6 +196,15 @@ for the build tools), and the same start: `http://127.0.0.1:8080`. Later runs of
|
||||
start the model directly. Options as on Windows (`./setup.sh --setup`, `--model Q2_0 --yes`, `--gguf-dir /data/Q2_0`).
|
||||
Terminal chat: `.venv/bin/python chat.py`.
|
||||
|
||||
- **Updating:** `git pull`, then `./setup.sh`: it 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.
|
||||
- **Other distributions** (Arch, Fedora, ...): install the C++ compiler and the CUDA Toolkit 13 with your package
|
||||
manager first (Arch: `sudo pacman -S base-devel cuda`); setup finds `nvcc` on PATH, in `/usr/local/cuda*` and in
|
||||
`/opt/cuda*`, and does the rest.
|
||||
- **WSL** works (Ubuntu 24.04 tested), with one limit: the NVIDIA driver pins only about 1 GB of RAM there, so KV
|
||||
streaming (`--kv-resident`) is off and the KV cache stays in VRAM, and the experts are copied to the GPU from
|
||||
unpinned RAM (slower prompts than native Linux).
|
||||
|
||||
---
|
||||
|
||||
## Using it
|
||||
@@ -171,6 +217,7 @@ The server listens on `http://127.0.0.1:8080` (change with `--port` in setup, or
|
||||
| Anthropic Messages (stream and non-stream, tools) | `POST /v1/messages` |
|
||||
| Model list / health | `GET /v1/models`, `GET /health` |
|
||||
| What the model is doing right now | `GET /status` |
|
||||
| Everything the Monitor tab shows (engine, live state, last requests, hardware) | `GET /metrics` |
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8080/v1/chat/completions -H "Content-Type: application/json" -d '{
|
||||
@@ -202,9 +249,18 @@ print(r.choices[0].message.content)
|
||||
15 s, and `GET /status` says what it is doing (`reading the prompt`, `answering`, tokens so far). Closing the
|
||||
connection or pressing stop in your app really stops the model, so the next request starts at once.
|
||||
- **Chat apps.** Any app with an "OpenAI-compatible" provider works: base URL `http://127.0.0.1:8080/v1`, any API key.
|
||||
- **Context.** Chosen in setup (8K-262K). Requests longer than that are refused, never silently cut.
|
||||
- **From other devices / the internet.** The server listens on your PC only (`127.0.0.1`). To reach it from elsewhere,
|
||||
put a tunnel in front of it, for example [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/):
|
||||
- **Context.** Chosen in setup (8K-262K). Requests longer than that are refused, never silently cut. A request whose
|
||||
`max_tokens` would run past the context is refused too (400); agents that always ask for their full output cap
|
||||
can instead get it shortened to the room left: add `"fit_max_tokens": true` to `strata-<model>.json` (or pass
|
||||
`--fit-max-tokens` to `serve/server.py`). A prompt that leaves no room at all is still refused.
|
||||
- **From other devices on your network.** The server listens on your PC only (`127.0.0.1`) unless you say otherwise:
|
||||
run setup with `START-HERE.bat --setup --host 0.0.0.0 --api-key some-long-secret` (or add `"host": "0.0.0.0"` and
|
||||
`"api_key": "..."` to `strata-<model>.json`). The server window then prints this PC's addresses
|
||||
(`from other devices: http://192.168.x.x:8080/`); open that on the other device, or use `.../v1` as an API base URL.
|
||||
On Windows the firewall blocks it until you allow it: accept its prompt for Python (private networks), or run
|
||||
`New-NetFirewallRule -DisplayName "Strata 8080" -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Allow -Profile Private`
|
||||
in an admin PowerShell, and make sure the network is set to Private.
|
||||
- **From the internet.** Put a tunnel in front of it, for example [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/):
|
||||
`cloudflared tunnel --url http://127.0.0.1:8080`. **Set a key first**, or anyone with the link can use your PC:
|
||||
add `"api_key": "some-long-secret"` to `strata-<model>.json` (or set the `STRATA_API_KEY` environment variable);
|
||||
clients then send it as their API key.
|
||||
@@ -301,6 +357,45 @@ test images.
|
||||
|
||||
---
|
||||
|
||||
## Experimental speed projection (EXPERIMENTAL, off by default)
|
||||
|
||||
**This is an experiment, not a finished feature.** It ships with Strata but stays off unless you turn it on.
|
||||
|
||||
A 480 KB control vector for Qwen3.8-Flash-Next (`data/experimental-speed-projection/`, see its README). After each
|
||||
of layers 4-44 the engine removes one direction from every hyper-connection stream of the residual: `h -= (h . v) v`,
|
||||
one unit vector `v` per layer, exactly as llama.cpp does with the package's `--cvec-mode project` patches.
|
||||
|
||||
**What it changes.** The vector's own package describes it as a **refusal-direction projection**: with it the model
|
||||
declines far fewer requests (it reports 1 of 50 vs 50 of 50 on its test set), and removing refusals removes a safety
|
||||
behaviour - you are responsible for what the model writes with it on. It also shifts ordinary answers a little
|
||||
(measured below). It is not an optimization in the engine: on the same text it costs 0.2-0.4% per token. What a
|
||||
chat's tokens/s does with it on depends on the text the model writes (length, repetition, how well the drafts land),
|
||||
so measure it on your own prompts; the Monitor marks every request ESP or stock.
|
||||
|
||||
**Turning it on (at setup).** `START-HERE.bat --setup` asks "Turn on the experimental speed projection?" (default:
|
||||
no), or pass `--experimental-speed-projection on` (`off`, or a path to another vector GGUF). Only for the original
|
||||
Qwen3.8-Flash-Next, not Swift 1.5. It writes these engine flags (llama.cpp's) into `strata-<model>.json`:
|
||||
|
||||
```
|
||||
--control-vector-scaled <Strata>\data\experimental-speed-projection\Qwen3.8-Flash-Next-experimental-speed-projection.gguf:1.0
|
||||
--control-vector-layer-range 4 44 --cvec-mode project --cvec-dir per-layer
|
||||
```
|
||||
|
||||
The engine log then says `control vector mode = project, dir = per-layer, layers 4..44 (41 steered)`, and the web
|
||||
app's About tab lists it. (`--cvec-mode add` is llama.cpp's stock additive mode, for additive vectors.)
|
||||
|
||||
**Per request.** A loaded vector is on for every request unless it says otherwise: the web app's Sampling drawer has
|
||||
a switch, and the API takes `"experimental_speed_projection": false` in the request body (OpenAI and Anthropic; a
|
||||
config default goes in `"sampling": {"experimental_speed_projection": false}`). Switching drops the conversation
|
||||
cache once, since the model state was computed the other way. Switched off, the output is token-for-token the stock
|
||||
model's.
|
||||
|
||||
**Measured here** (Q2_0, fixed experts, 2,557 teacher-forced tokens of code, a document and a chat): the top-1 token
|
||||
changes at 10% of positions, mean KL from the stock model 0.063 nats (max 4.1), perplexity +15% on code, +2.3% on
|
||||
the document, +0.4% on the chat. Details: `bench/results/2026-09-27-esp/`.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | What to do |
|
||||
@@ -309,14 +404,18 @@ test images.
|
||||
| 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. |
|
||||
| `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`. |
|
||||
| `cudaHostRegister ... out of memory` in the log | Normal on Windows: the engine pins the experts in per-layer slices instead. Only a problem if the load then fails. |
|
||||
| The first start takes minutes | It is reading 34-43 GB into RAM; the second start is faster while the files are in the OS cache. |
|
||||
| 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. |
|
||||
| The PC freezes for a few minutes at the first start | Normal the first time: the engine loads the experts into RAM, pins part of it for the GPU and sizes the expert cache. Wait; don't close the window. Still frozen after 10 minutes: restart the PC, close other programs, try again, or pick a smaller size. |
|
||||
| `the engine stopped unexpectedly (exit code ...)` | The engine process ended mid-answer - usually out of RAM (Linux ends the biggest program: `sudo dmesg \| grep -i -E 'killed process\|out of memory'`). The next request starts it again by itself. If it repeats: close other programs or pick a smaller size. The server also warns at start when the model's experts leave less than ~6 GB of RAM for everything else. |
|
||||
| Slow output, disk light busy | Not enough free RAM: close other programs, or choose Q2_0 / IQ2_XS. |
|
||||
| `prompt ... exceeds the context` | The request is longer than the context you chose: run setup again with a bigger `--context`. |
|
||||
| Slower than the tables | The monitor plugged into the GPU 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. |
|
||||
| `this server was started without the vision encoder` | The model was set up for text only: run setup again with `--vision gpu`. |
|
||||
| A picture is refused or `cannot read the image` | The file is not a picture Pillow can open (JPEG, PNG, WebP, GIF, BMP, TIFF, AVIF work). |
|
||||
| Pictures are slow (10-30 s) | The encoder runs on the CPU: run setup again with `--vision gpu` (needs ~1.4 GB of VRAM). |
|
||||
| A request never finishes (older engines, mostly with pictures) | Run `START-HERE.bat` once to get engine 0.1.2 or newer. It keeps a real margin of VRAM free: the log says `... MiB of VRAM free with everything loaded` and warns when it is close to 0. |
|
||||
| A request never finishes: "reading the prompt", GPU "100%" at low power | The GPU ran out of VRAM (engines before 0.1.9 could end with ~30 MiB free at large contexts). Run `START-HERE.bat` once to get engine 0.1.9 or newer; the log then says `... MiB of VRAM free with everything loaded` (a few hundred) and names the `--vram-reserve-mib` to add if it is low. |
|
||||
| Generation stops mid-answer, GPU "100%", one CPU core busy | Fixed in engine 0.1.12 (issue #29, a race in the CPU expert pool on big-VRAM cards). Since then a request that stops moving ends with an error instead of hanging (after 2 minutes; 1 minute from 0.1.13): the log says `no progress for ... s ... (issue #29)` with where it stopped, and the next request starts the engine again. If you see that line, please open an issue with it. Engine 0.1.13 adds a stall report under it (what every expert-pool thread and the GPU handshake were doing, memory and page faults) and, on Windows, a `strata-stall-<pid>.dmp` file with every thread's stack: attach both. (`STRATA_WATCHDOG_S` sets the time in seconds; 0 turns it off.) Engine 0.1.14 fixes the stall those reports found (issue #31: with the IQ packs the host could wait forever inside the NVIDIA driver while copying experts in a verify window; the experts are now copied by a GPU kernel, `--pcie-mode dma` restores the old way). |
|
||||
| `out of memory: cudaFuncSetAttribute` in the log (IQ3_XXS, long prompt) | Fixed in engine 0.1.15: CUDA loaded a kernel's code when it was first needed, and mid-prompt there was no VRAM left for it. Run `START-HERE.bat` (Windows) or `./setup.sh` (Linux) once to update. |
|
||||
| Anything else | The engine log is `strata-<model>.log` in this folder. |
|
||||
|
||||
---
|
||||
@@ -353,3 +452,5 @@ The full story, with measurements, bottlenecks and what comes next: **[docs/pape
|
||||
`third_party/ggml/LICENSE`.
|
||||
- Ideas from [Splash](https://github.com/incoai/splash), [ninfer](https://github.com/Neroued/ninfer) and
|
||||
[HyperQwen](https://github.com/syv-ai/HyperQwen); references in the paper.
|
||||
- The web app's font: [Outfit](https://github.com/Outfitio/Outfit-Fonts) (SIL Open Font License 1.1, see
|
||||
`serve/web/fonts/OFL.txt`). Its Monitor tab started from @code-martin's dashboard idea (PR #22).
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 375 KiB |
@@ -207,6 +207,10 @@ struct QsaState {
|
||||
int8_t* v_q = nullptr;
|
||||
uint16_t* k_scale = nullptr;
|
||||
uint16_t* v_scale = nullptr;
|
||||
/// PR #21: Q4_0 KV with Walsh-Hadamard rotation (qsa_set_kv_q4, kv_q4.hpp): 144 B per cell and head
|
||||
bool kv_q4 = false;
|
||||
uint8_t* k_q4 = nullptr;
|
||||
uint8_t* v_q4 = nullptr;
|
||||
int32_t* page_table = nullptr; ///< (n_pages,) logical page -> physical page (-1: not resident, streamed)
|
||||
int64_t n_pages = 0;
|
||||
int64_t max_cells = 0;
|
||||
@@ -269,6 +273,13 @@ uint64_t qsa_kv_host_bytes();
|
||||
/// initializing the session; default off until gate G-C accepts it.
|
||||
void qsa_set_kv_int8(bool enabled);
|
||||
bool qsa_kv_int8();
|
||||
/// PR #21: store K/V as Q4_0 after a Hadamard rotation (`--kv q4_0`): 576 B per cell, vs 1,056 in INT8.
|
||||
void qsa_set_kv_q4(bool enabled);
|
||||
bool qsa_kv_q4();
|
||||
/// The state's KV format for the block-moving functions of kv_stream.hpp (kKvF16 / kKvInt8 / kKvQ4).
|
||||
inline int qsa_kv_format(const QsaState& st) {
|
||||
return st.kv_q4 ? strata::kernels::kKvQ4 : st.kv_int8 ? strata::kernels::kKvInt8 : strata::kernels::kKvF16;
|
||||
}
|
||||
uint64_t qsa_state_init(const ModelGeometry& g, int64_t max_cells, void* base, QsaState& st,
|
||||
const QsaState* share_rope = nullptr, int64_t ring_cells = 0);
|
||||
/// KV streaming: the pools a reader sees (the VRAM slots) and, when streamed, make the selection's blocks resident.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// include/strata/core/progress.hpp - issue #29: whether a request is still moving, and where it is.
|
||||
//
|
||||
// The engine's host loop and the GPU wait on each other through flags; a protocol bug there does not crash, it
|
||||
// spins forever (the GPU "100%", one CPU core busy, no tokens). `--serve` runs a watchdog thread over this: a
|
||||
// request whose heartbeat stops for `STRATA_WATCHDOG_S` seconds (default 60; 0 = off) ends the engine with the
|
||||
// stage it was stuck in, and the server starts it again instead of hanging. Tokens, prompt chunks and verify
|
||||
// windows beat; the stage is two relaxed stores per layer, which the token path does not notice.
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace strata::core {
|
||||
|
||||
struct Progress {
|
||||
std::atomic<uint64_t> beats{0};
|
||||
std::atomic<bool> busy{false};
|
||||
std::atomic<const char*> where{"idle"};
|
||||
std::atomic<int64_t> detail{-1};
|
||||
std::atomic<int64_t> since_ms{0}; ///< when `where` was set (steady clock): how long a stage has lasted
|
||||
std::atomic<uint64_t> ticks{0}; ///< layers served: a window that still moves, slowly, against one that stopped
|
||||
};
|
||||
|
||||
inline int64_t progress_now_ms() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
/// What the watchdog prints about a part of the engine when it fires (the expert pool, the verify window).
|
||||
using DiagFn = void (*)(std::FILE*);
|
||||
inline std::atomic<DiagFn>& diag_pool_fn() { static std::atomic<DiagFn> f{nullptr}; return f; }
|
||||
inline std::atomic<DiagFn>& diag_verify_fn() { static std::atomic<DiagFn> f{nullptr}; return f; }
|
||||
|
||||
inline Progress& progress() {
|
||||
static Progress p;
|
||||
return p;
|
||||
}
|
||||
|
||||
inline void progress_at(const char* where, int64_t detail = -1) {
|
||||
Progress& p = progress();
|
||||
p.where.store(where, std::memory_order_relaxed);
|
||||
p.detail.store(detail, std::memory_order_relaxed);
|
||||
p.since_ms.store(progress_now_ms(), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
inline void progress_beat() { progress().beats.fetch_add(1, std::memory_order_relaxed); }
|
||||
inline void progress_tick() { progress().ticks.fetch_add(1, std::memory_order_relaxed); }
|
||||
|
||||
} // namespace strata::core
|
||||
@@ -21,6 +21,8 @@
|
||||
// selection, native indexer) and a profile-filled VRAM expert tier with its residency table on the device.
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#include "strata/core/expert_source.hpp"
|
||||
#include "strata/core/layer.hpp"
|
||||
#include "strata/core/session.hpp"
|
||||
@@ -52,6 +54,9 @@ public:
|
||||
Verifier(const Verifier&) = delete;
|
||||
Verifier& operator=(const Verifier&) = delete;
|
||||
|
||||
/// The watchdog's view of the window in flight (issue #31): the layer, the GPU's sequence, the flags.
|
||||
void diag(std::FILE* f) const;
|
||||
|
||||
/// `max_t` <= kVerifyMaxT. `head` may be null (the canonical head is then run per token).
|
||||
bool init(const WeightTable& wt, const ModelGeometry& g, SessionState& ss, const VerifyHits& hits,
|
||||
const NativeHead* head, int max_t, std::string& err);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// include/strata/kernels/cpu/iq_avx2.hpp - AVX-2 multi-token dot products for the i-quant expert
|
||||
// formats (IQ2_XXS, IQ2_XS, IQ2_S, IQ3_XXS, IQ3_S) against Q8_K activations (ggml's block_q8_K).
|
||||
//
|
||||
// The AVX-512 kernel's scheme on the CPUs without it (AMD Zen 2/3, Intel Core 12th-14th gen): the weights of a
|
||||
// 32-value chunk are decoded once per verify window and every token applies them with five instructions.
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace strata::kernels::cpu {
|
||||
|
||||
bool iq256_supported(int ggml_type) noexcept;
|
||||
/// ff[t][r] = silu(gate_r . a[t]) * (up_r . a[t]), rows [r0, r1); gate rows at blob, up rows at blob + up_off.
|
||||
void iq256_gu_rows(int ggml_type, const uint8_t* blob, size_t gu_row, size_t up_off, int n, const void* const* act,
|
||||
int nt, float* const* ff, int r0, int r1);
|
||||
/// out[t][r] = w_r . a[t], rows [r0, r1).
|
||||
void iq256_rows(int ggml_type, const uint8_t* w, size_t row_bytes, int n, const void* const* act, int nt,
|
||||
float* const* out, int r0, int r1);
|
||||
|
||||
/// out[t][r] = w_r . h[t] for IQ4_NL (type 20) rows against Q8_0 activations (ggml's block_q8_0).
|
||||
/// IQ4_NL is a 32-value-block format, so this does not go through iq256_rows (QK_K blocks, Q8_K acts).
|
||||
void iq4nl256_down_rows(const uint8_t* w, size_t row_bytes, int n, const void* const* hq, int nt,
|
||||
float* const* out, int r0, int r1);
|
||||
|
||||
} // namespace strata::kernels::cpu
|
||||
@@ -14,15 +14,25 @@
|
||||
// THE COMPLETION PROTOCOL, because this is where a pool usually goes wrong. `run()` waits for `done == n`
|
||||
// AND for every worker to PARK. Waiting only for `done` is not enough: a worker can still be inside the
|
||||
// drain loop after its last `done` increment, and the host resetting `head` underneath it would let that
|
||||
// worker claim a job from the NEXT batch before the next batch has been published. The
|
||||
// `done`-then-`parked` pair makes the handover unambiguous, and the second wait costs a few hundred cycles
|
||||
// against a layer that takes milliseconds.
|
||||
// worker claim a job from the NEXT batch before the next batch has been published.
|
||||
//
|
||||
// **AND `parked` IS NOT ENOUGH EITHER (issue #29).** A worker that went to sleep (after `kSpinBeforeSleep`) is
|
||||
// still counted as parked when it wakes, so for a moment after it has seen a new epoch the host believes it is
|
||||
// idle. On a card with most experts in VRAM the workers sleep in the middle of a request, tiny batches finish
|
||||
// before a sleeper is awake, and that moment comes round constantly: a late worker could claim from the NEXT
|
||||
// batch while the host was still writing it, run a job twice, or add to `done` after the host had reset it -
|
||||
// and `done != n` then never ended, with the GPU waiting on the pool forever. So every claim carries its
|
||||
// batch: `head` is one 64-bit word `epoch | njobs | index`, a claim is a CAS that only succeeds for the epoch
|
||||
// the worker woke for, and a late worker's claim simply fails. A claim that succeeds belongs to the current
|
||||
// batch, whose description the host cannot change until that job's `done` has landed.
|
||||
#pragma once
|
||||
|
||||
#include "strata/kernels/cpu/expert.hpp"
|
||||
#include "strata/kernels/cpu/native_expert.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
@@ -92,6 +102,8 @@ public:
|
||||
/// threads on six cores. `false` is the A/B arm and exists so the change is measurable rather than
|
||||
/// asserted - the counter it moves is `pool phases ... drain`, which is host-side and needs no profiler.
|
||||
explicit ExpertPool(int n_workers = 0, bool pin = true, bool host_works = true);
|
||||
/// The watchdog's view of the pool (issue #31): the batch, the counters, every thread's state.
|
||||
void diag(std::FILE* f) const;
|
||||
~ExpertPool();
|
||||
ExpertPool(const ExpertPool&) = delete;
|
||||
ExpertPool& operator=(const ExpertPool&) = delete;
|
||||
@@ -142,14 +154,26 @@ public:
|
||||
/// **A PARKED WORKER SPINS FOR THIS LONG, THEN SLEEPS.** The park is a `_mm_pause` spin because a layer's
|
||||
/// batches are microseconds apart and a wake-up from the OS costs more than that. But a spin that never ends
|
||||
/// keeps every worker's core at 100% while the engine waits for a request - issue #4, "CPU 50% even when
|
||||
/// doing nothing" (7 of the 5700X's 16 threads). Inside a request the gaps between batches are far below
|
||||
/// this, so the token path still never sleeps; between requests the workers block on `sleep_cv_`.
|
||||
/// doing nothing" (7 of the 5700X's 16 threads). Between requests the workers block on `sleep_cv_`. NOT only
|
||||
/// between requests: with most experts in VRAM (a 24 GB card) many layers have no CPU work, so the workers
|
||||
/// also sleep mid-request - which the claim protocol above must survive (issue #29).
|
||||
/// `STRATA_POOL_SPIN_US` overrides it (a test knob: a short spin makes the workers sleep constantly).
|
||||
static constexpr std::chrono::milliseconds kSpinBeforeSleep{20};
|
||||
/// A pool wait that sees no completion for this long is a bug; the engine stops with a message instead of
|
||||
/// spinning forever, and the server starts it again (issue #29).
|
||||
static constexpr std::chrono::seconds kStall{60};
|
||||
|
||||
private:
|
||||
void worker(int id);
|
||||
void drain(int id, ExpertScratch& scratch);
|
||||
void drain(int id, ExpertScratch& scratch, uint32_t epoch);
|
||||
void run_phase(int mode, int n_tasks);
|
||||
/// Claim the next job of batch `epoch`, or -1 (that batch is exhausted, or it is not the current one).
|
||||
int claim(uint32_t epoch);
|
||||
/// Publish the batch whose description the caller has just written: reset `done`, then `head`, then the epoch.
|
||||
uint32_t begin_batch(int n);
|
||||
/// The host's waits, bounded by `kStall`.
|
||||
void wait_parked(const char* what);
|
||||
void wait_done(int n);
|
||||
/// Bump `epoch_`, and wake the workers that went to sleep. Every publish goes through here.
|
||||
void publish();
|
||||
|
||||
@@ -176,7 +200,14 @@ private:
|
||||
//
|
||||
// The counter was diagnostic only - `pauses()` was read in one place, to print a number nothing branched on
|
||||
// - so it is deleted rather than amortised. `alignas(64)` then stops the remaining four sharing.
|
||||
alignas(64) std::atomic<uint32_t> head_{0};
|
||||
// issue #31 diagnostics: each worker's state (kParked, kSleeping, kBetween, or the job it runs) and the host's
|
||||
// (kIdle, kWaitParked, kWaitDone, or its job), printed by the serve watchdog through `diag`
|
||||
static constexpr int32_t kParked = -1, kSleeping = -2, kBetween = -3, kIdle = -10, kWaitParked = -11,
|
||||
kWaitDone = -12;
|
||||
std::unique_ptr<std::atomic<int32_t>[]> wstate_;
|
||||
std::atomic<int32_t> hstate_{kIdle};
|
||||
std::atomic<int64_t> hstate_ms_{0};
|
||||
alignas(64) std::atomic<uint64_t> head_{0}; // epoch << 32 | njobs << 16 | next index (issue #29)
|
||||
alignas(64) std::atomic<uint32_t> done_{0};
|
||||
alignas(64) std::atomic<uint32_t> parked_{0};
|
||||
alignas(64) std::atomic<uint32_t> epoch_{0};
|
||||
@@ -186,6 +217,7 @@ private:
|
||||
alignas(64) std::atomic<uint32_t> sleepers_{0};
|
||||
std::mutex sleep_mu_;
|
||||
std::condition_variable sleep_cv_;
|
||||
std::chrono::microseconds spin_before_sleep_{kSpinBeforeSleep};
|
||||
std::vector<std::thread> threads_;
|
||||
std::vector<ExpertScratch> scratch_; // one per worker: no allocation, no false sharing of the hot data
|
||||
// run_split state: mode 0 = whole experts, 1 = gate/up row parts, 2 = down row parts
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// include/strata/kernels/cvec.hpp - a control vector on the residual stream: the engine's side of llama.cpp's
|
||||
// `llama_adapter_cvec` with the `--cvec-mode` / `--cvec-dir` extension of the `experimental-speed-projection`
|
||||
// package (its `02-cvec-projection-mode.patch`, and `01-qwen4exp-cvec-hooks.patch` for WHERE it applies).
|
||||
//
|
||||
// After layer l's FFN write - llama.cpp's `build_cvec(res_hc, il)` right after the layer's second
|
||||
// `build_hc_combine` - every one of the hc residual streams h of every token becomes
|
||||
//
|
||||
// project: h <- h - s_l (h . v_l) v_l v_l = d / |d|, s_l = |d|, d = the scaled direction
|
||||
// add: h <- h + d llama.cpp's stock additive control vector
|
||||
//
|
||||
// for l in [first, last]. `d` is the GGUF's `direction.<src>` times the file's scale (summed over files), with
|
||||
// src = l (per-layer) or one fixed layer (single:L, project mode only), exactly as llama.cpp's loader places it;
|
||||
// layer 0 has no direction. Off unless a vector is loaded (--control-vector-scaled); a loaded one is switched
|
||||
// per request through a device flag, so the captured graphs are the same either way. With the flag off the
|
||||
// result is bit-identical to the engine without a vector: the kernel then only applies the pending residual
|
||||
// write, with the fused read's own arithmetic.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace strata::kernels {
|
||||
|
||||
struct Cvec {
|
||||
const float* dir = nullptr; ///< device, n_layers * n_embd: project = the unit v_l, add = d_l; zero rows elsewhere
|
||||
const float* s = nullptr; ///< device, n_layers: project = s_l, add = 1; 0 = the layer is not steered
|
||||
const int* on = nullptr; ///< device int: 0 = this request runs the stock model
|
||||
int mode = 0; ///< 0 = project, 1 = add
|
||||
int first = 0, last = -1; ///< the steered layers, inclusive
|
||||
int64_t n_embd = 0, hc = 0;
|
||||
bool loaded() const { return dir != nullptr; }
|
||||
/// Layer l's FFN write is followed by the vector (with a real direction there). Decided at load time, never
|
||||
/// per request: it changes where the residual write happens, so the graphs depend on it.
|
||||
bool covers(int64_t l) const { return loaded() && l >= first && l <= last && l < (int64_t) steered.size() && steered[(size_t) l]; }
|
||||
std::vector<bool> steered; ///< host copy of s_l != 0
|
||||
};
|
||||
|
||||
/// The loaded vector (empty until `cvec_upload`).
|
||||
const Cvec& cvec();
|
||||
|
||||
/// Upload a vector built by the loader: `dir` is n_layers * n_embd, `s` n_layers (see `Cvec`). Starts ON.
|
||||
bool cvec_upload(const std::vector<float>& dir, const std::vector<float>& s, int mode, int first, int last,
|
||||
int64_t n_embd, int64_t hc, std::string& err);
|
||||
|
||||
/// The per-request switch. Synchronizes the device when it changes, so call it between requests.
|
||||
void cvec_set_enabled(bool on);
|
||||
bool cvec_enabled();
|
||||
|
||||
/// Layer `layer`'s vector on T tokens' residual stacks (`R + t * r_ld`, hc streams of n_embd). With `write`, the
|
||||
/// pending FFN write `R += bo * 2 sigmoid(inj / hc)` (the fused read's arithmetic) is applied first, for callers
|
||||
/// whose writes are folded into the next layer's read; without it, R must already hold the layer's output.
|
||||
void cvec_apply(float* R, int64_t layer, int64_t T, int64_t r_ld, const float* bo, int64_t bo_ld, const float* inj,
|
||||
int64_t inj_ld, bool write, void* stream);
|
||||
|
||||
} // namespace strata::kernels
|
||||
@@ -0,0 +1,61 @@
|
||||
// include/strata/kernels/kv_q4.hpp - Q4_0 KV storage with Walsh-Hadamard rotation for the QSA layers (`--kv q4_0`).
|
||||
//
|
||||
// From PR #21 (code-martin). K and V are rotated by the orthonormal 256-point Hadamard matrix H before they are
|
||||
// quantized to ggml's q4_0 (32 values per block: an fp16 scale + 16 bytes of 4-bit codes, 144 B per head per cell,
|
||||
// 576 B per cell for K and V of both heads, vs 1,056 B in INT8). The rotation spreads a head's outlier channels
|
||||
// over all 256 dimensions, which is what makes 4 bits usable. The query is rotated the same way, so
|
||||
// <Hq, Hk> = <q, k> and the scores are unchanged, and the attention output (a mix of rotated values) is rotated
|
||||
// back with H (self-inverse). What is lossy is the 4-bit rounding itself: measured, not assumed, in
|
||||
// bench/results/2026-09-27-kv-q4.
|
||||
//
|
||||
// With KV streaming (--kv-resident) the codes live in the host copy and the resident slots like INT8's (KvHostPools
|
||||
// k_q4/v_q4, the same block granule), so both options combine.
|
||||
#pragma once
|
||||
|
||||
#include "strata/kernels/qsa.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace strata::kernels {
|
||||
|
||||
inline constexpr int QK4_0 = 32;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct block_q4_0 {
|
||||
uint16_t d; // scale (fp16 bits)
|
||||
uint8_t qs[QK4_0 / 2]; // 32 4-bit codes: element j in the low nibble of qs[j], j + 16 in the high one
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
static_assert(sizeof(block_q4_0) == 18, "block_q4_0 must be 18 bytes");
|
||||
|
||||
/// Bytes per cell and KV head: 8 blocks of 32 for head_dim 256 -> 144.
|
||||
inline uint64_t kv_q4_bytes_per_head(int head_dim) { return (uint64_t) (head_dim / QK4_0) * sizeof(block_q4_0); }
|
||||
|
||||
/// Bytes per cell (one token, one layer): K and V of every KV head.
|
||||
inline uint64_t kv_q4_bytes_per_cell(const QsaShapes& s) {
|
||||
return (uint64_t) s.n_head_kv * kv_q4_bytes_per_head((int) s.head_dim) * 2;
|
||||
}
|
||||
|
||||
/// Orthonormal Fast Walsh-Hadamard Transform of rows of 256 floats (scale 1/16): its own inverse.
|
||||
void fwht256_cuda(const float* src, float* dst, int64_t n_rows, void* stream);
|
||||
inline void fwht256_inplace_cuda(float* data, int64_t n_rows, void* stream) { fwht256_cuda(data, data, n_rows, stream); }
|
||||
|
||||
/// Append the (already rotated) cell at step[kStepPos]. With a host copy (KV streaming) it is written there too,
|
||||
/// and to VRAM only if its block is resident.
|
||||
void kv_append_q4_step(uint8_t* k_q4, uint8_t* v_q4, const int32_t* page_table, const int32_t* step,
|
||||
const float* kcur, const float* vcur, const QsaShapes& s, void* stream,
|
||||
const KvHostPools* host = nullptr);
|
||||
|
||||
/// The prompt path: T consecutive (rotated) cells from pos0, K/V [T, n_head_kv, 256]; also into `stage` (identity
|
||||
/// layout, the one-layer staging pool of a streamed session) when given.
|
||||
void kv_append_q4(uint8_t* k_q4, uint8_t* v_q4, const int32_t* page_table, int64_t pos0, int64_t T, const float* K,
|
||||
const float* V, const QsaShapes& s, void* stream, const KvHostPools* host = nullptr,
|
||||
const KvHostPools* stage = nullptr);
|
||||
|
||||
/// Gather step[kStepWidth] cells named by `ids` into FP16 scratch [id][kv_head][head_dim] (still rotated).
|
||||
void kv_gather_q4_step(const uint8_t* k_q4, const uint8_t* v_q4, const int32_t* page_table, const int32_t* ids,
|
||||
const int32_t* step, int64_t max_ids, const QsaShapes& s, uint16_t* k_scratch,
|
||||
uint16_t* v_scratch, void* stream);
|
||||
|
||||
} // namespace strata::kernels
|
||||
@@ -36,9 +36,15 @@ struct KvHostPools {
|
||||
int8_t* v_q = nullptr;
|
||||
uint16_t* k_scale = nullptr; ///< int8 mode: fp16 scale per 64 values
|
||||
uint16_t* v_scale = nullptr;
|
||||
bool present() const { return k_pool != nullptr || k_q != nullptr; }
|
||||
uint8_t* k_q4 = nullptr; ///< q4_0 mode (kv_q4.hpp): block_q4_0 codes, 144 B per cell and head
|
||||
uint8_t* v_q4 = nullptr;
|
||||
bool present() const { return k_pool != nullptr || k_q != nullptr || k_q4 != nullptr; }
|
||||
};
|
||||
|
||||
/// The KV storage format, for the functions below that move whole blocks (`fmt`): fp16, int8 (+ scales), q4_0.
|
||||
/// (A bool `int8` argument still reads as kKvF16 / kKvInt8.)
|
||||
enum KvFormat : int { kKvF16 = 0, kKvInt8 = 1, kKvQ4 = 2 };
|
||||
|
||||
/// The residency map of a streamed layer, all device memory at fixed addresses (the graphs bake them in).
|
||||
struct KvStreamMap {
|
||||
int32_t* page_table = nullptr; ///< (n_blocks,) block -> slot, or -1
|
||||
@@ -62,7 +68,7 @@ void kv_stream_reset(const KvStreamMap& m, void* stream);
|
||||
/// Make every block named by the selections of `n_q` queries resident (ids [n_q][cap], width from
|
||||
/// steps[q * kStepCount + kStepWidth]). Capturable. `n_slots` must hold the distinct blocks of one call
|
||||
/// (n_q x (cap / page_size + 2)); a call that cannot sets ctl[3] (see `kv_stream_counters`).
|
||||
void kv_stream_resolve(const KvStreamMap& m, const QsaAttnPools& slots, const KvHostPools& host, bool int8,
|
||||
void kv_stream_resolve(const KvStreamMap& m, const QsaAttnPools& slots, const KvHostPools& host, int fmt,
|
||||
const int32_t* ids, const int32_t* steps, int64_t n_q, int64_t cap, const QsaShapes& s,
|
||||
void* stream);
|
||||
|
||||
@@ -71,12 +77,12 @@ void kv_ring_table(int32_t* page_table, int64_t n_blocks, int64_t n_slots, void*
|
||||
|
||||
/// Copy blocks [b0, b1) of the host copy into the pool pages `page_table` names (host-side table `phys(b)`
|
||||
/// computed as `b % n_slots`: the ring restore). Not capturable.
|
||||
void kv_ring_restore(const QsaAttnPools& slots, const KvHostPools& host, bool int8, int64_t b0, int64_t b1,
|
||||
void kv_ring_restore(const QsaAttnPools& slots, const KvHostPools& host, int fmt, int64_t b0, int64_t b1,
|
||||
int64_t n_slots, const QsaShapes& s, void* stream);
|
||||
|
||||
/// Copy the first `n_blocks` blocks of the host copy into a fully resident (identity-layout) pool: the prompt
|
||||
/// path's staging of one layer. Not capturable (DMA).
|
||||
void kv_stage_from_host(const QsaAttnPools& stage, const KvHostPools& host, bool int8, int64_t n_blocks,
|
||||
void kv_stage_from_host(const QsaAttnPools& stage, const KvHostPools& host, int fmt, int64_t n_blocks,
|
||||
const QsaShapes& s, void* stream);
|
||||
|
||||
struct KvStreamCounters {
|
||||
@@ -87,6 +93,6 @@ struct KvStreamCounters {
|
||||
KvStreamCounters kv_stream_counters(const KvStreamMap& m);
|
||||
|
||||
/// Bytes of one block (page) of K and V together.
|
||||
uint64_t kv_block_bytes(const QsaShapes& s, bool int8);
|
||||
uint64_t kv_block_bytes(const QsaShapes& s, int fmt);
|
||||
|
||||
} // namespace strata::kernels
|
||||
@@ -27,4 +27,12 @@ void native_ple_postops(const float* projected_key, const float* hidden,
|
||||
const PleWeights& weights, const NativePlePostopsBuffers& buffers,
|
||||
void* stream);
|
||||
|
||||
/// The prompt path: T tokens at once, element for element the arithmetic of T calls of native_ple_postops with
|
||||
/// the history advanced after each. `key` (T x 10240) holds the projected keys and is normalized in place;
|
||||
/// `hidden` (T x 10240) becomes the result; `value` T x 2560; `query_norm` (T x 10240, the normalized hidden, then
|
||||
/// the normalized gated values), `gated` (T x 10240) and `gate` (T x 4) are scratch; `history` (9 rows per
|
||||
/// channel) is advanced past the T tokens.
|
||||
void native_ple_postops_batch(float* key, float* hidden, const float* value, float* history, const PleWeights& w,
|
||||
float* query_norm, float* gated, float* gate, int T, void* stream);
|
||||
|
||||
} // namespace strata::kernels
|
||||
@@ -27,6 +27,8 @@ struct QsaAttnPools {
|
||||
const int8_t* v_q = nullptr;
|
||||
const uint16_t* k_scale = nullptr; ///< fp16 [page][kv_head][page_size][head_dim / 64]
|
||||
const uint16_t* v_scale = nullptr;
|
||||
const uint8_t* k_q4 = nullptr; ///< q4_0 block_q4_0 [page][kv_head][page_size][head_dim / 32 * 18]
|
||||
const uint8_t* v_q4 = nullptr;
|
||||
const int32_t* page_table = nullptr;
|
||||
};
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ public:
|
||||
void native(const uint16_t* X, int ggml_type, const void* W_blocks, float* Y, int64_t T, int64_t N, int64_t K,
|
||||
int64_t ldy = 0, float beta = 0.0f);
|
||||
|
||||
/// Caller-owned buffers only: the scratch and workspace moved (the prompt path laid its buffers out again).
|
||||
void rebind(uint16_t* scratch, int64_t scratch_elems, void* workspace, size_t ws_bytes);
|
||||
|
||||
uint16_t* scratch() const { return scratch_; }
|
||||
int64_t scratch_elems() const { return scratch_elems_; }
|
||||
void* stream() const { return stream_; }
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// include/strata/prefill/moe_mmq.hpp - prompt-speed plan step 2b: the prompt path's experts through llama.cpp's MMQ
|
||||
// kernels (ggml-cuda mmq.cuh, MIT): the weights stay quantized and the activations are rounded to q8_1, the
|
||||
// products run on int8 tensor cores. The dequantize-to-FP16 + cuBLAS path wrote ~10 MB of FP16 per expert and
|
||||
// multiplied in FP16; this reads the ~1.4-2 MB expert once. A group of experts is gathered into one buffer
|
||||
// (`gather_*`, one launch per expert as its blob arrives) and multiplied in one launch per product.
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace strata::prefill::mmq {
|
||||
|
||||
/// This build has the MMQ path (the ggml sources were available to the build).
|
||||
bool built();
|
||||
/// MMQ covers this ggml type (the i-quants and Q2_0 the packs use; IQ1_M is not covered).
|
||||
bool supported(int ggml_type);
|
||||
/// 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).
|
||||
size_t q8_bytes(int64_t rows, int64_t cols);
|
||||
|
||||
/// q8_1 activations for MMQ against weights of `ggml_type`: row i of the output is row ids[i] of x (or row i when
|
||||
/// ids is null); `x` has `ld` floats per row.
|
||||
void quantize(const float* x, const int32_t* ids, void* xq, int ggml_type, int64_t cols, int64_t ld, int64_t rows,
|
||||
void* stream);
|
||||
|
||||
/// One launch over n experts whose weights lie `expert_bytes` apart from `w`: for expert e, the activation rows
|
||||
/// [bounds[e], bounds[e+1]) of `xq` (bounds on the device, n+1 entries) times its [w_rows, w_cols] matrix into
|
||||
/// dst rows of the same indices (`ld_dst` floats apart, via `ids`: dst row = ids[row], an identity table works).
|
||||
/// `total_rows`: the rows of xq; `max_rows`: the most rows one expert has (the launch grid).
|
||||
struct Product {
|
||||
const void* w = nullptr;
|
||||
int type = -1;
|
||||
int64_t w_rows = 0, w_cols = 0;
|
||||
size_t expert_bytes = 0;
|
||||
int n = 0;
|
||||
const void* xq = nullptr;
|
||||
const int32_t* bounds = nullptr;
|
||||
const int32_t* ids = nullptr;
|
||||
int64_t total_rows = 0, max_rows = 0;
|
||||
float* dst = nullptr;
|
||||
int64_t ld_dst = 0;
|
||||
};
|
||||
|
||||
/// The launch context (llama.cpp's MMQ keeps a small scratch pool for its stream-k fixup). One per prompt path.
|
||||
class Context {
|
||||
public:
|
||||
Context();
|
||||
~Context();
|
||||
Context(const Context&) = delete;
|
||||
Context& operator=(const Context&) = delete;
|
||||
void run(const Product& p, void* stream);
|
||||
|
||||
private:
|
||||
void* ctx_ = nullptr;
|
||||
};
|
||||
|
||||
/// A GGUF-native expert (gate at `gate`, up at `up`, down at `down`, each its GGUF rows) into a group buffer's
|
||||
/// slot: gate rows then up rows at `gu_dst`, down at `d_dst`.
|
||||
void gather_native(const void* gate, const void* up, size_t gu_half_bytes, const void* down, size_t d_bytes,
|
||||
void* gu_dst, void* d_dst, void* stream);
|
||||
/// A Strata-pack Q2_0 expert blob (codes and fp16 scales in separate planes, gate/up rows interleaved) into GGUF
|
||||
/// Q2_0 blocks: gate/up [1280, 2560] at `gu_dst` (rows stay interleaved), down [2560, 640] at `d_dst`. Same values.
|
||||
void gather_strata_q2(const uint8_t* blob, void* gu_dst, void* d_dst, void* stream);
|
||||
|
||||
/// h[r, k] = silu(gate) * up of GU rows [2 n_ff wide]: interleaved (gate 2k, up 2k+1: the Strata pack) or split
|
||||
/// (gate k, up n_ff + k: GGUF). FP32 out (the down product's quantizer reads floats).
|
||||
void swiglu(const float* gu, float* h, int64_t rows, int64_t n_ff, bool interleaved, void* stream);
|
||||
|
||||
/// dst[i] = i for i < n (the identity row map MMQ's MoE mode writes through).
|
||||
void iota(int32_t* dst, int64_t n, void* stream);
|
||||
|
||||
} // namespace strata::prefill::mmq
|
||||
@@ -48,6 +48,16 @@ public:
|
||||
core::ExpertSource* src, const core::ExpertCache* cache, const int32_t* host_res, int64_t chunk,
|
||||
void* stream, std::string& err, void* borrow = nullptr, uint64_t borrow_bytes = 0);
|
||||
|
||||
/// With borrowed buffers: lay them out again for chunks of `chunk` tokens (at most `init`'s) in `borrow` - a
|
||||
/// request lends only the slots its prompt needs. The stream must be idle (between prompts).
|
||||
bool relayout(int64_t chunk, void* borrow, uint64_t borrow_bytes, std::string& err);
|
||||
int64_t chunk() const;
|
||||
|
||||
/// The share of the streamed experts' bytes DMA-able straight from pinned RAM (1 = all). Sizes the streamed
|
||||
/// ring (a big one only pays when the copy engine, not the host copies, is the limit); set before bytes_needed.
|
||||
static void set_pinned_share(double share);
|
||||
static double pinned_share();
|
||||
|
||||
/// Device bytes `init` needs for a chunk of `chunk` tokens (what a borrowed region must hold).
|
||||
static uint64_t bytes_needed(const core::ModelGeometry& g, const core::SessionState& ss, int64_t chunk);
|
||||
|
||||
@@ -70,6 +80,7 @@ public:
|
||||
const float* const* embd_rows = nullptr;
|
||||
|
||||
private:
|
||||
bool carve(std::size_t T, void* alloc); // the device buffers of a chunk (prefill.cpp's Alloc)
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
PrefillStats stats_;
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Strata</title>
|
||||
<style>
|
||||
:root { --bg:#f6f7f9; --card:#fff; --fg:#1b1f24; --mute:#667085; --line:#e3e6ea; --acc:#2f6fed; --me:#e9f0ff; --ok:#1a7f37; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --bg:#111418; --card:#1a1e24; --fg:#e7e9ec; --mute:#9aa3ae; --line:#2b313a; --acc:#6b9bff; --me:#1f2b44; --ok:#3fb950; }
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin:0; background:var(--bg); color:var(--fg); font:15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
||||
main { max-width:860px; margin:0 auto; padding:16px; display:flex; flex-direction:column; gap:12px; min-height:100vh; }
|
||||
.card { background:var(--card); border:1px solid var(--line); border-radius:12px; padding:14px 16px; }
|
||||
h1 { font-size:20px; margin:0; display:flex; align-items:center; gap:8px; }
|
||||
.dot { width:10px; height:10px; border-radius:50%; background:var(--mute); display:inline-block; }
|
||||
.dot.on { background:var(--ok); }
|
||||
.mute { color:var(--mute); font-size:13px; }
|
||||
code { background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:1px 6px; font-size:13px; }
|
||||
.grid { display:grid; grid-template-columns: max-content 1fr; gap:4px 12px; margin-top:8px; font-size:14px; }
|
||||
#log { flex:1; display:flex; flex-direction:column; gap:10px; overflow-y:auto; min-height:240px; }
|
||||
.msg { padding:10px 12px; border-radius:10px; white-space:pre-wrap; word-wrap:break-word; }
|
||||
.msg.user { background:var(--me); align-self:flex-end; max-width:85%; }
|
||||
.msg.bot { background:var(--card); border:1px solid var(--line); }
|
||||
.msg img { max-width:220px; max-height:160px; display:block; border-radius:6px; margin-bottom:6px; }
|
||||
details { color:var(--mute); font-size:13px; margin-bottom:6px; }
|
||||
form { display:flex; gap:8px; align-items:flex-end; flex-wrap:wrap; }
|
||||
textarea { flex:1; min-width:200px; min-height:52px; max-height:200px; resize:vertical; padding:10px; border-radius:10px;
|
||||
border:1px solid var(--line); background:var(--card); color:var(--fg); font:inherit; }
|
||||
button, label.btn { border:1px solid var(--line); background:var(--card); color:var(--fg); border-radius:10px; padding:10px 14px;
|
||||
font:inherit; cursor:pointer; }
|
||||
button.primary { background:var(--acc); border-color:var(--acc); color:#fff; }
|
||||
button:disabled { opacity:.5; cursor:default; }
|
||||
.row { display:flex; gap:14px; align-items:center; flex-wrap:wrap; font-size:13px; color:var(--mute); }
|
||||
#att { font-size:13px; color:var(--mute); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1><span id="dot" class="dot"></span> Strata <span id="state" class="mute">connecting ...</span></h1>
|
||||
<div class="grid">
|
||||
<span class="mute">Model</span><span id="model">-</span>
|
||||
<span class="mute">Context</span><span id="ctx">-</span>
|
||||
<span class="mute">Images</span><span id="img">-</span>
|
||||
<span class="mute">OpenAI API</span><span><code id="oai"></code> (any API key<span id="keynote"></span>)</span>
|
||||
<span class="mute">Anthropic API</span><span><code id="ant"></code></span>
|
||||
</div>
|
||||
<p class="mute" style="margin:10px 0 0">Point any app with an "OpenAI-compatible" or "Anthropic" provider at these
|
||||
addresses. Close the Strata window to stop the model.</p>
|
||||
</div>
|
||||
|
||||
<div id="log" class="card"></div>
|
||||
|
||||
<div class="card">
|
||||
<form id="f">
|
||||
<textarea id="q" placeholder="Ask something ... (Enter sends, Shift+Enter = new line)"></textarea>
|
||||
<label class="btn" id="attbtn" hidden>Picture<input id="file" type="file" accept="image/png,image/jpeg,image/gif,image/bmp" hidden></label>
|
||||
<button class="primary" id="send">Send</button>
|
||||
<button type="button" id="stop" disabled>Stop</button>
|
||||
</form>
|
||||
<div class="row" style="margin-top:8px">
|
||||
<label>Thinking <select id="think" style="padding:4px;border-radius:8px;border:1px solid var(--line);background:var(--card);color:var(--fg)">
|
||||
<option value="none">off - answer at once</option>
|
||||
<option value="low">low</option>
|
||||
<option value="medium" selected>medium</option>
|
||||
<option value="high">high - best on hard questions, slowest</option>
|
||||
</select></label>
|
||||
<span id="att"></span>
|
||||
<button type="button" id="clear" style="padding:4px 10px">New chat</button>
|
||||
<input id="key" type="password" placeholder="API key" hidden style="padding:6px;border-radius:8px;border:1px solid var(--line)">
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
const $ = id => document.getElementById(id);
|
||||
const base = location.origin;
|
||||
$("oai").textContent = base + "/v1";
|
||||
$("ant").textContent = base + "/v1/messages";
|
||||
let history = [], image = null, ctrl = null;
|
||||
|
||||
async function status() {
|
||||
try {
|
||||
const h = await (await fetch("/health")).json();
|
||||
$("dot").classList.add("on"); $("state").textContent = "running";
|
||||
$("model").textContent = h.model; $("ctx").textContent = h.max_context.toLocaleString() + " tokens";
|
||||
$("img").textContent = h.images ? "on" : "off (run setup again and answer yes to images)";
|
||||
$("attbtn").hidden = !h.images;
|
||||
if (h.api_key) { $("key").hidden = false; $("keynote").textContent = " - this server needs its key"; }
|
||||
} catch (e) { $("state").textContent = "not reachable - is the Strata window still open?"; }
|
||||
}
|
||||
status();
|
||||
|
||||
function bubble(cls, text) {
|
||||
const d = document.createElement("div"); d.className = "msg " + cls; d.textContent = text || "";
|
||||
$("log").appendChild(d); $("log").scrollTop = $("log").scrollHeight; return d;
|
||||
}
|
||||
|
||||
$("file").onchange = () => {
|
||||
const f = $("file").files[0]; if (!f) return;
|
||||
const r = new FileReader();
|
||||
r.onload = () => { image = r.result; $("att").textContent = "picture: " + f.name; };
|
||||
r.readAsDataURL(f);
|
||||
};
|
||||
$("clear").onclick = () => { history = []; $("log").innerHTML = ""; };
|
||||
$("stop").onclick = () => ctrl && ctrl.abort();
|
||||
$("q").addEventListener("keydown", e => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); $("f").requestSubmit(); } });
|
||||
|
||||
$("f").onsubmit = async e => {
|
||||
e.preventDefault();
|
||||
const text = $("q").value.trim(); if (!text) return;
|
||||
$("q").value = "";
|
||||
const content = image ? [{type: "image_url", image_url: {url: image}}, {type: "text", text}] : text;
|
||||
const u = bubble("user", "");
|
||||
if (image) { const im = document.createElement("img"); im.src = image; u.appendChild(im); }
|
||||
u.appendChild(document.createTextNode(text));
|
||||
history.push({role: "user", content});
|
||||
image = null; $("att").textContent = ""; $("file").value = "";
|
||||
const b = bubble("bot", ""); const det = document.createElement("details"); const sum = document.createElement("summary");
|
||||
sum.textContent = "thinking"; det.appendChild(sum); const th = document.createElement("div"); det.appendChild(th);
|
||||
const ans = document.createElement("div"); b.appendChild(ans);
|
||||
$("send").disabled = true; $("stop").disabled = false; ctrl = new AbortController();
|
||||
let answer = "";
|
||||
try {
|
||||
const headers = {"Content-Type": "application/json"};
|
||||
if ($("key").value) headers["Authorization"] = "Bearer " + $("key").value;
|
||||
const body = {model: "strata", messages: history, stream: true, max_tokens: 8192};
|
||||
body.reasoning_effort = $("think").value;
|
||||
const r = await fetch("/v1/chat/completions", {method: "POST", headers, body: JSON.stringify(body), signal: ctrl.signal});
|
||||
if (!r.ok) { ans.textContent = "error: " + ((await r.json()).error || {}).message; throw 0; }
|
||||
const rd = r.body.getReader(), dec = new TextDecoder(); let buf = "";
|
||||
for (;;) {
|
||||
const {value, done} = await rd.read(); if (done) break;
|
||||
buf += dec.decode(value, {stream: true});
|
||||
let i;
|
||||
while ((i = buf.indexOf("\n\n")) >= 0) {
|
||||
const line = buf.slice(0, i).trim(); buf = buf.slice(i + 2);
|
||||
if (!line.startsWith("data: ") || line === "data: [DONE]") continue;
|
||||
const d = JSON.parse(line.slice(6)).choices[0].delta;
|
||||
if (d.reasoning_content) { if (!det.parentNode) b.insertBefore(det, ans); th.textContent += d.reasoning_content; }
|
||||
if (d.content) { answer += d.content; ans.textContent = answer; }
|
||||
$("log").scrollTop = $("log").scrollHeight;
|
||||
}
|
||||
}
|
||||
} catch (err) { if (err && err.name === "AbortError") ans.textContent = answer + " [stopped]"; }
|
||||
history.push({role: "assistant", content: answer});
|
||||
$("send").disabled = false; $("stop").disabled = true; ctrl = null;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+420
-18
@@ -19,6 +19,7 @@ talks to it over stdin/stdout; `MockEngine` is a scripted stand-in that makes ev
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
@@ -71,6 +72,10 @@ class MockEngine:
|
||||
yield t
|
||||
|
||||
|
||||
class EngineDied(RuntimeError):
|
||||
"""The engine process ended in the middle of a request (issue #27: on Linux, the out-of-memory killer)."""
|
||||
|
||||
|
||||
class StrataEngine:
|
||||
"""The resident engine: `strata --serve` reads `GEN <max_new> <ids>` lines and streams `T <id>` lines, then
|
||||
`DONE ...`. Requests are serialized by the service's FIFO, so one pipe is enough.
|
||||
@@ -82,13 +87,25 @@ class StrataEngine:
|
||||
|
||||
def __init__(self, exe: str, args: list[str], cwd: str | None = None, log: str | None = None,
|
||||
env: dict | None = None):
|
||||
self.spawn = (exe, list(args), cwd, log, env) # to start it again after it died (issue #27)
|
||||
self.log_path = log
|
||||
self.log = open(log, "a", encoding="utf-8") if log else subprocess.DEVNULL
|
||||
self.proc = subprocess.Popen([exe, "--serve", *args], cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=self.log, text=True, encoding="utf-8", bufsize=1, env=env)
|
||||
self.max_context = 0
|
||||
self.can_stop = False # the engine honours a STOP line mid-request (READY <ctx> stop)
|
||||
self.last = {}
|
||||
self.info = {} # INFO key=value facts (engine 0.1.8+): kv, expert slots, ... (Monitor tab)
|
||||
self.progress = None # (read, total) prompt tokens while a prompt is read, from PP lines
|
||||
try: # a ready-made engine's BUILD.json says its version
|
||||
self.info["version"] = json.loads((Path(exe).parent / "BUILD.json").read_text()).get("version")
|
||||
except (OSError, ValueError):
|
||||
self.info["version"] = None
|
||||
for line in self.proc.stdout:
|
||||
if line.startswith("INFO "):
|
||||
for kv in line.split()[1:]:
|
||||
k, _, v = kv.partition("=")
|
||||
self.info[k] = int(v) if v.lstrip("-").isdigit() else v
|
||||
if line.startswith("READY"):
|
||||
f = line.split()
|
||||
self.max_context = int(f[1])
|
||||
@@ -103,12 +120,53 @@ class StrataEngine:
|
||||
def _pump(self):
|
||||
for line in self.proc.stdout:
|
||||
self.lines.put(line)
|
||||
self.ended = True # its output closed: it is gone, even before the OS says so
|
||||
self.lines.put(None)
|
||||
|
||||
def death_note(self) -> str:
|
||||
"""Why the engine most likely ended, from the end of its log: its own watchdog (issue #29), else RAM."""
|
||||
tail = ""
|
||||
try:
|
||||
with open(self.log_path, "rb") as f:
|
||||
f.seek(0, 2)
|
||||
f.seek(max(0, f.tell() - 4096))
|
||||
tail = f.read().decode("utf-8", "replace")
|
||||
except (OSError, TypeError):
|
||||
pass
|
||||
for line in reversed(tail.splitlines()):
|
||||
if "issue #29" in line:
|
||||
return ("The engine stopped itself because it had stopped making progress - a hang it caught. Its log "
|
||||
"line: " + line.strip() + " - please report it at github.com/Niko1221/Strata/issues.")
|
||||
return ("The usual cause is running out of RAM: Linux then ends the biggest program (check: sudo dmesg | "
|
||||
"grep -i -E 'killed process|out of memory'); Windows slows down instead. Close other programs or use a "
|
||||
"smaller model (Q2_0 / IQ2_XS).")
|
||||
|
||||
def alive(self) -> bool:
|
||||
return not getattr(self, "ended", False) and self.proc.poll() is None
|
||||
|
||||
def exit_code(self):
|
||||
try:
|
||||
return self.proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
return None
|
||||
|
||||
def restart(self):
|
||||
"""Start the engine again (the same command) after it died; the new process has its own line queue."""
|
||||
try:
|
||||
self.proc.kill()
|
||||
except OSError:
|
||||
pass
|
||||
info = dict(self.info)
|
||||
self.ended = False
|
||||
self.__init__(*self.spawn)
|
||||
self.info = {**info, **self.info}
|
||||
|
||||
def _parse_done(self, line):
|
||||
f = line.split()
|
||||
self.last = {"generated": int(f[1]), "prompt_tokens": int(f[2]), "prompt_ms": float(f[3]),
|
||||
"decode_ms": float(f[4]), "finish": f[5]}
|
||||
if len(f) >= 9: # the conversation cache's fields (engine 0.1.3+)
|
||||
self.last.update(drafts_accepted=int(f[6]), drafts_offered=int(f[7]), reused=int(f[8]))
|
||||
|
||||
@staticmethod
|
||||
def sampling_keys(sampling: dict) -> str:
|
||||
@@ -147,16 +205,27 @@ class StrataEngine:
|
||||
seed = sampling.get("seed")
|
||||
if isinstance(seed, int) and seed > 0:
|
||||
keys += f" seed={seed}"
|
||||
return keys
|
||||
return keys + StrataEngine.projection_key(sampling)
|
||||
|
||||
@staticmethod
|
||||
def projection_key(sampling: dict) -> str:
|
||||
"""`cvec=0|1`: the experimental-speed-projection control vector for this request, when the engine was
|
||||
started with one (--control-vector-scaled; an engine without one ignores the key). Absent = on."""
|
||||
on = sampling.get("experimental_speed_projection")
|
||||
return f" cvec={int(on)}" if isinstance(on, bool) else ""
|
||||
|
||||
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
|
||||
"""Yields token ids, and None as a heartbeat every 10 s while the engine is quiet (reading a long prompt):
|
||||
the HTTP layer turns it into an SSE comment, which keeps clients' watchdogs calm and notices a client that
|
||||
has gone. A consumer that stops early (or `cancel`) makes the engine STOP, so it does not run to max_new."""
|
||||
head = f"GENI {int(max_new)} {embeddings}" if embeddings else \
|
||||
self.progress = None
|
||||
head = f"GENI {int(max_new)}{self.projection_key(sampling or {})} {embeddings}" if embeddings else \
|
||||
f"GEN {int(max_new)}{self.sampling_keys(sampling or {}) if not embeddings else ''}"
|
||||
self.proc.stdin.write(f"{head} {','.join(str(int(t)) for t in ids)}\n")
|
||||
self.proc.stdin.flush()
|
||||
try:
|
||||
self.proc.stdin.write(f"{head} {','.join(str(int(t)) for t in ids)}\n")
|
||||
self.proc.stdin.flush()
|
||||
except OSError: # the pipe is gone: the engine died (not the client)
|
||||
raise EngineDied(f"the engine stopped unexpectedly (exit code {self.exit_code()})") from None
|
||||
done = False
|
||||
try:
|
||||
while True:
|
||||
@@ -169,12 +238,15 @@ class StrataEngine:
|
||||
continue
|
||||
if line is None:
|
||||
done = True
|
||||
raise RuntimeError("the engine process ended")
|
||||
raise EngineDied(f"the engine stopped unexpectedly (exit code {self.exit_code()})")
|
||||
if line.startswith("T "):
|
||||
if cancel.is_set():
|
||||
return
|
||||
yield int(line[2:])
|
||||
elif line.startswith("PP "): # prompt progress, one per chunk: also a heartbeat (the
|
||||
elif line.startswith("PP "):
|
||||
f = line.split()
|
||||
if len(f) >= 3 and f[1].isdigit() and f[2].isdigit():
|
||||
self.progress = (int(f[1]), int(f[2])) # prompt progress, one per chunk: also a heartbeat (the
|
||||
if cancel.is_set(): # lines reset the 10 s wait, so without this a long prompt
|
||||
return # would send no keep-alives at all)
|
||||
yield None
|
||||
@@ -358,17 +430,104 @@ class Detokenizer:
|
||||
|
||||
class Service:
|
||||
def __init__(self, engine: Engine, tokenizer, template: ChatTemplate, model_name: str = "qwen3.8-flash-next",
|
||||
vision: Vision | None = None, sampling_defaults: dict | None = None):
|
||||
vision: Vision | None = None, sampling_defaults: dict | None = None,
|
||||
fit_max_tokens: bool = False):
|
||||
self.engine, self.tok, self.template, self.model, self.vision = engine, tokenizer, template, model_name, vision
|
||||
self.fit_max_tokens = fit_max_tokens # --fit-max-tokens: clamp the output cap instead of 400
|
||||
self.sampling_defaults = dict(sampling_defaults or {}) # the run config's `sampling` block
|
||||
self.shared = {} # the web app's Chat settings for every client (POST /settings)
|
||||
self.shared_path = None # where they are kept between starts (next to the config)
|
||||
self.fifo = threading.Lock()
|
||||
self.embeddings = threading.local() # the current request's image embeddings file (GENI)
|
||||
self.api_key = "" # when set, /v1/* needs it (Bearer or x-api-key)
|
||||
self.status = {"busy": False, "queued": 0} # GET /status: what the model is doing right now
|
||||
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}
|
||||
self.status_lock = threading.Lock()
|
||||
self.stop_ids = set(tokenizer.encode(IM_END, parse_special=True) +
|
||||
tokenizer.encode("<|endoftext|>", parse_special=True))
|
||||
|
||||
def set_shared(self, defaults) -> dict:
|
||||
"""The Chat settings every client gets for what it leaves out; {} / None = clients use their own again."""
|
||||
self.shared = clean_shared_defaults(defaults)
|
||||
if self.shared_path:
|
||||
try:
|
||||
if self.shared:
|
||||
Path(self.shared_path).write_text(json.dumps(self.shared, indent=1), encoding="utf-8")
|
||||
else:
|
||||
Path(self.shared_path).unlink(missing_ok=True)
|
||||
except OSError as e:
|
||||
print(f"[strata] could not save the shared settings: {e}", flush=True)
|
||||
return self.shared
|
||||
|
||||
def with_shared(self, req: dict, api: str) -> dict:
|
||||
"""The request with the shared thinking level and max tokens filled in where it has none of its own."""
|
||||
s = self.shared
|
||||
if not s:
|
||||
return req
|
||||
req = dict(req)
|
||||
if "max_tokens" in s and not req.get("max_tokens") and not req.get("max_completion_tokens"):
|
||||
req["max_tokens"] = s["max_tokens"]
|
||||
effort = s.get("reasoning_effort")
|
||||
if effort:
|
||||
if api == "openai":
|
||||
ctk = req.get("chat_template_kwargs") if isinstance(req.get("chat_template_kwargs"), dict) else {}
|
||||
if not req.get("reasoning_effort") and not req.get("reasoning") and \
|
||||
"enable_thinking" not in ctk and "reasoning_effort" not in ctk:
|
||||
req["reasoning_effort"] = effort
|
||||
elif not req.get("thinking") and not req.get("output_config"):
|
||||
if effort == "none":
|
||||
req["thinking"] = {"type": "disabled"}
|
||||
else:
|
||||
req["output_config"] = {"effort": effort}
|
||||
return req
|
||||
|
||||
def start_telemetry(self):
|
||||
"""The hardware sampler behind GET /metrics (serve/telemetry.py), recording this server's tok/s too."""
|
||||
if getattr(self, "telemetry", None) is None:
|
||||
from serve.telemetry import Telemetry
|
||||
self.telemetry = Telemetry(extra=lambda: {"tok_s": self._tok_s()})
|
||||
|
||||
def _tok_s(self):
|
||||
with self.status_lock:
|
||||
s = dict(self.status)
|
||||
if not s.get("busy") or not s.get("first_token"):
|
||||
return 0.0
|
||||
return s["generated"] / max(1e-6, time.time() - s["first_token"])
|
||||
|
||||
def metrics(self, all_requests=False) -> dict:
|
||||
"""GET /metrics: what the Monitor tab shows - the engine's facts, what it is doing, the last requests, and
|
||||
the hardware (with a minute of history per series)."""
|
||||
with self.status_lock:
|
||||
s = dict(self.status)
|
||||
hist = list(self.history)
|
||||
totals = dict(self.totals)
|
||||
now = time.time()
|
||||
progress = getattr(self.engine, "progress", None)
|
||||
if s.get("busy") and s.get("first_token") is None:
|
||||
state = "reading"
|
||||
elif s.get("busy"):
|
||||
state = "generating"
|
||||
else:
|
||||
state = "idle"
|
||||
live = {"state": state, "queued": s.get("queued", 0), "phase": s.get("phase") if s.get("busy") else None,
|
||||
"prompt_tokens": s.get("prompt_tokens") if s.get("busy") else None,
|
||||
"prompt_read": None, "prompt_total": None, "generated": s.get("generated") if s.get("busy") else None,
|
||||
"max_tokens": s.get("max_tokens") if s.get("busy") else None,
|
||||
"elapsed_s": round(now - s["started"], 1) if s.get("busy") and s.get("started") else None,
|
||||
"tok_s": round(self._tok_s(), 1) if state == "generating" else None}
|
||||
if state == "reading" and progress:
|
||||
live["prompt_read"], live["prompt_total"] = progress
|
||||
engine = {"model": self.model, "max_context": self.engine.max_context, "images": self.vision is not None,
|
||||
**dict(getattr(self.engine, "info", {}) or {})}
|
||||
tel = self.telemetry.snapshot() if getattr(self, "telemetry", None) else {"now": {}, "history": {}, "static": {}}
|
||||
return {"engine": engine, "live": live, "requests": hist[::-1][:None if all_requests else 12],
|
||||
"requests_kept": len(hist), "totals": totals, "hardware": tel["now"],
|
||||
"hardware_static":
|
||||
tel["static"], "history": tel["history"], "time": now}
|
||||
|
||||
def prepare(self, messages, tools, kwargs, max_new=None):
|
||||
"""-> (ids, thinking, max_new). An unset or non-positive max_new (some clients send -1) means "unlimited":
|
||||
the rest of the context."""
|
||||
@@ -403,14 +562,16 @@ class Service:
|
||||
f.write(path.read_bytes())
|
||||
self.embeddings.path = combined
|
||||
room = self.engine.max_context - CTX_SLACK - len(ids)
|
||||
if max_new is None or max_new <= 0:
|
||||
if max_new is None or max_new <= 0 or (self.fit_max_tokens and room < 1):
|
||||
if room < 1:
|
||||
raise ValueError(f"prompt ({len(ids)} tokens) leaves no room to answer in the context "
|
||||
f"({self.engine.max_context}); requests are never truncated")
|
||||
max_new = room
|
||||
elif max_new > room:
|
||||
raise ValueError(f"prompt ({len(ids)} tokens) + max tokens ({max_new}) exceeds the context "
|
||||
f"({self.engine.max_context}); requests are never truncated")
|
||||
if not self.fit_max_tokens:
|
||||
raise ValueError(f"prompt ({len(ids)} tokens) + max tokens ({max_new}) exceeds the context "
|
||||
f"({self.engine.max_context}); requests are never truncated")
|
||||
max_new = max(1, room) # --fit-max-tokens: a shorter completion beats a 400
|
||||
return ids, kwargs.get("enable_thinking", True) is not False, max_new
|
||||
|
||||
def _note(self, n, evs):
|
||||
@@ -439,7 +600,9 @@ class Service:
|
||||
s = dict(self.status)
|
||||
el = now - s.get("started", now)
|
||||
if s.get("first_token") is None:
|
||||
print(f"[strata] reading the prompt: {s.get('prompt_tokens', 0)} tokens, {el:.0f} s so far", flush=True)
|
||||
pr = getattr(self.engine, "progress", None) # (position reached, prompt tokens): a reused prefix counts
|
||||
done = f"{pr[0]:,} of {pr[1]:,}" if pr and pr[1] else f"{s.get('prompt_tokens', 0):,}" # as read (#29)
|
||||
print(f"[strata] reading the prompt: {done} tokens, {el:.0f} s so far", flush=True)
|
||||
else:
|
||||
rate = s["generated"] / max(1e-6, now - s["first_token"])
|
||||
print(f"[strata] {s['phase']}: {s['generated']} of max {s.get('max_tokens')} tokens, {rate:.1f} tok/s, "
|
||||
@@ -448,9 +611,10 @@ class Service:
|
||||
|
||||
def run(self, ids, thinking, tools, max_new, sampling, cancel) -> Iterator[tuple[str, object]]:
|
||||
"""Yields ("event", Event) as text arrives, then ("done", {"finish": .., "completion_tokens": ..})."""
|
||||
if self.sampling_defaults: # config defaults; the request's own fields win (explicit 0 stays greedy)
|
||||
defaults = {**self.sampling_defaults, **self.shared} # the config's, then the Chat settings shared with apps
|
||||
if defaults: # the request's own fields win (explicit 0 stays greedy)
|
||||
req_values = {k: v for k, v in (sampling or {}).items() if v is not None}
|
||||
sampling = {**self.sampling_defaults, **req_values}
|
||||
sampling = {**defaults, **req_values}
|
||||
parser = OutputParser(thinking=thinking, tools=tools, stream_tools=True)
|
||||
detok, n, finish = Detokenizer(self.tok), 0, "length"
|
||||
raw_ids = [] # every generated id (STRATA_DEBUG: dump raw model text)
|
||||
@@ -461,6 +625,14 @@ class Service:
|
||||
with self.fifo:
|
||||
with self.status_lock:
|
||||
self.status["queued"] -= 1
|
||||
if hasattr(self.engine, "alive") and not self.engine.alive():
|
||||
# issue #27: it died in an earlier request - start it again instead of failing every request
|
||||
code = self.engine.exit_code() if hasattr(self.engine, "exit_code") else None
|
||||
print(f"[strata] the engine had stopped (exit code {code}); starting it again "
|
||||
"(a minute or two) ...", flush=True)
|
||||
self.engine.restart()
|
||||
print("[strata] the engine is running again", flush=True)
|
||||
with self.status_lock:
|
||||
self.status.update(busy=True, phase="reading the prompt", prompt_tokens=len(ids), generated=0,
|
||||
started=time.time(), first_token=None, tool=None, tail="", max_tokens=max_new)
|
||||
last_print = time.time()
|
||||
@@ -485,6 +657,17 @@ class Service:
|
||||
yield "event", ev
|
||||
if cancel.is_set():
|
||||
finish = "cancel"
|
||||
except EngineDied as e:
|
||||
finish = "error"
|
||||
note = self.engine.death_note() if hasattr(self.engine, "death_note") else ""
|
||||
print(f"[strata] {e}. {note} The next request starts the engine again."
|
||||
f"{' Its log: ' + self.engine.log_path if getattr(self.engine, 'log_path', None) else ''}",
|
||||
flush=True)
|
||||
raise
|
||||
except ValueError as e: # the engine's ERR line (it may have ended after it)
|
||||
finish = "error"
|
||||
print(f"[strata] the engine reported an error: {e}", flush=True)
|
||||
raise
|
||||
finally:
|
||||
gen.close() # STOP+drain to THIS request's DONE while still holding the
|
||||
# fifo, so a stop-token break can't leave the shared engine
|
||||
@@ -497,6 +680,24 @@ class Service:
|
||||
Path(emb).unlink(missing_ok=True)
|
||||
with self.status_lock:
|
||||
if self.status.get("busy"):
|
||||
last = dict(getattr(self.engine, "last", {}) or {})
|
||||
started = self.status.get("started", time.time())
|
||||
loaded = str((getattr(self.engine, "info", {}) or {}).get("cvec", 0)) not in ("0", "", "None")
|
||||
self.history.append({
|
||||
"projection": (sampling or {}).get("experimental_speed_projection") is not False
|
||||
if loaded else None,
|
||||
"time": started, "duration_s": round(time.time() - started, 1), "finish": finish,
|
||||
"prompt_tokens": len(ids), "reused": last.get("reused"), "output_tokens": n,
|
||||
"prompt_ms": last.get("prompt_ms"), "decode_ms": last.get("decode_ms"),
|
||||
"decode_tok_s": round(last["generated"] / (last["decode_ms"] / 1000), 1)
|
||||
if n and last.get("generated") and last.get("decode_ms") else None})
|
||||
t = self.totals
|
||||
t["requests"] += 1
|
||||
t["prompt_tokens"] += len(ids)
|
||||
t["reused"] += last.get("reused") or 0
|
||||
t["output_tokens"] += n
|
||||
t["prompt_ms"] += last.get("prompt_ms") or 0.0
|
||||
t["decode_ms"] += last.get("decode_ms") or 0.0
|
||||
now = time.time()
|
||||
el = now - self.status.get("started", now)
|
||||
ft = self.status.get("first_token")
|
||||
@@ -713,8 +914,50 @@ def make_handler(svc: Service):
|
||||
|
||||
def do_GET(self):
|
||||
path = self.path.split("?")[0].rstrip("/")
|
||||
if path.startswith("/fonts/"):
|
||||
# the web app's font (Outfit, OFL: serve/web/fonts); the page falls back to the system font
|
||||
name = path[len("/fonts/"):]
|
||||
f = ROOT / "serve" / "web" / "fonts" / name
|
||||
if "/" in name or "\\" in name or not name.endswith(".woff2") or not f.is_file():
|
||||
self._json(404, {"error": {"message": "not found"}})
|
||||
return
|
||||
body = f.read_bytes()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "font/woff2")
|
||||
self.send_header("Cache-Control", "max-age=86400")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
return
|
||||
if path.startswith("/web/"):
|
||||
# the web app's own files (serve/web): styles, script, icon sprite - same origin, no CDN
|
||||
name = path[len("/web/"):]
|
||||
types = {".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8",
|
||||
".svg": "image/svg+xml"}
|
||||
f = ROOT / "serve" / "web" / name
|
||||
ext = os.path.splitext(name)[1]
|
||||
if "/" in name or "\\" in name or ext not in types or not f.is_file():
|
||||
self._json(404, {"error": {"message": "not found"}})
|
||||
return
|
||||
body = f.read_bytes()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", types[ext])
|
||||
self.send_header("Cache-Control", "no-cache")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
return
|
||||
if path == "/metrics":
|
||||
if self._authorized():
|
||||
# the last 12 requests; `?requests=all` every one kept (the Monitor's "Show all", issue #35)
|
||||
self._json(200, svc.metrics(all_requests="requests=all" in self.path))
|
||||
return
|
||||
if path == "/settings":
|
||||
if self._authorized():
|
||||
self._json(200, {"shared": bool(svc.shared), "defaults": svc.shared})
|
||||
return
|
||||
if path == "":
|
||||
body = (ROOT / "serve" / "index.html").read_bytes()
|
||||
body = (ROOT / "serve" / "web" / "index.html").read_bytes()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
@@ -743,6 +986,9 @@ def make_handler(svc: Service):
|
||||
def do_POST(self):
|
||||
if not self._authorized():
|
||||
return
|
||||
if self.path.rstrip("/") == "/settings":
|
||||
self._settings()
|
||||
return
|
||||
try:
|
||||
req = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))) or b"{}")
|
||||
if self.path.rstrip("/") == "/v1/chat/completions":
|
||||
@@ -753,6 +999,30 @@ def make_handler(svc: Service):
|
||||
self._json(404, {"error": {"message": "not found"}})
|
||||
except ValueError as e:
|
||||
self._json(400, {"error": {"type": "invalid_request_error", "message": str(e)}})
|
||||
except EngineDied as e: # before the answer started (not streamed)
|
||||
self._json(503, {"error": {"type": "server_error", "message": f"{e}; the next request restarts it"}})
|
||||
|
||||
def _settings(self):
|
||||
# They change what every client gets, so only the app's own page may set them: JSON only (a form or a
|
||||
# "simple" cross-site request can't send it without a CORS preflight, which this server never grants),
|
||||
# and no foreign Origin
|
||||
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
|
||||
if not self.headers.get("Content-Type", "").startswith("application/json"):
|
||||
self._json(415, {"error": {"message": "send application/json"}})
|
||||
return
|
||||
origin = self.headers.get("Origin")
|
||||
if origin and origin.split("://", 1)[-1] != self.headers.get("Host", ""):
|
||||
self._json(403, {"error": {"message": "settings can only be changed from Strata's own page"}})
|
||||
return
|
||||
try:
|
||||
req = json.loads(body or b"{}")
|
||||
shared = svc.set_shared(req.get("defaults") if isinstance(req, dict) else None)
|
||||
except ValueError as e:
|
||||
self._json(400, {"error": {"type": "invalid_request_error", "message": str(e)}})
|
||||
return
|
||||
print("[strata] other apps now use the Chat settings: " + ", ".join(f"{k}={v}" for k, v in shared.items())
|
||||
if shared else "[strata] other apps use their own settings again", flush=True)
|
||||
self._json(200, {"shared": bool(shared), "defaults": shared})
|
||||
|
||||
def _sse(self):
|
||||
self.send_response(200)
|
||||
@@ -761,6 +1031,7 @@ def make_handler(svc: Service):
|
||||
self.end_headers()
|
||||
|
||||
def _openai(self, req):
|
||||
req = svc.with_shared(req, "openai")
|
||||
messages, tools, kw = openai_to_messages(req)
|
||||
max_new = int(req.get("max_completion_tokens") or req.get("max_tokens") or 0) # 0/-1: the rest of the context
|
||||
ids, thinking, max_new = svc.prepare(messages, tools, kw, max_new)
|
||||
@@ -781,8 +1052,15 @@ def make_handler(svc: Service):
|
||||
except OSError:
|
||||
cancel.set() # client went away: stop the engine
|
||||
chunks.close()
|
||||
except EngineDied as e: # mid-stream: say so, then end the stream properly
|
||||
err = {"error": {"type": "server_error", "message": f"{e}; the next request restarts it"}}
|
||||
self.wfile.write(b"data: " + json.dumps(err).encode() + b"\n\ndata: [DONE]\n\n")
|
||||
except ValueError as e: # the engine's ERR after the stream started: the
|
||||
err = {"error": {"type": "server_error", "message": str(e)}} # headers are sent, so no 400 now
|
||||
self.wfile.write(b"data: " + json.dumps(err).encode() + b"\n\ndata: [DONE]\n\n")
|
||||
|
||||
def _anthropic(self, req):
|
||||
req = svc.with_shared(req, "anthropic")
|
||||
messages, tools, kw = anthropic_to_messages(req)
|
||||
max_new = int(req.get("max_tokens") or 0) # 0/-1: the rest of the context
|
||||
ids, thinking, max_new = svc.prepare(messages, tools, kw, max_new)
|
||||
@@ -804,6 +1082,12 @@ def make_handler(svc: Service):
|
||||
except OSError:
|
||||
cancel.set()
|
||||
events.close()
|
||||
except EngineDied as e: # mid-stream: Anthropic's error event
|
||||
err = {"type": "error", "error": {"type": "api_error", "message": f"{e}; the next request restarts it"}}
|
||||
self.wfile.write(b"event: error\ndata: " + json.dumps(err).encode() + b"\n\n")
|
||||
except ValueError as e: # the engine's ERR after the stream started
|
||||
err = {"type": "error", "error": {"type": "api_error", "message": str(e)}}
|
||||
self.wfile.write(b"event: error\ndata: " + json.dumps(err).encode() + b"\n\n")
|
||||
|
||||
return Handler
|
||||
|
||||
@@ -814,12 +1098,92 @@ class Server(ThreadingHTTPServer):
|
||||
allow_reuse_address = os.name != "nt"
|
||||
|
||||
|
||||
def warn_tight_ram(arena_mib) -> None:
|
||||
"""The model's experts live in RAM (INFO arena_mib, engine 0.1.10+). With less than ~6 GB left beside them for the
|
||||
system, the engine and this server, Linux ends the engine mid-answer when memory runs out (issue #27) and Windows
|
||||
pages to disk; say so at start instead of after a lost answer."""
|
||||
if not isinstance(arena_mib, int) or arena_mib <= 0:
|
||||
return
|
||||
try:
|
||||
import psutil
|
||||
total = psutil.virtual_memory().total
|
||||
except Exception: # noqa: BLE001 - psutil is optional here
|
||||
return
|
||||
left = total / 2**30 - arena_mib / 1024
|
||||
if left < 6:
|
||||
print(f"[strata] WARNING: RAM is tight - the model's experts take {arena_mib / 1024:.1f} GB of this PC's "
|
||||
f"{total / 2**30:.0f} GB, leaving {left:.1f} GB for everything else. "
|
||||
+ ("Linux may stop the engine in the middle of an answer. " if os.name != "nt" else
|
||||
"Windows will slow down (paging to disk). ")
|
||||
+ "Close other programs, or run START-HERE --setup and pick a smaller size (Q2_0 / IQ2_XS).", flush=True)
|
||||
|
||||
|
||||
def lan_addresses() -> list[str]:
|
||||
"""This PC's IPv4 addresses on its networks (what another device types in), without loopback/link-local."""
|
||||
import socket
|
||||
first, ips = None, set()
|
||||
try: # the address of the default route; sends nothing (UDP)
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
s.connect(("10.255.255.255", 1))
|
||||
first = s.getsockname()[0]
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET):
|
||||
ips.add(info[4][0])
|
||||
except OSError:
|
||||
pass
|
||||
ok = lambda ip: ip and not ip.startswith(("127.", "169.254.", "0."))
|
||||
return ([first] if ok(first) else []) + sorted(ip for ip in ips if ok(ip) and ip != first)
|
||||
|
||||
|
||||
def serve(svc: Service, host="127.0.0.1", port=8095) -> ThreadingHTTPServer:
|
||||
svc.start_telemetry()
|
||||
httpd = Server((host, port), make_handler(svc))
|
||||
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
||||
return httpd
|
||||
|
||||
|
||||
SHARED_KEYS = ("reasoning_effort", "temperature", "top_p", "top_k", "seed", "max_tokens", "experimental_speed_projection")
|
||||
|
||||
|
||||
def clean_shared_defaults(d) -> dict:
|
||||
"""The Chat settings other apps get (POST /settings): only known keys, each checked; ValueError names a bad one."""
|
||||
if d is None:
|
||||
return {}
|
||||
if not isinstance(d, dict):
|
||||
raise ValueError("defaults must be an object")
|
||||
out = {}
|
||||
for key, value in d.items():
|
||||
if value is None or value == "":
|
||||
continue
|
||||
number = isinstance(value, (int, float)) and not isinstance(value, bool)
|
||||
if key == "reasoning_effort":
|
||||
if value not in ("none", "low", "medium", "high"):
|
||||
raise ValueError("reasoning_effort: none, low, medium or high")
|
||||
elif key == "temperature":
|
||||
if not number or not 0 <= value <= 2:
|
||||
raise ValueError("temperature: 0..2")
|
||||
elif key == "top_p":
|
||||
if not number or not 0 < value <= 1:
|
||||
raise ValueError("top_p: 0 < top_p <= 1")
|
||||
elif key == "top_k":
|
||||
if not number or value != int(value) or not 1 <= value <= 64:
|
||||
raise ValueError("top_k: an integer 1..64")
|
||||
value = int(value)
|
||||
elif key in ("seed", "max_tokens"):
|
||||
if not number or value != int(value) or value <= 0:
|
||||
raise ValueError(f"{key}: a positive integer")
|
||||
value = int(value)
|
||||
elif key == "experimental_speed_projection":
|
||||
if not isinstance(value, bool):
|
||||
raise ValueError("experimental_speed_projection: true or false")
|
||||
else:
|
||||
raise ValueError(f"unknown setting {key!r}")
|
||||
out[key] = float(value) if key in ("temperature", "top_p") else value
|
||||
return out
|
||||
|
||||
|
||||
def sampling_defaults_from_config(cfg: dict) -> dict:
|
||||
"""The run config's optional `sampling` block: defaults for the sampling fields a request leaves out, so
|
||||
a plain client gets configured sampling instead of greedy. Supported: temperature, top_p, top_k, min_p,
|
||||
@@ -868,6 +1232,11 @@ def sampling_defaults_from_config(cfg: dict) -> dict:
|
||||
if not number or value != int(value) or value <= 0:
|
||||
raise SystemExit(f"[strata] config sampling.seed={value!r}: expected a positive integer")
|
||||
out[key] = int(value)
|
||||
elif key == "experimental_speed_projection":
|
||||
if not isinstance(value, bool):
|
||||
raise SystemExit(f"[strata] config sampling.experimental_speed_projection={value!r}: expected true or "
|
||||
"false (the default for requests that leave it out, when the engine has the vector)")
|
||||
out[key] = value
|
||||
else:
|
||||
print(f"[strata] config sampling.{key}={value!r}: unknown key, ignored", flush=True)
|
||||
return out
|
||||
@@ -878,16 +1247,22 @@ def main() -> int:
|
||||
ap.add_argument("--engine", choices=["mock", "strata"], default="mock")
|
||||
ap.add_argument("--config", help="strata engine config (JSON: exe, args, cwd, tokenizer, model_name), "
|
||||
"written by setup.py")
|
||||
ap.add_argument("--host", default="127.0.0.1")
|
||||
ap.add_argument("--host", default=None,
|
||||
help="the address to listen on: 127.0.0.1 = this PC only (the default), 0.0.0.0 = also other devices "
|
||||
"on your network (set an API key); also \"host\" in the config")
|
||||
ap.add_argument("--script", default="Thinking about it.</think>\n\nHello from the mock engine.")
|
||||
ap.add_argument("--port", type=int, default=8095)
|
||||
ap.add_argument("--tokenizer", default=str(ROOT / "pack/full/tokenizer"),
|
||||
help="pack tokenizer directory (falls back to a byte tokenizer if absent)")
|
||||
ap.add_argument("--open", action="store_true", help="open the local page in the browser once the model is ready")
|
||||
ap.add_argument("--fit-max-tokens", action="store_true",
|
||||
help="clamp max_tokens to the remaining context instead of rejecting the request "
|
||||
"(default: reject with 400, like llama.cpp; also \"fit_max_tokens\": true in the config)")
|
||||
ap.add_argument("--api-key", default=os.environ.get("STRATA_API_KEY", ""),
|
||||
help="require this key on /v1/* (Authorization: Bearer ... or x-api-key); also $STRATA_API_KEY")
|
||||
a = ap.parse_args()
|
||||
cfg = json.loads(Path(a.config).read_text(encoding="utf-8-sig")) if a.config else {} # Notepad adds a BOM
|
||||
a.host = a.host or cfg.get("host") or "127.0.0.1" # issue #26: the run scripts pass no --host, the config can
|
||||
try: # before the minutes of loading: is the port free?
|
||||
Server((a.host, a.port), BaseHTTPRequestHandler).server_close()
|
||||
except OSError:
|
||||
@@ -923,19 +1298,46 @@ def main() -> int:
|
||||
env=env)
|
||||
print("loading the model (the first start takes a minute or two) ...", flush=True)
|
||||
engine = StrataEngine(cfg["exe"], cfg["args"], cwd=cfg.get("cwd"), log=cfg.get("log"), env=env)
|
||||
warn_tight_ram(engine.info.get("arena_mib"))
|
||||
else:
|
||||
engine, vision, sampling_defaults = MockEngine(tok, a.script), None, {}
|
||||
# the model's own chat template (exported with its tokenizer), else the original model's
|
||||
tpl = tpath / "chat_template.jinja"
|
||||
svc = Service(engine, tok, ChatTemplate(tpl if tpl.exists() else ROOT / "serve/chat_template.jinja"),
|
||||
model_name=cfg.get("model_name", "qwen3.8-flash-next"), vision=vision,
|
||||
sampling_defaults=sampling_defaults)
|
||||
sampling_defaults=sampling_defaults,
|
||||
fit_max_tokens=a.fit_max_tokens or cfg.get("fit_max_tokens") is True)
|
||||
svc.api_key = a.api_key or cfg.get("api_key", "")
|
||||
if a.config: # the Chat settings shared with other apps, from last time
|
||||
svc.shared_path = str(Path(a.config).with_suffix("")) + ".shared-settings.json"
|
||||
try:
|
||||
svc.shared = clean_shared_defaults(json.loads(Path(svc.shared_path).read_text(encoding="utf-8")))
|
||||
if svc.shared:
|
||||
print("[strata] other apps use the Chat settings: " +
|
||||
", ".join(f"{k}={v}" for k, v in svc.shared.items()), flush=True)
|
||||
except (OSError, ValueError):
|
||||
svc.shared = {}
|
||||
httpd = serve(svc, host=a.host, port=a.port)
|
||||
print(f"ready: http://{a.host}:{a.port}/v1 (OpenAI: /v1/chat/completions, Anthropic: /v1/messages, "
|
||||
here = "127.0.0.1" if a.host in ("0.0.0.0", "", "::") else a.host
|
||||
print(f"ready: http://{here}:{a.port}/v1 (OpenAI: /v1/chat/completions, Anthropic: /v1/messages, "
|
||||
f"context {engine.max_context} tokens{', images on' if vision else ''}"
|
||||
f"{', API key required' if svc.api_key else ''})", flush=True)
|
||||
print(f" open http://{a.host}:{a.port}/ in a browser to chat; close this window to stop the model", flush=True)
|
||||
print(f" open http://{here}:{a.port}/ in a browser to chat; close this window to stop the model", flush=True)
|
||||
if a.host not in ("127.0.0.1", "localhost", "::1"):
|
||||
# issue #26: reachable from other devices - say at which address, and what can still block it
|
||||
ips = lan_addresses()
|
||||
for ip in ips:
|
||||
print(f" from other devices: http://{ip}:{a.port}/ (API: http://{ip}:{a.port}/v1)", flush=True)
|
||||
if not ips:
|
||||
print(" from other devices: http://<this PC's IP address>:" + str(a.port) + "/", flush=True)
|
||||
if not svc.api_key:
|
||||
print(" WARNING: no API key - anyone on your network can use this model. Add \"api_key\": \"...\" "
|
||||
"to the config (clients send it as their API key; the web page asks for it)", flush=True)
|
||||
if os.name == "nt":
|
||||
print(" nothing arrives? Windows Firewall blocks it until allowed: accept its prompt for Python, or run "
|
||||
"in an admin PowerShell:\n New-NetFirewallRule -DisplayName \"Strata " + str(a.port) + "\" "
|
||||
"-Direction Inbound -Protocol TCP -LocalPort " + str(a.port) + " -Action Allow -Profile Private\n"
|
||||
" (and set this network to Private in Windows' network settings)", flush=True)
|
||||
if a.open:
|
||||
import webbrowser
|
||||
webbrowser.open(f"http://{'127.0.0.1' if a.host in ('0.0.0.0', '') else a.host}:{a.port}/")
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
"""serve/telemetry.py - hardware readings for the web app's Monitor tab (idea from PR #22 by code-martin).
|
||||
|
||||
A background thread samples once a second and keeps the last 60 readings of each series for the sparklines:
|
||||
- GPU: NVIDIA's own NVML library (nvml.dll / libnvidia-ml.so.1, installed with every driver) through ctypes, so no
|
||||
pip package is needed: load, VRAM, temperature, power, PCIe link and throughput.
|
||||
- CPU, RAM, disk: `psutil` when it is installed (setup installs it); without it the CPU and RAM readings fall back to
|
||||
the OS (Windows GlobalMemoryStatusEx / GetSystemTimes, Linux /proc) and the disk rate is absent.
|
||||
Anything that cannot be read is None; nothing here can stop the server.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import ctypes
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
HISTORY = 60
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ NVML
|
||||
class _Nvml:
|
||||
class Util(ctypes.Structure):
|
||||
_fields_ = [("gpu", ctypes.c_uint), ("memory", ctypes.c_uint)]
|
||||
|
||||
class Mem(ctypes.Structure):
|
||||
_fields_ = [("total", ctypes.c_ulonglong), ("free", ctypes.c_ulonglong), ("used", ctypes.c_ulonglong)]
|
||||
|
||||
def __init__(self, index=0):
|
||||
self.lib = self.dev = None
|
||||
names = ["nvml.dll", os.path.join(os.environ.get("ProgramFiles", r"C:\Program Files"),
|
||||
"NVIDIA Corporation", "NVSMI", "nvml.dll")] if os.name == "nt" \
|
||||
else ["libnvidia-ml.so.1", "libnvidia-ml.so"]
|
||||
for n in names:
|
||||
try:
|
||||
self.lib = ctypes.CDLL(n)
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
if self.lib is None:
|
||||
return
|
||||
try:
|
||||
init = getattr(self.lib, "nvmlInit_v2", None) or self.lib.nvmlInit
|
||||
if init() != 0:
|
||||
self.lib = None
|
||||
return
|
||||
h = ctypes.c_void_p()
|
||||
get = getattr(self.lib, "nvmlDeviceGetHandleByIndex_v2", None) or self.lib.nvmlDeviceGetHandleByIndex
|
||||
if get(ctypes.c_uint(index), ctypes.byref(h)) != 0:
|
||||
self.lib = None
|
||||
return
|
||||
self.dev = h
|
||||
except (AttributeError, OSError):
|
||||
self.lib = None
|
||||
|
||||
def ok(self):
|
||||
return self.lib is not None and self.dev is not None
|
||||
|
||||
def _uint(self, fn, *args):
|
||||
v = ctypes.c_uint()
|
||||
try:
|
||||
return v.value if getattr(self.lib, fn)(self.dev, *args, ctypes.byref(v)) == 0 else None
|
||||
except (AttributeError, OSError):
|
||||
return None
|
||||
|
||||
def name(self):
|
||||
buf = ctypes.create_string_buffer(96)
|
||||
try:
|
||||
if self.lib.nvmlDeviceGetName(self.dev, buf, ctypes.c_uint(96)) == 0:
|
||||
return buf.value.decode(errors="replace")
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
return None
|
||||
|
||||
def read(self):
|
||||
out = {}
|
||||
u = self.Util()
|
||||
try:
|
||||
if self.lib.nvmlDeviceGetUtilizationRates(self.dev, ctypes.byref(u)) == 0:
|
||||
out["util"] = u.gpu
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
m = self.Mem()
|
||||
try:
|
||||
if self.lib.nvmlDeviceGetMemoryInfo(self.dev, ctypes.byref(m)) == 0:
|
||||
out["mem_used"], out["mem_total"] = m.used, m.total
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
out["temp"] = self._uint("nvmlDeviceGetTemperature", ctypes.c_uint(0)) # NVML_TEMPERATURE_GPU
|
||||
mw = self._uint("nvmlDeviceGetPowerUsage")
|
||||
out["power"] = mw / 1000.0 if mw is not None else None
|
||||
lim = self._uint("nvmlDeviceGetEnforcedPowerLimit")
|
||||
out["power_limit"] = lim / 1000.0 if lim is not None else None
|
||||
out["pcie_gen"] = self._uint("nvmlDeviceGetCurrPcieLinkGeneration") # drops at idle (power saving)
|
||||
out["pcie_gen_max"] = self._uint("nvmlDeviceGetMaxPcieLinkGeneration")
|
||||
out["pcie_width"] = self._uint("nvmlDeviceGetCurrPcieLinkWidth")
|
||||
rx = self._uint("nvmlDeviceGetPcieThroughput", ctypes.c_uint(1)) # NVML_PCIE_UTIL_RX_BYTES, KB/s
|
||||
tx = self._uint("nvmlDeviceGetPcieThroughput", ctypes.c_uint(0))
|
||||
out["pcie_rx_mb"] = rx / 1024.0 if rx is not None else None
|
||||
out["pcie_tx_mb"] = tx / 1024.0 if tx is not None else None
|
||||
return out
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ CPU / RAM
|
||||
def _cpu_name():
|
||||
if os.name == "nt":
|
||||
try:
|
||||
import winreg
|
||||
k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\CentralProcessor\0")
|
||||
return winreg.QueryValueEx(k, "ProcessorNameString")[0].strip()
|
||||
except OSError:
|
||||
pass
|
||||
elif os.path.exists("/proc/cpuinfo"):
|
||||
for line in open("/proc/cpuinfo", encoding="utf-8", errors="replace"):
|
||||
if line.startswith("model name"):
|
||||
return line.split(":", 1)[1].strip()
|
||||
return platform.processor() or None
|
||||
|
||||
|
||||
class _CpuRamFallback:
|
||||
"""CPU load and RAM without psutil."""
|
||||
|
||||
def __init__(self):
|
||||
self.prev = self._times()
|
||||
|
||||
def _times(self):
|
||||
if os.name == "nt":
|
||||
idle, kern, user = (ctypes.c_ulonglong() for _ in range(3))
|
||||
if ctypes.windll.kernel32.GetSystemTimes(ctypes.byref(idle), ctypes.byref(kern), ctypes.byref(user)):
|
||||
return idle.value, kern.value + user.value # kernel time includes idle
|
||||
return None
|
||||
try:
|
||||
f = [int(x) for x in open("/proc/stat").readline().split()[1:]]
|
||||
return f[3] + f[4], sum(f)
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
def cpu(self):
|
||||
cur = self._times()
|
||||
prev, self.prev = self.prev, cur
|
||||
if not cur or not prev or cur[1] == prev[1]:
|
||||
return None
|
||||
return max(0.0, min(100.0, 100.0 * (1 - (cur[0] - prev[0]) / (cur[1] - prev[1]))))
|
||||
|
||||
@staticmethod
|
||||
def ram():
|
||||
if os.name == "nt":
|
||||
class MS(ctypes.Structure):
|
||||
_fields_ = [("dwLength", ctypes.c_ulong), ("dwMemoryLoad", ctypes.c_ulong),
|
||||
("ullTotalPhys", ctypes.c_ulonglong), ("ullAvailPhys", ctypes.c_ulonglong),
|
||||
("ullTotalPageFile", ctypes.c_ulonglong), ("ullAvailPageFile", ctypes.c_ulonglong),
|
||||
("ullTotalVirtual", ctypes.c_ulonglong), ("ullAvailVirtual", ctypes.c_ulonglong),
|
||||
("ullAvailExtendedVirtual", ctypes.c_ulonglong)]
|
||||
m = MS()
|
||||
m.dwLength = ctypes.sizeof(MS)
|
||||
if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(m)):
|
||||
return m.ullTotalPhys - m.ullAvailPhys, m.ullTotalPhys
|
||||
return None, None
|
||||
try:
|
||||
info = dict(line.split(":", 1) for line in open("/proc/meminfo"))
|
||||
total = int(info["MemTotal"].split()[0]) * 1024
|
||||
avail = int(info["MemAvailable"].split()[0]) * 1024
|
||||
return total - avail, total
|
||||
except (OSError, KeyError, ValueError):
|
||||
return None, None
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------------------ the sampler
|
||||
class Telemetry:
|
||||
def __init__(self, extra=None):
|
||||
"""`extra()` -> dict of more series to record each second (the server's tok/s)."""
|
||||
self.extra = extra
|
||||
self.lock = threading.Lock()
|
||||
self.now: dict = {}
|
||||
self.hist = collections.defaultdict(lambda: collections.deque(maxlen=HISTORY))
|
||||
self.gpu = _Nvml()
|
||||
try:
|
||||
import psutil # noqa: F401
|
||||
self.ps = sys.modules["psutil"]
|
||||
except ImportError:
|
||||
self.ps = None
|
||||
self.fallback = _CpuRamFallback()
|
||||
self.static = {
|
||||
"gpu_name": self.gpu.name() if self.gpu.ok() else None,
|
||||
"cpu_name": _cpu_name(),
|
||||
"cores": (self.ps.cpu_count(logical=False) if self.ps else None) or None,
|
||||
"threads": os.cpu_count(),
|
||||
"psutil": self.ps is not None,
|
||||
}
|
||||
self._disk_prev = None
|
||||
threading.Thread(target=self._loop, daemon=True).start()
|
||||
|
||||
def _disk(self):
|
||||
if not self.ps:
|
||||
return None, None
|
||||
try:
|
||||
c = self.ps.disk_io_counters()
|
||||
except (OSError, RuntimeError):
|
||||
return None, None
|
||||
t = time.time()
|
||||
prev, self._disk_prev = self._disk_prev, (t, c.read_bytes, c.write_bytes)
|
||||
if prev is None or t <= prev[0]:
|
||||
return None, None
|
||||
dt = t - prev[0]
|
||||
return (c.read_bytes - prev[1]) / dt / 2**20, (c.write_bytes - prev[2]) / dt / 2**20
|
||||
|
||||
def sample(self):
|
||||
s = {}
|
||||
if self.gpu.ok():
|
||||
g = self.gpu.read()
|
||||
s.update({f"gpu_{k}": v for k, v in g.items()})
|
||||
if self.ps:
|
||||
try:
|
||||
s["cpu"] = self.ps.cpu_percent(interval=None)
|
||||
vm = self.ps.virtual_memory()
|
||||
s["ram_used"], s["ram_total"] = vm.total - vm.available, vm.total
|
||||
except (OSError, RuntimeError):
|
||||
pass
|
||||
else:
|
||||
s["cpu"] = self.fallback.cpu()
|
||||
s["ram_used"], s["ram_total"] = self.fallback.ram()
|
||||
s["disk_read_mb"], s["disk_write_mb"] = self._disk()
|
||||
if self.extra:
|
||||
try:
|
||||
s.update(self.extra())
|
||||
except Exception: # noqa: BLE001 - telemetry must never take the server down
|
||||
pass
|
||||
return s
|
||||
|
||||
def _loop(self):
|
||||
while True:
|
||||
s = self.sample()
|
||||
with self.lock:
|
||||
self.now = s
|
||||
for k in ("gpu_util", "gpu_mem_used", "gpu_temp", "gpu_power", "gpu_pcie_rx_mb", "cpu", "ram_used",
|
||||
"disk_read_mb", "tok_s"):
|
||||
v = s.get(k)
|
||||
self.hist[k].append(round(v, 2) if isinstance(v, float) else v)
|
||||
time.sleep(1.0)
|
||||
|
||||
def snapshot(self):
|
||||
with self.lock:
|
||||
return {"now": dict(self.now), "history": {k: list(v) for k, v in self.hist.items()},
|
||||
"static": dict(self.static)}
|
||||
+283
-1
@@ -14,7 +14,7 @@ from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from serve.frontend import ChatTemplate # noqa: E402
|
||||
from serve.server import CTX_SLACK, ByteTokenizer, MockEngine, Service, serve # noqa: E402
|
||||
from serve.server import CTX_SLACK, ByteTokenizer, EngineDied, MockEngine, Service, serve # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CTX = 4096
|
||||
@@ -126,5 +126,287 @@ class MaxTokens(unittest.TestCase):
|
||||
del os.environ["STRATA_DEBUG"]
|
||||
|
||||
|
||||
class FitMaxTokens(unittest.TestCase):
|
||||
"""PR #24: --fit-max-tokens clamps an explicit budget that overshoots the context instead of a 400."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
tok = ByteTokenizer()
|
||||
cls.engine = RecordingEngine(tok, "</think>\n\n" + ANSWER, max_context=CTX)
|
||||
cls.svc = Service(cls.engine, tok, ChatTemplate(ROOT / "serve/chat_template.jinja"), fit_max_tokens=True)
|
||||
cls.httpd = serve(cls.svc, port=0)
|
||||
cls.base = f"http://127.0.0.1:{cls.httpd.server_address[1]}"
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.httpd.shutdown()
|
||||
cls.httpd.server_close()
|
||||
|
||||
post = MaxTokens.post
|
||||
call = MaxTokens.call
|
||||
|
||||
def test_overshoot_is_clamped_to_the_room(self):
|
||||
for api in ("openai", "anthropic"):
|
||||
with self.subTest(api=api):
|
||||
s, b, pt, ct = self.call(api, max_tokens=CTX)
|
||||
self.assertEqual(s, 200, b)
|
||||
self.assertEqual(self.engine.last_max_new, CTX - CTX_SLACK - pt)
|
||||
|
||||
def test_a_budget_that_fits_is_unchanged(self):
|
||||
s, b, _, ct = self.call("openai", max_tokens=50)
|
||||
self.assertEqual(s, 200, b)
|
||||
self.assertEqual(self.engine.last_max_new, 50)
|
||||
|
||||
def test_no_room_is_still_a_400(self):
|
||||
_, _, pt0, _ = self.call("openai", max_tokens=1)
|
||||
overhead = pt0 - len("hi")
|
||||
s, b, _, _ = self.call("openai", text="y" * (CTX - CTX_SLACK - overhead), max_tokens=100)
|
||||
self.assertEqual(s, 400, b)
|
||||
self.assertIn("no room to answer", b["error"]["message"])
|
||||
|
||||
|
||||
class DyingEngine(MockEngine):
|
||||
"""Issue #27: an engine that dies after a few tokens of its first answer, and comes back when restarted."""
|
||||
|
||||
def __init__(self, tok, script, max_context):
|
||||
super().__init__(tok, script, max_context=max_context)
|
||||
self.dead, self.restarts, self.die_after = False, 0, 5
|
||||
|
||||
def alive(self):
|
||||
return not self.dead
|
||||
|
||||
def restart(self):
|
||||
self.dead, self.die_after = False, None
|
||||
self.restarts += 1
|
||||
|
||||
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
|
||||
for i, t in enumerate(super().generate(ids, max_new, sampling, cancel, embeddings)):
|
||||
if self.die_after is not None and i == self.die_after:
|
||||
self.dead = True
|
||||
raise EngineDied("the engine stopped unexpectedly (exit code -9)")
|
||||
yield t
|
||||
|
||||
|
||||
class EngineDeath(unittest.TestCase):
|
||||
"""Issue #27: a dead engine is an error (not "length"), and the next request starts it again."""
|
||||
|
||||
def test_error_then_restart(self):
|
||||
tok = ByteTokenizer()
|
||||
eng = DyingEngine(tok, "</think>\n\n" + ANSWER, max_context=CTX)
|
||||
svc = Service(eng, tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
|
||||
httpd = serve(svc, port=0)
|
||||
base = f"http://127.0.0.1:{httpd.server_address[1]}"
|
||||
try:
|
||||
def post(body):
|
||||
req = urllib.request.Request(base + "/v1/chat/completions", data=json.dumps(body).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return r.status, r.read().decode()
|
||||
except urllib.error.HTTPError as e:
|
||||
with e:
|
||||
return e.code, e.read().decode()
|
||||
msgs = [{"role": "user", "content": "hi"}]
|
||||
code, text = post({"model": "m", "messages": msgs, "max_tokens": 50, "stream": True})
|
||||
self.assertEqual(code, 200)
|
||||
self.assertIn('"error"', text)
|
||||
self.assertIn("stopped unexpectedly", text)
|
||||
self.assertTrue(text.rstrip().endswith("data: [DONE]"))
|
||||
self.assertEqual(svc.metrics()["requests"][0]["finish"], "error")
|
||||
code, text = post({"model": "m", "messages": msgs, "max_tokens": 50})
|
||||
self.assertEqual(code, 200, text)
|
||||
self.assertEqual(eng.restarts, 1)
|
||||
self.assertEqual(json.loads(text)["usage"]["completion_tokens"], 50)
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
httpd.server_close()
|
||||
|
||||
def test_engine_err_mid_stream(self):
|
||||
"""The engine's ERR line after the stream started reaches the client as an error event (it used to be a
|
||||
400 written into the open stream, which clients read as an empty answer)."""
|
||||
class ErrEngine(MockEngine):
|
||||
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
|
||||
yield None # a prompt-progress heartbeat: the stream has started
|
||||
raise ValueError("verify: layer 31 never rang (an illegal memory access was encountered)")
|
||||
|
||||
tok = ByteTokenizer()
|
||||
svc = Service(ErrEngine(tok, ANSWER, max_context=CTX), tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
|
||||
httpd = serve(svc, port=0)
|
||||
base = f"http://127.0.0.1:{httpd.server_address[1]}"
|
||||
try:
|
||||
for path, body in [("/v1/chat/completions", {"model": "m", "stream": True, "max_tokens": 20,
|
||||
"messages": [{"role": "user", "content": "hi"}]}),
|
||||
("/v1/messages", {"model": "m", "stream": True, "max_tokens": 20,
|
||||
"messages": [{"role": "user", "content": "hi"}]})]:
|
||||
req = urllib.request.Request(base + path, data=json.dumps(body).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
text = r.read().decode()
|
||||
self.assertIn("illegal memory access", text, path)
|
||||
self.assertNotIn("HTTP/1", text, path)
|
||||
self.assertEqual(svc.metrics()["requests"][0]["finish"], "error")
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
httpd.server_close()
|
||||
|
||||
|
||||
class SharedSettings(unittest.TestCase):
|
||||
"""The web app's "Use for other apps too": POST /settings makes its Chat settings every client's defaults."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
import tempfile
|
||||
|
||||
class Sampled(RecordingEngine):
|
||||
def generate(self, ids, max_new, sampling, cancel, embeddings=None):
|
||||
self.last_sampling = dict(sampling or {})
|
||||
yield from super().generate(ids, max_new, sampling, cancel, embeddings)
|
||||
|
||||
tok = ByteTokenizer()
|
||||
cls.engine = Sampled(tok, "</think>\n\nhello", max_context=CTX)
|
||||
cls.svc = Service(cls.engine, tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
|
||||
cls.tmp = tempfile.TemporaryDirectory()
|
||||
cls.svc.shared_path = os.path.join(cls.tmp.name, "strata-x.shared-settings.json")
|
||||
cls.httpd = serve(cls.svc, port=0)
|
||||
cls.base = f"http://127.0.0.1:{cls.httpd.server_address[1]}"
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.httpd.shutdown()
|
||||
cls.httpd.server_close()
|
||||
cls.tmp.cleanup()
|
||||
|
||||
def req(self, path, body, headers=None, raw=None):
|
||||
h = {"Content-Type": "application/json", **(headers or {})}
|
||||
r = urllib.request.Request(self.base + path, data=raw if raw is not None else json.dumps(body).encode(), headers=h)
|
||||
try:
|
||||
with urllib.request.urlopen(r, timeout=30) as resp:
|
||||
return resp.status, json.loads(resp.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
with e:
|
||||
return e.code, json.loads(e.read())
|
||||
|
||||
def chat(self, **extra):
|
||||
return self.req("/v1/chat/completions", {"model": "m", "messages": [{"role": "user", "content": "hi"}], **extra})
|
||||
|
||||
def tearDown(self):
|
||||
self.svc.set_shared(None)
|
||||
|
||||
def test_other_apps_get_the_chat_settings(self):
|
||||
d = {"temperature": 0.3, "top_p": 0.9, "top_k": 10, "seed": 7, "max_tokens": 77,
|
||||
"reasoning_effort": "low", "experimental_speed_projection": False}
|
||||
code, b = self.req("/settings", {"defaults": d})
|
||||
self.assertEqual(code, 200, b)
|
||||
self.assertTrue(b["shared"])
|
||||
self.assertTrue(os.path.exists(self.svc.shared_path))
|
||||
code, _ = self.chat() # a client that sets nothing
|
||||
self.assertEqual(code, 200)
|
||||
got = self.engine.last_sampling
|
||||
for k in ("temperature", "top_p", "top_k", "seed", "experimental_speed_projection"):
|
||||
self.assertEqual(got[k], d[k], k)
|
||||
self.assertEqual(self.engine.last_max_new, 77)
|
||||
code, _ = self.chat(temperature=0.9, max_tokens=5) # its own values win
|
||||
self.assertEqual(self.engine.last_sampling["temperature"], 0.9)
|
||||
self.assertEqual(self.engine.last_max_new, 5)
|
||||
r = self.svc.with_shared({"messages": []}, "openai")
|
||||
self.assertEqual(r["reasoning_effort"], "low")
|
||||
self.assertEqual(self.svc.with_shared({"reasoning_effort": "high"}, "openai")["reasoning_effort"], "high")
|
||||
self.assertEqual(self.svc.with_shared({}, "anthropic")["output_config"], {"effort": "low"})
|
||||
|
||||
def test_off_again(self):
|
||||
self.req("/settings", {"defaults": {"temperature": 0.3}})
|
||||
code, b = self.req("/settings", {"defaults": None})
|
||||
self.assertEqual((code, b["shared"]), (200, False))
|
||||
self.assertFalse(os.path.exists(self.svc.shared_path))
|
||||
self.chat()
|
||||
self.assertNotIn("temperature", self.engine.last_sampling)
|
||||
|
||||
def test_only_strata_s_own_page_may_set_them(self):
|
||||
code, _ = self.req("/settings", None, {"Content-Type": "text/plain"}, raw=b'{"defaults": {"temperature": 1}}')
|
||||
self.assertEqual(code, 415)
|
||||
code, _ = self.req("/settings", {"defaults": {"temperature": 1}}, {"Origin": "http://evil.example"})
|
||||
self.assertEqual(code, 403)
|
||||
code, b = self.req("/settings", {"defaults": {"temperature": 9}})
|
||||
self.assertEqual(code, 400)
|
||||
self.assertIn("temperature", b["error"]["message"])
|
||||
self.assertEqual(self.svc.shared, {})
|
||||
host = self.base.split("://", 1)[1]
|
||||
code, _ = self.req("/settings", {"defaults": {"temperature": 1}}, {"Origin": "http://" + host})
|
||||
self.assertEqual(code, 200)
|
||||
|
||||
def test_they_need_the_key_when_one_is_set(self):
|
||||
self.svc.api_key = "secret"
|
||||
try:
|
||||
self.assertEqual(self.req("/settings", {"defaults": {"temperature": 1}})[0], 401)
|
||||
self.assertEqual(self.req("/settings", {"defaults": {"temperature": 1}},
|
||||
{"Authorization": "Bearer secret"})[0], 200)
|
||||
finally:
|
||||
self.svc.api_key = ""
|
||||
|
||||
|
||||
class WebApp(unittest.TestCase):
|
||||
"""The web app (PR #22's dashboard idea, rebuilt): its page and files, and GET /metrics."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
tok = ByteTokenizer()
|
||||
cls.svc = Service(RecordingEngine(tok, "</think>\n\nhello", max_context=CTX), tok,
|
||||
ChatTemplate(ROOT / "serve/chat_template.jinja"))
|
||||
cls.httpd = serve(cls.svc, port=0)
|
||||
cls.base = f"http://127.0.0.1:{cls.httpd.server_address[1]}"
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.httpd.shutdown()
|
||||
|
||||
def get(self, path, headers=None):
|
||||
req = urllib.request.Request(self.base + path, headers=headers or {})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
return r.status, r.headers.get("Content-Type", ""), r.read()
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.headers.get("Content-Type", ""), e.read()
|
||||
|
||||
def test_page_and_files(self):
|
||||
code, ctype, body = self.get("/")
|
||||
self.assertEqual(code, 200)
|
||||
self.assertIn("text/html", ctype)
|
||||
self.assertIn(b"/web/app.js", body)
|
||||
for path, want in (("/web/app.js", "javascript"), ("/web/app.css", "text/css"), ("/web/tokens.css", "text/css"),
|
||||
("/web/components.css", "text/css"), ("/web/sprite.svg", "image/svg+xml")):
|
||||
with self.subTest(path=path):
|
||||
code, ctype, _ = self.get(path)
|
||||
self.assertEqual(code, 200)
|
||||
self.assertIn(want, ctype)
|
||||
|
||||
def test_only_the_app_files_are_served(self):
|
||||
for path in ("/web/..%2Fserver.py", "/web/index.html", "/web/test.py", "/fonts/..%2F..%2Fsetup.py",
|
||||
"/fonts/missing.woff2", "/fonts/x.ttf"):
|
||||
with self.subTest(path=path):
|
||||
self.assertEqual(self.get(path)[0], 404)
|
||||
|
||||
def test_metrics(self):
|
||||
data = json.dumps({"model": "m", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}).encode()
|
||||
urllib.request.urlopen(urllib.request.Request(self.base + "/v1/chat/completions", data=data,
|
||||
headers={"Content-Type": "application/json"}), timeout=10).read()
|
||||
code, ctype, body = self.get("/metrics")
|
||||
self.assertEqual(code, 200)
|
||||
m = json.loads(body)
|
||||
for key in ("engine", "live", "requests", "hardware", "hardware_static", "history"):
|
||||
self.assertIn(key, m)
|
||||
self.assertEqual(m["engine"]["max_context"], CTX)
|
||||
self.assertEqual(m["live"]["state"], "idle")
|
||||
self.assertEqual(m["requests"][0]["output_tokens"], 5)
|
||||
|
||||
def test_metrics_need_the_key_when_one_is_set(self):
|
||||
self.svc.api_key = "secret"
|
||||
try:
|
||||
self.assertEqual(self.get("/metrics")[0], 401)
|
||||
self.assertEqual(self.get("/metrics", {"Authorization": "Bearer secret"})[0], 200)
|
||||
self.assertEqual(self.get("/")[0], 200) # the page itself asks for the key
|
||||
finally:
|
||||
self.svc.api_key = ""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,146 @@
|
||||
/* Strata web app - layout on top of tokens.css + components.css (the Strata UI kit). Colours, type, radii and
|
||||
shadows come only from the kit's tokens. */
|
||||
[hidden] { display: none !important; }
|
||||
html, body { height: 100%; }
|
||||
body { display: flex; flex-direction: column; overflow: hidden; }
|
||||
main { flex: 1; min-height: 0; display: flex; }
|
||||
.view { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
||||
.view[hidden] { display: none; }
|
||||
.muted { color: var(--st-ink-muted); }
|
||||
.small { font-size: var(--st-fs-xs); }
|
||||
.card-title { font-weight: var(--st-fw-bold); font-size: var(--st-fs-base); color: var(--st-ink); }
|
||||
.st-header { position: relative; z-index: 2; flex: none; }
|
||||
.st-header .st-tabs { margin-left: var(--st-s-4); }
|
||||
#drawer-close .st-icon { transform: rotate(-90deg); }
|
||||
|
||||
/* ---------------------------------------------------------------- chat */
|
||||
.chat-scroll { flex: 1; min-height: 0; overflow-y: auto; }
|
||||
.chat { max-width: 860px; margin: 0 auto; padding: var(--st-s-8) var(--st-s-6) var(--st-s-6); display: flex;
|
||||
flex-direction: column; gap: var(--st-s-6); }
|
||||
.chat-empty { margin: 12vh auto 0; text-align: center; }
|
||||
.chat-empty__title { font-weight: var(--st-fw-black); font-size: var(--st-fs-display); line-height: var(--st-lh-tight);
|
||||
letter-spacing: -.02em; }
|
||||
.chat-empty__sub { margin-top: var(--st-s-3); color: var(--st-ink-muted); }
|
||||
.st-msg { width: 100%; }
|
||||
.st-msg--assistant { align-self: flex-start; }
|
||||
.st-msg--assistant .st-bubble { width: 100%; }
|
||||
.st-msg--user .st-bubble { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.st-bubble > :first-child { margin-top: 0; }
|
||||
.st-bubble > :last-child { margin-bottom: 0; }
|
||||
.st-bubble p, .st-bubble ul, .st-bubble ol { margin: 0 0 var(--st-s-3); }
|
||||
.st-bubble ul, .st-bubble ol { padding-left: var(--st-s-6); }
|
||||
.st-bubble li + li { margin-top: var(--st-s-1); }
|
||||
.st-bubble h3 { font-size: var(--st-fs-lg); margin: var(--st-s-4) 0 var(--st-s-2); color: var(--st-ink); }
|
||||
.st-bubble h4 { font-size: var(--st-fs-base); margin: var(--st-s-3) 0 var(--st-s-2); color: var(--st-ink); }
|
||||
.st-bubble code.inline { font: 400 .9em var(--st-font-mono); background: var(--st-surface-2); border: 1px solid var(--st-line-soft);
|
||||
border-radius: 6px; padding: 1px 5px; }
|
||||
.st-bubble .st-code { margin: var(--st-s-3) 0; }
|
||||
.st-bubble blockquote { margin: 0 0 var(--st-s-3); padding-left: var(--st-s-3); border-left: 3px solid var(--st-line);
|
||||
color: var(--st-ink-muted); }
|
||||
.st-bubble hr { border: 0; border-top: 1px solid var(--st-line); margin: var(--st-s-4) 0; }
|
||||
.st-bubble table { border-collapse: collapse; margin: 0 0 var(--st-s-3); font-size: var(--st-fs-md); }
|
||||
.st-bubble th, .st-bubble td { border: 1px solid var(--st-line); padding: 6px 10px; text-align: left; }
|
||||
.msg-images { display: flex; flex-wrap: wrap; gap: var(--st-s-2); justify-content: flex-end; }
|
||||
.msg-images img { max-width: 220px; max-height: 180px; border-radius: var(--st-r-md); border: 1px solid var(--st-line); }
|
||||
.st-msg .st-collapse { width: 100%; }
|
||||
.st-collapse__body.thinking { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 420px; overflow-y: auto; }
|
||||
.st-msg__meta .st-btn--icon { width: 28px; height: 28px; }
|
||||
.st-msg__meta .st-btn--icon .st-icon { width: 16px; height: 16px; }
|
||||
.cursor::after { content: ""; display: inline-block; width: 8px; height: 1.1em; margin-left: 2px; vertical-align: -3px;
|
||||
border-radius: 2px; background: var(--st-accent); animation: blink 1s steps(2) infinite; }
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
.msg-error { color: var(--st-danger-text); background: var(--st-danger-tint); border-radius: var(--st-r-md); padding: 10px 14px;
|
||||
font-size: var(--st-fs-md); }
|
||||
|
||||
.composer-wrap { flex: none; padding: 0 var(--st-s-6) var(--st-s-6); }
|
||||
.st-composer { max-width: 860px; margin: 0 auto; }
|
||||
.st-composer textarea { max-height: 40vh; }
|
||||
.st-btn--send { width: 42px; padding: 0; }
|
||||
.composer-hint { font-size: var(--st-fs-xs); color: var(--st-ink-muted); margin-right: var(--st-s-2); font-variant-numeric: tabular-nums; }
|
||||
.attachments { display: flex; flex-wrap: wrap; gap: var(--st-s-2); padding: 2px 4px 0; }
|
||||
.st-composer.dragging { outline: 2px dashed var(--st-accent); outline-offset: 3px; }
|
||||
.chip { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 6px 0 10px; border-radius: var(--st-r-pill);
|
||||
background: var(--st-surface-2); border: 1px solid var(--st-line); font-size: var(--st-fs-xs); color: var(--st-ink-soft); }
|
||||
.chip .st-btn--icon { width: 22px; height: 22px; border-radius: 50%; }
|
||||
.chip .st-btn--icon .st-icon { width: 14px; height: 14px; }
|
||||
|
||||
/* ---------------------------------------------------------------- monitor */
|
||||
.view--monitor, .view--about { overflow-y: auto; }
|
||||
.monitor, .about { max-width: 1240px; width: 100%; margin: 0 auto; padding: var(--st-s-6); display: flex; flex-direction: column;
|
||||
gap: var(--st-s-5); }
|
||||
.state-card { display: flex; flex-direction: column; gap: var(--st-s-3); }
|
||||
.state-card__head { display: flex; align-items: center; gap: var(--st-s-4); flex-wrap: wrap; }
|
||||
.badges { display: flex; gap: var(--st-s-2); flex-wrap: wrap; }
|
||||
.badges .st-badge { opacity: .38; transition: opacity var(--st-dur); }
|
||||
.badges .st-badge.on { opacity: 1; }
|
||||
.state-card__row { display: flex; justify-content: space-between; gap: var(--st-s-4); font-size: var(--st-fs-sm);
|
||||
font-weight: var(--st-fw-medium); font-variant-numeric: tabular-nums; }
|
||||
.metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--st-s-4); }
|
||||
.metric-card { padding: var(--st-s-4) var(--st-s-5); }
|
||||
.st-metric__value small { font-size: var(--st-fs-md); font-weight: var(--st-fw-medium); color: var(--st-ink-muted); margin-left: 4px; }
|
||||
.st-metric__sub { font-size: var(--st-fs-xs); color: var(--st-ink-muted); font-variant-numeric: tabular-nums; min-height: 1em; }
|
||||
.st-metric__spark { display: block; overflow: visible; }
|
||||
.st-metric__spark[data-tone="warn"] { color: var(--st-warn); }
|
||||
.st-metric__spark[data-tone="info"] { color: var(--st-info); }
|
||||
.monitor-row { display: grid; grid-template-columns: minmax(260px, 1fr) minmax(0, 2.4fr); gap: var(--st-s-4); }
|
||||
.ctx-card { display: flex; flex-direction: column; gap: var(--st-s-2); }
|
||||
.ctx-gauge { position: relative; width: 150px; height: 150px; margin: var(--st-s-2) auto var(--st-s-3); }
|
||||
.ctx-gauge .st-gauge { width: 150px; height: 150px; }
|
||||
.ctx-gauge .st-gauge__fill { transition: stroke-dasharray 400ms var(--st-ease); }
|
||||
.ctx-gauge__label { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
font-size: var(--st-fs-xs); }
|
||||
.ctx-gauge__pct { font-weight: var(--st-fw-black); font-size: var(--st-fs-xl); line-height: 1.1; font-variant-numeric: tabular-nums; }
|
||||
.bar-row { display: flex; justify-content: space-between; font-size: var(--st-fs-sm); font-weight: var(--st-fw-medium);
|
||||
margin-top: var(--st-s-2); font-variant-numeric: tabular-nums; }
|
||||
.req-card { display: flex; flex-direction: column; gap: var(--st-s-3); min-width: 0; }
|
||||
.table-wrap { overflow-x: auto; }
|
||||
.req-head { display: flex; align-items: center; justify-content: space-between; gap: var(--st-s-2); }
|
||||
.table-wrap.all { max-height: 420px; overflow-y: auto; }
|
||||
.req-totals { margin: 0; font-size: var(--st-fs-sm); }
|
||||
.req-totals:empty { display: none; }
|
||||
|
||||
/* ---------------------------------------------------------------- about */
|
||||
.about { max-width: 860px; }
|
||||
.about .st-card { display: flex; flex-direction: column; gap: var(--st-s-3); }
|
||||
.about p { margin: 0; }
|
||||
.facts { display: grid; grid-template-columns: minmax(140px, max-content) 1fr; gap: 10px var(--st-s-6); margin: 0; font-size: var(--st-fs-md); }
|
||||
.facts dt { color: var(--st-ink-muted); }
|
||||
.facts dd { margin: 0; color: var(--st-ink-soft); font-variant-numeric: tabular-nums; display: flex; align-items: center; gap: 6px;
|
||||
overflow-wrap: anywhere; }
|
||||
.facts dd code { font: 400 13px var(--st-font-mono); }
|
||||
.facts .st-btn--icon { width: 28px; height: 28px; }
|
||||
.facts .st-btn--icon .st-icon { width: 16px; height: 16px; }
|
||||
.toggle-row { display: flex; align-items: center; justify-content: space-between; gap: var(--st-s-4); font-size: var(--st-fs-sm);
|
||||
font-weight: var(--st-fw-medium); color: var(--st-ink-soft); }
|
||||
.toggle-row .st-toggle { flex: none; } /* a long label must not squeeze the switch */
|
||||
|
||||
/* ---------------------------------------------------------------- drawer */
|
||||
.st-drawer { z-index: 40; }
|
||||
.scrim { position: fixed; inset: 0; background: rgba(15, 23, 42, .18); z-index: 39; }
|
||||
.seg { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; padding: 4px; background: var(--st-surface-2);
|
||||
border: 1px solid var(--st-line); border-radius: var(--st-r-md); }
|
||||
.seg button { height: 32px; border: 0; border-radius: 10px; background: transparent; color: var(--st-ink-muted);
|
||||
font: 500 var(--st-fs-sm)/1 var(--st-font); cursor: pointer; }
|
||||
.seg button[aria-checked="true"] { background: var(--st-surface); color: var(--st-ink); font-weight: var(--st-fw-bold);
|
||||
box-shadow: var(--st-sh-card); }
|
||||
.drawer-actions { display: grid; grid-template-columns: 1fr 1fr; gap: var(--st-s-3); margin-top: var(--st-s-2); }
|
||||
.st-toast { animation: toast-in 180ms var(--st-ease); }
|
||||
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } }
|
||||
|
||||
/* ---------------------------------------------------------------- narrow screens */
|
||||
@media (max-width: 1000px) {
|
||||
.metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.monitor-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.st-header { padding: 0 var(--st-s-3); gap: var(--st-s-2); }
|
||||
.st-header .st-tabs { margin-left: 0; }
|
||||
.st-tab { padding: 0 10px; }
|
||||
.st-tab .st-icon { display: none; }
|
||||
#pill-text { display: none; }
|
||||
.chat { padding: var(--st-s-5) var(--st-s-4); }
|
||||
.composer-wrap { padding: 0 var(--st-s-3) var(--st-s-3); }
|
||||
.monitor, .about { padding: var(--st-s-4); }
|
||||
.facts { grid-template-columns: 1fr; gap: 2px; }
|
||||
.facts dd { margin-bottom: var(--st-s-2); }
|
||||
}
|
||||
@@ -0,0 +1,829 @@
|
||||
// serve/web/app.js - the Strata web app: Chat, Monitor, About. No framework, no network beyond this server.
|
||||
// The Monitor tab rebuilds PR #22's dashboard idea (code-martin) on the server's own /metrics.
|
||||
"use strict";
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const SPRITE = "/web/sprite.svg";
|
||||
const icon = (name, cls = "st-icon") => `<svg class="${cls}" aria-hidden="true"><use href="${SPRITE}#i-${name}"/></svg>`;
|
||||
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({"&": "&", "<": "<", ">": ">", '"': """, "'": "'"}[c]));
|
||||
const fmt = (n, d = 0) => (n == null || Number.isNaN(n) ? "–" : Number(n).toLocaleString(undefined, {maximumFractionDigits: d, minimumFractionDigits: d}));
|
||||
const kfmt = (n) => (n == null ? "–" : n >= 1000 ? `${fmt(n / 1000, n >= 10000 ? 0 : 1)}k` : fmt(n));
|
||||
// a context size: 32768 -> "32K" (powers of two), else like kfmt
|
||||
const ctxfmt = (n) => (n && n % 1024 === 0 ? `${fmt(n / 1024)}K` : kfmt(n));
|
||||
const gb = (b, d = 1) => (b == null ? "–" : fmt(b / 1073741824, d)); // memory: binary GB, as Windows shows it
|
||||
|
||||
const store = {
|
||||
get(k, d) { try { const v = localStorage.getItem("strata." + k); return v === null ? d : JSON.parse(v); } catch (e) { return d; } },
|
||||
set(k, v) { try { localStorage.setItem("strata." + k, JSON.stringify(v)); } catch (e) { /* private mode: in memory only */ } },
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------------ toasts
|
||||
function toast(kind, title, text = "", ms = 3500, action = null) {
|
||||
const names = {info: "info", success: "check", warn: "warning", error: "error"};
|
||||
const el = document.createElement("div");
|
||||
el.className = `st-toast st-toast--${kind}`;
|
||||
el.innerHTML = `${icon(names[kind] || "info")}<div><div class="st-toast__title"></div><div class="t-text"></div></div>`;
|
||||
el.querySelector(".st-toast__title").textContent = title;
|
||||
el.querySelector(".t-text").textContent = text;
|
||||
if (action) {
|
||||
const b = document.createElement("button");
|
||||
b.className = "st-btn st-btn--secondary";
|
||||
b.style.height = "32px";
|
||||
b.style.marginLeft = "auto";
|
||||
b.textContent = action.label;
|
||||
b.onclick = () => { action.run(); el.remove(); };
|
||||
el.appendChild(b);
|
||||
}
|
||||
$("toasts").appendChild(el);
|
||||
setTimeout(() => el.remove(), ms);
|
||||
}
|
||||
|
||||
async function copyText(text, btn) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (e) { // http on another host: no async clipboard
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); ta.remove();
|
||||
}
|
||||
if (btn) {
|
||||
const use = btn.querySelector("use");
|
||||
use.setAttribute("href", `${SPRITE}#i-check`);
|
||||
setTimeout(() => use.setAttribute("href", `${SPRITE}#i-copy`), 1500);
|
||||
}
|
||||
toast("success", "Copied to clipboard", "", 1800);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ theme and tabs
|
||||
// the system's theme until the user picks one (only a click is saved)
|
||||
function setTheme(t, save) {
|
||||
document.documentElement.dataset.theme = t;
|
||||
if (save) try { localStorage.setItem("strata.theme", t); } catch (e) { /* ignore */ }
|
||||
$("theme-icon").setAttribute("href", `${SPRITE}#i-${t === "dark" ? "sun" : "moon"}`);
|
||||
$("dark-toggle").setAttribute("aria-checked", String(t === "dark"));
|
||||
}
|
||||
const flipTheme = () => setTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark", true);
|
||||
$("theme-btn").onclick = flipTheme;
|
||||
$("dark-toggle").onclick = flipTheme;
|
||||
setTheme(document.documentElement.dataset.theme || "light", false);
|
||||
matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem("strata.theme"); } catch (err) { /* ignore */ }
|
||||
if (!saved) setTheme(e.matches ? "dark" : "light", false);
|
||||
});
|
||||
|
||||
let tab = "chat";
|
||||
function showTab(name) {
|
||||
tab = ["chat", "monitor", "about"].includes(name) ? name : "chat";
|
||||
for (const b of document.querySelectorAll(".st-tab")) b.setAttribute("aria-selected", String(b.dataset.tab === tab));
|
||||
for (const v of ["chat", "monitor", "about"]) $(`view-${v}`).hidden = v !== tab;
|
||||
if (location.hash.slice(1) !== tab) history.replaceState(null, "", tab === "chat" ? location.pathname : `#${tab}`);
|
||||
if (tab === "chat") $("input").focus();
|
||||
if (lastMetrics) render(lastMetrics);
|
||||
}
|
||||
for (const b of document.querySelectorAll(".st-tab")) b.onclick = () => showTab(b.dataset.tab);
|
||||
window.addEventListener("hashchange", () => showTab(location.hash.slice(1)));
|
||||
|
||||
// ------------------------------------------------------------------ server access
|
||||
function headers(json = false) {
|
||||
const h = {};
|
||||
const key = store.get("apikey", "");
|
||||
if (key) h.Authorization = "Bearer " + key;
|
||||
if (json) h["Content-Type"] = "application/json";
|
||||
return h;
|
||||
}
|
||||
$("api-key").value = store.get("apikey", "");
|
||||
$("api-key").onchange = () => { store.set("apikey", $("api-key").value.trim()); toast("success", "API key saved", "Kept in this browser only."); };
|
||||
|
||||
let health = {model: "strata", images: false, max_context: 0};
|
||||
async function loadHealth() {
|
||||
try {
|
||||
health = await (await fetch("/health")).json();
|
||||
$("attach-btn").title = health.images ? "Attach a text file or a picture (or drop it here)"
|
||||
: "Attach a text file (or drop it here)";
|
||||
$("chat-empty-sub").textContent = `${health.model} runs on this PC. Nothing leaves it.`;
|
||||
} catch (e) {
|
||||
setTimeout(loadHealth, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Monitor
|
||||
const METRICS = [
|
||||
{key: "speed", label: "Speed", icon: "gauge", unit: "tok/s", series: "tok_s"},
|
||||
{key: "gpu", label: "GPU load", icon: "gpu", unit: "%", series: "gpu_util", max: 100},
|
||||
{key: "vram", label: "VRAM", icon: "layers", unit: "GB", series: "gpu_mem_used"},
|
||||
{key: "temp", label: "GPU temp", icon: "thermometer", unit: "°C", series: "gpu_temp", tone: "warn"},
|
||||
{key: "power", label: "Power", icon: "bolt", unit: "W", series: "gpu_power"},
|
||||
{key: "pcie", label: "PCIe", icon: "link", unit: "", series: "gpu_pcie_rx_mb", tone: "info"},
|
||||
{key: "cpu", label: "CPU", icon: "cpu", unit: "%", series: "cpu", max: 100},
|
||||
{key: "disk", label: "Disk read", icon: "disk", unit: "MB/s", series: "disk_read_mb", tone: "info"},
|
||||
];
|
||||
$("metrics").innerHTML = METRICS.map((m) => `
|
||||
<div class="st-card metric-card"><div class="st-metric">
|
||||
<span class="st-metric__label">${icon(m.icon, "st-icon st-icon--sm")}${esc(m.label)}</span>
|
||||
<span class="st-metric__value" id="mv-${m.key}">–</span>
|
||||
<span class="st-metric__sub" id="ms-${m.key}"></span>
|
||||
<svg class="st-metric__spark" id="sp-${m.key}" viewBox="0 0 100 32" preserveAspectRatio="none"${m.tone ? ` data-tone="${m.tone}"` : ""}>
|
||||
<path class="area" fill="currentColor" opacity=".12"/><path class="line" fill="none" stroke="currentColor"
|
||||
stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke"/></svg>
|
||||
</div></div>`).join("");
|
||||
|
||||
function spark(id, values, max) {
|
||||
const svg = $(id);
|
||||
const v = (values || []).map((x) => (x == null ? 0 : x));
|
||||
if (v.length < 2) { svg.querySelector(".line").setAttribute("d", ""); svg.querySelector(".area").setAttribute("d", ""); return; }
|
||||
const top = Math.max(max || 0, ...v, 1e-9);
|
||||
const pts = v.map((x, i) => [(i / (v.length - 1)) * 100, 30 - (x / top) * 26]);
|
||||
const line = pts.map((p, i) => `${i ? "L" : "M"}${p[0].toFixed(2)},${p[1].toFixed(2)}`).join("");
|
||||
svg.querySelector(".line").setAttribute("d", line);
|
||||
svg.querySelector(".area").setAttribute("d", `${line}L100,32L0,32Z`);
|
||||
}
|
||||
function setMetric(key, value, unit, sub) {
|
||||
$(`mv-${key}`).innerHTML = value == null ? "–" : `${esc(value)}${unit ? `<small>${esc(unit)}</small>` : ""}`;
|
||||
$(`ms-${key}`).textContent = sub || "";
|
||||
}
|
||||
|
||||
let lastMetrics = null, metricsFailures = 0, keyWarned = false;
|
||||
let reqShowAll = false; // the Monitor's request table: the last 12, or every one the server keeps (issue #35)
|
||||
async function poll() {
|
||||
try {
|
||||
const r = await fetch(reqShowAll ? "/metrics?requests=all" : "/metrics", {headers: headers()});
|
||||
if (r.status === 401) {
|
||||
setPill("error", "API key needed");
|
||||
if (!keyWarned) { keyWarned = true; toast("warn", "API key needed", "This server needs a key: add it under About > Settings.", 6000); }
|
||||
} else if (r.ok) {
|
||||
lastMetrics = await r.json();
|
||||
metricsFailures = 0;
|
||||
render(lastMetrics);
|
||||
} else {
|
||||
throw new Error(`HTTP ${r.status}`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (++metricsFailures === 3) setPill("error", "Server not reachable");
|
||||
}
|
||||
setTimeout(poll, 1000);
|
||||
}
|
||||
|
||||
function setPill(state, text) {
|
||||
$("pill").dataset.state = state === "error" ? "queued" : state;
|
||||
$("pill-text").textContent = text;
|
||||
}
|
||||
|
||||
function render(m) {
|
||||
const live = m.live || {}, hw = m.hardware || {}, st = m.hardware_static || {}, eng = m.engine || {}, h = m.history || {};
|
||||
const last = (m.requests || [])[0];
|
||||
// the header pill
|
||||
if (live.state === "reading") {
|
||||
const pct = live.prompt_total ? Math.round((100 * live.prompt_read) / live.prompt_total) : null;
|
||||
setPill("reading", pct != null ? `Reading prompt · ${pct}%` : "Reading prompt");
|
||||
} else if (live.state === "generating") {
|
||||
setPill("generating", `Generating · ${fmt(live.tok_s, 1)} tok/s`);
|
||||
} else {
|
||||
setPill("idle", "Idle");
|
||||
}
|
||||
if (live.queued > 0) setPill("queued", `${live.queued} queued`);
|
||||
if (tab === "monitor") renderMonitor(live, hw, st, eng, h, last, m.requests || [], m.totals, m.requests_kept);
|
||||
if (tab === "about") renderAbout(eng, hw, st);
|
||||
}
|
||||
|
||||
function renderTotals(t) {
|
||||
if (!t || !t.requests) return "";
|
||||
const since = new Date(t.since * 1000).toLocaleString([], {weekday: "short", hour: "2-digit", minute: "2-digit"});
|
||||
const read = t.prompt_tokens - t.reused;
|
||||
const pSpeed = t.prompt_ms > 0 && read > 0 ? ` at ${fmt(read / (t.prompt_ms / 1000))} tok/s` : "";
|
||||
const oSpeed = t.decode_ms > 0 && t.output_tokens > 0 ? ` at ${fmt(t.output_tokens / (t.decode_ms / 1000), 1)} tok/s` : "";
|
||||
return `Since ${since}: ${fmt(t.requests)} requests · ${fmt(read)} prompt tokens read${pSpeed} (${fmt(t.reused)} reused) · ` +
|
||||
`${fmt(t.output_tokens)} written${oSpeed}`;
|
||||
}
|
||||
function renderMonitor(live, hw, st, eng, h, last, requests, totals, kept) {
|
||||
// model state
|
||||
const on = live.queued > 0 ? "queued" : live.state;
|
||||
for (const b of document.querySelectorAll("#state-badges .st-badge")) b.classList.toggle("on", b.dataset.s === on || b.dataset.s === live.state);
|
||||
const prog = $("state-progress");
|
||||
let label = "Waiting for a request", detail = "", pct = 0;
|
||||
if (live.state === "reading") {
|
||||
label = "Reading prompt";
|
||||
prog.dataset.tone = "info";
|
||||
if (live.prompt_total) {
|
||||
pct = (100 * live.prompt_read) / live.prompt_total;
|
||||
detail = `${fmt(live.prompt_read)} / ${fmt(live.prompt_total)} tokens · ${fmt(pct)}%`;
|
||||
} else {
|
||||
detail = `${fmt(live.prompt_tokens)} tokens`;
|
||||
}
|
||||
} else if (live.state === "generating") {
|
||||
label = live.phase ? live.phase[0].toUpperCase() + live.phase.slice(1) : "Generating";
|
||||
delete prog.dataset.tone;
|
||||
pct = live.max_tokens ? Math.min(100, (100 * live.generated) / live.max_tokens) : 0;
|
||||
detail = `${fmt(live.generated)} tokens · ${fmt(live.tok_s, 1)} tok/s`;
|
||||
} else if (last) {
|
||||
delete prog.dataset.tone;
|
||||
detail = `last: ${fmt(last.output_tokens)} tokens${last.decode_tok_s ? ` at ${fmt(last.decode_tok_s, 1)} tok/s` : ""}`;
|
||||
}
|
||||
$("state-label").textContent = label;
|
||||
$("state-detail").textContent = detail;
|
||||
$("state-bar").style.width = `${pct}%`;
|
||||
|
||||
// the eight cards
|
||||
const speed = live.state === "generating" ? live.tok_s : last ? last.decode_tok_s : null;
|
||||
setMetric("speed", speed == null ? null : fmt(speed, 1), "tok/s", live.state === "generating" ? "now" : last ? "last request" : "");
|
||||
spark("sp-speed", h.tok_s);
|
||||
setMetric("gpu", hw.gpu_util == null ? null : fmt(hw.gpu_util), "%", st.gpu_name || "");
|
||||
spark("sp-gpu", h.gpu_util, 100);
|
||||
setMetric("vram", hw.gpu_mem_used == null ? null : gb(hw.gpu_mem_used), hw.gpu_mem_total ? `/ ${gb(hw.gpu_mem_total, 0)} GB` : "GB",
|
||||
eng.expert_slots ? `${fmt(eng.expert_slots)} experts cached` : "");
|
||||
spark("sp-vram", h.gpu_mem_used, hw.gpu_mem_total);
|
||||
setMetric("temp", hw.gpu_temp == null ? null : fmt(hw.gpu_temp), "°C", "");
|
||||
spark("sp-temp", h.gpu_temp, 90);
|
||||
setMetric("power", hw.gpu_power == null ? null : fmt(hw.gpu_power), "W", hw.gpu_power_limit ? `of ${fmt(hw.gpu_power_limit)} W limit` : "");
|
||||
spark("sp-power", h.gpu_power, hw.gpu_power_limit);
|
||||
const gen = hw.gpu_pcie_gen_max || hw.gpu_pcie_gen;
|
||||
setMetric("pcie", gen ? `Gen${gen}` : null, hw.gpu_pcie_width ? `x${hw.gpu_pcie_width}` : "",
|
||||
hw.gpu_pcie_rx_mb == null ? "" : `to GPU ${fmt(hw.gpu_pcie_rx_mb, hw.gpu_pcie_rx_mb < 10 ? 1 : 0)} MB/s` +
|
||||
(hw.gpu_pcie_gen && gen && hw.gpu_pcie_gen < gen ? ` · idle Gen${hw.gpu_pcie_gen}` : ""));
|
||||
spark("sp-pcie", h.gpu_pcie_rx_mb);
|
||||
setMetric("cpu", hw.cpu == null ? null : fmt(hw.cpu), "%", st.threads ? `${st.cores ? `${st.cores} cores · ` : ""}${st.threads} threads` : "");
|
||||
spark("sp-cpu", h.cpu, 100);
|
||||
if (hw.disk_read_mb == null) {
|
||||
setMetric("disk", null, "", st.psutil ? "" : "needs psutil (setup installs it)");
|
||||
} else {
|
||||
const big = hw.disk_read_mb >= 1000;
|
||||
setMetric("disk", big ? fmt(hw.disk_read_mb / 1024, 2) : fmt(hw.disk_read_mb, hw.disk_read_mb < 10 ? 1 : 0), big ? "GB/s" : "MB/s",
|
||||
hw.disk_write_mb == null ? "" : `write ${fmt(hw.disk_write_mb, 1)} MB/s`);
|
||||
}
|
||||
spark("sp-disk", h.disk_read_mb);
|
||||
|
||||
// context fill: the running request, else the last one
|
||||
const ctx = eng.max_context || 0;
|
||||
let used = 0;
|
||||
if (live.state !== "idle") used = (live.prompt_tokens || 0) + (live.generated || 0);
|
||||
else if (last) used = (last.prompt_tokens || 0) + (last.output_tokens || 0);
|
||||
const frac = ctx ? Math.min(1, used / ctx) : 0;
|
||||
$("ctx-fill").setAttribute("stroke-dasharray", `${(235.6 * frac).toFixed(1)} 314.2`);
|
||||
$("ctx-fill").style.opacity = 235.6 * frac >= 3 ? "1" : "0"; // a near-zero arc would draw just its round cap
|
||||
$("ctx-pct").textContent = `${Math.round(frac * 100)}%`;
|
||||
$("ctx-sub").textContent = ctx ? `${kfmt(used)} / ${ctxfmt(ctx)}` : "–";
|
||||
const cacheBytes = (eng.expert_cache_mib || 0) * 1048576;
|
||||
$("slots-text").textContent = eng.expert_slots ? `${fmt(eng.expert_slots)} · ${gb(cacheBytes)} GB` : "–";
|
||||
$("slots-bar").style.width = hw.gpu_mem_total ? `${Math.min(100, (100 * cacheBytes) / hw.gpu_mem_total)}%` : "0%";
|
||||
$("ram-text").textContent = hw.ram_total ? `${gb(hw.ram_used)} / ${gb(hw.ram_total, 0)} GB` : "–";
|
||||
const ramPct = hw.ram_total ? (100 * hw.ram_used) / hw.ram_total : 0;
|
||||
$("ram-bar").style.width = `${ramPct}%`;
|
||||
if (ramPct > 92) $("ram-progress").dataset.tone = "danger"; else delete $("ram-progress").dataset.tone;
|
||||
$("temp-text").textContent = hw.gpu_temp == null ? "–" : `${fmt(hw.gpu_temp)} °C`;
|
||||
$("temp-bar").style.width = hw.gpu_temp == null ? "0%" : `${Math.min(100, hw.gpu_temp)}%`;
|
||||
|
||||
// recent requests
|
||||
const body = $("req-body");
|
||||
if (!requests.length) {
|
||||
body.innerHTML = `<tr><td colspan="7" class="muted">No requests yet</td></tr>`;
|
||||
} else {
|
||||
const badge = {stop: ["", "Done"], length: ["", "Max tokens"], cancel: ["st-badge--queued", "Stopped"],
|
||||
disconnect: ["st-badge--queued", "Closed"], error: ["st-badge--error", "Error"]};
|
||||
body.innerHTML = requests.slice(0, reqShowAll ? requests.length : 12).map((r) => {
|
||||
const [cls, text] = badge[r.finish] || ["", r.finish || "–"];
|
||||
const t = new Date(r.time * 1000).toLocaleTimeString([], {hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
||||
const proj = r.projection == null ? "" : ` <span class="st-badge${r.projection ? " st-badge--reading" : ""}" title="experimental speed projection ${r.projection ? "on" : "off"}">${r.projection ? "ESP" : "stock"}</span>`;
|
||||
return `<tr><td>${esc(t)}</td><td><span class="st-badge ${cls}">${esc(text)}</span>${proj}</td><td class="num">${fmt(r.prompt_tokens)}</td>
|
||||
<td class="num">${fmt(r.reused)}</td><td class="num">${fmt(r.output_tokens)}</td><td class="num">${fmt(r.decode_tok_s, 1)}</td>
|
||||
<td class="num">${fmt(r.duration_s, 1)} s</td></tr>`;
|
||||
}).join("");
|
||||
}
|
||||
const all = $("req-all");
|
||||
kept = kept == null ? requests.length : kept;
|
||||
all.hidden = kept <= 12;
|
||||
all.textContent = reqShowAll ? "Show fewer" : `Show all (${kept})`;
|
||||
$("req-wrap").classList.toggle("all", reqShowAll);
|
||||
$("req-totals").textContent = renderTotals(totals);
|
||||
}
|
||||
|
||||
function facts(el, rows) {
|
||||
el.innerHTML = rows.filter((r) => r[1] != null && r[1] !== "").map(([k, v, copy]) =>
|
||||
`<dt>${esc(k)}</dt><dd>${copy ? `<code>${esc(v)}</code><button class="st-btn st-btn--icon" data-copy="${esc(v)}" aria-label="Copy">${icon("copy")}</button>` : esc(v)}</dd>`).join("");
|
||||
}
|
||||
// INFO cvec=project:4-44[:singleL] | add:A-B | 0
|
||||
function projectionText(c) {
|
||||
if (!c || c === "0" || c === 0) return null;
|
||||
const [mode, range, single] = String(c).split(":");
|
||||
const [a, b] = (range || "").split("-");
|
||||
return `${mode === "project" ? "Projection" : "Additive"} control vector on layers ${a}–${b}` +
|
||||
`${single ? ` (layer ${single.replace("single", "")}'s direction)` : ""}. Per chat in Sampling. Its package ` +
|
||||
"describes the vector as a refusal-direction projection; measure the speed yourself";
|
||||
}
|
||||
function renderAbout(eng, hw, st) {
|
||||
const kv = {int8: "8-bit", q4_0: "4-bit (Hadamard-rotated)", fp16: "16-bit"}[eng.kv] || eng.kv;
|
||||
facts($("facts-engine"), [
|
||||
["Model", eng.model],
|
||||
["Engine", eng.version ? `v${eng.version}` : "built from source"],
|
||||
["Context", eng.max_context ? `${fmt(eng.max_context)} tokens` : null],
|
||||
["KV cache", kv ? `${kv}${eng.kv_resident ? `, streamed: ${fmt(eng.kv_resident)} positions per layer in VRAM, the rest in RAM` : ", all in VRAM"}` : null],
|
||||
["Experts in VRAM", eng.expert_slots ? `${fmt(eng.expert_slots)} (${gb((eng.expert_cache_mib || 0) * 1048576)} GB)` : null],
|
||||
["Speculation", eng.spec ? `MTP drafts up to ${Math.max(0, (eng.mtp_max || eng.spec) - 1)} tokens${eng.lookup ? ", prompt lookup on" : ""}` : null],
|
||||
["Images", eng.images ? "on" : "off"],
|
||||
["Experimental speed projection", projectionText(eng.cvec)],
|
||||
]);
|
||||
facts($("facts-hw"), [
|
||||
["GPU", st.gpu_name ? `${st.gpu_name}${hw.gpu_mem_total ? `, ${gb(hw.gpu_mem_total, 0)} GB` : ""}` : "not readable (NVML)"],
|
||||
["CPU", st.cpu_name ? `${st.cpu_name}${st.threads ? `, ${st.threads} threads` : ""}` : null],
|
||||
["RAM", hw.ram_total ? `${gb(hw.ram_total, 0)} GB` : null],
|
||||
]);
|
||||
const base = location.origin;
|
||||
facts($("facts-api"), [
|
||||
["OpenAI base URL", `${base}/v1`, true],
|
||||
["Anthropic base URL", base, true],
|
||||
["Model name", eng.model, true],
|
||||
]);
|
||||
}
|
||||
document.addEventListener("click", (e) => {
|
||||
const b = e.target.closest("[data-copy]");
|
||||
if (b) copyText(b.dataset.copy, b);
|
||||
});
|
||||
$("req-all").addEventListener("click", () => { reqShowAll = !reqShowAll; if (lastMetrics) render(lastMetrics); });
|
||||
|
||||
// ------------------------------------------------------------------ Markdown (escaped first, then formatted)
|
||||
function inline(s) {
|
||||
const codes = [];
|
||||
s = s.replace(/`([^`\n]+)`/g, (_, c) => { codes.push(c); return `\u0000${codes.length - 1}\u0000`; });
|
||||
s = esc(s)
|
||||
.replace(/\*\*([^*\n]+)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1<em>$2</em>")
|
||||
.replace(/\[([^\]\n]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||
return s.replace(/\u0000(\d+)\u0000/g, (_, i) => `<code class="inline">${esc(codes[+i])}</code>`);
|
||||
}
|
||||
function codeBlock(lang, code) {
|
||||
return `<div class="st-code"><div class="st-code__head"><span>${esc(lang || "code")}</span>` +
|
||||
`<button class="st-btn st-btn--icon" data-code-copy aria-label="Copy code">${icon("copy")}</button></div>` +
|
||||
`<pre><code>${esc(code)}</code></pre></div>`;
|
||||
}
|
||||
function blocks(text) {
|
||||
const out = [], lines = text.split("\n");
|
||||
let para = [], list = null;
|
||||
const flushPara = () => { if (para.length) out.push(`<p>${para.map(inline).join("<br>")}</p>`); para = []; };
|
||||
const flushList = () => { if (list) out.push(`<${list.tag}>${list.items.map((i) => `<li>${inline(i)}</li>`).join("")}</${list.tag}>`); list = null; };
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const l = lines[i];
|
||||
let m;
|
||||
if (!l.trim()) { flushPara(); flushList(); continue; }
|
||||
if ((m = l.match(/^(#{1,6})\s+(.*)$/))) { flushPara(); flushList(); out.push(`<${m[1].length <= 2 ? "h3" : "h4"}>${inline(m[2])}</${m[1].length <= 2 ? "h3" : "h4"}>`); continue; }
|
||||
if (/^\s*([-*_])\s*\1\s*\1[\s\1]*$/.test(l)) { flushPara(); flushList(); out.push("<hr>"); continue; }
|
||||
if ((m = l.match(/^>\s?(.*)$/))) { flushPara(); flushList(); out.push(`<blockquote>${inline(m[1])}</blockquote>`); continue; }
|
||||
if (/^\s*\|.*\|\s*$/.test(l) && i + 1 < lines.length && /^\s*\|?[\s:-]+\|[\s|:-]*$/.test(lines[i + 1])) {
|
||||
flushPara(); flushList();
|
||||
const cells = (row) => row.trim().replace(/^\||\|$/g, "").split("|").map((c) => inline(c.trim()));
|
||||
let html = `<table><thead><tr>${cells(l).map((c) => `<th>${c}</th>`).join("")}</tr></thead><tbody>`;
|
||||
i += 2;
|
||||
while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i])) html += `<tr>${cells(lines[i++]).map((c) => `<td>${c}</td>`).join("")}</tr>`;
|
||||
i--;
|
||||
out.push(html + "</tbody></table>");
|
||||
continue;
|
||||
}
|
||||
if ((m = l.match(/^\s*(?:[-*+]|(\d+)[.)])\s+(.*)$/))) {
|
||||
flushPara();
|
||||
const tag = m[1] ? "ol" : "ul";
|
||||
if (!list || list.tag !== tag) { flushList(); list = {tag, items: []}; }
|
||||
list.items.push(m[2]);
|
||||
continue;
|
||||
}
|
||||
if (list && /^\s{2,}\S/.test(l)) { list.items[list.items.length - 1] += " " + l.trim(); continue; }
|
||||
flushList();
|
||||
para.push(l);
|
||||
}
|
||||
flushPara(); flushList();
|
||||
return out.join("");
|
||||
}
|
||||
function markdown(text) {
|
||||
let html = "", rest = text;
|
||||
for (;;) {
|
||||
const m = rest.match(/(^|\n)```([^\n`]*)\n/);
|
||||
if (!m) { html += blocks(rest); break; }
|
||||
html += blocks(rest.slice(0, m.index));
|
||||
rest = rest.slice(m.index + m[0].length);
|
||||
const end = rest.match(/(^|\n)```[ \t]*(\n|$)/);
|
||||
if (!end) { html += codeBlock(m[2].trim(), rest); break; } // still streaming
|
||||
html += codeBlock(m[2].trim(), rest.slice(0, end.index));
|
||||
rest = rest.slice(end.index + end[0].length);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Chat
|
||||
const DEFAULTS = {thinking: "high", temperature: 0.6, top_p: 0.95, top_k: 20, max: "", seed: "", show: true, esp: true};
|
||||
let settings = {...DEFAULTS, ...store.get("sampling", {})};
|
||||
let messages = store.get("chat", []);
|
||||
let attachments = []; // {name, url}
|
||||
let busy = null; // {controller, msg}
|
||||
|
||||
function saveChat() {
|
||||
store.set("chat", messages.map((m) => ({...m, images: (m.images || []).map((i) => ({name: i.name})),
|
||||
files: (m.files || []).map((f) => ({name: f.name}))})));
|
||||
}
|
||||
function timeStr(t) { return new Date(t).toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); }
|
||||
|
||||
function msgEl(m, i) {
|
||||
const el = document.createElement("div");
|
||||
el.className = `st-msg st-msg--${m.role}`;
|
||||
el.dataset.i = i;
|
||||
if (m.role === "user") {
|
||||
if (m.files && m.files.length) {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "msg-images";
|
||||
for (const f of m.files) {
|
||||
const c = document.createElement("span");
|
||||
c.className = "chip";
|
||||
c.innerHTML = icon("attach", "st-icon st-icon--sm");
|
||||
c.append(f.name);
|
||||
wrap.appendChild(c);
|
||||
}
|
||||
el.appendChild(wrap);
|
||||
}
|
||||
if (m.images && m.images.length) {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "msg-images";
|
||||
for (const im of m.images) {
|
||||
if (im.url) { const img = document.createElement("img"); img.src = im.url; img.alt = im.name || "image"; wrap.appendChild(img); }
|
||||
else { const c = document.createElement("span"); c.className = "chip"; c.innerHTML = icon("image", "st-icon st-icon--sm"); c.append(im.name || "image"); wrap.appendChild(c); }
|
||||
}
|
||||
el.appendChild(wrap);
|
||||
}
|
||||
const b = document.createElement("div");
|
||||
b.className = "st-bubble";
|
||||
b.textContent = m.text;
|
||||
el.appendChild(b);
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "st-msg__meta";
|
||||
meta.textContent = `You · ${timeStr(m.time)}`;
|
||||
el.appendChild(meta);
|
||||
} else {
|
||||
el.innerHTML = `<details class="st-collapse" hidden><summary>${icon("thinking", "st-icon st-icon--sm")}<span class="think-title"></span>` +
|
||||
`${icon("chevron", "st-icon st-icon--sm st-chev")}</summary><div class="st-collapse__body thinking"></div></details>` +
|
||||
`<div class="st-bubble"></div><div class="st-msg__meta"><span class="meta-text"></span>` +
|
||||
`<button class="st-btn st-btn--icon" data-msg-copy aria-label="Copy the answer" title="Copy">${icon("copy")}</button></div>`;
|
||||
updateAssistant(el, m, false);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
function updateAssistant(el, m, streaming) {
|
||||
const det = el.querySelector("details");
|
||||
if (m.reasoning) {
|
||||
det.hidden = false;
|
||||
const thinkingNow = streaming && !m.text;
|
||||
el.querySelector(".think-title").textContent = thinkingNow ? "Thinking…" :
|
||||
m.thinkSecs != null ? `Thought for ${fmt(m.thinkSecs, 1)} s` : "Thoughts";
|
||||
const body = el.querySelector(".thinking");
|
||||
if (det.open || thinkingNow) body.textContent = m.reasoning;
|
||||
else body.dataset.pending = "1";
|
||||
// open while it streams (if wanted), closed once the answer starts - unless the user toggled it themselves
|
||||
if (thinkingNow && settings.show && !det.dataset.touched && !det.open) { det._auto = true; det.open = true; }
|
||||
if (!thinkingNow && det.open && !det.dataset.touched) { det._auto = true; det.open = false; }
|
||||
}
|
||||
const bubble = el.querySelector(".st-bubble");
|
||||
if (m.error) {
|
||||
bubble.innerHTML = `<div class="msg-error"></div>`;
|
||||
bubble.firstChild.textContent = m.error;
|
||||
} else if (!m.text && streaming) {
|
||||
bubble.innerHTML = m.reasoning ? `<span class="muted cursor">Writing</span>` : `<span class="cursor"></span>`;
|
||||
} else {
|
||||
bubble.innerHTML = markdown(m.text || "");
|
||||
if (streaming) bubble.classList.add("cursor"); else bubble.classList.remove("cursor");
|
||||
}
|
||||
el.querySelector(".meta-text").textContent = m.meta || (streaming ? "" : m.stopped ? "Stopped" : "");
|
||||
el.querySelector("[data-msg-copy]").hidden = streaming || !m.text;
|
||||
}
|
||||
function renderChat() {
|
||||
const chat = $("chat");
|
||||
chat.querySelectorAll(".st-msg").forEach((e) => e.remove());
|
||||
$("chat-empty").hidden = messages.length > 0;
|
||||
messages.forEach((m, i) => chat.appendChild(msgEl(m, i)));
|
||||
scrollDown(true);
|
||||
}
|
||||
function nearBottom() { const s = $("chat-scroll"); return s.scrollHeight - s.scrollTop - s.clientHeight < 120; }
|
||||
function scrollDown(force) { const s = $("chat-scroll"); if (force || nearBottom()) s.scrollTop = s.scrollHeight; }
|
||||
|
||||
$("chat").addEventListener("click", (e) => {
|
||||
const cc = e.target.closest("[data-code-copy]");
|
||||
if (cc) { copyText(cc.closest(".st-code").querySelector("pre").textContent, cc); return; }
|
||||
const mc = e.target.closest("[data-msg-copy]");
|
||||
if (mc) { const i = +mc.closest(".st-msg").dataset.i; copyText(messages[i].text, mc); }
|
||||
});
|
||||
$("chat").addEventListener("toggle", (e) => {
|
||||
const d = e.target;
|
||||
if (d.tagName !== "DETAILS") return;
|
||||
if (d._auto) { d._auto = false; return; } // our own open/close, not the user's
|
||||
d.dataset.touched = "1";
|
||||
const body = d.querySelector(".thinking");
|
||||
if (d.open && body.dataset.pending) { body.textContent = messages[+d.closest(".st-msg").dataset.i].reasoning; delete body.dataset.pending; }
|
||||
}, true);
|
||||
|
||||
function apiMessages() {
|
||||
const out = [];
|
||||
for (const m of messages) {
|
||||
if (m.role === "user") {
|
||||
const imgs = (m.images || []).filter((i) => i.url);
|
||||
const text = userText(m);
|
||||
out.push({role: "user", content: imgs.length ? [{type: "text", text},
|
||||
...imgs.map((i) => ({type: "image_url", image_url: {url: i.url}}))] : text});
|
||||
} else if (m.text && !m.error) {
|
||||
out.push({role: "assistant", content: m.text});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function setBusy(on) {
|
||||
$("stop-btn").hidden = !on;
|
||||
$("send-btn").disabled = on;
|
||||
$("composer-hint").textContent = on ? "" : "Shift+Enter: new line";
|
||||
}
|
||||
|
||||
async function send() {
|
||||
const text = $("input").value.trim();
|
||||
if ((!text && !attachments.length) || busy) return;
|
||||
messages.push({role: "user", text, images: attachments.filter((a) => a.kind !== "file"),
|
||||
files: attachments.filter((a) => a.kind === "file"), time: Date.now()});
|
||||
attachments = [];
|
||||
renderAttachments();
|
||||
$("input").value = "";
|
||||
autosize();
|
||||
const m = {role: "assistant", text: "", reasoning: "", time: Date.now()};
|
||||
messages.push(m);
|
||||
renderChat();
|
||||
const el = $("chat").lastElementChild;
|
||||
const controller = new AbortController();
|
||||
busy = {controller, msg: m};
|
||||
setBusy(true);
|
||||
|
||||
const body = {model: health.model, messages: apiMessages(), stream: true,
|
||||
reasoning_effort: settings.thinking};
|
||||
if (settings.temperature > 0) {
|
||||
Object.assign(body, {temperature: +settings.temperature, top_p: +settings.top_p, top_k: +settings.top_k});
|
||||
} else {
|
||||
body.temperature = 0;
|
||||
}
|
||||
if (settings.seed) body.seed = +settings.seed;
|
||||
if (settings.max) body.max_tokens = +settings.max;
|
||||
if (projectionLoaded()) body.experimental_speed_projection = !!settings.esp;
|
||||
|
||||
let firstAt = null, thinkStart = null, usage = null, frame = 0;
|
||||
const paint = () => { frame = 0; updateAssistant(el, m, true); scrollDown(); };
|
||||
try {
|
||||
const r = await fetch("/v1/chat/completions", {method: "POST", headers: headers(true), body: JSON.stringify(body),
|
||||
signal: controller.signal});
|
||||
if (!r.ok) {
|
||||
let msg = `HTTP ${r.status}`;
|
||||
try { msg = (await r.json()).error.message || msg; } catch (e) { /* not json */ }
|
||||
if (r.status === 401) msg = "This server needs an API key: add it under About > Settings.";
|
||||
throw new Error(msg);
|
||||
}
|
||||
const reader = r.body.getReader(), dec = new TextDecoder();
|
||||
let buf = "";
|
||||
for (;;) {
|
||||
const {value, done} = await reader.read();
|
||||
if (done) break;
|
||||
buf += dec.decode(value, {stream: true});
|
||||
let nl;
|
||||
while ((nl = buf.indexOf("\n")) >= 0) {
|
||||
const line = buf.slice(0, nl).trim();
|
||||
buf = buf.slice(nl + 1);
|
||||
if (!line.startsWith("data:")) continue; // ": keep-alive" comments while a long prompt is read
|
||||
const data = line.slice(5).trim();
|
||||
if (data === "[DONE]") continue;
|
||||
let j;
|
||||
try { j = JSON.parse(data); } catch (e) { continue; }
|
||||
if (j.error) throw new Error(j.error.message || "the engine reported an error");
|
||||
if (j.usage) usage = j.usage;
|
||||
const d = (j.choices && j.choices[0] && j.choices[0].delta) || {};
|
||||
if (d.reasoning_content) { if (!firstAt) firstAt = performance.now(); if (!thinkStart) thinkStart = performance.now(); m.reasoning += d.reasoning_content; }
|
||||
if (d.content) {
|
||||
if (!firstAt) firstAt = performance.now();
|
||||
if (thinkStart && m.thinkSecs == null) m.thinkSecs = (performance.now() - thinkStart) / 1000;
|
||||
m.text += d.content;
|
||||
}
|
||||
if (!frame) frame = requestAnimationFrame(paint);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name === "AbortError") m.stopped = true;
|
||||
else { m.error = e.message || String(e); toast("error", "The request failed", m.error, 6000); }
|
||||
}
|
||||
if (thinkStart && m.thinkSecs == null) m.thinkSecs = (performance.now() - thinkStart) / 1000;
|
||||
const n = usage ? usage.completion_tokens : null;
|
||||
if (n && firstAt) {
|
||||
const secs = (performance.now() - firstAt) / 1000;
|
||||
m.meta = `${fmt(n)} tokens${secs > 0.25 ? ` · ${fmt(n / secs, 1)} tok/s` : ""}${m.stopped ? " · stopped" : ""}` +
|
||||
(projectionLoaded() ? (settings.esp ? " · projection on" : " · projection off") : "");
|
||||
} else if (m.stopped) {
|
||||
m.meta = "Stopped";
|
||||
}
|
||||
busy = null;
|
||||
setBusy(false);
|
||||
if (frame) cancelAnimationFrame(frame);
|
||||
updateAssistant(el, m, false);
|
||||
saveChat();
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
$("composer").onsubmit = (e) => { e.preventDefault(); send(); };
|
||||
$("stop-btn").onclick = () => { if (busy) busy.controller.abort(); };
|
||||
$("input").addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); send(); }
|
||||
});
|
||||
function autosize() { const t = $("input"); t.style.height = "auto"; t.style.height = `${Math.min(t.scrollHeight, innerHeight * 0.4)}px`; }
|
||||
$("input").addEventListener("input", autosize);
|
||||
|
||||
$("new-btn").onclick = () => {
|
||||
if (busy) { toast("warn", "Still writing", "Stop the answer first."); return; }
|
||||
if (!messages.length) return;
|
||||
const backup = messages;
|
||||
messages = [];
|
||||
saveChat();
|
||||
renderChat();
|
||||
toast("info", "New chat", "The last one was cleared.", 6000, {label: "Undo", run: () => { messages = backup; saveChat(); renderChat(); }});
|
||||
};
|
||||
$("export-btn").onclick = () => {
|
||||
if (!messages.length) { toast("info", "Nothing to save yet"); return; }
|
||||
const md = messages.map((m) => m.role === "user" ? `## You\n\n${m.text}\n` :
|
||||
`## ${health.model}\n\n${m.reasoning ? `<details><summary>Thinking</summary>\n\n${m.reasoning}\n\n</details>\n\n` : ""}${m.text || m.error || ""}\n`).join("\n");
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(new Blob([md], {type: "text/markdown"}));
|
||||
a.download = `strata-chat-${new Date().toISOString().slice(0, 16).replace(/[:T]/g, "-")}.md`;
|
||||
a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
|
||||
};
|
||||
|
||||
// pictures and text files: the attach button, dropping them on the chat, or pasting a picture (issue #30)
|
||||
const TEXT_EXT = /\.(txt|md|markdown|rst|tex|py|pyi|ipynb|js|mjs|cjs|ts|tsx|jsx|vue|svelte|json|jsonl|csv|tsv|log|ya?ml|toml|ini|cfg|conf|env|xml|html?|css|scss|less|c|cc|cpp|cxx|h|hh|hpp|cu|cuh|rs|go|java|kt|kts|swift|rb|php|pl|lua|r|jl|scala|sql|sh|bash|zsh|fish|ps1|psm1|bat|cmd|diff|patch|gradle|cmake|mk|dockerfile|gitignore|proto|graphql)$/i;
|
||||
const MAX_TEXT_FILE = 512 * 1024;
|
||||
function isTextFile(f) {
|
||||
return f.type.startsWith("text/") || /json|xml|javascript|yaml|toml|x-sh|x-python/.test(f.type) ||
|
||||
TEXT_EXT.test(f.name) || /(^|[\\/])(makefile|dockerfile|readme|license)$/i.test(f.name);
|
||||
}
|
||||
function addFiles(files) {
|
||||
for (const f of files) {
|
||||
if (f.type.startsWith("image/")) {
|
||||
if (!health.images) { toast("warn", "Pictures are off", "This model was set up for text only."); continue; }
|
||||
if (f.size > 20e6) { toast("warn", "Picture too large", `${f.name} is over 20 MB.`); continue; }
|
||||
const r = new FileReader();
|
||||
r.onload = () => { attachments.push({kind: "image", name: f.name || "pasted image", url: r.result}); renderAttachments(); };
|
||||
r.readAsDataURL(f);
|
||||
continue;
|
||||
}
|
||||
if (!isTextFile(f)) { toast("warn", "Not a text file", `${f.name}: attach text files (code, notes, logs, data)${health.images ? " or pictures" : ""}.`); continue; }
|
||||
if (f.size > MAX_TEXT_FILE) { toast("warn", "File too large", `${f.name} is over 512 KB.`); continue; }
|
||||
const r = new FileReader();
|
||||
r.onload = () => {
|
||||
const text = String(r.result);
|
||||
if (text.includes("\u0000")) { toast("warn", "Not a text file", `${f.name} looks like a binary file.`); return; }
|
||||
attachments.push({kind: "file", name: f.name, text});
|
||||
renderAttachments();
|
||||
};
|
||||
r.readAsText(f);
|
||||
}
|
||||
}
|
||||
// a file's text in the message, fenced with more backticks than it contains itself
|
||||
function fileBlock(f) {
|
||||
const longest = Math.max(2, ...(f.text.match(/`+/g) || []).map((s) => s.length));
|
||||
const fence = "`".repeat(longest + 1);
|
||||
return `File: ${f.name}\n${fence}\n${f.text}\n${fence}`;
|
||||
}
|
||||
function userText(m) {
|
||||
const files = (m.files || []).filter((f) => f.text != null);
|
||||
return [m.text, ...files.map(fileBlock)].filter((s) => s).join("\n\n");
|
||||
}
|
||||
function renderAttachments() {
|
||||
const box = $("attachments");
|
||||
box.hidden = !attachments.length;
|
||||
box.innerHTML = "";
|
||||
attachments.forEach((a, i) => {
|
||||
const c = document.createElement("span");
|
||||
c.className = "chip";
|
||||
c.innerHTML = icon(a.kind === "file" ? "attach" : "image", "st-icon st-icon--sm");
|
||||
c.append(a.name);
|
||||
const x = document.createElement("button");
|
||||
x.type = "button"; x.className = "st-btn st-btn--icon"; x.setAttribute("aria-label", "Remove");
|
||||
x.innerHTML = icon("trash");
|
||||
x.onclick = () => { attachments.splice(i, 1); renderAttachments(); };
|
||||
c.appendChild(x);
|
||||
box.appendChild(c);
|
||||
});
|
||||
}
|
||||
$("attach-btn").onclick = () => $("file").click();
|
||||
// drop files on the chat or the message box
|
||||
for (const id of ["chat", "composer"]) {
|
||||
const el = $(id);
|
||||
el.addEventListener("dragover", (e) => {
|
||||
if (![...(e.dataTransfer || {}).types || []].includes("Files")) return;
|
||||
e.preventDefault();
|
||||
$("composer").classList.add("dragging");
|
||||
});
|
||||
el.addEventListener("dragleave", () => $("composer").classList.remove("dragging"));
|
||||
el.addEventListener("drop", (e) => {
|
||||
$("composer").classList.remove("dragging");
|
||||
if (!e.dataTransfer || !e.dataTransfer.files.length) return;
|
||||
e.preventDefault();
|
||||
addFiles(e.dataTransfer.files);
|
||||
$("input").focus();
|
||||
});
|
||||
}
|
||||
$("file").onchange = () => { addFiles($("file").files); $("file").value = ""; };
|
||||
$("input").addEventListener("paste", (e) => {
|
||||
if (!health.images) return;
|
||||
const files = [...(e.clipboardData || {}).files || []].filter((f) => f.type.startsWith("image/"));
|
||||
if (files.length) { e.preventDefault(); addFiles(files); }
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ the sampling drawer
|
||||
function openDrawer(open) {
|
||||
$("drawer").dataset.open = String(open);
|
||||
$("drawer").setAttribute("aria-hidden", String(!open));
|
||||
$("scrim").hidden = !open;
|
||||
if (open) { loadDrawer(); loadShared(); }
|
||||
}
|
||||
function loadDrawer(s = settings) {
|
||||
for (const b of $("s-thinking").children) b.setAttribute("aria-checked", String(b.dataset.v === s.thinking));
|
||||
$("s-temp").value = s.temperature; $("s-topp").value = s.top_p; $("s-topk").value = s.top_k;
|
||||
$("s-max").value = s.max; $("s-seed").value = s.seed;
|
||||
$("s-show").setAttribute("aria-checked", String(!!s.show));
|
||||
$("s-esp").setAttribute("aria-checked", String(s.esp !== false));
|
||||
$("esp-row").hidden = !projectionLoaded();
|
||||
$("s-share").setAttribute("aria-checked", String(sharedOn));
|
||||
outputs();
|
||||
}
|
||||
// "Use for other apps too": the server keeps these settings as every client's defaults (GET/POST /settings)
|
||||
let sharedOn = false;
|
||||
async function loadShared() {
|
||||
try {
|
||||
const r = await fetch("/settings", {headers: headers()});
|
||||
if (r.ok) sharedOn = !!(await r.json()).shared;
|
||||
} catch (e) { /* an older server: the switch just stays off */ }
|
||||
$("s-share").setAttribute("aria-checked", String(sharedOn));
|
||||
}
|
||||
function sharedDefaults(s) {
|
||||
const d = {reasoning_effort: s.thinking, temperature: +s.temperature};
|
||||
if (+s.temperature > 0) Object.assign(d, {top_p: +s.top_p, top_k: +s.top_k});
|
||||
if (s.seed) d.seed = +s.seed;
|
||||
if (s.max) d.max_tokens = +s.max;
|
||||
if (projectionLoaded()) d.experimental_speed_projection = s.esp !== false;
|
||||
return d;
|
||||
}
|
||||
async function saveShared(on, s) {
|
||||
const r = await fetch("/settings", {method: "POST", headers: headers(true),
|
||||
body: JSON.stringify({defaults: on ? sharedDefaults(s) : null})});
|
||||
if (!r.ok) {
|
||||
let msg = `HTTP ${r.status}`;
|
||||
try { msg = (await r.json()).error.message || msg; } catch (e) { /* not json */ }
|
||||
throw new Error(msg);
|
||||
}
|
||||
sharedOn = !!(await r.json()).shared;
|
||||
}
|
||||
// the engine was started with the experimental-speed-projection control vector (INFO cvec=...)
|
||||
function projectionLoaded() {
|
||||
const c = lastMetrics && lastMetrics.engine ? lastMetrics.engine.cvec : 0;
|
||||
return !!c && c !== "0";
|
||||
}
|
||||
function outputs() {
|
||||
const t = +$("s-temp").value;
|
||||
$("o-temp").textContent = t === 0 ? "0 · greedy" : t.toFixed(2);
|
||||
$("o-topp").textContent = (+$("s-topp").value).toFixed(2);
|
||||
$("o-topk").textContent = $("s-topk").value;
|
||||
const sel = [...$("s-thinking").children].find((b) => b.getAttribute("aria-checked") === "true");
|
||||
$("o-thinking").textContent = sel ? {none: "answers right away", low: "short", medium: "medium", high: "thorough (default)"}[sel.dataset.v] : "";
|
||||
for (const id of ["s-topp", "s-topk"]) $(id).disabled = t === 0;
|
||||
}
|
||||
for (const b of $("s-thinking").children) b.onclick = () => { for (const x of $("s-thinking").children) x.setAttribute("aria-checked", String(x === b)); outputs(); };
|
||||
for (const id of ["s-temp", "s-topp", "s-topk"]) $(id).oninput = outputs;
|
||||
$("s-show").onclick = () => $("s-show").setAttribute("aria-checked", String($("s-show").getAttribute("aria-checked") !== "true"));
|
||||
$("s-esp").onclick = () => $("s-esp").setAttribute("aria-checked", String($("s-esp").getAttribute("aria-checked") !== "true"));
|
||||
$("s-share").onclick = () => $("s-share").setAttribute("aria-checked", String($("s-share").getAttribute("aria-checked") !== "true"));
|
||||
$("s-reset").onclick = () => loadDrawer(DEFAULTS);
|
||||
$("s-apply").onclick = async () => {
|
||||
const sel = [...$("s-thinking").children].find((b) => b.getAttribute("aria-checked") === "true");
|
||||
settings = {thinking: sel ? sel.dataset.v : "high", temperature: +$("s-temp").value, top_p: +$("s-topp").value,
|
||||
top_k: +$("s-topk").value, max: $("s-max").value.trim(), seed: $("s-seed").value.trim(),
|
||||
show: $("s-show").getAttribute("aria-checked") === "true",
|
||||
esp: $("s-esp").getAttribute("aria-checked") === "true"};
|
||||
store.set("sampling", settings);
|
||||
const share = $("s-share").getAttribute("aria-checked") === "true";
|
||||
openDrawer(false);
|
||||
if (share || sharedOn) {
|
||||
try {
|
||||
await saveShared(share, settings);
|
||||
toast("success", "Sampling saved", share ? "Other apps (omp, API clients) use these settings from their next request."
|
||||
: "Other apps use their own settings again.");
|
||||
} catch (e) {
|
||||
toast("error", "Saved here, but not for other apps", e.message, 6000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
toast("success", "Sampling saved", settings.temperature === 0 ? "Greedy: the same question gives the same answer." : "");
|
||||
};
|
||||
$("sampling-btn").onclick = () => openDrawer(true);
|
||||
$("drawer-close").onclick = () => openDrawer(false);
|
||||
$("scrim").onclick = () => openDrawer(false);
|
||||
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && $("drawer").dataset.open === "true") openDrawer(false); });
|
||||
|
||||
// ------------------------------------------------------------------ start
|
||||
setBusy(false);
|
||||
renderChat();
|
||||
const startQuestion = new URLSearchParams(location.search).get("q"); // /?q=... starts a chat (a shortcut)
|
||||
if (startQuestion) history.replaceState(null, "", location.pathname + location.hash);
|
||||
loadHealth().then(() => { if (startQuestion) { $("input").value = startQuestion; send(); } });
|
||||
showTab(location.hash.slice(1) || "chat");
|
||||
poll();
|
||||
@@ -0,0 +1,128 @@
|
||||
/* Strata UI — components. Requires tokens.css. */
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{margin:0;background:var(--st-bg);color:var(--st-ink);font-family:var(--st-font);font-size:var(--st-fs-base);line-height:var(--st-lh-body);-webkit-font-smoothing:antialiased}
|
||||
a{color:var(--st-accent-text);text-decoration:none} a:hover{text-decoration:underline}
|
||||
:focus-visible{outline:2px solid var(--st-focus);outline-offset:2px}
|
||||
|
||||
/* icon — <svg class="st-icon"><use href="icons/sprite.svg#i-chat"/></svg> */
|
||||
.st-icon{width:20px;height:20px;flex:none;color:inherit}
|
||||
.st-icon--sm{width:16px;height:16px} .st-icon--lg{width:24px;height:24px}
|
||||
|
||||
/* header */
|
||||
.st-header{display:flex;align-items:center;gap:var(--st-s-4);height:60px;padding:0 var(--st-s-6);background:var(--st-surface);border-bottom:1px solid var(--st-line)}
|
||||
.st-header__brand{font-weight:900;font-size:var(--st-fs-lg);letter-spacing:-.01em}
|
||||
.st-header__spacer{flex:1}
|
||||
|
||||
/* status pill */
|
||||
.st-pill{display:inline-flex;align-items:center;gap:8px;height:30px;padding:0 12px;border-radius:var(--st-r-pill);border:1px solid var(--st-line);background:var(--st-surface-2);font-size:var(--st-fs-sm);font-weight:500;color:var(--st-ink-soft);font-variant-numeric:tabular-nums}
|
||||
.st-pill__dot{width:8px;height:8px;border-radius:50%;background:var(--st-ink-muted)}
|
||||
.st-pill[data-state="generating"] .st-pill__dot{background:var(--st-accent);box-shadow:0 0 0 4px var(--st-accent-tint)}
|
||||
.st-pill[data-state="reading"] .st-pill__dot{background:var(--st-info);box-shadow:0 0 0 4px var(--st-info-tint)}
|
||||
.st-pill[data-state="queued"] .st-pill__dot{background:var(--st-warn)}
|
||||
|
||||
/* tabs */
|
||||
.st-tabs{display:flex;gap:4px;padding:4px;background:var(--st-surface-2);border:1px solid var(--st-line);border-radius:var(--st-r-md)}
|
||||
.st-tab{display:inline-flex;align-items:center;gap:8px;height:36px;padding:0 14px;border:0;border-radius:10px;background:transparent;color:var(--st-ink-muted);font:500 var(--st-fs-md)/1 var(--st-font);cursor:pointer}
|
||||
.st-tab:hover{color:var(--st-ink)}
|
||||
.st-tab[aria-selected="true"]{background:var(--st-surface);color:var(--st-ink);font-weight:700;box-shadow:var(--st-sh-card)}
|
||||
|
||||
/* card + metric */
|
||||
.st-card{background:var(--st-surface);border:1px solid var(--st-line);border-radius:var(--st-r-lg);box-shadow:var(--st-sh-card);padding:var(--st-s-5)}
|
||||
.st-metric{display:flex;flex-direction:column;gap:10px}
|
||||
.st-metric__label{display:flex;align-items:center;gap:8px;font-size:var(--st-fs-sm);font-weight:500;color:var(--st-ink-muted)}
|
||||
.st-metric__value{font-weight:900;font-size:var(--st-fs-stat);line-height:1;letter-spacing:-.01em;font-variant-numeric:tabular-nums}
|
||||
.st-metric__unit{font-weight:500;font-size:var(--st-fs-md);color:var(--st-ink-muted);margin-left:4px}
|
||||
.st-metric__spark{width:100%;height:32px;color:var(--st-accent)}
|
||||
|
||||
/* progress + gauge */
|
||||
.st-progress{height:8px;border-radius:var(--st-r-pill);background:var(--st-surface-2);border:1px solid var(--st-line-soft);overflow:hidden}
|
||||
.st-progress__bar{height:100%;border-radius:inherit;background:var(--st-accent);transition:width var(--st-dur) var(--st-ease)}
|
||||
.st-progress[data-tone="info"] .st-progress__bar{background:var(--st-info)}
|
||||
.st-progress[data-tone="warn"] .st-progress__bar{background:var(--st-warn)}
|
||||
.st-progress[data-tone="danger"] .st-progress__bar{background:var(--st-danger)}
|
||||
/* gauge: see README for SVG markup (stroke-dasharray on a 270° arc) */
|
||||
.st-gauge{position:relative;width:120px;height:120px}
|
||||
.st-gauge__track{stroke:var(--st-surface-2)} .st-gauge__fill{stroke:var(--st-accent)}
|
||||
|
||||
/* table */
|
||||
.st-table{width:100%;border-collapse:collapse;font-size:var(--st-fs-md);font-variant-numeric:tabular-nums}
|
||||
.st-table th{text-align:left;font-weight:500;font-size:var(--st-fs-xs);letter-spacing:.04em;text-transform:uppercase;color:var(--st-ink-muted);padding:10px 12px;border-bottom:1px solid var(--st-line)}
|
||||
.st-table td{padding:12px;border-bottom:1px solid var(--st-line-soft);color:var(--st-ink-soft)}
|
||||
.st-table tr:hover td{background:var(--st-surface-2)}
|
||||
.st-table .num{text-align:right}
|
||||
|
||||
/* badges */
|
||||
.st-badge{display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 10px;border-radius:var(--st-r-pill);font-size:var(--st-fs-xs);font-weight:700;background:var(--st-surface-2);color:var(--st-ink-muted);border:1px solid var(--st-line)}
|
||||
.st-badge::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
|
||||
.st-badge--reading{background:var(--st-info-tint);color:var(--st-info-text);border-color:transparent}
|
||||
.st-badge--generating{background:var(--st-accent-tint);color:var(--st-accent-text);border-color:transparent}
|
||||
.st-badge--queued{background:var(--st-warn-tint);color:var(--st-warn-text);border-color:transparent}
|
||||
.st-badge--error{background:var(--st-danger-tint);color:var(--st-danger-text);border-color:transparent}
|
||||
|
||||
/* chat */
|
||||
.st-msg{display:flex;flex-direction:column;gap:6px;max-width:720px}
|
||||
.st-msg--user{align-self:flex-end;align-items:flex-end}
|
||||
.st-bubble{padding:12px 16px;border-radius:var(--st-r-lg);font-size:var(--st-fs-base);line-height:1.55;color:var(--st-ink-soft)}
|
||||
.st-msg--user .st-bubble{background:var(--st-accent);color:var(--st-accent-ink);border-bottom-right-radius:6px}
|
||||
.st-msg--assistant .st-bubble{background:var(--st-surface);border:1px solid var(--st-line);border-bottom-left-radius:6px}
|
||||
.st-msg__meta{font-size:var(--st-fs-xs);color:var(--st-ink-muted);display:flex;gap:8px;align-items:center}
|
||||
|
||||
/* collapsible — <details class="st-collapse"><summary>…</summary><div class="st-collapse__body">…</div></details> */
|
||||
.st-collapse{border:1px solid var(--st-line);border-radius:var(--st-r-md);background:var(--st-surface-2)}
|
||||
.st-collapse>summary{list-style:none;display:flex;align-items:center;gap:8px;padding:10px 14px;cursor:pointer;font-size:var(--st-fs-sm);font-weight:500;color:var(--st-ink-muted)}
|
||||
.st-collapse>summary::-webkit-details-marker{display:none}
|
||||
.st-collapse>summary .st-chev{margin-left:auto;transition:transform var(--st-dur) var(--st-ease)}
|
||||
.st-collapse[open]>summary .st-chev{transform:rotate(180deg)}
|
||||
.st-collapse__body{padding:0 14px 14px;font-size:var(--st-fs-sm);color:var(--st-ink-muted);line-height:1.6}
|
||||
|
||||
/* code */
|
||||
.st-code{border-radius:var(--st-r-md);background:var(--st-code-bg);color:var(--st-code-ink);overflow:hidden}
|
||||
.st-code__head{display:flex;align-items:center;justify-content:space-between;padding:8px 8px 8px 14px;font:500 var(--st-fs-xs)/1 var(--st-font);color:#9aa3ab;border-bottom:1px solid rgba(255,255,255,.06)}
|
||||
.st-code pre{margin:0;padding:14px;overflow:auto;font:400 13px/1.6 var(--st-font-mono)}
|
||||
.st-code .st-btn--icon{color:#9aa3ab} .st-code .st-btn--icon:hover{background:rgba(255,255,255,.08);color:#fff}
|
||||
|
||||
/* composer */
|
||||
.st-composer{display:flex;flex-direction:column;gap:8px;padding:10px;background:var(--st-surface);border:1px solid var(--st-line);border-radius:var(--st-r-lg);box-shadow:var(--st-sh-bar)}
|
||||
.st-composer:focus-within{border-color:var(--st-focus)}
|
||||
.st-composer textarea{border:0;outline:0;resize:none;background:transparent;color:var(--st-ink);font:400 var(--st-fs-base)/1.5 var(--st-font);padding:6px 8px;min-height:48px}
|
||||
.st-composer__bar{display:flex;align-items:center;gap:6px}
|
||||
.st-composer__bar .spacer{flex:1}
|
||||
|
||||
/* drawer */
|
||||
.st-drawer{position:fixed;top:0;right:0;bottom:0;width:360px;max-width:100vw;background:var(--st-surface);border-left:1px solid var(--st-line);box-shadow:var(--st-sh-pop);display:flex;flex-direction:column;transform:translateX(100%);transition:transform 220ms var(--st-ease)}
|
||||
.st-drawer[data-open="true"]{transform:none}
|
||||
.st-drawer__head{display:flex;align-items:center;justify-content:space-between;padding:18px 20px;border-bottom:1px solid var(--st-line)}
|
||||
.st-drawer__body{padding:20px;display:flex;flex-direction:column;gap:22px;overflow:auto}
|
||||
|
||||
/* buttons */
|
||||
.st-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:42px;padding:0 18px;border-radius:var(--st-r-md);border:0;font:700 var(--st-fs-md)/1 var(--st-font);cursor:pointer;transition:background var(--st-dur),transform 120ms}
|
||||
.st-btn:active{transform:translateY(1px)}
|
||||
.st-btn--primary{background:var(--st-accent);color:var(--st-accent-ink);box-shadow:0 6px 16px rgba(16,185,129,.24),inset 0 1px 0 rgba(255,255,255,.18)}
|
||||
.st-btn--primary:hover{background:var(--st-accent-hover)}
|
||||
.st-btn--secondary{background:transparent;color:var(--st-ink);border:1.5px solid var(--st-line)}
|
||||
.st-btn--secondary:hover{background:var(--st-surface-2)}
|
||||
.st-btn--danger{background:var(--st-danger-tint);color:var(--st-danger-text)}
|
||||
.st-btn--icon{width:36px;height:36px;padding:0;border-radius:10px;background:transparent;color:var(--st-ink-muted)}
|
||||
.st-btn--icon:hover{background:var(--st-surface-2);color:var(--st-ink)}
|
||||
.st-btn:disabled{opacity:.45;cursor:not-allowed}
|
||||
|
||||
/* form controls */
|
||||
.st-field{display:flex;flex-direction:column;gap:8px}
|
||||
.st-label{display:flex;justify-content:space-between;font-size:var(--st-fs-sm);font-weight:500;color:var(--st-ink-soft)}
|
||||
.st-label output{font-variant-numeric:tabular-nums;color:var(--st-ink-muted)}
|
||||
.st-input{height:44px;padding:0 14px;border-radius:var(--st-r-md);border:1.5px solid var(--st-line);background:var(--st-surface);color:var(--st-ink);font:400 var(--st-fs-md) var(--st-font)}
|
||||
.st-input:focus{outline:0;border-color:var(--st-focus);box-shadow:0 0 0 3px color-mix(in srgb,var(--st-focus) 20%,transparent)}
|
||||
.st-slider{width:100%;accent-color:var(--st-accent)}
|
||||
.st-toggle{position:relative;width:40px;height:24px;border-radius:var(--st-r-pill);border:0;background:var(--st-line);cursor:pointer;transition:background var(--st-dur)}
|
||||
.st-toggle::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:transform var(--st-dur) var(--st-ease)}
|
||||
.st-toggle[aria-checked="true"]{background:var(--st-accent)}
|
||||
.st-toggle[aria-checked="true"]::after{transform:translateX(16px)}
|
||||
|
||||
/* toast / alert */
|
||||
.st-toast{display:flex;align-items:flex-start;gap:12px;padding:14px 16px;border-radius:var(--st-r-md);background:var(--st-surface);border:1px solid var(--st-line);box-shadow:var(--st-sh-pop);font-size:var(--st-fs-md);color:var(--st-ink-soft);max-width:420px}
|
||||
.st-toast__title{font-weight:700;color:var(--st-ink)}
|
||||
.st-toast--info .st-icon{color:var(--st-info)}
|
||||
.st-toast--success .st-icon{color:var(--st-accent)}
|
||||
.st-toast--warn .st-icon{color:var(--st-warn)}
|
||||
.st-toast--error .st-icon{color:var(--st-danger)}
|
||||
.st-toasts{position:fixed;right:20px;bottom:20px;display:flex;flex-direction:column;gap:10px;z-index:50}
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2021 The Outfit Project Authors (https://github.com/Outfitio/Outfit-Fonts)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,213 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Strata</title>
|
||||
<link rel="stylesheet" href="/web/tokens.css">
|
||||
<link rel="stylesheet" href="/web/components.css">
|
||||
<link rel="stylesheet" href="/web/app.css">
|
||||
<script>
|
||||
// the theme before the first paint: the saved choice, else the system's
|
||||
try {
|
||||
const t = localStorage.getItem("strata.theme") ||
|
||||
(matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
||||
document.documentElement.dataset.theme = t;
|
||||
} catch (e) {}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="st-header">
|
||||
<span class="st-header__brand">Strata</span>
|
||||
<nav class="st-tabs" role="tablist" aria-label="Views">
|
||||
<button class="st-tab" role="tab" id="tab-btn-chat" aria-controls="view-chat" aria-selected="true" data-tab="chat">
|
||||
<svg class="st-icon st-icon--sm"><use href="/web/sprite.svg#i-chat"/></svg>Chat</button>
|
||||
<button class="st-tab" role="tab" id="tab-btn-monitor" aria-controls="view-monitor" aria-selected="false" data-tab="monitor">
|
||||
<svg class="st-icon st-icon--sm"><use href="/web/sprite.svg#i-activity"/></svg>Monitor</button>
|
||||
<button class="st-tab" role="tab" id="tab-btn-about" aria-controls="view-about" aria-selected="false" data-tab="about">
|
||||
<svg class="st-icon st-icon--sm"><use href="/web/sprite.svg#i-info"/></svg>About</button>
|
||||
</nav>
|
||||
<span class="st-header__spacer"></span>
|
||||
<span class="st-pill" id="pill" data-state="idle" role="status" aria-live="polite">
|
||||
<span class="st-pill__dot"></span><span id="pill-text">Connecting…</span></span>
|
||||
<button class="st-btn st-btn--icon" id="theme-btn" title="Light / dark" aria-label="Switch between light and dark">
|
||||
<svg class="st-icon"><use id="theme-icon" href="/web/sprite.svg#i-moon"/></svg></button>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- ============================ Chat ============================ -->
|
||||
<section class="view view--chat" id="view-chat" role="tabpanel" aria-labelledby="tab-btn-chat">
|
||||
<div class="chat-scroll" id="chat-scroll">
|
||||
<div class="chat" id="chat">
|
||||
<div class="chat-empty" id="chat-empty">
|
||||
<div class="chat-empty__title">Ask anything</div>
|
||||
<div class="chat-empty__sub" id="chat-empty-sub">The model runs on this PC. Nothing leaves it.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="composer-wrap">
|
||||
<form class="st-composer" id="composer">
|
||||
<div class="attachments" id="attachments" hidden></div>
|
||||
<textarea id="input" rows="2" placeholder="Ask anything…" aria-label="Message"></textarea>
|
||||
<div class="st-composer__bar">
|
||||
<button type="button" class="st-btn st-btn--icon" id="attach-btn" title="Attach a text file (or drop it here)" aria-label="Attach a file">
|
||||
<svg class="st-icon"><use href="/web/sprite.svg#i-attach"/></svg></button>
|
||||
<input type="file" id="file" multiple hidden>
|
||||
<button type="button" class="st-btn st-btn--icon" id="new-btn" title="New chat" aria-label="New chat">
|
||||
<svg class="st-icon"><use href="/web/sprite.svg#i-new-chat"/></svg></button>
|
||||
<button type="button" class="st-btn st-btn--icon" id="export-btn" title="Save this chat as Markdown" aria-label="Save this chat">
|
||||
<svg class="st-icon"><use href="/web/sprite.svg#i-download"/></svg></button>
|
||||
<button type="button" class="st-btn st-btn--icon" id="sampling-btn" title="Sampling and thinking" aria-label="Sampling and thinking">
|
||||
<svg class="st-icon"><use href="/web/sprite.svg#i-settings"/></svg></button>
|
||||
<span class="spacer"></span>
|
||||
<span class="composer-hint" id="composer-hint"></span>
|
||||
<button type="button" class="st-btn st-btn--secondary" id="stop-btn" hidden>
|
||||
<svg class="st-icon st-icon--sm"><use href="/web/sprite.svg#i-stop"/></svg>Stop</button>
|
||||
<button type="submit" class="st-btn st-btn--primary st-btn--send" id="send-btn" aria-label="Send">
|
||||
<svg class="st-icon"><use href="/web/sprite.svg#i-send"/></svg></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================ Monitor ============================ -->
|
||||
<section class="view view--monitor" id="view-monitor" role="tabpanel" aria-labelledby="tab-btn-monitor" hidden>
|
||||
<div class="monitor">
|
||||
<div class="st-card state-card">
|
||||
<div class="state-card__head">
|
||||
<span class="card-title">Model state</span>
|
||||
<span class="badges" id="state-badges">
|
||||
<span class="st-badge" data-s="idle">Idle</span>
|
||||
<span class="st-badge st-badge--reading" data-s="reading">Reading</span>
|
||||
<span class="st-badge st-badge--generating" data-s="generating">Generating</span>
|
||||
<span class="st-badge st-badge--queued" data-s="queued">Queued</span>
|
||||
<span class="st-badge st-badge--error" data-s="error">Error</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="state-card__row"><span id="state-label">Waiting for a request</span><span class="muted" id="state-detail"></span></div>
|
||||
<div class="st-progress" id="state-progress"><div class="st-progress__bar" id="state-bar" style="width:0%"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="metrics" id="metrics">
|
||||
<!-- filled by app.js: Speed, GPU load, VRAM, GPU temp, Power, PCIe, CPU, Disk read -->
|
||||
</div>
|
||||
|
||||
<div class="monitor-row">
|
||||
<div class="st-card ctx-card">
|
||||
<span class="card-title">Context fill</span>
|
||||
<div class="ctx-gauge">
|
||||
<svg class="st-gauge" viewBox="0 0 120 120" aria-hidden="true">
|
||||
<circle class="st-gauge__track" cx="60" cy="60" r="50" fill="none" stroke-width="10" stroke-linecap="round"
|
||||
stroke-dasharray="235.6 314.2" transform="rotate(135 60 60)"/>
|
||||
<circle class="st-gauge__fill" id="ctx-fill" cx="60" cy="60" r="50" fill="none" stroke-width="10"
|
||||
stroke-linecap="round" stroke-dasharray="0 314.2" transform="rotate(135 60 60)"/>
|
||||
</svg>
|
||||
<div class="ctx-gauge__label"><span class="ctx-gauge__pct" id="ctx-pct">0%</span><span class="muted" id="ctx-sub">–</span></div>
|
||||
</div>
|
||||
<div class="bar-row"><span>Experts in VRAM</span><span class="muted" id="slots-text">–</span></div>
|
||||
<div class="st-progress"><div class="st-progress__bar" id="slots-bar" style="width:0%"></div></div>
|
||||
<div class="bar-row"><span>System RAM</span><span class="muted" id="ram-text">–</span></div>
|
||||
<div class="st-progress" id="ram-progress"><div class="st-progress__bar" id="ram-bar" style="width:0%"></div></div>
|
||||
<div class="bar-row"><span>GPU temperature</span><span class="muted" id="temp-text">–</span></div>
|
||||
<div class="st-progress" data-tone="warn"><div class="st-progress__bar" id="temp-bar" style="width:0%"></div></div>
|
||||
</div>
|
||||
<div class="st-card req-card">
|
||||
<div class="req-head"><span class="card-title">Recent requests</span>
|
||||
<button class="st-btn st-btn--secondary" id="req-all" hidden>Show all</button></div>
|
||||
<div class="table-wrap" id="req-wrap">
|
||||
<table class="st-table">
|
||||
<thead><tr><th>Time</th><th>Status</th><th class="num">Prompt</th><th class="num">Reused</th><th class="num">Output</th>
|
||||
<th class="num">Tok/s</th><th class="num">Duration</th></tr></thead>
|
||||
<tbody id="req-body"><tr><td colspan="7" class="muted">No requests yet</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="muted req-totals" id="req-totals"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================ About ============================ -->
|
||||
<section class="view view--about" id="view-about" role="tabpanel" aria-labelledby="tab-btn-about" hidden>
|
||||
<div class="about">
|
||||
<div class="st-card">
|
||||
<span class="card-title">Model and engine</span>
|
||||
<dl class="facts" id="facts-engine"></dl>
|
||||
</div>
|
||||
<div class="st-card">
|
||||
<span class="card-title">This PC</span>
|
||||
<dl class="facts" id="facts-hw"></dl>
|
||||
</div>
|
||||
<div class="st-card">
|
||||
<span class="card-title">Connect your tools</span>
|
||||
<p class="muted">Any OpenAI- or Anthropic-compatible client works with these addresses.</p>
|
||||
<dl class="facts" id="facts-api"></dl>
|
||||
</div>
|
||||
<div class="st-card">
|
||||
<span class="card-title">Settings</span>
|
||||
<div class="st-field">
|
||||
<label class="st-label" for="api-key">API key <output>only if the server was started with one</output></label>
|
||||
<input class="st-input" id="api-key" type="password" autocomplete="off" placeholder="Not needed">
|
||||
</div>
|
||||
<div class="toggle-row"><span>Dark theme</span>
|
||||
<button class="st-toggle" id="dark-toggle" role="switch" aria-checked="false" aria-label="Dark theme"></button></div>
|
||||
<p class="muted small">Chats, settings and the key are kept in this browser only.
|
||||
<a href="https://github.com/Niko1221/Strata" target="_blank" rel="noopener noreferrer">Strata on GitHub</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- ============================ Sampling drawer ============================ -->
|
||||
<aside class="st-drawer" id="drawer" data-open="false" aria-label="Sampling and thinking" aria-hidden="true">
|
||||
<div class="st-drawer__head"><span class="card-title">Sampling</span>
|
||||
<button class="st-btn st-btn--icon" id="drawer-close" aria-label="Close">
|
||||
<svg class="st-icon"><use href="/web/sprite.svg#i-chevron"/></svg></button></div>
|
||||
<div class="st-drawer__body">
|
||||
<div class="st-field">
|
||||
<label class="st-label" for="s-thinking">Thinking <output id="o-thinking"></output></label>
|
||||
<div class="seg" id="s-thinking" role="radiogroup" aria-label="Thinking">
|
||||
<button type="button" role="radio" data-v="none">Off</button><button type="button" role="radio" data-v="low">Low</button>
|
||||
<button type="button" role="radio" data-v="medium">Medium</button><button type="button" role="radio" data-v="high">High</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="st-field">
|
||||
<label class="st-label" for="s-temp">Temperature <output id="o-temp"></output></label>
|
||||
<input class="st-slider" type="range" id="s-temp" min="0" max="1.5" step="0.05">
|
||||
<span class="muted small">0 = always the most likely word (exact, repeatable)</span>
|
||||
</div>
|
||||
<div class="st-field">
|
||||
<label class="st-label" for="s-topp">Top-p <output id="o-topp"></output></label>
|
||||
<input class="st-slider" type="range" id="s-topp" min="0.05" max="1" step="0.05">
|
||||
</div>
|
||||
<div class="st-field">
|
||||
<label class="st-label" for="s-topk">Top-k <output id="o-topk"></output></label>
|
||||
<input class="st-slider" type="range" id="s-topk" min="1" max="64" step="1">
|
||||
</div>
|
||||
<div class="st-field">
|
||||
<label class="st-label" for="s-max">Max tokens <output>empty = until done</output></label>
|
||||
<input class="st-input" id="s-max" type="number" min="1" step="1" placeholder="Until done">
|
||||
</div>
|
||||
<div class="st-field">
|
||||
<label class="st-label" for="s-seed">Seed <output>empty = random</output></label>
|
||||
<input class="st-input" id="s-seed" type="number" min="1" step="1" placeholder="Random">
|
||||
</div>
|
||||
<div class="toggle-row"><span>Show thinking<br><span class="muted small">expanded while it streams</span></span>
|
||||
<button class="st-toggle" id="s-show" role="switch" aria-checked="true" aria-label="Show thinking"></button></div>
|
||||
<div class="toggle-row" id="esp-row" hidden><span>Experimental speed projection<br><span class="muted small">the
|
||||
engine's control vector; off = the stock model. Switching reads the chat again once</span></span>
|
||||
<button class="st-toggle" id="s-esp" role="switch" aria-checked="true" aria-label="Experimental speed projection"></button></div>
|
||||
<div class="toggle-row"><span>Use for other apps too<br><span class="muted small">omp and other API clients get
|
||||
these settings for anything they don't set themselves</span></span>
|
||||
<button class="st-toggle" id="s-share" role="switch" aria-checked="false" aria-label="Use for other apps too"></button></div>
|
||||
<div class="drawer-actions">
|
||||
<button class="st-btn st-btn--secondary" id="s-reset" type="button">Reset</button>
|
||||
<button class="st-btn st-btn--primary" id="s-apply" type="button">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="scrim" id="scrim" hidden></div>
|
||||
<div class="st-toasts" id="toasts" aria-live="polite"></div>
|
||||
<script src="/web/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" xmlns:c2pa="http://c2pa.org/manifest"><metadata><c2pa:manifest>AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYToyMjRkYzc2Ny1hNTJmLTRhNTgtYmRhNi0wNjNjZjgxZDE0ZDkAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaFpCcvmnJ1xuOVFbOGxO7IQAAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDpjZTVmMTYyMy1hMGY4LTRjNDgtYmQ1MC0yOGRhYTVkZGQ3OWZscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNohRiWe4A1zTyq58r9HwVbdgAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggvnkoUEWEbTHaYh6o+DzKs/DGvSaEkSiXbTkyg+EGZBWkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaPM+mDLLCljfKcnx6K3jLsYAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCDyyT5tVoB8Cn5pGifFieqsDs7yYczybQ255HiPUQvocGRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBh8Zmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjIyNGRjNzY3LWE1MmYtNGE1OC1iZGE2LTA2M2NmODFkMTRkOS9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjYxMTcxNDYxLWZjODUtNDNkYi05NzdkLTVhNTQ0NWMwNzY0OXJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCC+eShQRYRtMdpiHqj4PMqz8Ma9JoSRKJdtOTKD4QZkFaJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggzN40WhHml0Alrpq9IN+inEViO5AnmGuZkyahASHk13qiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFgg1QfqlfahOW+5OQ+LKm+K4e2m36U3CKHsYWKitMx8+Mt0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALine truncated
|
||||
<symbol id="i-chat" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M21 11.5C21 16.194 16.971 20 12 20C10.8 20 9.65 19.78 8.6 19.37L4 20.5L5.2 16.8C3.83 15.34 3 13.5 3 11.5C3 6.806 7.029 3 12 3C16.971 3 21 6.806 21 11.5Z" fill="currentColor"></path><path d="M21 11.5C21 16.194 16.971 20 12 20C10.8 20 9.65 19.78 8.6 19.37L4 20.5L5.2 16.8C3.83 15.34 3 13.5 3 11.5C3 6.806 7.029 3 12 3C16.971 3 21 6.806 21 11.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M8 11.5H8.01M12 11.5H12.01M16 11.5H16.01" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-activity" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M2 12C2 7.286 2 4.929 3.464 3.464C4.929 2 7.286 2 12 2C16.714 2 19.071 2 20.536 3.464C22 4.929 22 7.286 22 12C22 16.714 22 19.071 20.536 20.536C19.071 22 16.714 22 12 22C7.286 22 4.929 22 3.464 20.536C2 19.071 2 16.714 2 12Z" fill="currentColor"></path><path d="M2 12C2 7.286 2 4.929 3.464 3.464C4.929 2 7.286 2 12 2C16.714 2 19.071 2 20.536 3.464C22 4.929 22 7.286 22 12C22 16.714 22 19.071 20.536 20.536C19.071 22 16.714 22 12 22C7.286 22 4.929 22 3.464 20.536C2 19.071 2 16.714 2 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M6 12.5H8.5L10.5 8L13.5 16L15.5 11.5H18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-settings" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M12.22 2H11.78A2 2 0 0 0 9.78 4V4.18A2 2 0 0 1 8.78 5.91L8.35 6.16A2 2 0 0 1 6.35 6.16L6.2 6.08A2 2 0 0 0 3.47 6.81L3.25 7.19A2 2 0 0 0 3.98 9.92L4.13 10.02A2 2 0 0 1 5.13 11.74V12.25A2 2 0 0 1 4.13 13.99L3.98 14.08A2 2 0 0 0 3.25 16.81L3.47 17.19A2 2 0 0 0 6.2 17.92L6.35 17.84A2 2 0 0 1 8.35 17.84L8.78 18.09A2 2 0 0 1 9.78 19.82V20A2 2 0 0 0 11.78 22H12.22A2 2 0 0 0 14.22 20V19.82A2 2 0 0 1 15.22 18.09L15.65 17.84A2 2 0 0 1 17.65 17.84L17.8 17.92A2 2 0 0 0 20.53 17.19L20.75 16.8A2 2 0 0 0 20.02 14.07L19.87 13.99A2 2 0 0 1 18.87 12.25V11.75A2 2 0 0 1 19.87 10.01L20.02 9.92A2 2 0 0 0 20.75 7.19L20.53 6.81A2 2 0 0 0 17.8 6.08L17.65 6.16A2 2 0 0 1 15.65 6.16L15.22 5.91A2 2 0 0 1 14.22 4.18V4A2 2 0 0 0 12.22 2Z" fill="currentColor"></path><path d="M12.22 2H11.78A2 2 0 0 0 9.78 4V4.18A2 2 0 0 1 8.78 5.91L8.35 6.16A2 2 0 0 1 6.35 6.16L6.2 6.08A2 2 0 0 0 3.47 6.81L3.25 7.19A2 2 0 0 0 3.98 9.92L4.13 10.02A2 2 0 0 1 5.13 11.74V12.25A2 2 0 0 1 4.13 13.99L3.98 14.08A2 2 0 0 0 3.25 16.81L3.47 17.19A2 2 0 0 0 6.2 17.92L6.35 17.84A2 2 0 0 1 8.35 17.84L8.78 18.09A2 2 0 0 1 9.78 19.82V20A2 2 0 0 0 11.78 22H12.22A2 2 0 0 0 14.22 20V19.82A2 2 0 0 1 15.22 18.09L15.65 17.84A2 2 0 0 1 17.65 17.84L17.8 17.92A2 2 0 0 0 20.53 17.19L20.75 16.8A2 2 0 0 0 20.02 14.07L19.87 13.99A2 2 0 0 1 18.87 12.25V11.75A2 2 0 0 1 19.87 10.01L20.02 9.92A2 2 0 0 0 20.75 7.19L20.53 6.81A2 2 0 0 0 17.8 6.08L17.65 6.16A2 2 0 0 1 15.65 6.16L15.22 5.91A2 2 0 0 1 14.22 4.18V4A2 2 0 0 0 12.22 2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M15 12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-gpu" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M2 7.5C2 6.12 3.12 5 4.5 5H19.5C20.88 5 22 6.12 22 7.5V15.5C22 16.88 20.88 18 19.5 18H4.5C3.12 18 2 16.88 2 15.5V7.5Z" fill="currentColor"></path><path d="M2 7.5C2 6.12 3.12 5 4.5 5H19.5C20.88 5 22 6.12 22 7.5V15.5C22 16.88 20.88 18 19.5 18H4.5C3.12 18 2 16.88 2 15.5V7.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12.5 11.5C12.5 13.433 10.933 15 9 15C7.067 15 5.5 13.433 5.5 11.5C5.5 9.567 7.067 8 9 8C10.933 8 12.5 9.567 12.5 11.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M16 9V14M19 9V14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M6 18V20.5M10 18V20.5M14 18V20.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M6 9C6 7.343 7.343 6 9 6H15C16.657 6 18 7.343 18 9V15C18 16.657 16.657 18 15 18H9C7.343 18 6 16.657 6 15V9Z" fill="currentColor"></path><path d="M6 9C6 7.343 7.343 6 9 6H15C16.657 6 18 7.343 18 9V15C18 16.657 16.657 18 15 18H9C7.343 18 6 16.657 6 15V9Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M10 11C10 10.448 10.448 10 11 10H13C13.552 10 14 10.448 14 11V13C14 13.552 13.552 14 13 14H11C10.448 14 10 13.552 10 13V11Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M9.5 2.5V6M14.5 2.5V6M9.5 18V21.5M14.5 18V21.5M2.5 9.5H6M2.5 14.5H6M18 9.5H21.5M18 14.5H21.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-memory" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M2 8C2 6.895 2.895 6 4 6H20C21.105 6 22 6.895 22 8V15H2V8Z" fill="currentColor"></path><path d="M2 8C2 6.895 2.895 6 4 6H20C21.105 6 22 6.895 22 8V15H2V8Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M6 9.5V11.5M10 9.5V11.5M14 9.5V11.5M18 9.5V11.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M4 15V18M8 15V18M12 15V18M16 15V18M20 15V18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-disk" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M2 9C2 7.343 3.343 6 5 6H19C20.657 6 22 7.343 22 9V15C22 16.657 20.657 18 19 18H5C3.343 18 2 16.657 2 15V9Z" fill="currentColor"></path><path d="M2 9C2 7.343 3.343 6 5 6H19C20.657 6 22 7.343 22 9V15C22 16.657 20.657 18 19 18H5C3.343 18 2 16.657 2 15V9Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M6 12H12.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M17.5 12H17.51" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-thermometer" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M14 14.76V5C14 3.895 13.105 3 12 3C10.895 3 10 3.895 10 5V14.76C8.8 15.45 8 16.73 8 18.2C8 20.4 9.8 22 12 22C14.2 22 16 20.4 16 18.2C16 16.73 15.2 15.45 14 14.76Z" fill="currentColor"></path><path d="M14 14.76V5C14 3.895 13.105 3 12 3C10.895 3 10 3.895 10 5V14.76C8.8 15.45 8 16.73 8 18.2C8 20.4 9.8 22 12 22C14.2 22 16 20.4 16 18.2C16 16.73 15.2 15.45 14 14.76Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 17.5V9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-bolt" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M13 2L4 14H11L10 22L20 9.5H13L13 2Z" fill="currentColor"></path><path d="M13 2L4 14H11L10 22L20 9.5H13L13 2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-link" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M8.46 20.06L14.26 14.26A3.2 3.2 0 0 0 9.74 9.74L3.94 15.54A3.2 3.2 0 0 0 8.46 20.06Z" fill="currentColor"></path><path d="M10 13C10.43 13.57 10.98 14.05 11.61 14.39C12.24 14.74 12.93 14.94 13.65 14.99C14.36 15.04 15.08 14.94 15.75 14.69C16.42 14.44 17.03 14.05 17.54 13.54L20.54 10.54C21.45 9.6 21.95 8.33 21.94 7.02C21.93 5.71 21.41 4.46 20.48 3.53C19.55 2.6 18.3 2.08 16.99 2.07C15.68 2.06 14.41 2.56 13.47 3.47L11.75 5.18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M14 11C13.57 10.43 13.02 9.95 12.39 9.61C11.76 9.26 11.07 9.06 10.35 9.01C9.64 8.96 8.92 9.06 8.25 9.31C7.58 9.56 6.97 9.95 6.46 10.46L3.46 13.46C2.55 14.4 2.05 15.67 2.06 16.98C2.07 18.29 2.59 19.54 3.52 20.47C4.45 21.4 5.7 21.92 7.01 21.93C8.32 21.94 9.59 21.44 10.53 20.53L12.24 18.82" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-clock" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" fill="currentColor"></path><path d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 7V12L15 14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-gauge" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M2 15C2 9.477 6.477 5 12 5C17.523 5 22 9.477 22 15C22 16.5 21.7 17.8 21.1 19H2.9C2.3 17.8 2 16.5 2 15Z" fill="currentColor"></path><path d="M2 15C2 9.477 6.477 5 12 5C17.523 5 22 9.477 22 15C22 16.5 21.7 17.8 21.1 19H2.9C2.3 17.8 2 16.5 2 15Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 15L16 10.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M13 15C13 15.552 12.552 16 12 16C11.448 16 11 15.552 11 15C11 14.448 11.448 14 12 14C12.552 14 13 14.448 13 15Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-layers" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M12 3L22 8L12 13L2 8L12 3Z" fill="currentColor"></path><path d="M12 3L22 8L12 13L2 8L12 3Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M2 16L12 21L22 16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-experts" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M3 5.5C3 4.119 4.119 3 5.5 3H6.5C7.881 3 9 4.119 9 5.5V6.5C9 7.881 7.881 9 6.5 9H5.5C4.119 9 3 7.881 3 6.5V5.5Z" fill="currentColor"></path><path opacity="0.12" d="M15 17.5C15 16.119 16.119 15 17.5 15H18.5C19.881 15 21 16.119 21 17.5V18.5C21 19.881 19.881 21 18.5 21H17.5C16.119 21 15 19.881 15 18.5V17.5Z" fill="currentColor"></path><path d="M3 5.5C3 4.119 4.119 3 5.5 3H6.5C7.881 3 9 4.119 9 5.5V6.5C9 7.881 7.881 9 6.5 9H5.5C4.119 9 3 7.881 3 6.5V5.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M15 5.5C15 4.119 16.119 3 17.5 3H18.5C19.881 3 21 4.119 21 5.5V6.5C21 7.881 19.881 9 18.5 9H17.5C16.119 9 15 7.881 15 6.5V5.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 17.5C3 16.119 4.119 15 5.5 15H6.5C7.881 15 9 16.119 9 17.5V18.5C9 19.881 7.881 21 6.5 21H5.5C4.119 21 3 19.881 3 18.5V17.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M15 17.5C15 16.119 16.119 15 17.5 15H18.5C19.881 15 21 16.119 21 17.5V18.5C21 19.881 19.881 21 18.5 21H17.5C16.119 21 15 19.881 15 18.5V17.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-send" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M21 3L2 10L10 13.5L13.5 21L21 3Z" fill="currentColor"></path><path d="M21 3L2 10L10 13.5L13.5 21L21 3Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M10 13.5L21 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-stop" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M5 8C5 6.343 6.343 5 8 5H16C17.657 5 19 6.343 19 8V16C19 17.657 17.657 19 16 19H8C6.343 19 5 17.657 5 16V8Z" fill="currentColor"></path><path d="M5 8C5 6.343 6.343 5 8 5H16C17.657 5 19 6.343 19 8V16C19 17.657 17.657 19 16 19H8C6.343 19 5 17.657 5 16V8Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-attach" viewBox="0 0 24 24" fill="none"><path d="M21.44 11.05L12.25 20.24A6 6 0 0 1 3.76 11.75L12.33 3.18A4 4 0 1 1 18 8.84L9.41 17.41A2 2 0 0 1 6.58 14.58L15.07 6.1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-image" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M3 7C3 4.791 4.791 3 7 3H17C19.209 3 21 4.791 21 7V17C21 19.209 19.209 21 17 21H7C4.791 21 3 19.209 3 17V7Z" fill="currentColor"></path><path d="M3 7C3 4.791 4.791 3 7 3H17C19.209 3 21 4.791 21 7V17C21 19.209 19.209 21 17 21H7C4.791 21 3 19.209 3 17V7Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M10 8.5C10 9.328 9.328 10 8.5 10C7.672 10 7 9.328 7 8.5C7 7.672 7.672 7 8.5 7C9.328 7 10 7.672 10 8.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3.5 17.5L8 13L11 16L15 12L20.5 17.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-copy" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M8 10C8 8.895 8.895 8 10 8H18C19.105 8 20 8.895 20 10V18C20 19.105 19.105 20 18 20H10C8.895 20 8 19.105 8 18V10Z" fill="currentColor"></path><path d="M8 10C8 8.895 8.895 8 10 8H18C19.105 8 20 8.895 20 10V18C20 19.105 19.105 20 18 20H10C8.895 20 8 19.105 8 18V10Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M16 8V6C16 4.895 15.105 4 14 4H6C4.895 4 4 4.895 4 6V14C4 15.105 4.895 16 6 16H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-check" viewBox="0 0 24 24" fill="none"><path d="M4.5 12.5L9.5 17.5L19.5 6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-trash" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M5 6L6 19C6.08 20.1 7 21 8.1 21H15.9C17 21 17.92 20.1 18 19L19 6H5Z" fill="currentColor"></path><path d="M5 6L6 19C6.08 20.1 7 21 8.1 21H15.9C17 21 17.92 20.1 18 19L19 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M3 6H21" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M9 6V4.5C9 3.672 9.672 3 10.5 3H13.5C14.328 3 15 3.672 15 4.5V6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M10 10.5V16.5M14 10.5V16.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-new-chat" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M21 11.5C21 16.194 16.971 20 12 20C10.8 20 9.65 19.78 8.6 19.37L4 20.5L5.2 16.8C3.83 15.34 3 13.5 3 11.5C3 6.806 7.029 3 12 3C16.971 3 21 6.806 21 11.5Z" fill="currentColor"></path><path d="M21 11.5C21 16.194 16.971 20 12 20C10.8 20 9.65 19.78 8.6 19.37L4 20.5L5.2 16.8C3.83 15.34 3 13.5 3 11.5C3 6.806 7.029 3 12 3C16.971 3 21 6.806 21 11.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 8.5V14.5M9 11.5H15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-chevron" viewBox="0 0 24 24" fill="none"><path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-thinking" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M12 2.5C8.41 2.5 5.5 5.41 5.5 9C5.5 11.3 6.7 13.3 8.5 14.5V16.5C8.5 17.05 8.95 17.5 9.5 17.5H14.5C15.05 17.5 15.5 17.05 15.5 16.5V14.5C17.3 13.3 18.5 11.3 18.5 9C18.5 5.41 15.59 2.5 12 2.5Z" fill="currentColor"></path><path d="M12 2.5C8.41 2.5 5.5 5.41 5.5 9C5.5 11.3 6.7 13.3 8.5 14.5V16.5C8.5 17.05 8.95 17.5 9.5 17.5H14.5C15.05 17.5 15.5 17.05 15.5 16.5V14.5C17.3 13.3 18.5 11.3 18.5 9C18.5 5.41 15.59 2.5 12 2.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M9.5 20.5H14.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-tool" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M14.7 6.3A1 1 0 0 0 14.7 7.7L16.3 9.3A1 1 0 0 0 17.7 9.3L21.47 5.53A6 6 0 0 1 13.53 13.47L6.62 20.38A2.12 2.12 0 0 1 3.62 17.38L10.53 10.47A6 6 0 0 1 18.47 2.53L14.71 6.29Z" fill="currentColor"></path><path d="M14.7 6.3A1 1 0 0 0 14.7 7.7L16.3 9.3A1 1 0 0 0 17.7 9.3L21.47 5.53A6 6 0 0 1 13.53 13.47L6.62 20.38A2.12 2.12 0 0 1 3.62 17.38L10.53 10.47A6 6 0 0 1 18.47 2.53L14.71 6.29Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-download" viewBox="0 0 24 24" fill="none"><path d="M4 15V17C4 18.657 5.343 20 7 20H17C18.657 20 20 18.657 20 17V15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 4V14M12 14L8 10M12 14L16 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-warning" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M10.24 3.99L2.4 17.5C1.62 18.83 2.58 20.5 4.12 20.5H19.88C21.42 20.5 22.38 18.83 21.6 17.5L13.76 3.99C12.98 2.66 11.02 2.66 10.24 3.99Z" fill="currentColor"></path><path d="M10.24 3.99L2.4 17.5C1.62 18.83 2.58 20.5 4.12 20.5H19.88C21.42 20.5 22.38 18.83 21.6 17.5L13.76 3.99C12.98 2.66 11.02 2.66 10.24 3.99Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 9V13M12 16.5H12.01" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-error" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" fill="currentColor"></path><path d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M15 9L9 15M9 9L15 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-info" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" fill="currentColor"></path><path d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 11V16.5M12 7.5H12.01" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-refresh" viewBox="0 0 24 24" fill="none"><path d="M21 12A9 9 0 1 1 18.36 5.64L21 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M21 3V8H16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-sun" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M16 12C16 14.209 14.209 16 12 16C9.791 16 8 14.209 8 12C8 9.791 9.791 8 12 8C14.209 8 16 9.791 16 12Z" fill="currentColor"></path><path d="M16 12C16 14.209 14.209 16 12 16C9.791 16 8 14.209 8 12C8 9.791 9.791 8 12 8C14.209 8 16 9.791 16 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M12 2V4M12 20V22M4.93 4.93L6.34 6.34M17.66 17.66L19.07 19.07M2 12H4M20 12H22M4.93 19.07L6.34 17.66M17.66 6.34L19.07 4.93" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
<symbol id="i-moon" viewBox="0 0 24 24" fill="none"><path opacity="0.12" d="M21 12.79A9 9 0 1 1 11.21 3A7 7 0 0 0 21 12.79Z" fill="currentColor"></path><path d="M21 12.79A9 9 0 1 1 11.21 3A7 7 0 0 0 21 12.79Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,87 @@
|
||||
/* Strata UI — tokens. Derived from the Gainsty design system.
|
||||
Light is default; dark via <html data-theme="dark">.
|
||||
Values marked (derived) are not in the Gainsty source and were added for the app. */
|
||||
|
||||
/* Outfit (SIL Open Font License 1.1, serve/web/fonts/OFL.txt): one variable font, weights 100-900 */
|
||||
@font-face{font-family:"Outfit";src:url("/fonts/outfit-latin-wght.woff2") format("woff2-variations");font-weight:100 900;font-display:swap;unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:"Outfit";src:url("/fonts/outfit-latin-ext-wght.woff2") format("woff2-variations");font-weight:100 900;font-display:swap;unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}
|
||||
|
||||
:root{
|
||||
/* type */
|
||||
--st-font:"Outfit",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
||||
--st-font-mono:ui-monospace,"SF Mono","Cascadia Code",Consolas,"Liberation Mono",monospace; /* (derived) */
|
||||
--st-fs-xs:12px; --st-fs-sm:13px; --st-fs-md:14px; --st-fs-base:16px;
|
||||
--st-fs-lg:18px; --st-fs-xl:24px; --st-fs-stat:32px; --st-fs-display:40px;
|
||||
--st-fw-regular:400; --st-fw-medium:500; --st-fw-bold:700; --st-fw-black:900;
|
||||
--st-lh-tight:1.1; --st-lh-body:1.55;
|
||||
|
||||
/* spacing (4pt) */
|
||||
--st-s-1:4px; --st-s-2:8px; --st-s-3:12px; --st-s-4:16px; --st-s-5:20px;
|
||||
--st-s-6:24px; --st-s-8:32px; --st-s-10:40px; --st-s-12:48px; --st-s-16:64px;
|
||||
|
||||
/* radius */
|
||||
--st-r-sm:8px; --st-r-md:14px; --st-r-lg:20px; --st-r-xl:24px; --st-r-pill:9999px;
|
||||
|
||||
/* motion (derived) */
|
||||
--st-ease:cubic-bezier(.2,.7,.2,1); --st-dur:160ms;
|
||||
|
||||
/* color — light */
|
||||
--st-bg:#f9fafb;
|
||||
--st-surface:#ffffff;
|
||||
--st-surface-2:#f7f7f7;
|
||||
--st-line:#dee5ee;
|
||||
--st-line-soft:#eeeeee;
|
||||
--st-ink:#221e1f;
|
||||
--st-ink-soft:#303030;
|
||||
--st-ink-muted:#6f6f6f; /* Gainsty #7a7a7a darkened slightly for 4.5:1 on white */
|
||||
--st-icon:#2F384C; /* Anron default */
|
||||
|
||||
--st-accent:#10b981;
|
||||
--st-accent-hover:#0c8a60;
|
||||
--st-accent-ink:#ffffff;
|
||||
--st-accent-tint:#ecfdf5;
|
||||
--st-accent-text:#0c8a60; /* accent used as text on light */
|
||||
--st-focus:#405de6;
|
||||
|
||||
--st-info:#0f97ff; --st-info-tint:#e8f4ff; --st-info-text:#0668c2;
|
||||
--st-warn:#e39b0b; --st-warn-tint:#fff6e0; --st-warn-text:#8a5a00; /* (derived) */
|
||||
--st-danger:#e0283f; --st-danger-tint:#fdecee; --st-danger-text:#b01a2e; /* (derived) */
|
||||
|
||||
--st-code-bg:#1b1f23; --st-code-ink:#e6e9ec; /* code blocks stay dark in both themes (derived) */
|
||||
|
||||
--st-sh-card:0 18px 7px rgba(0,0,0,.01),0 10px 6px rgba(0,0,0,.02),0 4px 4px rgba(0,0,0,.03),0 1px 2px rgba(0,0,0,.04);
|
||||
--st-sh-bar:0 54px 21px rgba(0,0,0,.01),0 30px 18px rgba(0,0,0,.03),0 13px 13px rgba(0,0,0,.05),0 3px 7px rgba(0,0,0,.06);
|
||||
--st-sh-pop:0 10px 40px rgba(15,23,42,.08),0 1px 3px rgba(15,23,42,.06);
|
||||
color-scheme:light;
|
||||
}
|
||||
|
||||
/* color — dark (all derived) */
|
||||
[data-theme="dark"]{
|
||||
--st-bg:#0e1113;
|
||||
--st-surface:#161a1d;
|
||||
--st-surface-2:#1d2226;
|
||||
--st-line:#2a3036;
|
||||
--st-line-soft:#22272c;
|
||||
--st-ink:#eef1f3;
|
||||
--st-ink-soft:#c7cdd3;
|
||||
--st-ink-muted:#8e979f;
|
||||
--st-icon:#c7cdd3;
|
||||
|
||||
--st-accent:#34d399;
|
||||
--st-accent-hover:#4ade80;
|
||||
--st-accent-ink:#052e20;
|
||||
--st-accent-tint:rgba(52,211,153,.12);
|
||||
--st-accent-text:#4ade80;
|
||||
--st-focus:#7b93ff;
|
||||
|
||||
--st-info:#3aa9ff; --st-info-tint:rgba(58,169,255,.12); --st-info-text:#7cc4ff;
|
||||
--st-warn:#f5b53d; --st-warn-tint:rgba(245,181,61,.12); --st-warn-text:#f8c96a;
|
||||
--st-danger:#ff5a6e; --st-danger-tint:rgba(255,90,110,.12);--st-danger-text:#ff8a98;
|
||||
|
||||
--st-code-bg:#0a0c0e; --st-code-ink:#e6e9ec;
|
||||
|
||||
--st-sh-card:0 1px 2px rgba(0,0,0,.4);
|
||||
--st-sh-bar:0 12px 32px rgba(0,0,0,.45),0 2px 6px rgba(0,0,0,.35);
|
||||
--st-sh-pop:0 16px 48px rgba(0,0,0,.55),0 1px 3px rgba(0,0,0,.4);
|
||||
color-scheme:dark;
|
||||
}
|
||||
@@ -21,6 +21,8 @@ What the first run does (each step is skipped when it is already done):
|
||||
|
||||
Options: --family qwen|swift, --model Q2_0|IQ2_XS|IQ3_XXS|IQ3_S, --context 32768, --vision yes|no|gpu|cpu, --port 8080, --yes (recommended
|
||||
answers, no questions), --setup (install another model / change settings instead of starting), --no-start,
|
||||
--host 0.0.0.0 --api-key KEY (reach it from other devices on your network), --experimental-speed-projection on|off
|
||||
(EXPERIMENTAL, off by default),
|
||||
--models-dir DIR, --gguf-dir DIR (use GGUF files you already have), --build (compile instead of the ready-made
|
||||
engine), --check (only check this PC).
|
||||
"""
|
||||
@@ -28,6 +30,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
@@ -54,8 +57,8 @@ PREBUILT_ASSET = "strata-windows-x64.zip" if WIN else "strata-linux-x64.zip"
|
||||
# the CUDA libraries the ready-made engine loads (the same CUDA 13.0 it is built with), from NVIDIA's pip packages
|
||||
CUDA_WHEELS = ["nvidia-cublas==13.0.2.14", "nvidia-cuda-runtime==13.0.96"]
|
||||
MIN_DRIVER = 580 # CUDA 13.0
|
||||
MIN_ENGINE = (0, 1, 7) # per-request sampling on the GEN line (the server sends it), v0.1.7
|
||||
PY_PACKAGES = ["numpy", "jinja2", "regex", "pyyaml", "tqdm", "requests", "cmake", "ninja", "pillow"]
|
||||
MIN_ENGINE = (0, 1, 15) # v0.1.15: kernels loaded at start (IQ3_XXS 64K+); v0.1.14: #31; v0.1.13: --prefill auto
|
||||
PY_PACKAGES = ["numpy", "jinja2", "regex", "pyyaml", "tqdm", "requests", "cmake", "ninja", "pillow", "psutil"]
|
||||
|
||||
MODELS = {
|
||||
"Q2_0": {"about": "2-bit, the fastest", "download_gb": 66.4, "ram_gb": 48, "arena_gb": 34.0},
|
||||
@@ -86,6 +89,8 @@ FAMILIES = {
|
||||
"license": "Swift Open License 1.0: https://huggingface.co/ukisai/Swift-1.5-Qwen3.8-Flash-Next-GSQ-RCO-GGUF"},
|
||||
}
|
||||
MMPROJ = "mmproj-Qwen3.8-Flash-Next-BF16.gguf"
|
||||
# EXPERIMENTAL, off by default (setup asks): a control vector shipped with the repository, see its README
|
||||
ESP_VECTOR = ROOT / "data" / "experimental-speed-projection" / "Qwen3.8-Flash-Next-experimental-speed-projection.gguf"
|
||||
# the image encoder on the GPU (~1.2 GB at 1024 image tokens) warms up before the engine starts, so the engine
|
||||
# sizes its expert slots around it and the default reserve (700 MiB) is enough; engines before 0.1.2 need more
|
||||
VISION = {"gpu": {"max_tokens": 1024, "reserve_mib": 700},
|
||||
@@ -244,6 +249,7 @@ def find_nvcc():
|
||||
cands += [str(p / "bin" / "nvcc.exe") for p in sorted(base.iterdir(), reverse=True)]
|
||||
else:
|
||||
cands += [str(p / "bin" / "nvcc") for p in sorted(Path("/usr/local").glob("cuda*"), reverse=True)]
|
||||
cands += [str(p / "bin" / "nvcc") for p in sorted(Path("/opt").glob("cuda*"), reverse=True)] # Arch (#46)
|
||||
best = (None, None)
|
||||
for c in dict.fromkeys(cands): # every toolkit found; the newest wins
|
||||
if c and Path(c).exists():
|
||||
@@ -471,7 +477,13 @@ def update_installed_engine(url_base) -> None:
|
||||
meta_text = info.read_text()
|
||||
meta = json.loads(meta_text)
|
||||
ver = tuple(int(x) for x in str(meta.get("version", "0")).split(".")[:3] if x.isdigit())
|
||||
if meta.get("source") == "local" or ver >= MIN_ENGINE:
|
||||
local = meta.get("source") == "local"
|
||||
vision = meta.get("vision") or "none"
|
||||
if local: # compiled here: is it older than the source (a git pull)?
|
||||
if meta.get("src") == source_hash(ENGINE_SOURCES) and \
|
||||
(vision == "none" or meta.get("vision_src") == source_hash(VISION_SOURCES)):
|
||||
return
|
||||
elif ver >= MIN_ENGINE:
|
||||
return
|
||||
try: # a running engine cannot be replaced (Windows keeps it locked)
|
||||
for x in (EXE, VEXE):
|
||||
@@ -479,9 +491,17 @@ def update_installed_engine(url_base) -> None:
|
||||
with open(eng / x, "r+b"):
|
||||
pass
|
||||
except OSError:
|
||||
warn(f"engine {meta.get('version')} is in use: close the model window and run this again to update it")
|
||||
warn(f"engine {meta.get('version') or ''} is in use: close the model window and run this again to update it")
|
||||
return
|
||||
gpu = gpu_info()
|
||||
if local:
|
||||
try: # a failed compile must not stop the model from starting
|
||||
if gpu is None:
|
||||
raise RuntimeError("no NVIDIA GPU found")
|
||||
build_engine(gpu, vision, False, get_llama_cpp())
|
||||
except (Exception, SystemExit) as e:
|
||||
warn(f"could not compile the updated engine{'' if isinstance(e, SystemExit) else f' ({e})'}: starting the installed one")
|
||||
return
|
||||
new = None
|
||||
if gpu is not None:
|
||||
try:
|
||||
@@ -531,8 +551,9 @@ def install_build_tools(gpu, yes):
|
||||
else:
|
||||
apt = shutil.which("apt-get")
|
||||
if apt is None:
|
||||
fail("automatic install is only done on Ubuntu/Debian",
|
||||
"install g++ and the CUDA Toolkit 13 (https://developer.nvidia.com/cuda-downloads), then run it again")
|
||||
fail("missing: " + " and ".join(missing) + " (the automatic install is only done on Ubuntu/Debian)",
|
||||
"install them with your distribution's packages (Arch: pacman -S base-devel cuda; nvcc is found on "
|
||||
"PATH, in /usr/local/cuda* and in /opt/cuda*), then run it again")
|
||||
if not have_cc:
|
||||
run(["sudo", "apt-get", "install", "-y", "build-essential"])
|
||||
if nvcc is None or cuda_v < need_cuda:
|
||||
@@ -563,35 +584,61 @@ def cmake_build(src, bdir, target, defs, vcvars, bat_name):
|
||||
conf = [cmake, "-G", "Ninja", f"-DCMAKE_MAKE_PROGRAM={ninja}", "-S", str(src), "-B", str(bdir),
|
||||
"-DCMAKE_BUILD_TYPE=Release", *defs]
|
||||
build = [cmake, "--build", str(bdir), "--target", target, "-j", str(max(2, (os.cpu_count() or 4) // 2))]
|
||||
# A failed build is tried once more: CUDA 13.0's ptxas now and then fails to parse a PTX file it just wrote, and
|
||||
# the same command then gets past it (issue #45); a second attempt only compiles what is still missing.
|
||||
if WIN:
|
||||
bat = ROOT / bat_name
|
||||
q = lambda c: " ".join(f'"{x}"' if " " in str(x) else str(x) for x in c) # noqa: E731
|
||||
bat.write_text(f'@echo off\r\ncall "{vcvars}" >nul\r\n{q(conf)} || exit /b 1\r\n{q(build)} || exit /b 1\r\n',
|
||||
bat.write_text(f'@echo off\r\ncall "{vcvars}" >nul\r\n{q(conf)} || exit /b 1\r\n{q(build)} && exit /b 0\r\n'
|
||||
f'echo (the build stopped - trying it once more)\r\n{q(build)} || exit /b 1\r\n',
|
||||
encoding="utf-8")
|
||||
run(["cmd", "/c", str(bat)])
|
||||
else:
|
||||
run(conf)
|
||||
run(build)
|
||||
if run(build, check=False).returncode != 0:
|
||||
say(" (the build stopped - trying it once more)")
|
||||
run(build)
|
||||
|
||||
|
||||
ENGINE_SOURCES = ("CMakeLists.txt", "src", "include", "third_party/ggml")
|
||||
VISION_SOURCES = ("tools/vision",)
|
||||
|
||||
|
||||
def source_hash(parts) -> str:
|
||||
"""A fingerprint of the files a compiled engine is built from, kept in engine/BUILD.json: when a `git pull`
|
||||
changes them, the engine is compiled again (issue #31)."""
|
||||
h = hashlib.sha256(LLAMA_CPP_COMMIT.encode())
|
||||
for part in parts:
|
||||
base = ROOT / part
|
||||
for f in [base] if base.is_file() else sorted(x for x in base.rglob("*") if x.is_file()):
|
||||
h.update(f.relative_to(ROOT).as_posix().encode() + b"\0" + f.read_bytes().replace(b"\r\n", b"\n"))
|
||||
return h.hexdigest()[:16]
|
||||
|
||||
|
||||
def build_engine(gpu, vision, yes, llama) -> Path:
|
||||
"""Compile the engine (and, for images, the encoder) for this GPU; the results go to engine/."""
|
||||
"""Compile the engine (and, for images, the encoder) for this GPU; the results go to engine/. A compiled
|
||||
engine whose source files changed since (a `git pull`) is compiled again: only the changed files, a few minutes."""
|
||||
eng = ROOT / "engine"
|
||||
eng.mkdir(exist_ok=True)
|
||||
stamp = eng / "BUILD.json"
|
||||
meta = json.loads(stamp.read_text()) if stamp.exists() else {}
|
||||
want_vision = vision != "none"
|
||||
if meta.get("source") == "local" and (eng / EXE).exists() and (not want_vision or (eng / VEXE).exists()):
|
||||
local = meta.get("source") == "local"
|
||||
src, vsrc = source_hash(ENGINE_SOURCES), source_hash(VISION_SOURCES)
|
||||
engine_ok = local and (eng / EXE).exists() and meta.get("src") == src
|
||||
vision_ok = not want_vision or ((eng / VEXE).exists() and (not local or meta.get("vision_src") == vsrc))
|
||||
if engine_ok and vision_ok:
|
||||
ok("engine already built for this PC")
|
||||
return eng
|
||||
nvcc, vcvars = install_build_tools(gpu, yes)
|
||||
if not (eng / EXE).exists() or meta.get("source") != "local":
|
||||
say(" Compiling the Strata engine for your GPU (10-20 minutes, once) ...")
|
||||
if not engine_ok:
|
||||
say(" The engine's source changed: compiling it again (only what changed, a few minutes) ..."
|
||||
if local and (eng / EXE).exists() else " Compiling the Strata engine for your GPU (10-20 minutes, once) ...")
|
||||
cmake_build(ROOT, ROOT / "build", "strata",
|
||||
["-DSTRATA_ENABLE_CUDA=ON", "-DSTRATA_BUILD_TESTS=OFF", f"-DCMAKE_CUDA_ARCHITECTURES={gpu['arch']}",
|
||||
f"-DCMAKE_CUDA_COMPILER={nvcc}", f"-DSTRATA_GGML_DIR={llama}"], vcvars, "build-strata.bat")
|
||||
shutil.copy2(ROOT / "build" / EXE, eng / EXE)
|
||||
if want_vision and not (eng / VEXE).exists():
|
||||
if not vision_ok:
|
||||
say(" Compiling the image encoder" + (" with CUDA (10-20 minutes, once) ..." if vision == "gpu" else " ..."))
|
||||
defs = [f"-DLLAMA_DIR={llama}", f"-DSTRATA_VISION_CUDA={'ON' if vision == 'gpu' else 'OFF'}"]
|
||||
if vision == "gpu":
|
||||
@@ -600,8 +647,9 @@ def build_engine(gpu, vision, yes, llama) -> Path:
|
||||
shutil.copy2(ROOT / "build-vision" / "bin" / VEXE, eng / VEXE)
|
||||
bindir = Path(nvcc).parent # the toolkit's own libraries (bin, bin/x64, lib64)
|
||||
dirs = [str(d) for d in (bindir, bindir / "x64", bindir.parent / "lib64") if d.is_dir()]
|
||||
stamp.write_text(json.dumps({"source": "local", "archs": [int(gpu["arch"])], "vision": vision,
|
||||
"cuda_dirs": dirs}, indent=1))
|
||||
stamp.write_text(json.dumps({"source": "local", "version": source_version(), "archs": [int(gpu["arch"])],
|
||||
"vision": vision,
|
||||
"cuda_dirs": dirs, "src": src, "vision_src": vsrc if want_vision else None}, indent=1))
|
||||
ok(f"engine compiled: {eng / EXE}")
|
||||
return eng
|
||||
|
||||
@@ -611,8 +659,61 @@ def installed_configs():
|
||||
return sorted(ROOT.glob("strata-*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
|
||||
|
||||
def source_version() -> str:
|
||||
"""The engine version the source tree builds (CMakeLists.txt's project version)."""
|
||||
m = re.search(r"project\(strata VERSION ([\d.]+)", (ROOT / "CMakeLists.txt").read_text(encoding="utf-8"))
|
||||
return m.group(1) if m else "0"
|
||||
|
||||
|
||||
def engine_version(exe: Path) -> tuple:
|
||||
"""The version in the engine folder's BUILD.json, or else the one compiled into the binary. A locally compiled
|
||||
engine is not necessarily the source's version: when compiling a `git pull` fails, the previous engine is kept
|
||||
(issue #49)."""
|
||||
try:
|
||||
meta = json.loads((Path(exe).parent / "BUILD.json").read_text())
|
||||
except (OSError, ValueError):
|
||||
meta = {}
|
||||
v = str(meta.get("version") or "")
|
||||
if not v: # the version compiled into the binary: 0.1.13 and newer
|
||||
try: # carry it, so a binary without it is older
|
||||
m = re.search(rb"engine=(\d+\.\d+\.\d+)\n", Path(exe).read_bytes())
|
||||
v = m.group(1).decode() if m else "0.1.12"
|
||||
except OSError:
|
||||
v = "0"
|
||||
return tuple(int(x) for x in v.split(".")[:3] if x.isdigit())
|
||||
|
||||
|
||||
def is_wsl() -> bool:
|
||||
return sys.platform.startswith("linux") and "microsoft" in platform.uname().release.lower()
|
||||
|
||||
|
||||
def upgrade_config(cfg_path: Path, cfg: dict) -> dict:
|
||||
"""Configs written before v0.1.13 read prompts in fixed 2048-token chunks; the engine now picks the chunk
|
||||
itself (`--prefill auto`: up to 8192, as the free VRAM allows - about 2x faster on long prompts). Under WSL,
|
||||
KV streaming is dropped: its RAM copy must be pinned, and the driver pins only about 1 GB there."""
|
||||
a = cfg.get("args", [])
|
||||
changed = False
|
||||
ver = engine_version(cfg["exe"]) if "--prefill" in a else (0, 0, 0)
|
||||
if "--prefill" in a and a[a.index("--prefill") + 1] == "2048" and ver >= (0, 1, 13):
|
||||
a[a.index("--prefill") + 1] = "auto"
|
||||
changed = True
|
||||
ok("prompt reading: the engine now picks its chunk size (--prefill auto)")
|
||||
elif "--prefill" in a and a[a.index("--prefill") + 1] == "auto" and (0, 0, 0) < ver < (0, 1, 13):
|
||||
a[a.index("--prefill") + 1] = "2048" # an older engine kept after a failed update (issue #49)
|
||||
changed = True
|
||||
warn(f"the installed engine is {'.'.join(map(str, ver))}: prompts are read in 2048-token chunks until it is updated")
|
||||
if is_wsl() and "--kv-resident" in a:
|
||||
i = a.index("--kv-resident")
|
||||
del a[i:i + 2]
|
||||
changed = True
|
||||
ok("WSL: KV streaming off (the driver pins only about 1 GB of RAM); the KV cache stays in VRAM")
|
||||
if changed:
|
||||
cfg_path.write_text(json.dumps(cfg, indent=1), encoding="utf-8")
|
||||
return cfg
|
||||
|
||||
|
||||
def start(cfg_path: Path, port: int | None, open_browser=True) -> int:
|
||||
cfg = json.loads(cfg_path.read_text(encoding="utf-8-sig"))
|
||||
cfg = upgrade_config(cfg_path, json.loads(cfg_path.read_text(encoding="utf-8-sig")))
|
||||
missing = [p for p in [cfg["exe"], *[a for a in cfg["args"] if a.endswith(".gguf")]] if not Path(p).exists()]
|
||||
if missing:
|
||||
fail(f"{cfg_path.name} refers to missing files: {missing[0]}", "run it again with --setup to repair")
|
||||
@@ -647,9 +748,18 @@ def main() -> int:
|
||||
ap.add_argument("--family", choices=list(FAMILIES), help="qwen = Qwen3.8-Flash-Next, swift = Swift 1.5")
|
||||
ap.add_argument("--model", choices=list(MODELS))
|
||||
ap.add_argument("--context", type=int)
|
||||
ap.add_argument("--kv", choices=["int8", "q4_0"],
|
||||
help="KV cache precision above 8K context: int8 (default) or q4_0 (half the memory, a little less "
|
||||
"precise)")
|
||||
ap.add_argument("--vision", choices=["yes", "no", "none", "gpu", "cpu"],
|
||||
help="let the model read images (yes = the encoder on the GPU)")
|
||||
ap.add_argument("--port", type=int, default=8080)
|
||||
ap.add_argument("--experimental-speed-projection", metavar="on|off|GGUF",
|
||||
help="EXPERIMENTAL, off by default: the control vector in data/experimental-speed-projection "
|
||||
"(or another GGUF) as a projection on layers 4-44; see docs/DETAILS.md")
|
||||
ap.add_argument("--port", type=int, help="the server's port (default: the one the install was set up with, 8080 for a new one)")
|
||||
ap.add_argument("--host", help="where the server listens: 127.0.0.1 = this PC only (default), 0.0.0.0 = also other "
|
||||
"devices on your network (issue #26; set --api-key too)")
|
||||
ap.add_argument("--api-key", help="require this key from clients (recommended with --host 0.0.0.0)")
|
||||
ap.add_argument("--models-dir", default=str(ROOT / "models"), help="where the GGUF files go (~70 GB)")
|
||||
ap.add_argument("--gguf-dir", help="use GGUF files you already have (a folder with the two shards)")
|
||||
ap.add_argument("--yes", action="store_true", help="accept the recommended answers")
|
||||
@@ -661,6 +771,7 @@ def main() -> int:
|
||||
ap.add_argument("--check", action="store_true", help="only check this PC and exit")
|
||||
ap.add_argument("--skip-build", action="store_true", help=argparse.SUPPRESS)
|
||||
a = ap.parse_args()
|
||||
port = a.port or 8080 # a new install's port (issue #32: --port for an existing one)
|
||||
|
||||
say("Strata - Qwen3.8-Flash-Next on a normal PC (NVIDIA GPU + system RAM + CPU)")
|
||||
|
||||
@@ -670,14 +781,14 @@ def main() -> int:
|
||||
if not a.build:
|
||||
update_installed_engine(a.prebuilt)
|
||||
if len(have) == 1:
|
||||
return start(have[0], None)
|
||||
return start(have[0], a.port)
|
||||
say()
|
||||
for i, c in enumerate(have, 1):
|
||||
say(f" {i}) {json.loads(c.read_text(encoding='utf-8-sig')).get('model_name', c.stem)}")
|
||||
say(f" {len(have) + 1}) install another model / change settings")
|
||||
pick = int(ask("Which one?", [str(i) for i in range(1, len(have) + 2)], "1", a.yes))
|
||||
if pick <= len(have):
|
||||
return start(have[pick - 1], None)
|
||||
return start(have[pick - 1], a.port)
|
||||
|
||||
# ---- 1. the PC
|
||||
step(1, "checking your PC")
|
||||
@@ -759,6 +870,17 @@ def main() -> int:
|
||||
"+ the context): using 128K")
|
||||
ctx = 131072
|
||||
ok(f"context: {ctx} tokens")
|
||||
# the KV cache (the model's memory of the conversation): 8-bit, or 4-bit after a Hadamard rotation (PR #21)
|
||||
kv = "fp16" if ctx <= 8192 else (a.kv or "int8")
|
||||
if ctx > 8192 and not a.kv and not a.yes:
|
||||
say()
|
||||
say(" KV cache precision (the model's memory of the conversation):")
|
||||
say(" 1) 8-bit (recommended: what every published number was measured with)")
|
||||
say(" 2) 4-bit half the memory (about 4% faster at 128K), but measurably less precise on long")
|
||||
say(" documents; long-context lookups (needle tests) still pass")
|
||||
kv = ["int8", "q4_0"][int(ask("KV cache?", ["1", "2"], "1", a.yes)) - 1]
|
||||
if ctx > 8192:
|
||||
ok(f"KV cache: {'8-bit' if kv == 'int8' else '4-bit (Hadamard-rotated)'}")
|
||||
if a.vision:
|
||||
vision = {"yes": "gpu", "no": "none"}.get(a.vision, a.vision)
|
||||
else:
|
||||
@@ -767,6 +889,25 @@ def main() -> int:
|
||||
say(" download and keeps ~1.4 GB of VRAM free for the image encoder, so text is a few % slower.")
|
||||
vision = "gpu" if ask("Do you want images?", ["y", "n"], "n", a.yes) == "y" else "none"
|
||||
ok("images: " + {"none": "off", "gpu": "on", "cpu": "on (encoder on the CPU)"}[vision])
|
||||
# EXPERIMENTAL: the experimental-speed-projection control vector (data/experimental-speed-projection), off unless
|
||||
# chosen here; with it loaded, the web app and the API switch it off per request
|
||||
esp = None
|
||||
esp_choice = (a.experimental_speed_projection or "").strip()
|
||||
if family == "qwen":
|
||||
if not esp_choice:
|
||||
say()
|
||||
say(" EXPERIMENTAL - speed projection: a small control vector applied while the model runs (layers 4-44).")
|
||||
say(" It changes how the model answers: its package describes it as a refusal-direction projection (the")
|
||||
say(" model declines far fewer requests). Off unless you choose it; when on, the web app can switch it off")
|
||||
say(" per chat. Details: data/experimental-speed-projection/README.md")
|
||||
esp_choice = "on" if ask("Turn on the experimental speed projection?", ["y", "n"], "n", a.yes) == "y" else "off"
|
||||
if esp_choice.lower() not in ("off", "no", "n", "0"):
|
||||
esp = ESP_VECTOR if esp_choice.lower() in ("on", "yes", "y", "1") else Path(esp_choice).expanduser().resolve()
|
||||
if not esp.is_file():
|
||||
fail(f"the experimental speed projection's vector is missing: {esp}")
|
||||
ok("experimental speed projection: " + ("ON (experimental)" if esp else "off"))
|
||||
elif esp_choice.lower() not in ("", "off", "no", "n", "0"):
|
||||
warn("the experimental speed projection is made for the original Qwen3.8-Flash-Next, not Swift 1.5: left off")
|
||||
models_dir = Path(a.gguf_dir) if a.gguf_dir else Path(a.models_dir) / tag
|
||||
shards = [models_dir / fam["file"].format(q=model, i=i) for i in (1, 2)]
|
||||
have_model = all(s.exists() and (done(s) or a.gguf_dir) for s in shards)
|
||||
@@ -777,7 +918,7 @@ def main() -> int:
|
||||
|
||||
# ---- 3. python packages
|
||||
step(3, "Python packages")
|
||||
pip_install(PY_PACKAGES, "numpy, jinja2, regex, pyyaml, tqdm, requests, cmake, ninja, pillow")
|
||||
pip_install(PY_PACKAGES, "numpy, jinja2, regex, pyyaml, tqdm, requests, cmake, ninja, pillow, psutil")
|
||||
|
||||
# ---- 4. the engine
|
||||
step(4, "the Strata engine")
|
||||
@@ -866,22 +1007,33 @@ def main() -> int:
|
||||
fail("the model has no per_layer_token_embd tensor (is this a Qwen3.8-Flash-Next GGUF?)")
|
||||
args = ["--pack", str(pack), "--native", str(shards[0]), "--ple-gguf", str(ple),
|
||||
"--expert-profile", str(ROOT / "data" / "expert-profile.bin"), "--expert-cache", "auto",
|
||||
"--prefill", "2048", "--spec", "4", "--spec-min-p", "0.5", "--mtp", str(rt),
|
||||
"--prefill", "auto", "--spec", "4", "--spec-min-p", "0.5", "--mtp", str(rt),
|
||||
"--max-context", str(ctx)]
|
||||
if ctx > 8192:
|
||||
args += ["--kv", "int8"]
|
||||
args += ["--kv", kv]
|
||||
# KV streaming: from 64K up the whole KV cache lives in RAM and only the part the attention reads (32K positions
|
||||
# per layer) stays in VRAM; the VRAM it frees holds more experts (+6% at 128K, +23% at 262K with Q2_0). It
|
||||
# costs ~13.7 KB of RAM per context token (1.7 GB at 128K, 3.4 GB at 262K), so only when that fits.
|
||||
kv_ram_gb = ctx * 13728 / 1e9
|
||||
if ctx >= 65536 and ram >= MODELS[model]["ram_gb"] + kv_ram_gb + 1:
|
||||
# costs ~13.7 KB of RAM per context token with 8-bit KV (1.7 GB at 128K), 7.5 KB with 4-bit, so only when it fits.
|
||||
kv_ram_gb = ctx * (13 * (576 if kv == "q4_0" else 1056)) / 1e9 # 12 QSA layers + the draft layer
|
||||
# the RAM copy must be pinned, and under WSL the NVIDIA driver pins only about 1 GB in all
|
||||
if is_wsl() and ctx >= 65536:
|
||||
ok("WSL: KV streaming off (the driver pins only about 1 GB of RAM); the KV cache stays in VRAM")
|
||||
elif ctx >= 65536 and ram >= MODELS[model]["ram_gb"] + kv_ram_gb + 1:
|
||||
args += ["--kv-resident", "32768"]
|
||||
ok(f"KV streaming on: the context's KV cache lives in RAM ({kv_ram_gb:.1f} GB), more experts fit in VRAM")
|
||||
if vision != "none":
|
||||
args += ["--vision", "--vram-reserve-mib", str(VISION[vision]["reserve_mib"])]
|
||||
if esp is not None:
|
||||
# the package's profile, with llama.cpp's flags (the engine takes the same ones)
|
||||
args += ["--control-vector-scaled", f"{esp}:1.0", "--control-vector-layer-range", "4", "44",
|
||||
"--cvec-mode", "project", "--cvec-dir", "per-layer"]
|
||||
cfg = {"exe": str(eng / EXE), "args": args, "cwd": str(ROOT), "tokenizer": str(pack / "tokenizer"),
|
||||
"model_name": f"{fam['name']}-{model.lower()}", "log": str(ROOT / f"strata-{tag.lower()}.log"),
|
||||
"lib_dirs": lib_dirs, "port": a.port}
|
||||
"lib_dirs": lib_dirs, "port": port}
|
||||
if a.host:
|
||||
cfg["host"] = a.host
|
||||
if a.api_key:
|
||||
cfg["api_key"] = a.api_key
|
||||
if vision != "none":
|
||||
cfg["vision"] = {"exe": str(eng / VEXE), "mmproj": str(mmproj), "model": str(shards[0]),
|
||||
"gpu": vision == "gpu", "max_tokens": VISION[vision]["max_tokens"]}
|
||||
@@ -889,19 +1041,22 @@ def main() -> int:
|
||||
cfg["vision"]["threads"] = max(1, (os.cpu_count() or 8) // 2)
|
||||
cfg_path = ROOT / f"strata-{tag.lower()}.json"
|
||||
cfg_path.write_text(json.dumps(cfg, indent=1), encoding="utf-8")
|
||||
script = write_run_script(tag, cfg_path, a.port)
|
||||
script = write_run_script(tag, cfg_path, port)
|
||||
ok(f"start script: {script.name}")
|
||||
|
||||
say()
|
||||
say("All set.")
|
||||
say(f" API (OpenAI): http://127.0.0.1:{a.port}/v1 (any API key; model name: anything)")
|
||||
say(f" API (Anthropic): http://127.0.0.1:{a.port}/v1/messages")
|
||||
say(f" API (OpenAI): http://127.0.0.1:{port}/v1 (any API key; model name: anything)")
|
||||
say(f" API (Anthropic): http://127.0.0.1:{port}/v1/messages")
|
||||
if a.host and a.host not in ("127.0.0.1", "localhost"):
|
||||
say(f" Other devices: the server window prints this PC's address (http://<IP>:{port}/)"
|
||||
+ ("" if a.api_key else " - no API key set: anyone on your network can use it"))
|
||||
say(f" Next time: just run {'START-HERE.bat' if WIN else './setup.sh'} (or {script.name}) - it starts right away")
|
||||
if vision != "none":
|
||||
say(" Images: send them in the chat page, in chat.py (/image <path>) or over the API")
|
||||
if a.no_start:
|
||||
return 0
|
||||
return start(cfg_path, a.port)
|
||||
return start(cfg_path, port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,10 +2,17 @@
|
||||
# Strata for Linux: the first run installs everything and starts the model; later runs just start it.
|
||||
# Needs only an NVIDIA driver. 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
|
||||
# python3-venv package), and a venv made without it has no pip
|
||||
ok_py() { "$1" -c 'import sys, venv, ensurepip; sys.exit(0 if sys.version_info >= (3, 10) else 1)' 2>/dev/null; }
|
||||
# a .venv from an earlier run that failed half-way has a python but no pip: start it again
|
||||
if [ -x .venv/bin/python ] && ! .venv/bin/python -m pip --version >/dev/null 2>&1; then
|
||||
rm -rf .venv
|
||||
fi
|
||||
if [ ! -x .venv/bin/python ]; then
|
||||
PY=""
|
||||
for c in python3 python; do
|
||||
if command -v $c >/dev/null 2>&1 && $c -c 'import sys, venv; sys.exit(0 if sys.version_info >= (3, 10) else 1)' 2>/dev/null; then
|
||||
if command -v $c >/dev/null 2>&1 && ok_py $c; then
|
||||
PY=$c; break
|
||||
fi
|
||||
done
|
||||
@@ -19,12 +26,13 @@ if [ ! -x .venv/bin/python ]; then
|
||||
sudo pacman -S --noconfirm python python-pip
|
||||
fi
|
||||
PY=python3
|
||||
if ! $PY -c 'import sys, venv; sys.exit(0 if sys.version_info >= (3, 10) else 1)' 2>/dev/null; then
|
||||
echo "Please install Python 3.10 or newer (with venv), then run ./setup.sh again."
|
||||
if ! ok_py $PY; then
|
||||
echo "Please install Python 3.10 or newer with venv (Ubuntu/Debian: sudo apt install python3-venv), then run"
|
||||
echo "./setup.sh again."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
# a private environment inside this folder (system Python stays untouched; newer distros refuse global pip)
|
||||
$PY -m venv .venv || { echo "could not create .venv: sudo apt install python3-venv"; exit 1; }
|
||||
$PY -m venv .venv || { rm -rf .venv; echo "could not create .venv: sudo apt install python3-venv"; exit 1; }
|
||||
fi
|
||||
exec .venv/bin/python setup.py "$@"
|
||||
+58
-20
@@ -7,6 +7,7 @@
|
||||
#include "strata/kernels/elementwise.hpp"
|
||||
#include "strata/kernels/gdn.hpp"
|
||||
#include "strata/kernels/qsa.hpp"
|
||||
#include "strata/kernels/kv_q4.hpp"
|
||||
#include "strata/kernels/kv_q8.hpp"
|
||||
#include "strata/kernels/quantize_act.hpp"
|
||||
#include "strata/kernels/rope.hpp"
|
||||
@@ -24,6 +25,7 @@
|
||||
#include "strata/kernels/native_rope.hpp"
|
||||
#include "strata/kernels/native_flash_attn.hpp"
|
||||
#include "strata/kernels/fused_gr.hpp"
|
||||
#include "strata/kernels/cvec.hpp"
|
||||
#include "strata/kernels/qsa_decode_attn.hpp"
|
||||
#include "strata/kernels/fused_gdn.hpp"
|
||||
#include "strata/kernels/qsa_select.hpp"
|
||||
@@ -170,9 +172,11 @@ if (!w.wants_q8k()) { s_gemv_q8_0_split(x80, p.codes, p.scales, p.offset,
|
||||
// namespace
|
||||
void layer_set_native_bf16(bool enabled) { native_bf16_projections = enabled; }
|
||||
void layer_set_native_flash_attn_short(bool enabled) { native_flash_attn_short = enabled; }
|
||||
namespace { bool g_kv_int8 = false; }
|
||||
namespace { bool g_kv_int8 = false, g_kv_q4 = false; }
|
||||
void qsa_set_kv_int8(bool enabled) { g_kv_int8 = enabled; }
|
||||
bool qsa_kv_int8() { return g_kv_int8; }
|
||||
void qsa_set_kv_q4(bool enabled) { g_kv_q4 = enabled; }
|
||||
bool qsa_kv_q4() { return g_kv_q4; }
|
||||
uint64_t gdn_buffers_bytes(const ModelGeometry& g) { const int64_t C = g.ssm_conv_channels; const int64_t V = g.ssm_value_dim; const uint64_t parts[] = { q8k_bytes(g.n_embd),
|
||||
// x_q8k
|
||||
(uint64_t) (g.n_embd / 32) * 34,
|
||||
@@ -507,6 +511,7 @@ KvPlan kv_plan(const QsaShapes& s, int64_t max_cells, int64_t ring_cells) {
|
||||
return p;
|
||||
}
|
||||
uint64_t kv_pool_bytes(const QsaShapes& s, int64_t pages) {
|
||||
if (g_kv_q4) return (uint64_t) pages * s.page_size * strata::kernels::kv_q4_bytes_per_cell(s) + 64;
|
||||
return g_kv_int8 ? (uint64_t) pages * s.page_size * strata::kernels::kv_q8_bytes_per_cell(s) + 64
|
||||
: (uint64_t) pages * s.n_head_kv * s.page_size * s.head_dim * 2 * 2;
|
||||
}
|
||||
@@ -540,11 +545,16 @@ uint64_t qsa_state_init(const ModelGeometry& g, int64_t max_cells, void* base, Q
|
||||
const KvPlan p = kv_plan(s, max_cells, ring_cells);
|
||||
const int64_t pages = p.pages;
|
||||
Cursor c{(uint8_t*) base};
|
||||
st.kv_int8 = g_kv_int8;
|
||||
st.kv_int8 = g_kv_int8 && !g_kv_q4;
|
||||
st.kv_q4 = g_kv_q4;
|
||||
st.kv_mode = p.mode;
|
||||
st.n_slots = p.slots;
|
||||
const uint64_t rows = (uint64_t) p.slots * s.n_head_kv * s.page_size; // VRAM rows: the slots
|
||||
if (g_kv_int8) {
|
||||
const uint64_t q4_row = strata::kernels::kv_q4_bytes_per_head((int) s.head_dim);
|
||||
if (st.kv_q4) {
|
||||
st.k_q4 = c.take<uint8_t>(rows * q4_row);
|
||||
st.v_q4 = c.take<uint8_t>(rows * q4_row);
|
||||
} else if (g_kv_int8) {
|
||||
st.k_q = c.take<int8_t>(rows * s.head_dim);
|
||||
st.v_q = c.take<int8_t>(rows * s.head_dim);
|
||||
st.k_scale = c.take<uint16_t>(rows * (s.head_dim / strata::kernels::KV_Q8_GROUP));
|
||||
@@ -595,18 +605,24 @@ uint64_t qsa_state_init(const ModelGeometry& g, int64_t max_cells, void* base, Q
|
||||
st.host = strata::kernels::KvHostPools{};
|
||||
if (p.mode != 0) {
|
||||
const uint64_t hrows = (uint64_t) pages * s.n_head_kv * s.page_size;
|
||||
const uint64_t bytes = (uint64_t) pages * strata::kernels::kv_block_bytes(s, g_kv_int8) + 4 * 256;
|
||||
const uint64_t bytes = (uint64_t) pages * strata::kernels::kv_block_bytes(s, qsa_kv_format(st)) + 4 * 256;
|
||||
uint8_t* h = nullptr;
|
||||
uint8_t* d = nullptr;
|
||||
if (cudaHostAlloc((void**) &h, bytes, cudaHostAllocMapped | cudaHostAllocPortable) != cudaSuccess ||
|
||||
cudaHostGetDevicePointer((void**) &d, h, 0) != cudaSuccess) {
|
||||
// under WSL the NVIDIA driver pins only ~1 GiB in all, which is less than 128K of 8-bit KV needs
|
||||
if (p.mode == 1) std::fprintf(stderr, "strata: KV streaming: cannot pin %.2f GiB of RAM for a layer's KV copy "
|
||||
"(lower the context, or run without --kv-resident)\n", (double) bytes / 1073741824.0);
|
||||
"(%.2f GiB pinned so far) - lower the context, or run without --kv-resident (under "
|
||||
"WSL the driver pins only about 1 GiB in all)\n", (double) bytes / 1073741824.0,
|
||||
(double) g_kv_host_bytes / 1073741824.0);
|
||||
return 0;
|
||||
}
|
||||
g_kv_host_bytes += bytes;
|
||||
Cursor hc{d};
|
||||
if (g_kv_int8) {
|
||||
if (st.kv_q4) {
|
||||
st.host.k_q4 = hc.take<uint8_t>(hrows * q4_row);
|
||||
st.host.v_q4 = hc.take<uint8_t>(hrows * q4_row);
|
||||
} else if (g_kv_int8) {
|
||||
st.host.k_q = hc.take<int8_t>(hrows * s.head_dim);
|
||||
st.host.v_q = hc.take<int8_t>(hrows * s.head_dim);
|
||||
st.host.k_scale = hc.take<uint16_t>(hrows * (s.head_dim / strata::kernels::KV_Q8_GROUP));
|
||||
@@ -645,7 +661,10 @@ void qsa_state_zero(const QsaState& st, const ModelGeometry& g, void* stream) {
|
||||
const QsaShapes s = qsa_shapes(g);
|
||||
cudaStream_t cs = (cudaStream_t) stream;
|
||||
const size_t rows = (size_t) st.n_slots * s.n_head_kv * s.page_size;
|
||||
if (st.kv_int8) {
|
||||
if (st.kv_q4) {
|
||||
cudaMemsetAsync(st.k_q4, 0, rows * strata::kernels::kv_q4_bytes_per_head((int) s.head_dim), cs);
|
||||
cudaMemsetAsync(st.v_q4, 0, rows * strata::kernels::kv_q4_bytes_per_head((int) s.head_dim), cs);
|
||||
} else if (st.kv_int8) {
|
||||
cudaMemsetAsync(st.k_q, 0, rows * s.head_dim, cs);
|
||||
cudaMemsetAsync(st.v_q, 0, rows * s.head_dim, cs);
|
||||
cudaMemsetAsync(st.k_scale, 0, rows * (s.head_dim / strata::kernels::KV_Q8_GROUP) * 2, cs);
|
||||
@@ -666,7 +685,8 @@ void qsa_state_zero(const QsaState& st, const ModelGeometry& g, void* stream) {
|
||||
strata::kernels::QsaAttnPools qsa_attn_pools(const QsaState& st) {
|
||||
strata::kernels::QsaAttnPools pools;
|
||||
pools.page_table = st.page_table;
|
||||
if (st.kv_int8) { pools.k_q = st.k_q; pools.v_q = st.v_q; pools.k_scale = st.k_scale; pools.v_scale = st.v_scale; }
|
||||
if (st.kv_q4) { pools.k_q4 = st.k_q4; pools.v_q4 = st.v_q4; }
|
||||
else if (st.kv_int8) { pools.k_q = st.k_q; pools.v_q = st.v_q; pools.k_scale = st.k_scale; pools.v_scale = st.v_scale; }
|
||||
else { pools.k_pool = st.k_pool; pools.v_pool = st.v_pool; }
|
||||
return pools;
|
||||
}
|
||||
@@ -674,7 +694,7 @@ strata::kernels::QsaAttnPools qsa_attn_pools(const QsaState& st) {
|
||||
void qsa_kv_resolve(const QsaState& st, const ModelGeometry& g, const int32_t* ids, const int32_t* steps, int64_t n_q,
|
||||
int64_t cap, void* stream) {
|
||||
if (st.kv_mode != 1) return;
|
||||
strata::kernels::kv_stream_resolve(st.map, qsa_attn_pools(st), st.host, st.kv_int8, ids, steps, n_q, cap,
|
||||
strata::kernels::kv_stream_resolve(st.map, qsa_attn_pools(st), st.host, qsa_kv_format(st), ids, steps, n_q, cap,
|
||||
qsa_shapes(g), stream);
|
||||
}
|
||||
|
||||
@@ -846,7 +866,11 @@ if (!gemv_quantized(*w_attnk, p_k, f_k, b.x_q8_0, b.x_q8k, b.kcur, g.n_embd, g.n
|
||||
// EVERY ENTRY POINT FROM HERE ON IS THE CAPTURABLE ONE: the per-token counts come from `st.step` and every
|
||||
// launch is sized from a capacity in `st`/`b`, so this sequence can be captured and replayed. The
|
||||
// host-scalar wrappers would be correct here today and silently wrong in a graph.
|
||||
if (st.kv_int8) kv_append_q8_step(st.k_q, st.v_q, st.k_scale, st.v_scale, st.page_table, st.step, b.kcur, b.vcur, s, stream, &st.host); else kv_append_step(st.k_pool, st.v_pool, st.page_table, st.step, b.kcur, b.vcur, s, stream, &st.host); { const uint64_t nvk = (uint64_t) g.n_head_kv * g.head_dim; const uint64_t base = (uint64_t) 2 * g.n_embd + 2 * g.hc + (uint64_t) g.n_head * g.head_dim + 2 * nvk + 8; if (!st.kv_int8) dump_slot(dump, g, layer, (const float*) st.k_pool, base, nvk / 2, stream); if (!st.kv_int8) dump_slot(dump, g, layer, (const float*) st.v_pool, base + nvk / 2, nvk / 2, stream); dump_slot(dump, g, layer, b.vcur, base + nvk, nvk, stream); dump_slot(dump, g, layer, b.kcur, base + 2 * nvk, nvk, stream); } { const QsaIndexerBuffers ib{st.idx_tail, st.idx_dead, st.idx_pooled, st.idx_block_pos}; if (native_qsa_indexer_enabled()) {
|
||||
if (st.kv_q4) {
|
||||
strata::kernels::fwht256_inplace_cuda(b.kcur, g.n_head_kv, stream); // Q4_0: rotated K and V (kv_q4.hpp)
|
||||
strata::kernels::fwht256_inplace_cuda(b.vcur, g.n_head_kv, stream);
|
||||
strata::kernels::kv_append_q4_step(st.k_q4, st.v_q4, st.page_table, st.step, b.kcur, b.vcur, s, stream, &st.host);
|
||||
} else if (st.kv_int8) kv_append_q8_step(st.k_q, st.v_q, st.k_scale, st.v_scale, st.page_table, st.step, b.kcur, b.vcur, s, stream, &st.host); else kv_append_step(st.k_pool, st.v_pool, st.page_table, st.step, b.kcur, b.vcur, s, stream, &st.host); { const uint64_t nvk = (uint64_t) g.n_head_kv * g.head_dim; const uint64_t base = (uint64_t) 2 * g.n_embd + 2 * g.hc + (uint64_t) g.n_head * g.head_dim + 2 * nvk + 8; if (!st.kv_int8) dump_slot(dump, g, layer, (const float*) st.k_pool, base, nvk / 2, stream); if (!st.kv_int8) dump_slot(dump, g, layer, (const float*) st.v_pool, base + nvk / 2, nvk / 2, stream); dump_slot(dump, g, layer, b.vcur, base + nvk, nvk, stream); dump_slot(dump, g, layer, b.kcur, base + 2 * nvk, nvk, stream); } { const QsaIndexerBuffers ib{st.idx_tail, st.idx_dead, st.idx_pooled, st.idx_block_pos}; if (native_qsa_indexer_enabled()) {
|
||||
try {
|
||||
native_qsa_indexer_append(b.idx_raw, st.step + kStepPos, pos_base,
|
||||
(const float*) w_ikn->data, RMS_EPS, ib, s, st.max_cells, (float) qsa_freq_base(), stream);
|
||||
@@ -880,14 +904,13 @@ int64_t max_blocks = (st.max_cells / s.idx_block) + 2;
|
||||
}
|
||||
// KV streaming: every block the selection names is made resident before anything reads it
|
||||
qsa_kv_resolve(st, g, b.ids, st.step, 1, cap, stream);
|
||||
if (st.kv_q4) strata::kernels::fwht256_inplace_cuda(b.qcur, g.n_head, stream); // <Hq, Hk> = <q, k>
|
||||
if (g_fast_attn && !native_flash_attn_short && dump == nullptr) {
|
||||
strata::kernels::QsaAttnPools pools;
|
||||
pools.k_pool = st.k_pool; pools.v_pool = st.v_pool; pools.k_q = st.k_q; pools.v_q = st.v_q;
|
||||
pools.k_scale = st.k_scale; pools.v_scale = st.v_scale; pools.page_table = st.page_table;
|
||||
if (!st.kv_int8) { pools.k_q = nullptr; pools.v_q = nullptr; }
|
||||
const strata::kernels::QsaAttnPools pools = qsa_attn_pools(st);
|
||||
strata::kernels::qsa_decode_attn_step(b.qcur, pools, b.ids, st.step, cap, s, b.attn_scratch, b.attn, stream);
|
||||
} else {
|
||||
if (st.kv_int8) kv_gather_q8_step(st.k_q, st.v_q, st.k_scale, st.v_scale, st.page_table, b.ids, st.step, cap, s, b.k_scratch, b.v_scratch, stream); else kv_gather_step(st.k_pool, st.v_pool, st.page_table, b.ids, st.step, cap, s, b.k_scratch, b.v_scratch, stream); if (native_flash_attn_short) {
|
||||
if (st.kv_q4) strata::kernels::kv_gather_q4_step(st.k_q4, st.v_q4, st.page_table, b.ids, st.step, cap, s, b.k_scratch, b.v_scratch, stream);
|
||||
else if (st.kv_int8) kv_gather_q8_step(st.k_q, st.v_q, st.k_scale, st.v_scale, st.page_table, b.ids, st.step, cap, s, b.k_scratch, b.v_scratch, stream); else kv_gather_step(st.k_pool, st.v_pool, st.page_table, b.ids, st.step, cap, s, b.k_scratch, b.v_scratch, stream); if (native_flash_attn_short) {
|
||||
if (st.max_cells < 1 || st.max_cells > 256 || !st.attention_status || !st.host_step) {
|
||||
err = v.name("native_flash_attn") + ": short adapter requires context <=256 and persistent status storage";
|
||||
return false;
|
||||
@@ -902,6 +925,7 @@ int64_t max_blocks = (st.max_cells / s.idx_block) + 2;
|
||||
}
|
||||
} else qsa_attend_step(b.qcur, b.k_scratch, b.v_scratch, st.step, cap, s, b.attn, nullptr, stream);
|
||||
}
|
||||
if (st.kv_q4) strata::kernels::fwht256_inplace_cuda(b.attn, g.n_head, stream); // the output back: H is self-inverse
|
||||
dump_slot(dump, g, layer, b.attn, (uint64_t) 2 * g.n_embd + 2 * g.hc, (uint64_t) g.n_head * g.head_dim, stream); { const uint64_t vs = (uint64_t) 2 * g.n_embd + 2 * g.hc + (uint64_t) g.n_head * g.head_dim + (uint64_t) 2 * g.n_head_kv * g.head_dim; dump_slot(dump, g, layer, (const float*) b.v_scratch, vs, (uint64_t) g.n_head_kv * g.head_dim / 2, stream); dump_slot(dump, g, layer, (const float*) b.ids, vs + (uint64_t) g.n_head_kv * g.head_dim / 2, 4, stream); dump_slot(dump, g, layer, (const float*) st.step, vs + (uint64_t) g.n_head_kv * g.head_dim / 2 + 4, 4, stream); }
|
||||
// ---- 9. Gate the attention output before its projection. Native CUDA keeps F32
|
||||
// sigmoid/multiply arithmetic and uses Q8_1 for the native quantized projection.
|
||||
@@ -1087,7 +1111,8 @@ bool block_layer_pre(const WeightTable& tables, const ModelGeometry& g, int64_t
|
||||
// the device half: `ple_block` reading `emb_dev`, and the history shift.
|
||||
const bool fused = g_fused_gr && stage_prefix == 0 && half == 0 &&
|
||||
strata::kernels::fused_gr_supported(g.n_embd, g.hc, g.hc_lr);
|
||||
bool pending_ffn = fused && layer > 0; // layer-1's FFN write has not been applied to R yet
|
||||
// layer-1's FFN write has not been applied to R yet - unless a control vector follows it, which needs R
|
||||
bool pending_ffn = fused && layer > 0 && !strata::kernels::cvec().covers(layer - 1);
|
||||
if (ple != nullptr && ple->ready() && layer == 1 && (half == 0 || half == 1)) {
|
||||
if (pending_ffn) {
|
||||
const strata::kernels::GrShapes gs0{g.n_embd, g.hc, g.hc_lr};
|
||||
@@ -1222,10 +1247,23 @@ st_begin(layer, 5, stream);
|
||||
if (g_shared_early ? !moe_combine_parts(g, layer, k, mb, parts, bb.block_out, stream, err)
|
||||
: !moe_finish(tables, g, layer, k, mb, bb.mixed, parts, bb.block_out, stream, err)) return false;
|
||||
st_end(layer, 5, stream); dump_half(bb, g, layer, bb.block_out, (uint64_t) g.n_embd, g.n_embd, stream); dump_half(bb, g, layer, bb.inject, (uint64_t) 2 * g.n_embd + g.hc, g.hc, stream); st_begin(layer, 6, stream);
|
||||
if (!(g_fused_gr && strata::kernels::fused_gr_supported(g.n_embd, g.hc, g.hc_lr)))
|
||||
gr_write(bb.R, bb.block_out, bb.inject, gs, bb.R, stream);
|
||||
else if (layer == g.n_layers - 1)
|
||||
gr_write(bb.R, bb.block_out, bb.inject2, gs, bb.R, stream); // materialise R for the head
|
||||
const bool steer = strata::kernels::cvec().covers(layer); // --control-vector-scaled: after this write
|
||||
try {
|
||||
if (!(g_fused_gr && strata::kernels::fused_gr_supported(g.n_embd, g.hc, g.hc_lr))) {
|
||||
gr_write(bb.R, bb.block_out, bb.inject, gs, bb.R, stream);
|
||||
if (steer) strata::kernels::cvec_apply(bb.R, layer, 1, g.hc * g.n_embd, nullptr, 0, nullptr, 0, false, stream);
|
||||
} else if (layer == g.n_layers - 1) {
|
||||
gr_write(bb.R, bb.block_out, bb.inject2, gs, bb.R, stream); // materialise R for the head
|
||||
if (steer) strata::kernels::cvec_apply(bb.R, layer, 1, g.hc * g.n_embd, nullptr, 0, nullptr, 0, false, stream);
|
||||
} else if (steer) {
|
||||
// the write the next layer's fused read would have folded, then the vector
|
||||
strata::kernels::cvec_apply(bb.R, layer, 1, g.hc * g.n_embd, bb.block_out, g.n_embd, bb.inject2, g.hc,
|
||||
true, stream);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
err = std::string("block_layer_post: ") + e.what();
|
||||
return false;
|
||||
}
|
||||
st_end(layer, 6, stream); return true;}
|
||||
bool block_layer(const WeightTable& tables, const ModelGeometry& g, int64_t layer, int64_t pos, int32_t pos_base, const GdnBuffers& gb, const QsaState& qst, const QsaBuffers& qb, const MoEBuffers& mb, int64_t k, const BlockBuffers& bb, const float* parts, void* stream, std::string& err, const Doorbell* db, const PleRun* ple) {
|
||||
// The two halves back to back. `parts` must be THIS layer's experts and must be ready before the call -
|
||||
|
||||
+10
-2
@@ -7,6 +7,7 @@
|
||||
#include "strata/kernels/elementwise.hpp"
|
||||
#include "strata/kernels/fused_gr.hpp"
|
||||
#include "strata/kernels/gr.hpp"
|
||||
#include "strata/kernels/kv_q4.hpp"
|
||||
#include "strata/kernels/kv_q8.hpp"
|
||||
#include "strata/kernels/native_moe.hpp"
|
||||
#include "strata/kernels/native_mmvq.hpp"
|
||||
@@ -338,7 +339,12 @@ bool MtpDrafter::record_forward(int T, int step_row0, cudaStream_t cs, std::stri
|
||||
native_mmvq(GGML_Q8_0, q8("self_attn.v_proj.weight"), xq_, vcur_, (int) N, (int) (NKV * HD), T, cs);
|
||||
for (int t = 0; t < T; ++t) {
|
||||
norm_rope(kcur_ + t * NKV * HD, f32("self_attn.k_norm.weight"), (int) NKV, (int) HD, pos + t * NH);
|
||||
if (st_.kv_int8)
|
||||
if (st_.kv_q4) { // Q4_0 KV (kv_q4.hpp): rotated K and V
|
||||
fwht256_inplace_cuda(kcur_ + t * NKV * HD, NKV, cs);
|
||||
fwht256_inplace_cuda(vcur_ + t * NKV * HD, NKV, cs);
|
||||
kv_append_q4_step(st_.k_q4, st_.v_q4, st_.page_table, step + t * 4, kcur_ + t * NKV * HD,
|
||||
vcur_ + t * NKV * HD, s, cs, &st_.host);
|
||||
} else if (st_.kv_int8)
|
||||
kv_append_q8_step(st_.k_q, st_.v_q, st_.k_scale, st_.v_scale, st_.page_table, step + t * 4,
|
||||
kcur_ + t * NKV * HD, vcur_ + t * NKV * HD, s, cs, &st_.host);
|
||||
else
|
||||
@@ -355,10 +361,12 @@ bool MtpDrafter::record_forward(int T, int step_row0, cudaStream_t cs, std::stri
|
||||
return false;
|
||||
}
|
||||
norm_rope(qc, f32("self_attn.q_norm.weight"), (int) NH, (int) HD, pos + t * NH);
|
||||
if (st_.kv_q4) fwht256_inplace_cuda(qc, NH, cs);
|
||||
}
|
||||
const QsaAttnPools pools = qsa_attn_pools(st_);
|
||||
if (window_ > 0) window_ids(const_cast<int32_t*>(step), T, (int) window_, ident_, cap_, cs);
|
||||
qsa_decode_attn_batch(qcur_, pools, ident_, step, cap_, s, attn_scratch_, attn_, T, cs);
|
||||
if (st_.kv_q4) fwht256_inplace_cuda(attn_, (int64_t) T * NH, cs);
|
||||
for (int t = 0; t < T; ++t)
|
||||
native_qsa_gate_apply(attn_ + t * NH * HD, qfull_ + t * NH * 2 * HD, attn32_ + t * NH * HD, (int) NH, (int) HD, cs);
|
||||
native_quantize_q8_1(attn32_, xq_, (int) (NH * HD), T, cs);
|
||||
@@ -507,7 +515,7 @@ void MtpDrafter::kv_restore(int64_t upto) {
|
||||
// the ring's blocks below `upto`, from the host copy: a checkpoint resume may have left later cells in them
|
||||
const strata::kernels::QsaShapes s = shapes_of(*g_);
|
||||
const int64_t b1 = (upto + s.page_size - 1) / s.page_size, b0 = std::max<int64_t>(0, b1 - st_.n_slots);
|
||||
strata::kernels::kv_ring_restore(qsa_attn_pools(st_), st_.host, st_.kv_int8, b0, b1, st_.n_slots, s, cs_);
|
||||
strata::kernels::kv_ring_restore(qsa_attn_pools(st_), st_.host, qsa_kv_format(st_), b0, b1, st_.n_slots, s, cs_);
|
||||
cudaStreamSynchronize(cs_);
|
||||
}
|
||||
|
||||
|
||||
+31
-5
@@ -29,20 +29,46 @@ namespace {
|
||||
// A 2 MB-aligned reservation. Large pages first, then the largest alignment the OS will give us for free.
|
||||
void* reserve(uint64_t bytes, PageBacking& got, std::string& note) {
|
||||
#ifdef _WIN32
|
||||
// MEM_LARGE_PAGES needs SeLockMemoryPrivilege. Having it assigned to the account is not enough: the
|
||||
// PROCESS must enable it in its own token (AdjustTokenPrivileges) before VirtualAlloc, or the call fails.
|
||||
// An account without the assignment, or a failure to enable, leaves the process as it was: VirtualAlloc
|
||||
// then refuses and the 4 KB fallback below runs - that is the EXPECTED outcome on a desktop.
|
||||
{
|
||||
HANDLE tok = nullptr;
|
||||
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &tok)) {
|
||||
TOKEN_PRIVILEGES tp{};
|
||||
tp.PrivilegeCount = 1;
|
||||
if (LookupPrivilegeValueW(nullptr, L"SeLockMemoryPrivilege", &tp.Privileges[0].Luid)) {
|
||||
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
|
||||
if (!AdjustTokenPrivileges(tok, FALSE, &tp, 0, nullptr, nullptr) && GetLastError() != ERROR_NOT_ALL_ASSIGNED)
|
||||
(void) 0; // nothing actionable: the large-page attempt below reports the outcome
|
||||
}
|
||||
CloseHandle(tok);
|
||||
}
|
||||
}
|
||||
// MEM_LARGE_PAGES needs SeLockMemoryPrivilege; a normal account does not have it and VirtualAlloc then
|
||||
// fails with ERROR_PRIVILEGE_NOT_HELD. That is the EXPECTED outcome on a desktop, not an error.
|
||||
SIZE_T large = GetLargePageMinimum();
|
||||
if (large > 0) {
|
||||
void* p = VirtualAlloc(nullptr, (SIZE_T) bytes, MEM_RESERVE | MEM_COMMIT | MEM_LARGE_PAGES,
|
||||
// A/B switch: STRATA_NO_LARGEPAGES=1 skips the large-page attempt, same run, same boot.
|
||||
if (large > 0 && std::getenv("STRATA_NO_LARGEPAGES") == nullptr) {
|
||||
// MEM_LARGE_PAGES requires the allocation size to be an exact multiple of the large page size -
|
||||
// anything else is ERROR_INVALID_PARAMETER (87), which reads like a privilege problem but is not.
|
||||
// Round up: the slack is under 2 MB and the tail stays unused.
|
||||
const SIZE_T lbytes = (SIZE_T) (((SIZE_T) bytes + large - 1) / large * large);
|
||||
void* p = VirtualAlloc(nullptr, lbytes, MEM_RESERVE | MEM_COMMIT | MEM_LARGE_PAGES,
|
||||
PAGE_READWRITE);
|
||||
if (p) {
|
||||
got = PageBacking::LargePages;
|
||||
note = "large pages (" + std::to_string((unsigned long long) large) + " B)";
|
||||
return p;
|
||||
}
|
||||
note = "large pages refused (GetLargePageMinimum=" + std::to_string((unsigned long long) large) +
|
||||
", VirtualAlloc error " + std::to_string((unsigned long long) GetLastError()) +
|
||||
" - needs SeLockMemoryPrivilege); using 4 KB pages";
|
||||
// 1450 (ERROR_NO_SYSTEM_RESOURCES) is the large-page pool saying no, 87 is a size that is not a
|
||||
// multiple of the minimum, 1314 is the privilege: without the byte count the three read as one bug.
|
||||
note = "large pages refused for " + std::to_string((unsigned long long) lbytes) + " B (GetLargePageMinimum=" +
|
||||
std::to_string((unsigned long long) large) + ", VirtualAlloc error " +
|
||||
std::to_string((unsigned long long) GetLastError()) + "); using 4 KB pages";
|
||||
} else if (std::getenv("STRATA_NO_LARGEPAGES") != nullptr) {
|
||||
note = "large pages skipped (STRATA_NO_LARGEPAGES); using 4 KB pages";
|
||||
} else {
|
||||
note = "this system has no large-page minimum; using 4 KB pages";
|
||||
}
|
||||
|
||||
+11
-2
@@ -1,5 +1,6 @@
|
||||
// src/core/session.cpp - one token through all 48 layers. See the header for why the graphs are per-layer.
|
||||
#include "strata/core/session.hpp"
|
||||
#include "strata/core/progress.hpp"
|
||||
|
||||
#include "strata/kernels/qsa.hpp"
|
||||
#include "strata/kernels/elementwise.hpp"
|
||||
@@ -87,9 +88,12 @@ uint64_t session_init(const ModelGeometry& g, int64_t max_cells, int64_t k, void
|
||||
s.qsa_buf_arena = take(qsa_buffers_bytes(g, max_cells));
|
||||
|
||||
uint8_t* qp = (uint8_t*) s.qsa_state_arena;
|
||||
// a layer whose pinned RAM could not be had (KV streaming's host copy) is half-built: going on would have the
|
||||
// attention read null host pointers at the first request ("illegal memory access"), so the session fails here
|
||||
for (int64_t i = 0; i < g.n_qsa_layers(); ++i)
|
||||
qsa_state_init(g, max_cells, qp + (i == 0 ? 0 : first + (uint64_t) (i - 1) * rest), s.qsa_states[i],
|
||||
i == 0 ? nullptr : &s.qsa_states[0]);
|
||||
if (qsa_state_init(g, max_cells, qp + (i == 0 ? 0 : first + (uint64_t) (i - 1) * rest), s.qsa_states[i],
|
||||
i == 0 ? nullptr : &s.qsa_states[0]) == 0)
|
||||
return 0;
|
||||
qsa_buffers_init(g, max_cells, s.qsa_buf_arena, s.qsa_bufs);
|
||||
|
||||
s.moe_arena = take(moe_buffers_bytes(g, k));
|
||||
@@ -888,6 +892,7 @@ bool session_run_token(const ModelGeometry& g, int64_t pos, int32_t pos_base, Se
|
||||
const auto t0 = Clock::now();
|
||||
auto last_flush = t0;
|
||||
uint32_t spins = 0;
|
||||
progress_at("token: waiting for the GPU to reach layer", l);
|
||||
while (*seq < want) {
|
||||
STRATA_SPIN_PAUSE();
|
||||
if ((++spins & 1023u) != 0) continue;
|
||||
@@ -909,6 +914,7 @@ bool session_run_token(const ModelGeometry& g, int64_t pos, int32_t pos_base, Se
|
||||
}
|
||||
}
|
||||
const auto t1 = Clock::now();
|
||||
progress_at("token: the CPU experts of layer", l);
|
||||
if (pool != nullptr) pool(user, s.db->h_x_f, s.db->h_ids, s.db->h_weights, g.n_embd, s.k, y_miss_host);
|
||||
std::atomic_thread_fence(std::memory_order_seq_cst);
|
||||
_mm_sfence();
|
||||
@@ -917,8 +923,11 @@ bool session_run_token(const ModelGeometry& g, int64_t pos, int32_t pos_base, Se
|
||||
tg.ms_wait += std::chrono::duration<double, std::milli>(t1 - t0).count();
|
||||
tg.ms_pool += std::chrono::duration<double, std::milli>(t2 - t1).count();
|
||||
}
|
||||
progress_at("token: waiting for the GPU to finish the token");
|
||||
const cudaError_t se = cudaStreamSynchronize(cs);
|
||||
if (se != cudaSuccess) { err = std::string("session_run_token: ") + cudaGetErrorString(se); return false; }
|
||||
progress_at("decode");
|
||||
progress_beat();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+63
-3
@@ -11,7 +11,9 @@
|
||||
#include "strata/kernels/cpu/expert.hpp"
|
||||
#include "strata/kernels/elementwise.hpp"
|
||||
#include "strata/kernels/fused_gr.hpp"
|
||||
#include "strata/kernels/cvec.hpp"
|
||||
#include "strata/kernels/gr.hpp"
|
||||
#include "strata/kernels/kv_q4.hpp"
|
||||
#include "strata/kernels/kv_q8.hpp"
|
||||
#include "strata/kernels/native_mmvq.hpp"
|
||||
#include "strata/kernels/native_qsa.hpp"
|
||||
@@ -26,11 +28,13 @@
|
||||
#include "strata/kernels/rope.hpp"
|
||||
#include "strata/kernels/s2_expert_grouped.hpp"
|
||||
#include "strata/kernels/sampler.hpp"
|
||||
#include "strata/core/progress.hpp"
|
||||
#include "strata/kernels/shared_expert.hpp"
|
||||
#include "strata/kernels/verify_kernels.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -89,7 +93,23 @@ bool native_of(const WeightRef* w, const std::string& name, std::string& err) {
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
std::atomic<const Verifier*> g_diag_verifier{nullptr};
|
||||
void diag_active_verifier(std::FILE* f) {
|
||||
if (const Verifier* v = g_diag_verifier.load()) v->diag(f);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void Verifier::diag(std::FILE* f) const {
|
||||
auto rd = [](const uint32_t* p) { return p ? *(const volatile uint32_t*) p : 0u; };
|
||||
std::fprintf(f, " verify window: %d tokens at position %lld, host at layer step %u; the GPU rang %u; flags: "
|
||||
"served %u, plan (A) %u, copies (B) %u\n", last_t_, (long long) last_pos0_, cur_layer_ + 1,
|
||||
rd(h_seq_), rd(h_flag_), rd(h_flagA_), rd(h_flagB_));
|
||||
}
|
||||
|
||||
Verifier::~Verifier() {
|
||||
const Verifier* self = this;
|
||||
g_diag_verifier.compare_exchange_strong(self, nullptr);
|
||||
if (cs_) cudaStreamSynchronize(cs_);
|
||||
for (auto& e : exec_)
|
||||
if (e) cudaGraphExecDestroy(e);
|
||||
@@ -105,6 +125,8 @@ Verifier::~Verifier() {
|
||||
|
||||
bool Verifier::init(const WeightTable& wt, const ModelGeometry& g, SessionState& ss, const VerifyHits& hits,
|
||||
const NativeHead* head, int max_t, std::string& err) {
|
||||
g_diag_verifier.store(this);
|
||||
diag_verify_fn().store(&diag_active_verifier);
|
||||
wt_ = &wt;
|
||||
g_ = &g;
|
||||
ss_ = &ss;
|
||||
@@ -330,7 +352,9 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) {
|
||||
wi[h] = need(v, (std::string(pfx[h]) + "inject.weight").c_str(), err);
|
||||
if (!wn[h] || !wd[h] || !wu[h] || !wi[h]) return false;
|
||||
}
|
||||
bool pending = l > 0; // the previous layer's FFN write, folded into this layer's first read
|
||||
// the previous layer's FFN write, folded into this layer's first read (a control vector after it has
|
||||
// already applied it)
|
||||
bool pending = l > 0 && !cvec().covers(l - 1);
|
||||
if (l == 1 && ple_on) {
|
||||
float* normalized = (float*) ((uint8_t*) ss.ple.scratch + ple_block_scratch_bytes());
|
||||
for (int t = tb; t < te; ++t) {
|
||||
@@ -421,10 +445,17 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) {
|
||||
native_mmvq(wk->native_type, wk->native_data, xq_, kcur_ + tb * NKV * HD, (int) N, (int) (NKV * HD), n, cs);
|
||||
native_mmvq(wv->native_type, wv->native_data, xq_, vcur_ + tb * NKV * HD, (int) N, (int) (NKV * HD), n, cs);
|
||||
for (int t = tb; t < te; ++t) norm_rope(kcur_ + t * NKV * HD, wkn, (int) NKV, (int) HD, pos_ + t * NH);
|
||||
if (st.kv_q4) { // Q4_0 KV (kv_q4.hpp): K and V rotated before they are stored
|
||||
fwht256_inplace_cuda(kcur_ + tb * NKV * HD, (int64_t) n * NKV, cs);
|
||||
fwht256_inplace_cuda(vcur_ + tb * NKV * HD, (int64_t) n * NKV, cs);
|
||||
}
|
||||
if (grp == 0) copy_from_mapped(tail_snap_ + (size_t) qi * TS, st.idx_tail, TS, cs);
|
||||
for (int t = tb; t < te; ++t) {
|
||||
const int32_t* step_t = step_ + t * kStepCount;
|
||||
if (st.kv_int8)
|
||||
if (st.kv_q4)
|
||||
kv_append_q4_step(st.k_q4, st.v_q4, st.page_table, step_t, kcur_ + t * NKV * HD,
|
||||
vcur_ + t * NKV * HD, s, cs, &st.host);
|
||||
else if (st.kv_int8)
|
||||
kv_append_q8_step(st.k_q, st.v_q, st.k_scale, st.v_scale, st.page_table, step_t,
|
||||
kcur_ + t * NKV * HD, vcur_ + t * NKV * HD, s, cs, &st.host);
|
||||
else
|
||||
@@ -446,6 +477,7 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) {
|
||||
return false;
|
||||
}
|
||||
norm_rope(qc, wqn, (int) NH, (int) HD, pos_ + t * NH);
|
||||
if (st.kv_q4) fwht256_inplace_cuda(qc, NH, cs); // <Hq, Hk> = <q, k>
|
||||
}
|
||||
for (int t = tb; t < te; ++t) {
|
||||
float* qx = qidx_ + t * IQ * ID;
|
||||
@@ -461,6 +493,7 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) {
|
||||
const QsaAttnPools pools = qsa_attn_pools(st);
|
||||
qsa_decode_attn_batch(qcur_ + tb * NH * HD, pools, sel_ + (size_t) tb * cap_, step_ + tb * kStepCount, cap_,
|
||||
s, attn_scratch_ + (size_t) tb * attn_scratch_floats_, attn_ + tb * NH * HD, n, cs);
|
||||
if (st.kv_q4) fwht256_inplace_cuda(attn_ + tb * NH * HD, (int64_t) n * NH, cs); // back: H^-1 = H
|
||||
for (int t = tb; t < te; ++t) {
|
||||
if (native_qsa_enabled())
|
||||
native_qsa_gate_apply(attn_ + t * NH * HD, qfull_ + t * NH * 2 * HD, attn32_ + t * NH * HD,
|
||||
@@ -559,8 +592,12 @@ bool Verifier::record_window(int T, cudaStream_t cs, std::string& err) {
|
||||
mb.weights = w_ + t * K; mb.shared = shared_ + t * N;
|
||||
if (!moe_combine_parts(g, l, K, mb, parts_ + (size_t) t * K * N, bo_ + t * N, cs, err)) return false;
|
||||
}
|
||||
if (l == g.n_layers - 1)
|
||||
if (l == g.n_layers - 1) {
|
||||
for (int t = tb; t < te; ++t) gr_write(Rt(t), bo_ + t * N, inj2_ + t * HC, gs, Rt(t), cs);
|
||||
if (cvec().covers(l)) cvec_apply(Rt(tb), l, n, HC * N, nullptr, 0, nullptr, 0, false, cs);
|
||||
} else if (cvec().covers(l)) {
|
||||
cvec_apply(Rt(tb), l, n, HC * N, bo_ + tb * N, N, inj2_ + tb * HC, HC, true, cs);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -755,6 +792,7 @@ bool Verifier::run(int T, const int32_t* tokens, int64_t pos0, PoolMultiFn pool,
|
||||
const Clock::time_point a = Clock::now();
|
||||
auto last_flush = a;
|
||||
uint32_t spins = 0;
|
||||
progress_at("verify window: waiting for the GPU to reach layer", l);
|
||||
while (*seq < want) {
|
||||
_mm_pause();
|
||||
if ((++spins & 1023u) != 0) continue;
|
||||
@@ -775,10 +813,12 @@ bool Verifier::run(int T, const int32_t* tokens, int64_t pos0, PoolMultiFn pool,
|
||||
cur_layer_ = want - 1;
|
||||
set_plan_slot(grp);
|
||||
const int tb = gtb[grp], n = gte[grp] - gtb[grp];
|
||||
progress_at("verify window: the CPU experts of layer", l);
|
||||
if (pool != nullptr)
|
||||
pool(user, h_x_ + (size_t) tb * g.n_embd, h_ids_ + (size_t) tb * ss.k, n, ss.k,
|
||||
h_ymiss_ + (size_t) tb * ss.k * g.n_embd, l);
|
||||
VDBG("layer %lld served\n", (long long) l);
|
||||
progress_tick();
|
||||
std::atomic_thread_fence(std::memory_order_seq_cst);
|
||||
_mm_sfence();
|
||||
if (*(volatile uint32_t*) h_flagA_ != want) { // the pool did not publish a plan: an empty one
|
||||
@@ -795,8 +835,10 @@ bool Verifier::run(int T, const int32_t* tokens, int64_t pos0, PoolMultiFn pool,
|
||||
ms_wait += std::chrono::duration<double, std::milli>(b - a).count();
|
||||
ms_pool += ms_since(b);
|
||||
}
|
||||
progress_at("verify window: waiting for the GPU to finish the window (flags A/B/M raised)", (int64_t) T);
|
||||
const cudaError_t se = cudaStreamSynchronize(cs_);
|
||||
if (se != cudaSuccess) { err = std::string("verify: ") + cudaGetErrorString(se); return false; }
|
||||
progress_at("verify window: waiting for the expert copies", (int64_t) T);
|
||||
cudaStreamSynchronize(copy_); // no host function of this window may raise flag B in the next one
|
||||
// ---- a sampled or penalized request: the head's sampling again, host-side so its parameters are this call's
|
||||
// own (a captured kernel would replay the same draws forever). Row t's draw is Philox(seed, pos0 + t): tied to
|
||||
@@ -813,8 +855,26 @@ bool Verifier::run(int T, const int32_t* tokens, int64_t pos0, PoolMultiFn pool,
|
||||
}
|
||||
}
|
||||
for (int t = 0; t < T; ++t) out[t] = ((volatile int32_t*) h_out_)[t];
|
||||
if (static const bool dbg = std::getenv("STRATA_DBG_NAN") != nullptr; dbg) { // debug: the first non-finite head
|
||||
static bool reported = false;
|
||||
if (!reported) {
|
||||
std::vector<float> h((size_t) T * (size_t) n_vocab_);
|
||||
cudaMemcpy(h.data(), head_logits_, h.size() * 4, cudaMemcpyDeviceToHost);
|
||||
for (int t = 0; t < T && !reported; ++t) {
|
||||
int64_t bad = 0;
|
||||
for (int64_t v = 0; v < n_vocab_; ++v) bad += !std::isfinite(h[(size_t) t * n_vocab_ + v]);
|
||||
if (bad) {
|
||||
reported = true;
|
||||
std::fprintf(stderr, "strata dbg: verify window at position %lld, row %d: %lld of %lld logits non-finite "
|
||||
"(token out %d)\n", (long long) pos0, t, (long long) bad, (long long) n_vocab_, out[t]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
VDBG("window done\n");
|
||||
++windows;
|
||||
progress_at("decode");
|
||||
progress_beat();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
// src/kernels/cpu/iq_avx2.cpp - the i-quant expert rows in 256-bit lanes, several tokens at once.
|
||||
//
|
||||
// The AVX-512 kernels' (iq_avx512.cpp) multi-token scheme on the CPUs without AVX-512 (AMD Zen 2/3, Intel
|
||||
// Core 12th-14th gen and Core Ultra): each 32-value chunk is decoded once - grid lookups, one sign vector,
|
||||
// one scale vector - and every token of the verify window applies them with a load, a `sign`, a `maddubs`,
|
||||
// a `madd` and an `add`. ggml-cpu's own AVX2 dot products for these formats are single-token: every token
|
||||
// re-does the codebook lookups and the sign expansion. The sign vector is ggml's bit_selector pattern
|
||||
// (ggml-cpu/arch/x86/quants.c): pshufb-broadcast of each sign byte, AND with the bit selector, CMPEQ, OR
|
||||
// with one, `vpsignb`. The arithmetic is ggml's (ggml-cpu/quants.c, the `_generic` references) - only the
|
||||
// order of the float additions differs.
|
||||
//
|
||||
// Formats: IQ2_XXS (16), IQ2_XS (17), IQ3_XXS (18), IQ3_S (21), IQ2_S (22). IQ1_M stays on ggml-cpu.
|
||||
#include "strata/kernels/cpu/iq_avx2.hpp"
|
||||
|
||||
#define GGML_COMMON_DECL_CPP
|
||||
#define GGML_COMMON_IMPL_CPP
|
||||
#include "ggml-common.h"
|
||||
|
||||
#include <immintrin.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace strata::kernels::cpu {
|
||||
namespace {
|
||||
|
||||
inline float h2f(uint16_t h) { return _mm_cvtss_f32(_mm_cvtph_ps(_mm_cvtsi32_si128((int) h))); }
|
||||
inline uint32_t u32(const uint8_t* p) { uint32_t v; std::memcpy(&v, p, 4); return v; }
|
||||
inline uint16_t u16(const uint8_t* p) { uint16_t v; std::memcpy(&v, p, 2); return v; }
|
||||
inline uint64_t u64(const uint8_t* p) { uint64_t v; std::memcpy(&v, p, 8); return v; }
|
||||
|
||||
// 32 sign bits -> 32 bytes of -1 (bit set) / +1: ggml's bit_selector pattern, shared by all tokens.
|
||||
inline __m256i sgn_vec(uint32_t m) {
|
||||
const __m128i bm = _mm_set1_epi32((int) m);
|
||||
const __m128i lo = _mm_shuffle_epi8(bm, _mm_setr_epi8(0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1));
|
||||
const __m128i hi = _mm_shuffle_epi8(bm, _mm_setr_epi8(2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3));
|
||||
const __m256i bits = _mm256_inserti128_si256(_mm256_castsi128_si256(lo), hi, 1);
|
||||
const __m256i sel = _mm256_setr_epi8(1, 2, 4, 8, 16, 32, 64, (char) 0x80,
|
||||
1, 2, 4, 8, 16, 32, 64, (char) 0x80,
|
||||
1, 2, 4, 8, 16, 32, 64, (char) 0x80,
|
||||
1, 2, 4, 8, 16, 32, 64, (char) 0x80);
|
||||
const __m256i nz = _mm256_cmpeq_epi8(_mm256_and_si256(bits, sel), sel);
|
||||
return _mm256_or_si256(nz, _mm256_set1_epi8(1));
|
||||
}
|
||||
|
||||
// The two 16-value scales of one 32-value half (IQ2_XS, IQ2_S): int16 lanes 0-7 = a (values 0-15),
|
||||
// 8-15 = b (values 16-31). NOT a broadcast dword [a,b] - that would alternate the scales lane by lane.
|
||||
inline __m256i sc16(int a, int b) {
|
||||
return _mm256_inserti128_si256(_mm256_castsi128_si256(_mm_set1_epi16((short) a)),
|
||||
_mm_set1_epi16((short) b), 1);
|
||||
}
|
||||
// One scale for the whole 32-value half (IQ2_XXS, IQ3_XXS, IQ3_S).
|
||||
inline __m256i sc32(int s) { return _mm256_set1_epi16(s); }
|
||||
|
||||
// keven_signs_q2xs (ggml keeps it static in arch/x86/quants.c): one u64 per 7-bit sign index, byte k
|
||||
// = 0xFF when bit k of ksigns_iq2xs[i] is set, 0x01 otherwise. With this a whole 32-value sign vector
|
||||
// is four scalar loads and a set_epi64x - no ksigns byte packing chain and no bit_selector expansion.
|
||||
// Built once at load time from the shared ggml-common table so it can never drift from it.
|
||||
struct EvenSigns {
|
||||
uint64_t v[128];
|
||||
EvenSigns() {
|
||||
for (int i = 0; i < 128; ++i) {
|
||||
uint64_t r = 0;
|
||||
for (int k = 0; k < 8; ++k)
|
||||
r |= (uint64_t) (((ksigns_iq2xs[i] >> k) & 1) ? 0xFF : 0x01) << (8 * k);
|
||||
v[i] = r;
|
||||
}
|
||||
}
|
||||
};
|
||||
static const EvenSigns even_signs;
|
||||
|
||||
inline float hsum8(__m256 v) {
|
||||
const __m128 lo = _mm256_castps256_ps128(v), hi = _mm256_extractf128_ps(v, 1);
|
||||
__m128 s = _mm_add_ps(lo, hi);
|
||||
s = _mm_hadd_ps(s, s);
|
||||
s = _mm_hadd_ps(s, s);
|
||||
return _mm_cvtss_f32(s);
|
||||
}
|
||||
|
||||
// ---- per format: one 32-value half (values 64*j + 32*half .. +31) -> grid magnitudes, sign vector, scales
|
||||
template <int TY> struct Fmt32;
|
||||
|
||||
template <> struct Fmt32<16> { // IQ2_XXS: d, qs[32] u16
|
||||
static constexpr int bytes = 66;
|
||||
static constexpr float K = 0.125f;
|
||||
static inline void decode(const uint8_t* b, int j, int half, __m256i& g, __m256i& sgn, __m256i& sc) {
|
||||
const uint8_t* q = b + 2 + 16 * j + 8 * half;
|
||||
const uint32_t w0 = u32(q), w1 = u32(q + 4);
|
||||
g = _mm256_set_epi64x((long long) iq2xxs_grid[w0 >> 24], (long long) iq2xxs_grid[(w0 >> 16) & 255],
|
||||
(long long) iq2xxs_grid[(w0 >> 8) & 255], (long long) iq2xxs_grid[w0 & 255]);
|
||||
sgn = _mm256_set_epi64x((long long) even_signs.v[(w1 >> 21) & 127], (long long) even_signs.v[(w1 >> 14) & 127],
|
||||
(long long) even_signs.v[(w1 >> 7) & 127], (long long) even_signs.v[w1 & 127]);
|
||||
sc = sc32(2 * (int) (w1 >> 28) + 1);
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct Fmt32<17> { // IQ2_XS: d, qs[32] u16 (9-bit grid index + 7-bit sign index), scales[8]
|
||||
static constexpr int bytes = 74;
|
||||
static constexpr float K = 0.125f;
|
||||
static inline void decode(const uint8_t* b, int j, int half, __m256i& g, __m256i& sgn, __m256i& sc) {
|
||||
uint16_t v[8];
|
||||
std::memcpy(v, b + 2 + 16 * j, 16);
|
||||
const int o = 4 * half;
|
||||
g = _mm256_set_epi64x((long long) iq2xs_grid[v[o + 3] & 511], (long long) iq2xs_grid[v[o + 2] & 511],
|
||||
(long long) iq2xs_grid[v[o + 1] & 511], (long long) iq2xs_grid[v[o] & 511]);
|
||||
uint32_t s = 0;
|
||||
for (int l = 0; l < 4; ++l) s |= (uint32_t) ksigns_iq2xs[v[o + l] >> 9] << (8 * l);
|
||||
sgn = sgn_vec(s);
|
||||
const uint8_t sb = b[66 + 2 * j + half];
|
||||
sc = sc16(2 * (sb & 15) + 1, 2 * (sb >> 4) + 1);
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct Fmt32<22> { // IQ2_S: d, qs[64] (32 grid bytes, 32 sign bytes), qh[8], scales[8]
|
||||
static constexpr int bytes = 82;
|
||||
static constexpr float K = 0.125f;
|
||||
static inline void decode(const uint8_t* b, int j, int half, __m256i& g, __m256i& sgn, __m256i& sc) {
|
||||
const uint8_t* qs = b + 2 + 8 * j;
|
||||
const uint8_t h = b[66 + 2 * j + half];
|
||||
const int o = 4 * half;
|
||||
g = _mm256_set_epi64x((long long) iq2s_grid[qs[o + 3] | ((h << 2) & 0x300)],
|
||||
(long long) iq2s_grid[qs[o + 2] | ((h << 4) & 0x300)],
|
||||
(long long) iq2s_grid[qs[o + 1] | ((h << 6) & 0x300)],
|
||||
(long long) iq2s_grid[qs[o] | ((h << 8) & 0x300)]);
|
||||
const uint64_t m = u64(b + 2 + 32 + 8 * j);
|
||||
sgn = sgn_vec(half ? (uint32_t) (m >> 32) : (uint32_t) m);
|
||||
const uint8_t sb = b[74 + 2 * j + half];
|
||||
sc = sc16(2 * (sb & 15) + 1, 2 * (sb >> 4) + 1);
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct Fmt32<18> { // IQ3_XXS: d, qs[64] grid bytes, 8 x u32 (4 x 7-bit sign index + 4-bit scale)
|
||||
static constexpr int bytes = 98;
|
||||
static constexpr float K = 0.25f;
|
||||
static inline void decode(const uint8_t* b, int j, int half, __m256i& g, __m256i& sgn, __m256i& sc) {
|
||||
const uint8_t* q = b + 2 + 16 * j + 8 * half;
|
||||
g = _mm256_set_epi32((int) iq3xxs_grid[q[7]], (int) iq3xxs_grid[q[6]], (int) iq3xxs_grid[q[5]], (int) iq3xxs_grid[q[4]],
|
||||
(int) iq3xxs_grid[q[3]], (int) iq3xxs_grid[q[2]], (int) iq3xxs_grid[q[1]], (int) iq3xxs_grid[q[0]]);
|
||||
const uint32_t w = u32(b + 2 + 64 + 8 * j + 4 * half);
|
||||
sgn = _mm256_set_epi64x((long long) even_signs.v[(w >> 21) & 127], (long long) even_signs.v[(w >> 14) & 127],
|
||||
(long long) even_signs.v[(w >> 7) & 127], (long long) even_signs.v[w & 127]);
|
||||
sc = sc32(2 * (int) (w >> 28) + 1);
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct Fmt32<21> { // IQ3_S: d, qs[64], qh[8], signs[32], scales[4]
|
||||
static constexpr int bytes = 110;
|
||||
static constexpr float K = 1.0f;
|
||||
static inline void decode(const uint8_t* b, int j, int half, __m256i& g, __m256i& sgn, __m256i& sc) {
|
||||
const uint8_t* q = b + 2 + 16 * j + 8 * half;
|
||||
const uint32_t h = b[66 + 2 * j + half];
|
||||
#define G3(k) (int) iq3s_grid[q[k] | (((h >> k) & 1u) << 8)]
|
||||
g = _mm256_set_epi32(G3(7), G3(6), G3(5), G3(4), G3(3), G3(2), G3(1), G3(0));
|
||||
#undef G3
|
||||
const uint64_t m = u64(b + 74 + 8 * j);
|
||||
sgn = sgn_vec(half ? (uint32_t) (m >> 32) : (uint32_t) m);
|
||||
const uint8_t s = b[106 + j];
|
||||
sc = sc32(half ? 2 * (s >> 4) + 1 : 2 * (s & 15) + 1);
|
||||
}
|
||||
};
|
||||
|
||||
template <int TY, int NT>
|
||||
inline void row_dot(const uint8_t* row, int nblocks, const block_q8_K* const* y, float* res) {
|
||||
__m256 accf[NT];
|
||||
for (int t = 0; t < NT; ++t) accf[t] = _mm256_setzero_ps();
|
||||
for (int i = 0; i < nblocks; ++i) {
|
||||
const uint8_t* blk = row + (size_t) i * Fmt32<TY>::bytes;
|
||||
__m256i acci[NT];
|
||||
for (int t = 0; t < NT; ++t) acci[t] = _mm256_setzero_si256();
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
for (int half = 0; half < 2; ++half) {
|
||||
__m256i g, sgn, sc;
|
||||
Fmt32<TY>::decode(blk, j, half, g, sgn, sc);
|
||||
const int off = 64 * j + 32 * half;
|
||||
for (int t = 0; t < NT; ++t) {
|
||||
const __m256i yv = _mm256_loadu_si256((const __m256i*) (y[t][i].qs + off));
|
||||
const __m256i ys = _mm256_sign_epi8(yv, sgn);
|
||||
acci[t] = _mm256_add_epi32(acci[t], _mm256_madd_epi16(_mm256_maddubs_epi16(g, ys), sc));
|
||||
}
|
||||
}
|
||||
}
|
||||
const float dx = h2f(u16(blk)) * Fmt32<TY>::K;
|
||||
for (int t = 0; t < NT; ++t)
|
||||
accf[t] = _mm256_fmadd_ps(_mm256_set1_ps(dx * y[t][i].d), _mm256_cvtepi32_ps(acci[t]), accf[t]);
|
||||
}
|
||||
for (int t = 0; t < NT; ++t) res[t] = hsum8(accf[t]);
|
||||
}
|
||||
|
||||
// ---- IQ2_XS (17) with ggml's vectorized sign decode (arch/x86/quants.c, ggml_vec_dot_iq2_xs_q8_K):
|
||||
// the 7-bit sign indices never touch ksigns_iq2xs - the 8th sign bit is reconstructed by parity (two
|
||||
// shifts, a xor and ggml's bit_helper pshufb) and the per-half sign vectors come out of shuffles of one
|
||||
// 32-byte load. The 8 scale bytes become all 16 half-scales with ggml's unpack trick. The grid lookups
|
||||
// stay scalar loads into set_epi64x (ggml does the same). Every token still only pays a load, a sign,
|
||||
// a maddubs, a madd and an add per half, into alternating accumulators.
|
||||
template <int NT>
|
||||
inline void row_dot_iq2xs(const uint8_t* row, int nblocks, const block_q8_K* const* y, float* res) {
|
||||
static const uint8_t bit_sel[32] = {
|
||||
1, 2, 4, 8, 16, 32, 64, (uint8_t) 0x80, 1, 2, 4, 8, 16, 32, 64, (uint8_t) 0x80,
|
||||
1, 2, 4, 8, 16, 32, 64, (uint8_t) 0x80, 1, 2, 4, 8, 16, 32, 64, (uint8_t) 0x80 };
|
||||
static const char shuf_even[32] = { // sign bits of u16 0,1,2,3 (even bytes of the sign register)
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6 };
|
||||
static const char shuf_odd[32] = { // sign bits of u16 4,5,6,7
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 10, 10, 10, 10, 10, 10, 10, 10,
|
||||
12, 12, 12, 12, 12, 12, 12, 12, 14, 14, 14, 14, 14, 14, 14, 14 };
|
||||
static const uint8_t bit_help[32] = { // 0x80 when the 4-bit index has odd popcount (parity bit 7)
|
||||
0x00, (uint8_t) 0x80, (uint8_t) 0x80, 0x00, (uint8_t) 0x80, 0x00, 0x00, (uint8_t) 0x80,
|
||||
(uint8_t) 0x80, 0x00, 0x00, (uint8_t) 0x80, 0x00, (uint8_t) 0x80, (uint8_t) 0x80, 0x00,
|
||||
0x00, (uint8_t) 0x80, (uint8_t) 0x80, 0x00, (uint8_t) 0x80, 0x00, 0x00, (uint8_t) 0x80,
|
||||
(uint8_t) 0x80, 0x00, 0x00, (uint8_t) 0x80, 0x00, (uint8_t) 0x80, (uint8_t) 0x80, 0x00 };
|
||||
static const uint8_t k_sc_shuffle[128] = { // half h -> scale bytes 2h (lanes 0-7), 2h+1 (lanes 8-15)
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7,
|
||||
8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9,
|
||||
10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11,
|
||||
12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13,
|
||||
14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15 };
|
||||
const __m256i bsel = _mm256_loadu_si256((const __m256i*) bit_sel);
|
||||
const __m256i shev = _mm256_loadu_si256((const __m256i*) shuf_even);
|
||||
const __m256i shod = _mm256_loadu_si256((const __m256i*) shuf_odd);
|
||||
const __m256i bhelp = _mm256_loadu_si256((const __m256i*) bit_help);
|
||||
const __m256i m511 = _mm256_set1_epi16(511);
|
||||
const __m128i m4 = _mm_set1_epi8(0xf), m1 = _mm_set1_epi8(1);
|
||||
const __m256i one8 = _mm256_set1_epi8(1);
|
||||
|
||||
__m256 accf[NT];
|
||||
for (int t = 0; t < NT; ++t) accf[t] = _mm256_setzero_ps();
|
||||
for (int i = 0; i < nblocks; ++i) {
|
||||
const uint8_t* blk = row + (size_t) i * 74;
|
||||
// the 8 scale bytes -> 16 half-scales of 2*s+1, interleaved [a0, b0, a1, b1, ...] (ggml's unpack)
|
||||
__m128i st = _mm_set1_epi64x((long long) u64(blk + 66));
|
||||
st = _mm_unpacklo_epi8(_mm_and_si128(st, m4), _mm_and_si128(_mm_srli_epi16(st, 4), m4));
|
||||
const __m128i scales = _mm_add_epi8(_mm_slli_epi16(st, 1), m1);
|
||||
__m256i acc[NT][2];
|
||||
for (int t = 0; t < NT; ++t) acc[t][0] = acc[t][1] = _mm256_setzero_si256();
|
||||
for (int jj = 0; jj < 2; ++jj) { // 128 values (16 u16) per step
|
||||
const __m256i q2 = _mm256_loadu_si256((const __m256i*) (blk + 2 + 32 * jj));
|
||||
const __m256i p7 = _mm256_srli_epi16(q2, 9); // 7 sign bits per u16, even bytes
|
||||
const __m256i p3 = _mm256_srli_epi16(q2, 13); // sign index bits 4-6
|
||||
const __m256i fsb = _mm256_or_si256(p7,
|
||||
_mm256_shuffle_epi8(bhelp, _mm256_xor_si256(p7, p3))); // + the parity sign bit 7
|
||||
const __m256i s01 = _mm256_broadcastsi128_si256(_mm256_castsi256_si128(fsb)); // u16 0-7
|
||||
const __m256i s23 = _mm256_broadcastsi128_si256(_mm256_extracti128_si256(fsb, 1)); // u16 8-15
|
||||
alignas(32) uint16_t gi[16];
|
||||
_mm256_store_si256((__m256i*) gi, _mm256_and_si256(q2, m511));
|
||||
for (int h = 0; h < 4; ++h) { // the four 32-value halves of this group
|
||||
const int H = 4 * jj + h; // global half index -> scales byte pair
|
||||
const __m256i g = _mm256_set_epi64x((long long) iq2xs_grid[gi[4 * h + 3]],
|
||||
(long long) iq2xs_grid[gi[4 * h + 2]],
|
||||
(long long) iq2xs_grid[gi[4 * h + 1]],
|
||||
(long long) iq2xs_grid[gi[4 * h]]);
|
||||
const __m256i sb = _mm256_shuffle_epi8(h < 2 ? s01 : s23, (h & 1) ? shod : shev);
|
||||
const __m256i sgn = _mm256_or_si256(
|
||||
_mm256_cmpeq_epi8(_mm256_and_si256(sb, bsel), bsel), one8);
|
||||
const __m256i sc = _mm256_cvtepi8_epi16(
|
||||
_mm_shuffle_epi8(scales, _mm_loadu_si128((const __m128i*) k_sc_shuffle + H)));
|
||||
const int off = 128 * jj + 32 * h;
|
||||
for (int t = 0; t < NT; ++t) {
|
||||
const __m256i yv = _mm256_loadu_si256((const __m256i*) (y[t][i].qs + off));
|
||||
const __m256i ys = _mm256_sign_epi8(yv, sgn);
|
||||
acc[t][h & 1] = _mm256_add_epi32(acc[t][h & 1],
|
||||
_mm256_madd_epi16(_mm256_maddubs_epi16(g, ys), sc));
|
||||
}
|
||||
}
|
||||
}
|
||||
const float dx = h2f(u16(blk)) * 0.125f;
|
||||
for (int t = 0; t < NT; ++t)
|
||||
accf[t] = _mm256_fmadd_ps(_mm256_set1_ps(dx * y[t][i].d),
|
||||
_mm256_cvtepi32_ps(_mm256_add_epi32(acc[t][0], acc[t][1])), accf[t]);
|
||||
}
|
||||
for (int t = 0; t < NT; ++t) res[t] = hsum8(accf[t]);
|
||||
}
|
||||
|
||||
template <int TY, int NT>
|
||||
inline void row_dot_any(const uint8_t* row, int nblocks, const block_q8_K* const* y, float* res) {
|
||||
if constexpr (TY == 17) row_dot_iq2xs<NT>(row, nblocks, y, res);
|
||||
else row_dot<TY, NT>(row, nblocks, y, res);
|
||||
}
|
||||
|
||||
template <int TY, int NT>
|
||||
void gu_rows(const uint8_t* blob, size_t gu_row, size_t up_off, int n, const void* const* act, float* const* ff,
|
||||
int r0, int r1) {
|
||||
const block_q8_K* y[NT];
|
||||
for (int t = 0; t < NT; ++t) y[t] = (const block_q8_K*) act[t];
|
||||
const int nb = n / QK_K;
|
||||
float g[NT], u[NT];
|
||||
for (int r = r0; r < r1; ++r) {
|
||||
row_dot_any<TY, NT>(blob + (size_t) r * gu_row, nb, y, g);
|
||||
row_dot_any<TY, NT>(blob + up_off + (size_t) r * gu_row, nb, y, u);
|
||||
for (int t = 0; t < NT; ++t) ff[t][r] = (g[t] / (1.f + std::exp(-g[t]))) * u[t];
|
||||
}
|
||||
}
|
||||
|
||||
template <int TY, int NT>
|
||||
void dot_rows(const uint8_t* w, size_t row_bytes, int n, const void* const* act, float* const* out, int r0, int r1) {
|
||||
const block_q8_K* y[NT];
|
||||
for (int t = 0; t < NT; ++t) y[t] = (const block_q8_K*) act[t];
|
||||
float res[NT];
|
||||
for (int r = r0; r < r1; ++r) {
|
||||
row_dot_any<TY, NT>(w + (size_t) r * row_bytes, n / QK_K, y, res);
|
||||
for (int t = 0; t < NT; ++t) out[t][r] = res[t];
|
||||
}
|
||||
}
|
||||
|
||||
template <int TY>
|
||||
void gu_rows_nt(int nt, const uint8_t* blob, size_t gu_row, size_t up_off, int n, const void* const* act,
|
||||
float* const* ff, int r0, int r1) {
|
||||
switch (nt) {
|
||||
case 1: gu_rows<TY, 1>(blob, gu_row, up_off, n, act, ff, r0, r1); break;
|
||||
case 2: gu_rows<TY, 2>(blob, gu_row, up_off, n, act, ff, r0, r1); break;
|
||||
case 3: gu_rows<TY, 3>(blob, gu_row, up_off, n, act, ff, r0, r1); break;
|
||||
case 4: gu_rows<TY, 4>(blob, gu_row, up_off, n, act, ff, r0, r1); break;
|
||||
case 5: gu_rows<TY, 5>(blob, gu_row, up_off, n, act, ff, r0, r1); break;
|
||||
case 6: gu_rows<TY, 6>(blob, gu_row, up_off, n, act, ff, r0, r1); break;
|
||||
case 7: gu_rows<TY, 7>(blob, gu_row, up_off, n, act, ff, r0, r1); break;
|
||||
default: gu_rows<TY, 8>(blob, gu_row, up_off, n, act, ff, r0, r1); break;
|
||||
}
|
||||
}
|
||||
|
||||
template <int TY>
|
||||
void dot_rows_nt(int nt, const uint8_t* w, size_t row_bytes, int n, const void* const* act, float* const* out, int r0,
|
||||
int r1) {
|
||||
switch (nt) {
|
||||
case 1: dot_rows<TY, 1>(w, row_bytes, n, act, out, r0, r1); break;
|
||||
case 2: dot_rows<TY, 2>(w, row_bytes, n, act, out, r0, r1); break;
|
||||
case 3: dot_rows<TY, 3>(w, row_bytes, n, act, out, r0, r1); break;
|
||||
case 4: dot_rows<TY, 4>(w, row_bytes, n, act, out, r0, r1); break;
|
||||
default: for (int t0 = 0; t0 < nt; t0 += 4) {
|
||||
const int k = nt - t0 < 4 ? nt - t0 : 4;
|
||||
dot_rows_nt<TY>(k, w, row_bytes, n, act + t0, out + t0, r0, r1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool iq256_supported(int type) noexcept {
|
||||
return type == 16 || type == 17 || type == 18 || type == 21 || type == 22;
|
||||
}
|
||||
|
||||
void iq256_gu_rows(int type, const uint8_t* blob, size_t gu_row, size_t up_off, int n, const void* const* act, int nt,
|
||||
float* const* ff, int r0, int r1) {
|
||||
switch (type) {
|
||||
case 16: gu_rows_nt<16>(nt, blob, gu_row, up_off, n, act, ff, r0, r1); break;
|
||||
case 17: gu_rows_nt<17>(nt, blob, gu_row, up_off, n, act, ff, r0, r1); break;
|
||||
case 18: gu_rows_nt<18>(nt, blob, gu_row, up_off, n, act, ff, r0, r1); break;
|
||||
case 21: gu_rows_nt<21>(nt, blob, gu_row, up_off, n, act, ff, r0, r1); break;
|
||||
case 22: gu_rows_nt<22>(nt, blob, gu_row, up_off, n, act, ff, r0, r1); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
void iq256_rows(int type, const uint8_t* w, size_t row_bytes, int n, const void* const* act, int nt, float* const* out,
|
||||
int r0, int r1) {
|
||||
switch (type) {
|
||||
case 16: dot_rows_nt<16>(nt, w, row_bytes, n, act, out, r0, r1); break;
|
||||
case 17: dot_rows_nt<17>(nt, w, row_bytes, n, act, out, r0, r1); break;
|
||||
case 18: dot_rows_nt<18>(nt, w, row_bytes, n, act, out, r0, r1); break;
|
||||
case 21: dot_rows_nt<21>(nt, w, row_bytes, n, act, out, r0, r1); break;
|
||||
case 22: dot_rows_nt<22>(nt, w, row_bytes, n, act, out, r0, r1); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- IQ4_NL (type 20): 32-value blocks of 18 bytes (f16 d + 16 nibble bytes) against Q8_0 activations.
|
||||
// ggml-cpu's own AVX-2 dot (ggml_vec_dot_iq4_nl_q8_0) is single-token: every token redoes the nibble ->
|
||||
// kvalues_iq4nl pshufb decode and the |w| half of the signed x signed product. Here both are computed once
|
||||
// per block; every token then costs a sign, a maddubs, a madd and an fmadd. The arithmetic is ggml's - only
|
||||
// the order of the float additions differs.
|
||||
template <int NT>
|
||||
void iq4nl_rows(const uint8_t* w, size_t row_bytes, int n, const block_q8_0* const* y, float* const* out,
|
||||
int r0, int r1) {
|
||||
const __m128i values = _mm_loadu_si128((const __m128i*) kvalues_iq4nl);
|
||||
const __m128i m4b = _mm_set1_epi8(0x0f);
|
||||
const __m256i ones = _mm256_set1_epi16(1);
|
||||
const int nb = n / QK4_NL;
|
||||
for (int r = r0; r < r1; ++r) {
|
||||
const uint8_t* row = w + (size_t) r * row_bytes;
|
||||
__m256 accf[NT];
|
||||
for (int t = 0; t < NT; ++t) accf[t] = _mm256_setzero_ps();
|
||||
for (int ib = 0; ib < nb; ++ib) {
|
||||
const uint8_t* blk = row + (size_t) ib * sizeof(block_iq4_nl);
|
||||
const __m128i bits = _mm_loadu_si128((const __m128i*) (blk + 2));
|
||||
const __m128i lo = _mm_and_si128(bits, m4b); // values 0..15
|
||||
const __m128i hi = _mm_and_si128(_mm_srli_epi16(bits, 4), m4b); // values 16..31
|
||||
const __m256i q4 = _mm256_inserti128_si256(_mm256_castsi128_si256(_mm_shuffle_epi8(values, lo)),
|
||||
_mm_shuffle_epi8(values, hi), 1);
|
||||
const __m256i aq = _mm256_sign_epi8(q4, q4); // |w|: the unsigned operand of maddubs
|
||||
const float dx = h2f(u16(blk));
|
||||
for (int t = 0; t < NT; ++t) {
|
||||
const block_q8_0& b = y[t][ib];
|
||||
const __m256i q8 = _mm256_loadu_si256((const __m256i*) b.qs);
|
||||
const __m256i p = _mm256_madd_epi16(_mm256_maddubs_epi16(aq, _mm256_sign_epi8(q8, q4)), ones);
|
||||
accf[t] = _mm256_fmadd_ps(_mm256_set1_ps(dx * h2f(b.d)), _mm256_cvtepi32_ps(p), accf[t]);
|
||||
}
|
||||
}
|
||||
for (int t = 0; t < NT; ++t) out[t][r] = hsum8(accf[t]);
|
||||
}
|
||||
}
|
||||
|
||||
void iq4nl256_down_rows(const uint8_t* w, size_t row_bytes, int n, const void* const* hq, int nt, float* const* out,
|
||||
int r0, int r1) {
|
||||
const block_q8_0* y[8];
|
||||
for (int t = 0; t < nt; ++t) y[t] = (const block_q8_0*) hq[t];
|
||||
switch (nt) {
|
||||
case 1: iq4nl_rows<1>(w, row_bytes, n, y, out, r0, r1); break;
|
||||
case 2: iq4nl_rows<2>(w, row_bytes, n, y, out, r0, r1); break;
|
||||
case 3: iq4nl_rows<3>(w, row_bytes, n, y, out, r0, r1); break;
|
||||
case 4: iq4nl_rows<4>(w, row_bytes, n, y, out, r0, r1); break;
|
||||
default: for (int t0 = 0; t0 < nt; t0 += 4) {
|
||||
const int k = nt - t0 < 4 ? nt - t0 : 4;
|
||||
iq4nl256_down_rows(w, row_bytes, n, hq + t0, k, out + t0, r0, r1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace strata::kernels::cpu
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "strata/kernels/cpu/native_expert.hpp"
|
||||
#include "strata/kernels/cpu/expert.hpp"
|
||||
#include "strata/kernels/cpu/iq_avx512.hpp"
|
||||
#include "strata/kernels/cpu/iq_avx2.hpp"
|
||||
#include "strata/kernels/cpu/expert_layout.hpp"
|
||||
|
||||
#include "ggml.h"
|
||||
@@ -76,12 +77,21 @@ void native_quant_h(const NativeFmt& f, const float* h, void* dst) {
|
||||
|
||||
void native_gu_rows(const NativeFmt& f, const uint8_t* blob, const void* const* act, int nt, float* const* ff,
|
||||
int r0, int r1) {
|
||||
// the AVX-512 kernels decode the weights once for all tokens: 2.0-2.4x ggml-cpu at three tokens, no faster at
|
||||
// one (both are bound by the codebook lookups, ~5 GB/s per core), measured by native_expert_parity
|
||||
// the multi-token kernels decode the weights once for all tokens: 2.0-2.4x ggml-cpu at three tokens, no faster
|
||||
// at one (all are bound by the codebook lookups, ~5 GB/s per core), measured by native_expert_parity. AVX-512
|
||||
// first, then the AVX-2 one (Zen 2/3, Intel 12th-14th gen); STRATA_NO_IQ512 / STRATA_NO_IQ256 fall back to
|
||||
// ggml-cpu's single-token vec_dot.
|
||||
static const bool avx512 = cpu_avx512_ok() && std::getenv("STRATA_NO_IQ512") == nullptr;
|
||||
if (avx512 && nt >= 2 && iq512_supported(f.gu_type)) { // one token: ggml-cpu is as fast or faster
|
||||
iq512_gu_rows(f.gu_type, blob, f.gu_row, f.up_off, (int) f.n_embd, act, nt, ff, r0, r1);
|
||||
return;
|
||||
static const bool avx2 = std::getenv("STRATA_NO_IQ256") == nullptr;
|
||||
if (nt >= 2 && iq512_supported(f.gu_type)) { // one token: ggml-cpu is as fast or faster
|
||||
if (avx512) {
|
||||
iq512_gu_rows(f.gu_type, blob, f.gu_row, f.up_off, (int) f.n_embd, act, nt, ff, r0, r1);
|
||||
return;
|
||||
}
|
||||
if (avx2) {
|
||||
iq256_gu_rows(f.gu_type, blob, f.gu_row, f.up_off, (int) f.n_embd, act, nt, ff, r0, r1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const ggml_vec_dot_t dot = traits(f.gu_type)->vec_dot;
|
||||
const int n = (int) f.n_embd;
|
||||
@@ -99,6 +109,13 @@ void native_gu_rows(const NativeFmt& f, const uint8_t* blob, const void* const*
|
||||
|
||||
void native_down_rows(const NativeFmt& f, const uint8_t* blob, const void* const* hq, int nt, float* const* out,
|
||||
int r0, int r1) {
|
||||
// IQ4_NL down rows: the AVX-2 multi-token kernel decodes the nibbles and absolutises the weights once per
|
||||
// block instead of once per token; ggml-cpu's dot is single-token. STRATA_NO_IQ4NL falls back to it.
|
||||
static const bool iq4nl_mt = std::getenv("STRATA_NO_IQ4NL") == nullptr;
|
||||
if (nt >= 2 && f.d_type == 20 && iq4nl_mt) {
|
||||
iq4nl256_down_rows(blob + f.down_off, f.d_row, (int) f.n_ff, hq, nt, out, r0, r1);
|
||||
return;
|
||||
}
|
||||
const ggml_vec_dot_t dot = traits(f.d_type)->vec_dot;
|
||||
const int n = (int) f.n_ff;
|
||||
for (int r = r0; r < r1; ++r) {
|
||||
|
||||
+177
-25
@@ -1,5 +1,6 @@
|
||||
// src/kernels/cpu/pool.cpp - P2.S3: the CPU expert pool. Read pool.hpp first; it explains the protocol.
|
||||
#include "strata/kernels/cpu/pool.hpp"
|
||||
#include "strata/core/progress.hpp"
|
||||
#include "strata/kernels/cpu/expert_layout.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -8,6 +9,7 @@
|
||||
#include <immintrin.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
@@ -19,6 +21,12 @@
|
||||
|
||||
namespace strata::kernels::cpu {
|
||||
|
||||
namespace {
|
||||
constexpr uint64_t pack_head(uint32_t epoch, uint32_t n, uint32_t i) {
|
||||
return ((uint64_t) epoch << 32) | ((uint64_t) n << 16) | (uint64_t) i;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::vector<int> physical_cores(bool skip_first) {
|
||||
std::vector<int> cores;
|
||||
#if defined(_WIN32)
|
||||
@@ -47,13 +55,38 @@ std::vector<int> physical_cores(bool skip_first) {
|
||||
}
|
||||
}
|
||||
#else
|
||||
// The logical CPUs this process may run on, ONE PER PHYSICAL CORE (issue #40): SMT siblings share a core's
|
||||
// load/store bandwidth, so a worker on each would put two workers on one core, as the Windows branch above
|
||||
// explains. sysfs names each CPU's (package, core); the first allowed CPU of each pair is kept, so a taskset
|
||||
// that leaves out the first sibling still gets its core. Without sysfs every allowed CPU counts, as before.
|
||||
auto topo = [](int cpu, const char* what) -> long {
|
||||
char path[96];
|
||||
std::snprintf(path, sizeof path, "/sys/devices/system/cpu/cpu%d/topology/%s", cpu, what);
|
||||
long v = -1;
|
||||
if (std::FILE* f = std::fopen(path, "r")) {
|
||||
if (std::fscanf(f, "%ld", &v) != 1) v = -1;
|
||||
std::fclose(f);
|
||||
}
|
||||
return v;
|
||||
};
|
||||
std::vector<int> allowed;
|
||||
cpu_set_t set;
|
||||
CPU_ZERO(&set);
|
||||
if (sched_getaffinity(0, sizeof set, &set) == 0) {
|
||||
for (int i = 0; i < CPU_SETSIZE; ++i)
|
||||
if (CPU_ISSET(i, &set)) cores.push_back(i);
|
||||
if (CPU_ISSET(i, &set)) allowed.push_back(i);
|
||||
} else {
|
||||
for (unsigned i = 0; i < std::thread::hardware_concurrency(); ++i) cores.push_back((int) i);
|
||||
for (unsigned i = 0; i < std::thread::hardware_concurrency(); ++i) allowed.push_back((int) i);
|
||||
}
|
||||
std::vector<std::pair<long, long>> seen;
|
||||
for (int cpu : allowed) {
|
||||
const long pkg = topo(cpu, "physical_package_id"), core = topo(cpu, "core_id");
|
||||
if (pkg >= 0 && core >= 0) {
|
||||
const std::pair<long, long> key{pkg, core};
|
||||
if (std::find(seen.begin(), seen.end(), key) != seen.end()) continue; // an SMT sibling
|
||||
seen.push_back(key);
|
||||
}
|
||||
cores.push_back(cpu);
|
||||
}
|
||||
#endif
|
||||
if (skip_first && !cores.empty()) cores.erase(cores.begin());
|
||||
@@ -108,11 +141,50 @@ void restore_thread_affinity(long long previous) {
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::atomic<const ExpertPool*> g_diag_pool{nullptr};
|
||||
void diag_active_pool(std::FILE* f) {
|
||||
if (const ExpertPool* p = g_diag_pool.load()) p->diag(f);
|
||||
}
|
||||
int64_t now_ms() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void ExpertPool::diag(std::FILE* f) const {
|
||||
const uint64_t h = head_.load();
|
||||
std::fprintf(f, " expert pool: epoch %u, batch epoch %u: %u of %u jobs claimed, %u done; %u of %d workers parked, "
|
||||
"%u sleeping; mode %d\n", epoch_.load(), (uint32_t) (h >> 32), (uint32_t) h & 0xffffu,
|
||||
(uint32_t) (h >> 16) & 0xffffu, done_.load(), parked_.load(), n_, sleepers_.load(), mode_);
|
||||
std::fprintf(f, " expert pool threads:");
|
||||
for (int i = 0; i < n_; ++i) {
|
||||
const int32_t s = wstate_[(size_t) i].load();
|
||||
if (s == kParked) std::fprintf(f, " w%d=parked", i);
|
||||
else if (s == kSleeping) std::fprintf(f, " w%d=sleeping", i);
|
||||
else if (s == kBetween) std::fprintf(f, " w%d=draining", i);
|
||||
else std::fprintf(f, " w%d=job%d", i, s);
|
||||
}
|
||||
const int32_t hs = hstate_.load();
|
||||
const char* hn = hs == kIdle ? "idle" : hs == kWaitParked ? "waiting for the workers to park"
|
||||
: hs == kWaitDone ? "waiting for the jobs to finish" : "running a job";
|
||||
std::fprintf(f, "; host %s", hn);
|
||||
if (hs >= 0) std::fprintf(f, " %d", hs);
|
||||
std::fprintf(f, " for %lld ms\n", (long long) (now_ms() - hstate_ms_.load()));
|
||||
}
|
||||
|
||||
ExpertPool::ExpertPool(int n_workers, bool pin, bool host_works) : host_works_(host_works) {
|
||||
if (const char* e = std::getenv("STRATA_POOL_SPIN_US")) // a test knob; see kSpinBeforeSleep
|
||||
spin_before_sleep_ = std::chrono::microseconds((std::max)(0, std::atoi(e)));
|
||||
const std::vector<int> cores = physical_cores(true);
|
||||
n_ = n_workers > 0 ? n_workers : (int) cores.size();
|
||||
if (n_ < 1) n_ = 1;
|
||||
scratch_.resize((size_t) n_);
|
||||
wstate_.reset(new std::atomic<int32_t>[(size_t) n_]);
|
||||
for (int i = 0; i < n_; ++i) wstate_[(size_t) i].store(kParked);
|
||||
hstate_ms_.store(now_ms());
|
||||
g_diag_pool.store(this);
|
||||
strata::core::diag_pool_fn().store(&diag_active_pool);
|
||||
split_.resize((size_t) kMaxSplit);
|
||||
split_multi_.resize((size_t) kMaxSplitMulti);
|
||||
threads_.reserve((size_t) n_);
|
||||
@@ -126,6 +198,8 @@ ExpertPool::ExpertPool(int n_workers, bool pin, bool host_works) : host_works_(h
|
||||
}
|
||||
|
||||
ExpertPool::~ExpertPool() {
|
||||
const ExpertPool* self = this;
|
||||
g_diag_pool.compare_exchange_strong(self, nullptr);
|
||||
stop_.store(true, std::memory_order_release);
|
||||
// Bump the epoch so a PARKED worker notices the stop flag rather than sleeping through it.
|
||||
publish();
|
||||
@@ -170,30 +244,106 @@ void ExpertPool::worker(int id) {
|
||||
if (stop_.load(std::memory_order_relaxed)) return;
|
||||
_mm_pause();
|
||||
if ((++spins & 1023u) != 0) continue;
|
||||
if (std::chrono::steady_clock::now() - parked_at < kSpinBeforeSleep) continue;
|
||||
if (std::chrono::steady_clock::now() - parked_at < spin_before_sleep_) continue;
|
||||
std::unique_lock<std::mutex> lk(sleep_mu_);
|
||||
wstate_[(size_t) id].store(kSleeping, std::memory_order_relaxed);
|
||||
sleepers_.fetch_add(1, std::memory_order_seq_cst);
|
||||
sleep_cv_.wait(lk, [&] {
|
||||
return epoch_.load(std::memory_order_seq_cst) != seen || stop_.load(std::memory_order_relaxed);
|
||||
});
|
||||
sleepers_.fetch_sub(1, std::memory_order_relaxed);
|
||||
wstate_[(size_t) id].store(kParked, std::memory_order_relaxed);
|
||||
}
|
||||
if (stop_.load(std::memory_order_acquire)) return;
|
||||
seen = epoch_.load(std::memory_order_relaxed);
|
||||
// acquire: the batch this epoch published (`head`, and the description before it) is visible from here
|
||||
seen = epoch_.load(std::memory_order_acquire);
|
||||
parked_.fetch_sub(1, std::memory_order_acq_rel); // leaving the park
|
||||
|
||||
// Drain: one claim per iteration, so a slow worker takes fewer experts and a fast one takes more.
|
||||
// Every job is the same size (all experts are 1,382,400 bytes), so there is nothing to schedule.
|
||||
drain(id, scratch_[(size_t) id]);
|
||||
// Every job is the same size (all experts are 1,382,400 bytes), so there is nothing to schedule. Only
|
||||
// this epoch's jobs: if the host has already moved on, the claims fail and the worker parks again.
|
||||
wstate_[(size_t) id].store(kBetween, std::memory_order_relaxed);
|
||||
drain(id, scratch_[(size_t) id], seen);
|
||||
wstate_[(size_t) id].store(kParked, std::memory_order_relaxed);
|
||||
parked_.fetch_add(1, std::memory_order_acq_rel); // back at the park
|
||||
}
|
||||
}
|
||||
|
||||
void ExpertPool::drain(int id, ExpertScratch& scratch) {
|
||||
int ExpertPool::claim(uint32_t epoch) {
|
||||
uint64_t h = head_.load(std::memory_order_acquire);
|
||||
for (;;) {
|
||||
if ((uint32_t) (h >> 32) != epoch) return -1; // not the batch this thread woke for
|
||||
const uint32_t n = (uint32_t) (h >> 16) & 0xffffu, i = (uint32_t) h & 0xffffu;
|
||||
if (i >= n) return -1; // exhausted
|
||||
if (head_.compare_exchange_weak(h, h + 1, std::memory_order_acq_rel, std::memory_order_acquire))
|
||||
return (int) i;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ExpertPool::begin_batch(int n) {
|
||||
if (n < 0 || n > 0xffff) {
|
||||
std::fprintf(stderr, "strata: expert pool batch of %d jobs is out of range\n", n);
|
||||
std::abort();
|
||||
}
|
||||
// Every job of the previous batch has completed (`wait_done`), and a claim of it can no longer succeed, so
|
||||
// nothing adds to `done` until this batch's first claim - which the release below orders after the reset.
|
||||
done_.store(0, std::memory_order_relaxed);
|
||||
const uint32_t e = epoch_.load(std::memory_order_relaxed) + 1; // only the host bumps the epoch
|
||||
head_.store(pack_head(e, (uint32_t) n, 0), std::memory_order_release);
|
||||
publish();
|
||||
return e;
|
||||
}
|
||||
|
||||
void ExpertPool::wait_parked(const char* what) {
|
||||
hstate_.store(kWaitParked, std::memory_order_relaxed);
|
||||
hstate_ms_.store(now_ms(), std::memory_order_relaxed);
|
||||
uint32_t spins = 0;
|
||||
std::chrono::steady_clock::time_point t0{};
|
||||
while (parked_.load(std::memory_order_acquire) != (uint32_t) n_) {
|
||||
_mm_pause();
|
||||
if ((++spins & 1023u) != 0) continue;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (spins == 1024u) t0 = now;
|
||||
else if (now - t0 > kStall) {
|
||||
std::fprintf(stderr, "strata: the CPU expert pool stalled %s (%u of %d workers parked) - stopping the engine "
|
||||
"so the server can start it again (issue #29)\n",
|
||||
what, parked_.load(), n_);
|
||||
std::fflush(stderr);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ExpertPool::wait_done(int n) {
|
||||
hstate_.store(kWaitDone, std::memory_order_relaxed);
|
||||
hstate_ms_.store(now_ms(), std::memory_order_relaxed);
|
||||
uint32_t spins = 0, seen = 0;
|
||||
std::chrono::steady_clock::time_point t0{};
|
||||
for (;;) {
|
||||
const uint32_t d = done_.load(std::memory_order_acquire);
|
||||
if (d >= (uint32_t) n) return; // `>=`: never a wait that an overshoot outlives
|
||||
_mm_pause();
|
||||
if ((++spins & 1023u) != 0) continue;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (spins == 1024u || d != seen) { t0 = now; seen = d; } // progress restarts the clock
|
||||
else if (now - t0 > kStall) {
|
||||
std::fprintf(stderr, "strata: the CPU expert pool stalled: %u of %d jobs done, %u of %d workers parked - "
|
||||
"stopping the engine so the server can start it again (issue #29)\n",
|
||||
d, n, parked_.load(), n_);
|
||||
std::fflush(stderr);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ExpertPool::drain(int id, ExpertScratch& scratch, uint32_t epoch) {
|
||||
(void) id;
|
||||
for (;;) {
|
||||
const uint32_t i = head_.fetch_add(1, std::memory_order_relaxed);
|
||||
if (i >= (uint32_t) njobs_) break;
|
||||
const int ci = claim(epoch);
|
||||
if (ci < 0) break;
|
||||
const uint32_t i = (uint32_t) ci;
|
||||
if (id >= 0) wstate_[(size_t) id].store(ci, std::memory_order_relaxed);
|
||||
else { hstate_.store(ci, std::memory_order_relaxed); hstate_ms_.store(now_ms(), std::memory_order_relaxed); }
|
||||
if (mode_ == 0) {
|
||||
const ExpertJob& j = jobs_[i];
|
||||
s2_expert_vnni_q(j.blob, *j.act, j.out, scratch);
|
||||
@@ -267,15 +417,15 @@ void ExpertPool::drain(int id, ExpertScratch& scratch) {
|
||||
}
|
||||
|
||||
void ExpertPool::run_phase(int mode, int n_tasks) {
|
||||
while (parked_.load(std::memory_order_acquire) != (uint32_t) n_) _mm_pause();
|
||||
wait_parked("before a phase");
|
||||
mode_ = mode;
|
||||
njobs_ = n_tasks;
|
||||
head_.store(0, std::memory_order_relaxed);
|
||||
done_.store(0, std::memory_order_relaxed);
|
||||
publish();
|
||||
if (host_works_) drain(-1, host_scratch_);
|
||||
while (done_.load(std::memory_order_acquire) != (uint32_t) n_tasks) _mm_pause();
|
||||
while (parked_.load(std::memory_order_acquire) != (uint32_t) n_) _mm_pause();
|
||||
const uint32_t e = begin_batch(n_tasks);
|
||||
if (host_works_) drain(-1, host_scratch_, e);
|
||||
wait_done(n_tasks);
|
||||
wait_parked("after a phase");
|
||||
hstate_.store(kIdle, std::memory_order_relaxed);
|
||||
hstate_ms_.store(now_ms(), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void ExpertPool::run_split(ExpertJob* jobs, int n) {
|
||||
@@ -376,14 +526,12 @@ void ExpertPool::run(ExpertJob* jobs, int n) {
|
||||
// THE THREE PHASES ARE TIMED SEPARATELY. They were one number, which cannot distinguish a pool that is
|
||||
// slow at the WORK from one that is slow at the SYNCHRONISATION - and those need opposite fixes.
|
||||
const auto t_a = std::chrono::steady_clock::now();
|
||||
while (parked_.load(std::memory_order_acquire) != (uint32_t) n_) _mm_pause();
|
||||
wait_parked("before a batch");
|
||||
const auto t_b = std::chrono::steady_clock::now();
|
||||
jobs_ = jobs;
|
||||
njobs_ = n;
|
||||
mode_ = 0;
|
||||
head_.store(0, std::memory_order_relaxed);
|
||||
done_.store(0, std::memory_order_relaxed);
|
||||
publish(); // a release (seq_cst): jobs_/njobs_ are visible before the bump
|
||||
const uint32_t e = begin_batch(n); // done, then head (release), then the epoch: the batch is described first
|
||||
|
||||
// ---- **THE HOST DRAINS TOO (R2.2), INSTEAD OF SPINNING ON `done_`.**
|
||||
//
|
||||
@@ -401,19 +549,23 @@ void ExpertPool::run(ExpertJob* jobs, int n) {
|
||||
// device after `run()` returns, so the write must be published, not merely performed.
|
||||
if (host_works_) {
|
||||
for (;;) {
|
||||
const uint32_t i = head_.fetch_add(1, std::memory_order_relaxed);
|
||||
if (i >= (uint32_t) n) break;
|
||||
const ExpertJob& j = jobs_[i];
|
||||
const int ci = claim(e);
|
||||
if (ci < 0) break;
|
||||
hstate_.store(ci, std::memory_order_relaxed);
|
||||
hstate_ms_.store(now_ms(), std::memory_order_relaxed);
|
||||
const ExpertJob& j = jobs_[ci];
|
||||
s2_expert_vnni_q(j.blob, *j.act, j.out, host_scratch_);
|
||||
done_.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
while (done_.load(std::memory_order_acquire) != (uint32_t) n) _mm_pause();
|
||||
wait_done(n);
|
||||
// And park again, so the next `run` starts from a known state. See the header for why `done` alone is
|
||||
// not enough.
|
||||
const auto t_c = std::chrono::steady_clock::now();
|
||||
while (parked_.load(std::memory_order_acquire) != (uint32_t) n_) _mm_pause();
|
||||
wait_parked("after a batch");
|
||||
hstate_.store(kIdle, std::memory_order_relaxed);
|
||||
hstate_ms_.store(now_ms(), std::memory_order_relaxed);
|
||||
const auto t_d = std::chrono::steady_clock::now();
|
||||
|
||||
ms_wait_park_ += std::chrono::duration<double, std::milli>(t_b - t_a).count();
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// src/kernels/cpu/pool_stress.cpp - issue #29: the expert pool under the load a big-VRAM card gives it.
|
||||
//
|
||||
// With most experts resident on the GPU, many layers hand the pool nothing, the workers go to sleep in the middle
|
||||
// of a request, and the next tiny batch (one or two experts) is often finished by the host and the awake workers
|
||||
// before a sleeper has even woken. That late wake-up is what the park/claim protocol has to survive. This drives
|
||||
// hundreds of thousands of such batches - 0-4 jobs, run() and run_split(), with pauses long enough for the workers
|
||||
// to sleep - and checks that every batch returns, with every job's output written. A watchdog thread turns a
|
||||
// hang into a failure instead of a frozen test.
|
||||
//
|
||||
// pool_stress [seconds] (default 20)
|
||||
#include "strata/kernels/cpu/pool.hpp"
|
||||
#include "strata/kernels/cpu/expert.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace cpu = strata::kernels::cpu;
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const double seconds = argc > 1 ? std::atof(argv[1]) : 20.0;
|
||||
const cpu::CpuFeatures feat = cpu::cpu_features();
|
||||
if (!feat.usable()) {
|
||||
std::printf("CPU lacks %s: pool_stress SKIPPED\n", feat.reason());
|
||||
return 0;
|
||||
}
|
||||
constexpr int NJ = 4;
|
||||
std::vector<std::vector<uint8_t>> blobs(NJ, std::vector<uint8_t>(cpu::BLOB, 0));
|
||||
std::vector<float> x(cpu::H, 0.25f);
|
||||
cpu::ActQ act;
|
||||
cpu::act_quant_q8_1(x.data(), cpu::H, act);
|
||||
std::vector<float> out((size_t) NJ * cpu::H);
|
||||
std::vector<cpu::ExpertJob> jobs(NJ);
|
||||
for (int e = 0; e < NJ; ++e) {
|
||||
jobs[(size_t) e].blob = blobs[(size_t) e].data();
|
||||
jobs[(size_t) e].act = &act;
|
||||
jobs[(size_t) e].out = out.data() + (size_t) e * cpu::H;
|
||||
jobs[(size_t) e].slot = e;
|
||||
}
|
||||
|
||||
cpu::ExpertPool pool;
|
||||
std::printf("pool_stress: %d workers + host, %.0f s\n", pool.workers(), seconds);
|
||||
|
||||
// the watchdog: a batch that does not return within 10 s is a hang
|
||||
std::atomic<long long> beat{0};
|
||||
std::atomic<bool> finished{false};
|
||||
std::thread dog([&] {
|
||||
long long last = -1;
|
||||
auto since = std::chrono::steady_clock::now();
|
||||
while (!finished.load()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
const long long b = beat.load();
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (b != last) { last = b; since = now; continue; }
|
||||
if (now - since > std::chrono::seconds(10)) {
|
||||
std::printf("\npool_stress: HANG - batch %lld has not returned for 10 s\n", b);
|
||||
std::fflush(stdout);
|
||||
std::_Exit(3);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
std::mt19937 rng(29);
|
||||
const float SENTINEL = -1.2345e33f;
|
||||
long long batches = 0, sleeps = 0, missed = 0;
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
while (std::chrono::steady_clock::now() - t0 < std::chrono::duration<double>(seconds)) {
|
||||
// now and then a gap long enough for every worker to go to sleep (the pool sleeps after 20 ms)
|
||||
if (rng() % 64 == 0) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(21 + (int) (rng() % 10)));
|
||||
++sleeps;
|
||||
} else if (rng() % 4 == 0) {
|
||||
std::this_thread::sleep_for(std::chrono::microseconds(rng() % 300));
|
||||
}
|
||||
const int n = (int) (rng() % (NJ + 1));
|
||||
for (int e = 0; e < n; ++e) std::fill(jobs[(size_t) e].out, jobs[(size_t) e].out + cpu::H, SENTINEL);
|
||||
if (rng() % 2) pool.run(jobs.data(), n);
|
||||
else pool.run_split(jobs.data(), n);
|
||||
for (int e = 0; e < n; ++e)
|
||||
if (jobs[(size_t) e].out[0] == SENTINEL || jobs[(size_t) e].out[cpu::H - 1] == SENTINEL) ++missed;
|
||||
beat.fetch_add(1);
|
||||
++batches;
|
||||
}
|
||||
finished.store(true);
|
||||
dog.join();
|
||||
std::printf("pool_stress: %lld batches (%lld after the workers slept), %lld jobs not run\n", batches, sleeps,
|
||||
missed);
|
||||
if (missed) {
|
||||
std::printf("pool_stress: FAILED\n");
|
||||
return 1;
|
||||
}
|
||||
std::printf("pool_stress OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// src/kernels/cuda/cvec.cu - see include/strata/kernels/cvec.hpp.
|
||||
#include "strata/kernels/cvec.hpp"
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace strata::kernels {
|
||||
namespace {
|
||||
|
||||
constexpr int THREADS = 256;
|
||||
constexpr int MAXK = 16; // n_embd up to 4096, held in registers between the dot and the update
|
||||
|
||||
Cvec g_cvec;
|
||||
int* g_on = nullptr;
|
||||
bool g_on_host = false;
|
||||
|
||||
// the fused hyper-connection read's gate (fused_gr.cu), so a write done here is bitwise the one it would have folded
|
||||
__device__ __forceinline__ float sigmoidf_(float x) { return 1.0f / (1.0f + __expf(-x)); }
|
||||
|
||||
// one block per (stream, token): the pending write, then h . v over the stream, then the update
|
||||
__global__ void cvec_kernel(float* __restrict__ R, const float* __restrict__ dir, const float* __restrict__ s_l,
|
||||
const int* __restrict__ on, int mode, int64_t layer, int n, int hc, int64_t r_ld,
|
||||
const float* __restrict__ bo, int64_t bo_ld, const float* __restrict__ inj,
|
||||
int64_t inj_ld, int write) {
|
||||
const int c = blockIdx.x;
|
||||
const int64_t t = blockIdx.y;
|
||||
float* r = R + t * r_ld + (int64_t) c * n;
|
||||
const float s = s_l[layer];
|
||||
const bool steer = *on != 0 && s != 0.0f; // uniform over the block
|
||||
if (!steer && !write) return;
|
||||
const float* v = dir + layer * n;
|
||||
const float w = write ? 2.0f * sigmoidf_(inj[t * inj_ld + c] / (float) hc) : 0.0f;
|
||||
const float* b = write ? bo + t * bo_ld : nullptr;
|
||||
float x[MAXK];
|
||||
float dot = 0.0f;
|
||||
#pragma unroll
|
||||
for (int k = 0; k < MAXK; ++k) {
|
||||
const int d = threadIdx.x + k * THREADS;
|
||||
if (d < n) {
|
||||
float xv = r[d];
|
||||
if (write) xv = fmaf(b[d], w, xv);
|
||||
x[k] = xv;
|
||||
if (steer && mode == 0) dot = fmaf(xv, v[d], dot);
|
||||
}
|
||||
}
|
||||
if (steer && mode == 0) {
|
||||
__shared__ float part[THREADS / 32];
|
||||
#pragma unroll
|
||||
for (int o = 16; o > 0; o >>= 1) dot += __shfl_xor_sync(0xffffffffu, dot, o);
|
||||
if ((threadIdx.x & 31) == 0) part[threadIdx.x >> 5] = dot;
|
||||
__syncthreads();
|
||||
if (threadIdx.x < 32) {
|
||||
float p = threadIdx.x < THREADS / 32 ? part[threadIdx.x] : 0.0f;
|
||||
#pragma unroll
|
||||
for (int o = 16; o > 0; o >>= 1) p += __shfl_xor_sync(0xffffffffu, p, o);
|
||||
if (threadIdx.x == 0) part[0] = p;
|
||||
}
|
||||
__syncthreads();
|
||||
dot = part[0] * s; // s (h . v)
|
||||
}
|
||||
#pragma unroll
|
||||
for (int k = 0; k < MAXK; ++k) {
|
||||
const int d = threadIdx.x + k * THREADS;
|
||||
if (d < n) {
|
||||
float xv = x[k];
|
||||
if (steer) xv = mode == 0 ? fmaf(-dot, v[d], xv) : xv + v[d];
|
||||
r[d] = xv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const Cvec& cvec() { return g_cvec; }
|
||||
|
||||
bool cvec_upload(const std::vector<float>& dir, const std::vector<float>& s, int mode, int first, int last,
|
||||
int64_t n_embd, int64_t hc, std::string& err) {
|
||||
if (n_embd < 1 || n_embd > (int64_t) THREADS * MAXK) { err = "control vector: unsupported n_embd"; return false; }
|
||||
if (s.empty() || dir.size() != s.size() * (size_t) n_embd) { err = "control vector: bad table sizes"; return false; }
|
||||
float* d_dir = nullptr;
|
||||
float* d_s = nullptr;
|
||||
int* d_on = nullptr;
|
||||
const int one = 1;
|
||||
if (cudaMalloc(&d_dir, dir.size() * sizeof(float)) != cudaSuccess ||
|
||||
cudaMalloc(&d_s, s.size() * sizeof(float)) != cudaSuccess || cudaMalloc(&d_on, sizeof(int)) != cudaSuccess ||
|
||||
cudaMemcpy(d_dir, dir.data(), dir.size() * sizeof(float), cudaMemcpyHostToDevice) != cudaSuccess ||
|
||||
cudaMemcpy(d_s, s.data(), s.size() * sizeof(float), cudaMemcpyHostToDevice) != cudaSuccess ||
|
||||
cudaMemcpy(d_on, &one, sizeof(int), cudaMemcpyHostToDevice) != cudaSuccess) {
|
||||
err = "control vector: device allocation failed";
|
||||
return false;
|
||||
}
|
||||
g_cvec.dir = d_dir;
|
||||
g_cvec.s = d_s;
|
||||
g_cvec.on = d_on;
|
||||
g_cvec.mode = mode;
|
||||
g_cvec.first = first;
|
||||
g_cvec.last = last;
|
||||
g_cvec.n_embd = n_embd;
|
||||
g_cvec.hc = hc;
|
||||
g_cvec.steered.assign(s.size(), false);
|
||||
for (size_t l = 0; l < s.size(); ++l) g_cvec.steered[l] = s[l] != 0.0f;
|
||||
g_on = d_on;
|
||||
g_on_host = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void cvec_set_enabled(bool on) {
|
||||
if (g_on == nullptr || on == g_on_host) return;
|
||||
cudaDeviceSynchronize(); // nothing in flight may still read the flag
|
||||
const int v = on ? 1 : 0;
|
||||
cudaMemcpy(g_on, &v, sizeof(int), cudaMemcpyHostToDevice);
|
||||
g_on_host = on;
|
||||
}
|
||||
|
||||
bool cvec_enabled() { return g_on != nullptr && g_on_host; }
|
||||
|
||||
void cvec_apply(float* R, int64_t layer, int64_t T, int64_t r_ld, const float* bo, int64_t bo_ld, const float* inj,
|
||||
int64_t inj_ld, bool write, void* stream) {
|
||||
if (!g_cvec.loaded() || T < 1) return;
|
||||
const dim3 grid((unsigned) g_cvec.hc, (unsigned) T);
|
||||
cvec_kernel<<<grid, THREADS, 0, (cudaStream_t) stream>>>(R, g_cvec.dir, g_cvec.s, g_cvec.on, g_cvec.mode, layer,
|
||||
(int) g_cvec.n_embd, (int) g_cvec.hc, r_ld, bo, bo_ld,
|
||||
inj, inj_ld, write ? 1 : 0);
|
||||
if (cudaPeekAtLastError() != cudaSuccess) throw std::runtime_error("cvec_apply: launch failed");
|
||||
}
|
||||
|
||||
} // namespace strata::kernels
|
||||
@@ -0,0 +1,231 @@
|
||||
// src/kernels/cuda/kv_q4.cu - see include/strata/kernels/kv_q4.hpp. Q4_0 KV with Walsh-Hadamard rotation
|
||||
// (from PR #21 by code-martin; KV-streaming integration and the deterministic group maximum added on merge).
|
||||
#include "strata/kernels/kv_q4.hpp"
|
||||
#include "strata/kernels/f16_bits.hpp"
|
||||
#include "strata/kernels/kv_stream.hpp"
|
||||
|
||||
#include <cuda_fp16.h>
|
||||
#include <cuda_runtime.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace strata::kernels {
|
||||
namespace {
|
||||
|
||||
void check(const char* what) {
|
||||
const cudaError_t e = cudaGetLastError();
|
||||
if (e != cudaSuccess) {
|
||||
std::fprintf(stderr, "kv_q4: %s: %s\n", what, cudaGetErrorString(e));
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Fast Walsh-Hadamard Transform for N = 256: one warp per row, 8 values per lane in registers.
|
||||
// Orthonormal (scale 1/sqrt(256) = 1/16), so it is its own inverse.
|
||||
__global__ void fwht256_kernel(const float* __restrict__ src, float* __restrict__ dst, int64_t n_rows, float scale) {
|
||||
constexpr int warp_size = 32;
|
||||
constexpr int N = 256;
|
||||
constexpr int el_w = N / warp_size; // 8
|
||||
|
||||
const int64_t r = (int64_t) blockIdx.x * blockDim.y + threadIdx.y;
|
||||
if (r >= n_rows) return;
|
||||
|
||||
const float* row_src = src + r * N;
|
||||
float* row_dst = dst + r * N;
|
||||
|
||||
float reg[el_w];
|
||||
const int lane = threadIdx.x;
|
||||
|
||||
#pragma unroll
|
||||
for (int i = 0; i < el_w; ++i) reg[i] = row_src[i * warp_size + lane] * scale;
|
||||
|
||||
// the low 5 index bits live across lanes
|
||||
#pragma unroll
|
||||
for (int h = 1; h < warp_size; h *= 2) {
|
||||
#pragma unroll
|
||||
for (int j = 0; j < el_w; ++j) {
|
||||
const float val = reg[j];
|
||||
const float val2 = __shfl_xor_sync(0xffffffffu, val, h, warp_size);
|
||||
reg[j] = (lane & h) == 0 ? val + val2 : val2 - val;
|
||||
}
|
||||
}
|
||||
// the high 3 bits across each lane's registers
|
||||
#pragma unroll
|
||||
for (int h = warp_size; h < N; h *= 2) {
|
||||
const int step = h / warp_size;
|
||||
#pragma unroll
|
||||
for (int j = 0; j < el_w; j += 2 * step) {
|
||||
#pragma unroll
|
||||
for (int k = 0; k < step; ++k) {
|
||||
const float x = reg[j + k];
|
||||
const float y = reg[j + k + step];
|
||||
reg[j + k] = x + y;
|
||||
reg[j + k + step] = x - y;
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma unroll
|
||||
for (int i = 0; i < el_w; ++i) row_dst[i * warp_size + lane] = reg[i];
|
||||
}
|
||||
|
||||
// One 32-value group in one warp (lane = element), ggml's q4_0: d = (the value of largest |x|) / -8,
|
||||
// q = clamp(trunc(x / d + 8.5), 0, 15). Returns the scale bits; `byte` is lane t's packed byte for t < 16
|
||||
// (element t in the low nibble, t + 16 in the high one). Ties in |x| resolve to the larger value in EVERY lane,
|
||||
// so all lanes agree on d (a plain `a > amax` could leave lanes with opposite signs and one block two scales).
|
||||
__device__ __forceinline__ uint16_t q4_group(float x, int lane, uint8_t& byte) {
|
||||
float amax = fabsf(x), mval = x;
|
||||
#pragma unroll
|
||||
for (int o = 16; o > 0; o >>= 1) {
|
||||
const float a = __shfl_xor_sync(0xffffffffu, amax, o);
|
||||
const float v = __shfl_xor_sync(0xffffffffu, mval, o);
|
||||
if (a > amax || (a == amax && v > mval)) { amax = a; mval = v; }
|
||||
}
|
||||
const float d = mval / -8.0f;
|
||||
const float id = d != 0.0f ? 1.0f / d : 0.0f;
|
||||
int q = __float2int_rz(x * id + 8.5f);
|
||||
const uint8_t qc = (uint8_t) (q < 0 ? 0 : (q > 15 ? 15 : q));
|
||||
const uint8_t qhi = __shfl_down_sync(0xffffffffu, qc, 16);
|
||||
byte = (uint8_t) (qc | (qhi << 4));
|
||||
(void) lane;
|
||||
return f16_from_f32(d);
|
||||
}
|
||||
|
||||
__device__ __forceinline__ void q4_store(uint8_t* pool, long long row, int b, int lane, uint16_t d, uint8_t byte) {
|
||||
block_q4_0* blk = reinterpret_cast<block_q4_0*>(pool + row * (long long) sizeof(block_q4_0) * 8) + b;
|
||||
if (lane == 0) blk->d = d;
|
||||
if (lane < 16) blk->qs[lane] = byte;
|
||||
}
|
||||
|
||||
// One block = one 32-value group of one KV head of K (blockIdx.z = 0) or V (1); 32 threads. KV streaming: the VRAM
|
||||
// page only if the block is resident (table >= 0), the host copy always (identity layout) when there is one.
|
||||
__global__ void kv_append_q4_kernel(uint8_t* __restrict__ k_q4, uint8_t* __restrict__ v_q4,
|
||||
const int32_t* __restrict__ table, const int32_t* __restrict__ step,
|
||||
const float* __restrict__ kcur, const float* __restrict__ vcur,
|
||||
int kv_heads, int head_dim, int page_size, KvHostPools host) {
|
||||
const long long pos = (long long) __ldg(step + kStepPos);
|
||||
const int h = blockIdx.x, b = blockIdx.y, t = threadIdx.x;
|
||||
const bool is_v = blockIdx.z == 1;
|
||||
const float x = (is_v ? vcur : kcur)[h * head_dim + b * QK4_0 + t];
|
||||
uint8_t byte;
|
||||
const uint16_t d = q4_group(x, t, byte);
|
||||
const long long page = (long long) table[pos / page_size];
|
||||
if (page >= 0) q4_store(is_v ? v_q4 : k_q4, (page * kv_heads + h) * page_size + (pos % page_size), b, t, d, byte);
|
||||
if (host.k_q4 != nullptr)
|
||||
q4_store(is_v ? host.v_q4 : host.k_q4, ((pos / page_size) * kv_heads + h) * page_size + (pos % page_size), b, t,
|
||||
d, byte);
|
||||
}
|
||||
|
||||
// The prompt path: grid (T, kv_heads, groups), K then V; also into the staging pool (identity layout) when given.
|
||||
__global__ void kv_append_q4_batch_kernel(uint8_t* __restrict__ k_q4, uint8_t* __restrict__ v_q4,
|
||||
const int32_t* __restrict__ table, int64_t pos0,
|
||||
const float* __restrict__ K, const float* __restrict__ V,
|
||||
int kv_heads, int head_dim, int page_size, int is_v_grid, KvHostPools host,
|
||||
KvHostPools stage) {
|
||||
const long long t = blockIdx.x;
|
||||
const long long pos = pos0 + t;
|
||||
const int h = blockIdx.y, b = blockIdx.z, th = threadIdx.x;
|
||||
const bool is_v = is_v_grid != 0;
|
||||
const float x = (is_v ? V : K)[t * (kv_heads * head_dim) + h * head_dim + b * QK4_0 + th];
|
||||
uint8_t byte;
|
||||
const uint16_t d = q4_group(x, th, byte);
|
||||
const long long page = (long long) table[pos / page_size];
|
||||
const long long row_id = ((pos / page_size) * kv_heads + h) * page_size + (pos % page_size);
|
||||
if (page >= 0) q4_store(is_v ? v_q4 : k_q4, (page * kv_heads + h) * page_size + (pos % page_size), b, th, d, byte);
|
||||
if (host.k_q4 != nullptr) q4_store(is_v ? host.v_q4 : host.k_q4, row_id, b, th, d, byte);
|
||||
if (stage.k_q4 != nullptr) q4_store(is_v ? stage.v_q4 : stage.k_q4, row_id, b, th, d, byte);
|
||||
}
|
||||
|
||||
// Gather step[kStepWidth] cells into FP16 scratch (the non-fused attention paths)
|
||||
__global__ void kv_gather_q4_kernel(const uint8_t* __restrict__ k_q4, const uint8_t* __restrict__ v_q4,
|
||||
const int32_t* __restrict__ table, const int32_t* __restrict__ ids,
|
||||
const int32_t* __restrict__ step, int kv_heads, int head_dim, int page_size,
|
||||
uint16_t* __restrict__ k_scratch, uint16_t* __restrict__ v_scratch) {
|
||||
const long long n_ids = (long long) __ldg(step + kStepWidth);
|
||||
const int blocks_per_head = head_dim / QK4_0; // 8
|
||||
const int bytes_per_head = blocks_per_head * sizeof(block_q4_0); // 144
|
||||
const long long total_blocks = n_ids * kv_heads * blocks_per_head;
|
||||
|
||||
const long long blk_idx = (long long) blockIdx.x * blockDim.y + threadIdx.y;
|
||||
if (blk_idx >= total_blocks) return;
|
||||
|
||||
const int t = threadIdx.x;
|
||||
const long long id = blk_idx / (kv_heads * blocks_per_head);
|
||||
const int rem = (int) (blk_idx % (kv_heads * blocks_per_head));
|
||||
const int h = rem / blocks_per_head;
|
||||
const int b = rem % blocks_per_head;
|
||||
|
||||
const int cell = ids[id];
|
||||
const long long page = (long long) table[cell / page_size];
|
||||
const long long row = (page * kv_heads + h) * page_size + (cell % page_size);
|
||||
|
||||
const block_q4_0* k_blk = reinterpret_cast<const block_q4_0*>(k_q4 + row * bytes_per_head) + b;
|
||||
const block_q4_0* v_blk = reinterpret_cast<const block_q4_0*>(v_q4 + row * bytes_per_head) + b;
|
||||
const float kd = f32_from_f16(k_blk->d);
|
||||
const float vd = f32_from_f16(v_blk->d);
|
||||
const int j = t < 16 ? t : (t - 16);
|
||||
const uint8_t k_byte = k_blk->qs[j];
|
||||
const uint8_t v_byte = v_blk->qs[j];
|
||||
const int kq = (t < 16) ? ((k_byte & 0x0F) - 8) : ((k_byte >> 4) - 8);
|
||||
const int vq = (t < 16) ? ((v_byte & 0x0F) - 8) : ((v_byte >> 4) - 8);
|
||||
const long long dst_offset = ((id * kv_heads + h) * head_dim) + (b * QK4_0 + t);
|
||||
k_scratch[dst_offset] = f16_from_f32((float) kq * kd);
|
||||
v_scratch[dst_offset] = f16_from_f32((float) vq * vd);
|
||||
}
|
||||
|
||||
void need_256(const QsaShapes& s, const char* what) {
|
||||
if (s.head_dim != 256) {
|
||||
std::fprintf(stderr, "%s: head_dim must be 256 (the Hadamard transform's size)\n", what);
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void fwht256_cuda(const float* src, float* dst, int64_t n_rows, void* stream) {
|
||||
if (n_rows <= 0) return;
|
||||
const int rows_per_block = 4;
|
||||
const int64_t num_blocks = (n_rows + rows_per_block - 1) / rows_per_block;
|
||||
fwht256_kernel<<<dim3((unsigned) num_blocks), dim3(32, rows_per_block), 0, (cudaStream_t) stream>>>(
|
||||
src, dst, n_rows, 1.0f / 16.0f);
|
||||
check("fwht256 launch");
|
||||
}
|
||||
|
||||
void kv_append_q4_step(uint8_t* k_q4, uint8_t* v_q4, const int32_t* page_table, const int32_t* step,
|
||||
const float* kcur, const float* vcur, const QsaShapes& s, void* stream,
|
||||
const KvHostPools* host) {
|
||||
need_256(s, "kv_append_q4");
|
||||
const dim3 grid((unsigned) s.n_head_kv, (unsigned) (s.head_dim / QK4_0), 2);
|
||||
kv_append_q4_kernel<<<grid, 32, 0, (cudaStream_t) stream>>>(
|
||||
k_q4, v_q4, page_table, step, kcur, vcur, (int) s.n_head_kv, (int) s.head_dim, (int) s.page_size,
|
||||
host ? *host : KvHostPools{});
|
||||
check("kv_append_q4 launch");
|
||||
}
|
||||
|
||||
void kv_append_q4(uint8_t* k_q4, uint8_t* v_q4, const int32_t* page_table, int64_t pos0, int64_t T, const float* K,
|
||||
const float* V, const QsaShapes& s, void* stream, const KvHostPools* host, const KvHostPools* stage) {
|
||||
if (T <= 0) return;
|
||||
need_256(s, "kv_append_q4");
|
||||
const dim3 grid((unsigned) T, (unsigned) s.n_head_kv, (unsigned) (s.head_dim / QK4_0));
|
||||
cudaStream_t cs = (cudaStream_t) stream;
|
||||
const KvHostPools h = host ? *host : KvHostPools{}, st = stage ? *stage : KvHostPools{};
|
||||
for (int is_v = 0; is_v < 2; ++is_v)
|
||||
kv_append_q4_batch_kernel<<<grid, 32, 0, cs>>>(k_q4, v_q4, page_table, pos0, K, V, (int) s.n_head_kv,
|
||||
(int) s.head_dim, (int) s.page_size, is_v, h, st);
|
||||
check("kv_append_q4 batch launch");
|
||||
}
|
||||
|
||||
void kv_gather_q4_step(const uint8_t* k_q4, const uint8_t* v_q4, const int32_t* page_table, const int32_t* ids,
|
||||
const int32_t* step, int64_t max_ids, const QsaShapes& s, uint16_t* k_scratch,
|
||||
uint16_t* v_scratch, void* stream) {
|
||||
if (max_ids <= 0) return;
|
||||
const int blocks_per_head = (int) (s.head_dim / QK4_0);
|
||||
const int64_t total_blocks = max_ids * s.n_head_kv * blocks_per_head;
|
||||
const int rows_per_block = 4;
|
||||
const unsigned num_blocks = (unsigned) ((total_blocks + rows_per_block - 1) / rows_per_block);
|
||||
kv_gather_q4_kernel<<<dim3(num_blocks), dim3(32, rows_per_block), 0, (cudaStream_t) stream>>>(
|
||||
k_q4, v_q4, page_table, ids, step, (int) s.n_head_kv, (int) s.head_dim, (int) s.page_size, k_scratch,
|
||||
v_scratch);
|
||||
check("kv_gather_q4 launch");
|
||||
}
|
||||
|
||||
} // namespace strata::kernels
|
||||
@@ -1,5 +1,6 @@
|
||||
// src/kernels/cuda/kv_stream.cu - see include/strata/kernels/kv_stream.hpp.
|
||||
#include "strata/kernels/kv_stream.hpp"
|
||||
#include "strata/kernels/kv_q4.hpp"
|
||||
#include "strata/kernels/kv_q8.hpp"
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
@@ -29,10 +30,15 @@ struct Runs {
|
||||
int n;
|
||||
};
|
||||
|
||||
Runs runs_of(const QsaAttnPools& slots, const KvHostPools& host, bool int8, const QsaShapes& s) {
|
||||
Runs runs_of(const QsaAttnPools& slots, const KvHostPools& host, int fmt, const QsaShapes& s) {
|
||||
const int rows = (int) (s.n_head_kv * s.page_size);
|
||||
Runs r{};
|
||||
if (int8) {
|
||||
if (fmt == kKvQ4) {
|
||||
const int bytes = rows * (int) kv_q4_bytes_per_head((int) s.head_dim);
|
||||
r.src[0] = (const uint8_t*) host.k_q4; r.dst[0] = (uint8_t*) slots.k_q4; r.len[0] = bytes;
|
||||
r.src[1] = (const uint8_t*) host.v_q4; r.dst[1] = (uint8_t*) slots.v_q4; r.len[1] = bytes;
|
||||
r.n = 2;
|
||||
} else if (fmt == kKvInt8) {
|
||||
const int codes = rows * (int) s.head_dim, scales = rows * (int) (s.head_dim / KV_Q8_GROUP) * 2;
|
||||
r.src[0] = (const uint8_t*) host.k_q; r.dst[0] = (uint8_t*) slots.k_q; r.len[0] = codes;
|
||||
r.src[1] = (const uint8_t*) host.v_q; r.dst[1] = (uint8_t*) slots.v_q; r.len[1] = codes;
|
||||
@@ -183,9 +189,10 @@ __global__ void ring_kernel(int32_t* table, long long n_blocks, long long n_slot
|
||||
|
||||
} // namespace
|
||||
|
||||
uint64_t kv_block_bytes(const QsaShapes& s, bool int8) {
|
||||
uint64_t kv_block_bytes(const QsaShapes& s, int fmt) {
|
||||
const uint64_t rows = (uint64_t) (s.n_head_kv * s.page_size);
|
||||
return int8 ? rows * (uint64_t) s.head_dim * 2 + rows * (uint64_t) (s.head_dim / KV_Q8_GROUP) * 2 * 2
|
||||
if (fmt == kKvQ4) return rows * kv_q4_bytes_per_head((int) s.head_dim) * 2;
|
||||
return fmt == kKvInt8 ? rows * (uint64_t) s.head_dim * 2 + rows * (uint64_t) (s.head_dim / KV_Q8_GROUP) * 2 * 2
|
||||
: rows * (uint64_t) s.head_dim * 2 * 2;
|
||||
}
|
||||
|
||||
@@ -194,7 +201,7 @@ void kv_stream_reset(const KvStreamMap& m, void* stream) {
|
||||
check("reset");
|
||||
}
|
||||
|
||||
void kv_stream_resolve(const KvStreamMap& m, const QsaAttnPools& slots, const KvHostPools& host, bool int8,
|
||||
void kv_stream_resolve(const KvStreamMap& m, const QsaAttnPools& slots, const KvHostPools& host, int fmt,
|
||||
const int32_t* ids, const int32_t* steps, int64_t n_q, int64_t cap, const QsaShapes& s,
|
||||
void* stream) {
|
||||
if (n_q <= 0) return;
|
||||
@@ -204,7 +211,7 @@ void kv_stream_resolve(const KvStreamMap& m, const QsaAttnPools& slots, const Kv
|
||||
}
|
||||
resolve_kernel<<<1, RT, 0, (cudaStream_t) stream>>>(m, ids, steps, (int) n_q, (int) cap, (int) s.page_size);
|
||||
check("resolve");
|
||||
copy_kernel<<<96, 128, 0, (cudaStream_t) stream>>>(m, runs_of(slots, host, int8, s));
|
||||
copy_kernel<<<96, 128, 0, (cudaStream_t) stream>>>(m, runs_of(slots, host, fmt, s));
|
||||
check("copy");
|
||||
}
|
||||
|
||||
@@ -213,9 +220,9 @@ void kv_ring_table(int32_t* page_table, int64_t n_blocks, int64_t n_slots, void*
|
||||
check("ring table");
|
||||
}
|
||||
|
||||
void kv_ring_restore(const QsaAttnPools& slots, const KvHostPools& host, bool int8, int64_t b0, int64_t b1,
|
||||
void kv_ring_restore(const QsaAttnPools& slots, const KvHostPools& host, int fmt, int64_t b0, int64_t b1,
|
||||
int64_t n_slots, const QsaShapes& s, void* stream) {
|
||||
const Runs r = runs_of(slots, host, int8, s);
|
||||
const Runs r = runs_of(slots, host, fmt, s);
|
||||
for (int64_t b = b0; b < b1;) {
|
||||
const int64_t sl = b % n_slots, run = std::min<int64_t>(b1 - b, n_slots - sl); // up to the ring's end
|
||||
for (int a = 0; a < r.n; ++a)
|
||||
@@ -226,10 +233,10 @@ void kv_ring_restore(const QsaAttnPools& slots, const KvHostPools& host, bool in
|
||||
}
|
||||
}
|
||||
|
||||
void kv_stage_from_host(const QsaAttnPools& stage, const KvHostPools& host, bool int8, int64_t n_blocks,
|
||||
void kv_stage_from_host(const QsaAttnPools& stage, const KvHostPools& host, int fmt, int64_t n_blocks,
|
||||
const QsaShapes& s, void* stream) {
|
||||
if (n_blocks <= 0) return;
|
||||
const Runs r = runs_of(stage, host, int8, s);
|
||||
const Runs r = runs_of(stage, host, fmt, s);
|
||||
for (int a = 0; a < r.n; ++a)
|
||||
if (cudaMemcpyAsync(r.dst[a], r.src[a], (size_t) (n_blocks * r.len[a]), cudaMemcpyDefault,
|
||||
(cudaStream_t) stream) != cudaSuccess)
|
||||
|
||||
@@ -86,6 +86,76 @@ __global__ void conv_residual_kernel(const float* history, const float* normaliz
|
||||
// Exact hidden/result alias is safe: each thread owns one element.
|
||||
result[c] = __fadd_rn(hidden[c], __fadd_rn(gated[c], activation));
|
||||
}
|
||||
// ---- the batch: T tokens, the same arithmetic per element as the kernels above
|
||||
// weighted_rms_norm (native_gr_norm.cu) with the gamma row repeating every H rows (one token's H groups)
|
||||
__device__ float norm_warp_sum(float value) {
|
||||
#pragma unroll
|
||||
for (int offset = 16; offset > 0; offset >>= 1) value += __shfl_xor_sync(0xffffffffu, value, offset, 32);
|
||||
return value;
|
||||
}
|
||||
__global__ void rms_rep_kernel(const float* __restrict__ input, const float* __restrict__ gamma,
|
||||
float* __restrict__ output) {
|
||||
constexpr int BlockSize = 1024; // native_gr_rms_norm_weighted's choice for 2560 columns
|
||||
const int tid = threadIdx.x;
|
||||
const size_t row_offset = size_t(blockIdx.x) * N;
|
||||
input += row_offset;
|
||||
output += row_offset;
|
||||
gamma += size_t(blockIdx.x % H) * N;
|
||||
float partial = 0.0f;
|
||||
for (int col = tid; col < N; col += BlockSize) {
|
||||
const float value = input[col];
|
||||
partial += value * value;
|
||||
}
|
||||
__shared__ float sums[32];
|
||||
partial = norm_warp_sum(partial);
|
||||
const int lane = tid % 32;
|
||||
if (lane == 0) sums[tid / 32] = partial;
|
||||
__syncthreads();
|
||||
partial = 0.0f;
|
||||
if (lane < BlockSize / 32) partial = sums[lane];
|
||||
partial = norm_warp_sum(partial);
|
||||
const float mean = partial / N;
|
||||
const float scale = rsqrtf(mean + NG_RMS_EPS);
|
||||
for (int col = tid; col < N; col += BlockSize) output[col] = scale * input[col] * gamma[col];
|
||||
}
|
||||
__global__ void broadcast_batch_kernel(const float* value, const float* gate, float* gated, int T) {
|
||||
const size_t i = size_t(blockIdx.x) * blockDim.x + threadIdx.x;
|
||||
if (i >= size_t(T) * D) return;
|
||||
const size_t t = i / D, d = i % D;
|
||||
gated[i] = __fmul_rn(value[t * N + d % N], gate[t * H + d / N]);
|
||||
}
|
||||
// the dilated conv (taps 9, 6, 3 tokens back and this one) and the residual; a tap before the chunk reads the history
|
||||
__global__ void conv_residual_batch_kernel(const float* history, const float* normalized, const uint16_t* weights,
|
||||
float* hidden, const float* gated, int T) {
|
||||
const size_t i = size_t(blockIdx.x) * blockDim.x + threadIdx.x;
|
||||
if (i >= size_t(T) * D) return;
|
||||
const int t = int(i / D), c = int(i % D);
|
||||
float sum = 0;
|
||||
#pragma unroll
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
const int p = t - 9 + 3 * k; // the token this tap reads (k == 3: this one)
|
||||
const float x = p >= 0 ? normalized[size_t(p) * D + c] : history[size_t(c) * HISTORY + (9 + p)];
|
||||
const float wk = __half2float(__ushort_as_half(weights[c * 4 + k]));
|
||||
const float term = __fmul_rn(x, wk);
|
||||
sum = k == 0 ? term : __fadd_rn(sum, term);
|
||||
}
|
||||
const float activation = sum / (1.0f + expf(-sum));
|
||||
hidden[i] = __fadd_rn(hidden[i], __fadd_rn(gated[i], activation));
|
||||
}
|
||||
// the history after the chunk: the last nine normalized rows (older ones from the history when T < 9)
|
||||
__global__ void history_batch_kernel(float* history, const float* normalized, int T) {
|
||||
const int c = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (c >= D) return;
|
||||
float h[HISTORY];
|
||||
#pragma unroll
|
||||
for (int r = 0; r < HISTORY; ++r) {
|
||||
const int p = T - HISTORY + r;
|
||||
h[r] = p >= 0 ? normalized[size_t(p) * D + c] : history[size_t(c) * HISTORY + (T + r)];
|
||||
}
|
||||
#pragma unroll
|
||||
for (int r = 0; r < HISTORY; ++r) history[size_t(c) * HISTORY + r] = h[r];
|
||||
}
|
||||
|
||||
struct Span { const void* p; size_t bytes; size_t alignment; };
|
||||
bool overlaps(Span a, Span b) {
|
||||
const auto x = reinterpret_cast<uintptr_t>(a.p), y = reinterpret_cast<uintptr_t>(b.p);
|
||||
@@ -131,4 +201,21 @@ void native_ple_postops(const float* projected_key, const float* hidden,
|
||||
conv_residual_kernel<<<D/256,256,0,st>>>(history,b.normalized,w.conv1d_f16,hidden,b.gated,b.conv,b.result);
|
||||
launch_check();
|
||||
}
|
||||
|
||||
void native_ple_postops_batch(float* key, float* hidden, const float* value, float* history, const PleWeights& w,
|
||||
float* query_norm, float* gated, float* gate, int T, void* stream) {
|
||||
if (!stream || T <= 0 || !key || !hidden || !value || !history || !query_norm || !gated || !gate)
|
||||
throw std::invalid_argument("native PLE postops batch: null input or empty batch");
|
||||
auto st = static_cast<cudaStream_t>(stream);
|
||||
const unsigned rows = unsigned(T) * H;
|
||||
const unsigned blocks = unsigned((size_t(T) * D + 255) / 256);
|
||||
rms_rep_kernel<<<rows, 1024, 0, st>>>(key, w.norm_key, key);
|
||||
rms_rep_kernel<<<rows, 1024, 0, st>>>(hidden, w.norm_query, query_norm);
|
||||
gate_kernel<<<rows, 512, 0, st>>>(key, query_norm, gate, 1.0f / std::sqrt(float(N)));
|
||||
broadcast_batch_kernel<<<blocks, 256, 0, st>>>(value, gate, gated, T);
|
||||
rms_rep_kernel<<<rows, 1024, 0, st>>>(gated, w.norm_conv, query_norm);
|
||||
conv_residual_batch_kernel<<<blocks, 256, 0, st>>>(history, query_norm, w.conv1d_f16, hidden, gated, T);
|
||||
history_batch_kernel<<<D / 256, 256, 0, st>>>(history, query_norm, T);
|
||||
launch_check();
|
||||
}
|
||||
} // namespace strata::kernels
|
||||
@@ -1,6 +1,7 @@
|
||||
// src/kernels/cuda/qsa_decode_attn.cu - see include/strata/kernels/qsa_decode_attn.hpp.
|
||||
#include "strata/kernels/qsa_decode_attn.hpp"
|
||||
#include "strata/kernels/kv_q8.hpp"
|
||||
#include "strata/kernels/kv_q4.hpp"
|
||||
|
||||
#include <cuda_fp16.h>
|
||||
#include <cuda_runtime.h>
|
||||
@@ -30,9 +31,9 @@ __device__ __forceinline__ float warp_max(float v) {
|
||||
}
|
||||
|
||||
// 8 consecutive values of one cell's key or value row for KV head `kvh`, dimensions [d0, d0+8).
|
||||
template <bool INT8>
|
||||
template <int KV_MODE>
|
||||
__device__ __forceinline__ void load8(const QsaAttnPools& p, bool value, long long row, int d0, float* out) {
|
||||
if constexpr (!INT8) {
|
||||
if constexpr (KV_MODE == 0) {
|
||||
const uint16_t* base = (value ? p.v_pool : p.k_pool) + row * HD + d0;
|
||||
const uint4 raw = *reinterpret_cast<const uint4*>(base);
|
||||
const __half2* h2 = reinterpret_cast<const __half2*>(&raw);
|
||||
@@ -42,7 +43,7 @@ __device__ __forceinline__ void load8(const QsaAttnPools& p, bool value, long lo
|
||||
out[2 * j] = f.x;
|
||||
out[2 * j + 1] = f.y;
|
||||
}
|
||||
} else {
|
||||
} else if constexpr (KV_MODE == 1) {
|
||||
const int8_t* codes = (value ? p.v_q : p.k_q) + row * HD + d0;
|
||||
const uint16_t sbits = (value ? p.v_scale : p.k_scale)[row * (HD / KV_Q8_GROUP) + d0 / KV_Q8_GROUP];
|
||||
const float sc = __half2float(__ushort_as_half(sbits));
|
||||
@@ -50,10 +51,25 @@ __device__ __forceinline__ void load8(const QsaAttnPools& p, bool value, long lo
|
||||
const int8_t* c = reinterpret_cast<const int8_t*>(&raw);
|
||||
#pragma unroll
|
||||
for (int j = 0; j < 8; ++j) out[j] = (float) c[j] * sc;
|
||||
} else {
|
||||
constexpr int bytes_per_head = (HD / QK4_0) * sizeof(block_q4_0);
|
||||
const int b = d0 / QK4_0;
|
||||
const int rem = d0 % QK4_0;
|
||||
const block_q4_0* blk = reinterpret_cast<const block_q4_0*>((value ? p.v_q4 : p.k_q4) + row * bytes_per_head) + b;
|
||||
const float d = __half2float(__ushort_as_half(blk->d));
|
||||
const int j = (rem == 0 || rem == 16) ? 0 : 8;
|
||||
const uint8_t* bytes = blk->qs + j;
|
||||
if (rem < 16) {
|
||||
#pragma unroll
|
||||
for (int k = 0; k < 8; ++k) out[k] = (float) ((int)(bytes[k] & 0x0F) - 8) * d;
|
||||
} else {
|
||||
#pragma unroll
|
||||
for (int k = 0; k < 8; ++k) out[k] = (float) ((int)(bytes[k] >> 4) - 8) * d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <bool INT8>
|
||||
template <int KV_MODE>
|
||||
__global__ void __launch_bounds__(THREADS) attn_chunk_kernel(const float* __restrict__ q, QsaAttnPools p,
|
||||
const int32_t* __restrict__ ids,
|
||||
const int32_t* __restrict__ step, int n_kv_heads,
|
||||
@@ -98,7 +114,7 @@ __global__ void __launch_bounds__(THREADS) attn_chunk_kernel(const float* __rest
|
||||
continue;
|
||||
}
|
||||
float k8[8];
|
||||
load8<INT8>(p, false, srow[c], lane * 8, k8);
|
||||
load8<KV_MODE>(p, false, srow[c], lane * 8, k8);
|
||||
#pragma unroll
|
||||
for (int h = 0; h < G; ++h) {
|
||||
const float4 qa = *reinterpret_cast<const float4*>(&sq[h][lane * 8]);
|
||||
@@ -128,11 +144,21 @@ __global__ void __launch_bounds__(THREADS) attn_chunk_kernel(const float* __rest
|
||||
for (int h = 0; h < G; ++h) acc[h] = 0.0f;
|
||||
for (int c = 0; c < n_here; ++c) {
|
||||
float v;
|
||||
if constexpr (!INT8) {
|
||||
if constexpr (KV_MODE == 0) {
|
||||
v = __half2float(__ushort_as_half(p.v_pool[srow[c] * HD + t]));
|
||||
} else {
|
||||
} else if constexpr (KV_MODE == 1) {
|
||||
const float sc = __half2float(__ushort_as_half(p.v_scale[srow[c] * (HD / KV_Q8_GROUP) + t / KV_Q8_GROUP]));
|
||||
v = (float) p.v_q[srow[c] * HD + t] * sc;
|
||||
} else {
|
||||
constexpr int bytes_per_head = (HD / QK4_0) * sizeof(block_q4_0);
|
||||
const int b = t / QK4_0;
|
||||
const int rem = t % QK4_0;
|
||||
const block_q4_0* blk = reinterpret_cast<const block_q4_0*>(p.v_q4 + srow[c] * bytes_per_head) + b;
|
||||
const float d = __half2float(__ushort_as_half(blk->d));
|
||||
const int j = rem < 16 ? rem : (rem - 16);
|
||||
const uint8_t byte = blk->qs[j];
|
||||
const int nibble = (rem < 16) ? ((byte & 0x0F) - 8) : ((byte >> 4) - 8);
|
||||
v = (float) nibble * d;
|
||||
}
|
||||
#pragma unroll
|
||||
for (int h = 0; h < G; ++h) acc[h] = fmaf(sp[h][c], v, acc[h]);
|
||||
@@ -176,7 +202,7 @@ void qsa_decode_attn_batch(const float* q, const QsaAttnPools& pools, const int3
|
||||
std::fprintf(stderr, "qsa_decode_attn_batch: unsupported geometry or missing buffers\n");
|
||||
std::exit(1);
|
||||
}
|
||||
const bool int8 = pools.k_q != nullptr;
|
||||
const int kv_mode = pools.k_q4 != nullptr ? 2 : (pools.k_q != nullptr ? 1 : 0);
|
||||
const int n_chunks = (int) ((cap + CHUNK - 1) / CHUNK);
|
||||
// per query: [acc: n_chunks*n_head*HD][m: n_chunks*n_head][l: n_chunks*n_head], all offsets from one stride
|
||||
const long long stride = (long long) qsa_decode_attn_scratch_floats(cap, s);
|
||||
@@ -186,12 +212,15 @@ void qsa_decode_attn_batch(const float* q, const QsaAttnPools& pools, const int3
|
||||
const float scale = 1.0f / sqrtf((float) HD);
|
||||
const dim3 grid((unsigned) n_chunks, (unsigned) s.n_head_kv, (unsigned) n_q);
|
||||
cudaStream_t st = (cudaStream_t) stream;
|
||||
if (int8)
|
||||
attn_chunk_kernel<true><<<grid, THREADS, 0, st>>>(q, pools, ids, steps, (int) s.n_head_kv, (int) s.page_size,
|
||||
scale, part_acc, part_m, part_l, n_chunks, (int) cap, stride);
|
||||
if (kv_mode == 2)
|
||||
attn_chunk_kernel<2><<<grid, THREADS, 0, st>>>(q, pools, ids, steps, (int) s.n_head_kv, (int) s.page_size,
|
||||
scale, part_acc, part_m, part_l, n_chunks, (int) cap, stride);
|
||||
else if (kv_mode == 1)
|
||||
attn_chunk_kernel<1><<<grid, THREADS, 0, st>>>(q, pools, ids, steps, (int) s.n_head_kv, (int) s.page_size,
|
||||
scale, part_acc, part_m, part_l, n_chunks, (int) cap, stride);
|
||||
else
|
||||
attn_chunk_kernel<false><<<grid, THREADS, 0, st>>>(q, pools, ids, steps, (int) s.n_head_kv, (int) s.page_size,
|
||||
scale, part_acc, part_m, part_l, n_chunks, (int) cap, stride);
|
||||
attn_chunk_kernel<0><<<grid, THREADS, 0, st>>>(q, pools, ids, steps, (int) s.n_head_kv, (int) s.page_size,
|
||||
scale, part_acc, part_m, part_l, n_chunks, (int) cap, stride);
|
||||
attn_merge_kernel<<<dim3((unsigned) s.n_head, (unsigned) n_q), HD, 0, st>>>(part_acc, part_m, part_l, n_chunks,
|
||||
attn, stride);
|
||||
const cudaError_t e = cudaGetLastError();
|
||||
@@ -213,8 +242,8 @@ void qsa_decode_attn_step(const float* q, const QsaAttnPools& pools, const int32
|
||||
std::fprintf(stderr, "qsa_decode_attn: unsupported geometry or missing buffers\n");
|
||||
std::exit(1);
|
||||
}
|
||||
const bool int8 = pools.k_q != nullptr;
|
||||
if (int8 ? (!pools.v_q || !pools.k_scale || !pools.v_scale) : (!pools.k_pool || !pools.v_pool)) {
|
||||
const int kv_mode = pools.k_q4 != nullptr ? 2 : (pools.k_q != nullptr ? 1 : 0);
|
||||
if (kv_mode == 2 ? (!pools.v_q4) : (kv_mode == 1 ? (!pools.v_q || !pools.k_scale || !pools.v_scale) : (!pools.k_pool || !pools.v_pool))) {
|
||||
std::fprintf(stderr, "qsa_decode_attn: incomplete KV pools\n");
|
||||
std::exit(1);
|
||||
}
|
||||
@@ -225,12 +254,15 @@ void qsa_decode_attn_step(const float* q, const QsaAttnPools& pools, const int32
|
||||
const float scale = 1.0f / sqrtf((float) HD);
|
||||
const dim3 grid((unsigned) n_chunks, (unsigned) s.n_head_kv);
|
||||
cudaStream_t st = (cudaStream_t) stream;
|
||||
if (int8)
|
||||
attn_chunk_kernel<true><<<grid, THREADS, 0, st>>>(q, pools, ids, step, (int) s.n_head_kv, (int) s.page_size,
|
||||
scale, part_acc, part_m, part_l, n_chunks);
|
||||
if (kv_mode == 2)
|
||||
attn_chunk_kernel<2><<<grid, THREADS, 0, st>>>(q, pools, ids, step, (int) s.n_head_kv, (int) s.page_size,
|
||||
scale, part_acc, part_m, part_l, n_chunks);
|
||||
else if (kv_mode == 1)
|
||||
attn_chunk_kernel<1><<<grid, THREADS, 0, st>>>(q, pools, ids, step, (int) s.n_head_kv, (int) s.page_size,
|
||||
scale, part_acc, part_m, part_l, n_chunks);
|
||||
else
|
||||
attn_chunk_kernel<false><<<grid, THREADS, 0, st>>>(q, pools, ids, step, (int) s.n_head_kv, (int) s.page_size,
|
||||
scale, part_acc, part_m, part_l, n_chunks);
|
||||
attn_chunk_kernel<0><<<grid, THREADS, 0, st>>>(q, pools, ids, step, (int) s.n_head_kv, (int) s.page_size,
|
||||
scale, part_acc, part_m, part_l, n_chunks);
|
||||
attn_merge_kernel<<<(unsigned) s.n_head, HD, 0, st>>>(part_acc, part_m, part_l, n_chunks, attn);
|
||||
const cudaError_t e = cudaGetLastError();
|
||||
if (e != cudaSuccess) {
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
// src/kernels/cvec_parity.cpp - the control vector kernel (strata/kernels/cvec.hpp) against a host reference.
|
||||
// 1. project: h - s (h.v) v per stream and token, against double precision; the steered component is gone.
|
||||
// 2. add: h + d.
|
||||
// 3. switched off: R unchanged without a pending write; with one, R is BITWISE the write the fused read folds
|
||||
// (fused_gr_read with apply), which is what keeps a loaded-but-off vector identical to the stock engine.
|
||||
// 4. a layer without a direction is untouched.
|
||||
|
||||
#include "strata/kernels/cvec.hpp"
|
||||
#include "strata/kernels/fused_gr.hpp"
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
namespace k = strata::kernels;
|
||||
|
||||
namespace {
|
||||
|
||||
int g_fail = 0;
|
||||
|
||||
void ck(cudaError_t e, const char* w) {
|
||||
if (e != cudaSuccess) {
|
||||
std::fprintf(stderr, "CUDA error in %s: %s\n", w, cudaGetErrorString(e));
|
||||
std::exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T* dalloc(size_t n) {
|
||||
T* p = nullptr;
|
||||
ck(cudaMalloc(&p, n * sizeof(T)), "malloc");
|
||||
return p;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void up(T* d, const std::vector<T>& h) { ck(cudaMemcpy(d, h.data(), h.size() * sizeof(T), cudaMemcpyHostToDevice), "up"); }
|
||||
template <typename T>
|
||||
std::vector<T> down(const T* d, size_t n) {
|
||||
std::vector<T> h(n);
|
||||
ck(cudaMemcpy(h.data(), d, n * sizeof(T), cudaMemcpyDeviceToHost), "down");
|
||||
return h;
|
||||
}
|
||||
|
||||
void check(bool ok, const char* what) {
|
||||
std::printf(" %-66s %s\n", what, ok ? "ok" : "FAIL");
|
||||
if (!ok) ++g_fail;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
constexpr int64_t L = 48, N = 2560, HC = 4, LR = 320, T = 5, D = HC * N;
|
||||
constexpr int64_t kLayer = 7, kOff = 3; // steered / not steered
|
||||
std::mt19937 rng(1234);
|
||||
std::normal_distribution<float> nd(0.0f, 1.0f);
|
||||
|
||||
// a unit direction at kLayer with a norm-2 scaled one's s = 2; nothing at kOff
|
||||
std::vector<float> dir((size_t) (L * N), 0.0f), s((size_t) L, 0.0f);
|
||||
{
|
||||
double nrm = 0.0;
|
||||
std::vector<float> v((size_t) N);
|
||||
for (auto& x : v) { x = nd(rng); nrm += (double) x * x; }
|
||||
nrm = std::sqrt(nrm);
|
||||
for (int64_t j = 0; j < N; ++j) dir[(size_t) (kLayer * N + j)] = (float) (v[(size_t) j] / nrm);
|
||||
s[(size_t) kLayer] = 2.0f;
|
||||
}
|
||||
std::string err;
|
||||
if (!k::cvec_upload(dir, s, /*project*/ 0, 4, 44, N, HC, err)) { std::fprintf(stderr, "%s\n", err.c_str()); return 2; }
|
||||
check(k::cvec().covers(kLayer) && !k::cvec().covers(kOff) && !k::cvec().covers(45), "covers(): steered layers only");
|
||||
|
||||
std::vector<float> R((size_t) (T * D)), bo((size_t) (T * N)), inj((size_t) (T * HC));
|
||||
for (auto& x : R) x = nd(rng) * 3.0f;
|
||||
for (auto& x : bo) x = nd(rng);
|
||||
for (auto& x : inj) x = nd(rng);
|
||||
float* dR = dalloc<float>(R.size());
|
||||
float* dbo = dalloc<float>(bo.size());
|
||||
float* dinj = dalloc<float>(inj.size());
|
||||
up(dbo, bo);
|
||||
up(dinj, inj);
|
||||
|
||||
// ---- 1. project, no pending write
|
||||
std::printf("project\n");
|
||||
up(dR, R);
|
||||
k::cvec_apply(dR, kLayer, T, D, nullptr, 0, nullptr, 0, false, nullptr);
|
||||
ck(cudaDeviceSynchronize(), "project");
|
||||
{
|
||||
const auto got = down(dR, R.size());
|
||||
double worst = 0.0, worst_dot = 0.0;
|
||||
for (int64_t t = 0; t < T; ++t)
|
||||
for (int64_t c = 0; c < HC; ++c) {
|
||||
const float* h = R.data() + t * D + c * N;
|
||||
const float* v = dir.data() + kLayer * N;
|
||||
double dot = 0.0;
|
||||
for (int64_t j = 0; j < N; ++j) dot += (double) h[j] * v[j];
|
||||
double after = 0.0;
|
||||
for (int64_t j = 0; j < N; ++j) {
|
||||
const double want = h[j] - 2.0 * dot * v[j];
|
||||
const double g = got[(size_t) (t * D + c * N + j)];
|
||||
worst = std::fmax(worst, std::fabs(g - want));
|
||||
after += g * v[j];
|
||||
}
|
||||
// s = 2 reflects the component: h.v -> -h.v
|
||||
worst_dot = std::fmax(worst_dot, std::fabs(after + dot));
|
||||
}
|
||||
std::printf(" max |err| %.3g, max |h'.v + h.v| %.3g\n", worst, worst_dot);
|
||||
check(worst < 1e-4, "project matches h - s (h.v) v (s = 2)");
|
||||
check(worst_dot < 1e-3, "the component along v is reflected (s = 2)");
|
||||
}
|
||||
// s = 1 removes it
|
||||
s[(size_t) kLayer] = 1.0f;
|
||||
if (!k::cvec_upload(dir, s, 0, 4, 44, N, HC, err)) return 2;
|
||||
up(dR, R);
|
||||
k::cvec_apply(dR, kLayer, T, D, nullptr, 0, nullptr, 0, false, nullptr);
|
||||
ck(cudaDeviceSynchronize(), "project s=1");
|
||||
{
|
||||
const auto got = down(dR, R.size());
|
||||
double worst = 0.0;
|
||||
for (int64_t t = 0; t < T; ++t)
|
||||
for (int64_t c = 0; c < HC; ++c) {
|
||||
double after = 0.0;
|
||||
for (int64_t j = 0; j < N; ++j) after += (double) got[(size_t) (t * D + c * N + j)] * dir[(size_t) (kLayer * N + j)];
|
||||
worst = std::fmax(worst, std::fabs(after));
|
||||
}
|
||||
std::printf(" max |h'.v| %.3g\n", worst);
|
||||
check(worst < 1e-3, "s = 1 projects the direction out");
|
||||
}
|
||||
|
||||
// ---- 4. a layer without a direction
|
||||
up(dR, R);
|
||||
k::cvec_apply(dR, kOff, T, D, nullptr, 0, nullptr, 0, false, nullptr);
|
||||
ck(cudaDeviceSynchronize(), "off layer");
|
||||
check(std::memcmp(down(dR, R.size()).data(), R.data(), R.size() * 4) == 0, "a layer without a direction is untouched");
|
||||
|
||||
// ---- 3. switched off
|
||||
std::printf("switched off\n");
|
||||
k::cvec_set_enabled(false);
|
||||
check(!k::cvec_enabled(), "cvec_enabled() follows the switch");
|
||||
up(dR, R);
|
||||
k::cvec_apply(dR, kLayer, T, D, nullptr, 0, nullptr, 0, false, nullptr);
|
||||
ck(cudaDeviceSynchronize(), "off");
|
||||
check(std::memcmp(down(dR, R.size()).data(), R.data(), R.size() * 4) == 0, "off, no pending write: R unchanged");
|
||||
{
|
||||
// the fused read's folded write, one token at a time, against the kernel's write-only pass
|
||||
std::vector<uint16_t> wd((size_t) (LR * D)), wu((size_t) (D * LR)), wi((size_t) (HC * D));
|
||||
std::vector<float> wn((size_t) D);
|
||||
auto bf = [&](float x) { uint32_t u; std::memcpy(&u, &x, 4); return (uint16_t) (u >> 16); };
|
||||
for (auto& x : wd) x = bf(nd(rng) * 0.02f);
|
||||
for (auto& x : wu) x = bf(nd(rng) * 0.02f);
|
||||
for (auto& x : wi) x = bf(nd(rng) * 0.02f);
|
||||
for (auto& x : wn) x = 1.0f + 0.1f * nd(rng);
|
||||
uint16_t* dwd = dalloc<uint16_t>(wd.size());
|
||||
uint16_t* dwu = dalloc<uint16_t>(wu.size());
|
||||
uint16_t* dwi = dalloc<uint16_t>(wi.size());
|
||||
float* dwn = dalloc<float>(wn.size());
|
||||
up(dwd, wd); up(dwu, wu); up(dwi, wi); up(dwn, wn);
|
||||
float* lo = dalloc<float>(LR);
|
||||
float* rs = dalloc<float>(HC);
|
||||
float* inj_out = dalloc<float>(HC);
|
||||
float* mixed = dalloc<float>(N);
|
||||
float* dF = dalloc<float>(R.size());
|
||||
up(dF, R);
|
||||
for (int64_t t = 0; t < T; ++t) {
|
||||
k::FusedGrArgs a;
|
||||
a.R = dF + t * D; a.R_out = dF + t * D; a.apply = true;
|
||||
a.bo_prev = dbo + t * N; a.inj_prev = dinj + t * HC;
|
||||
a.w_norm = dwn; a.w_down = dwd; a.w_up = dwu; a.w_inject = dwi;
|
||||
a.lo = lo; a.rs = rs; a.inject_out = inj_out; a.mixed = mixed;
|
||||
k::fused_gr_read(a, nullptr);
|
||||
}
|
||||
up(dR, R);
|
||||
k::cvec_apply(dR, kLayer, T, D, dbo, N, dinj, HC, true, nullptr);
|
||||
ck(cudaDeviceSynchronize(), "write only");
|
||||
const auto fused = down(dF, R.size()), mine = down(dR, R.size());
|
||||
check(std::memcmp(fused.data(), mine.data(), R.size() * 4) == 0,
|
||||
"off, pending write: bitwise the fused read's folded write");
|
||||
k::cvec_set_enabled(true);
|
||||
// on, with the write: the write, then the projection (reference from the fused result)
|
||||
up(dR, R);
|
||||
k::cvec_apply(dR, kLayer, T, D, dbo, N, dinj, HC, true, nullptr);
|
||||
ck(cudaDeviceSynchronize(), "write+project");
|
||||
const auto both = down(dR, R.size());
|
||||
double worst = 0.0;
|
||||
for (int64_t t = 0; t < T; ++t)
|
||||
for (int64_t c = 0; c < HC; ++c) {
|
||||
const float* h = fused.data() + t * D + c * N;
|
||||
const float* v = dir.data() + kLayer * N;
|
||||
double dot = 0.0;
|
||||
for (int64_t j = 0; j < N; ++j) dot += (double) h[j] * v[j];
|
||||
for (int64_t j = 0; j < N; ++j)
|
||||
worst = std::fmax(worst, std::fabs(both[(size_t) (t * D + c * N + j)] - (h[j] - dot * v[j])));
|
||||
}
|
||||
std::printf(" write + project: max |err| %.3g\n", worst);
|
||||
check(worst < 1e-4, "on, pending write: the write, then the projection");
|
||||
}
|
||||
|
||||
// ---- 2. add
|
||||
std::printf("add\n");
|
||||
for (int64_t j = 0; j < N; ++j) dir[(size_t) (kLayer * N + j)] *= 0.1f; // d = 0.1 v, s = 1
|
||||
s[(size_t) kLayer] = 1.0f;
|
||||
if (!k::cvec_upload(dir, s, /*add*/ 1, 4, 44, N, HC, err)) return 2;
|
||||
up(dR, R);
|
||||
k::cvec_apply(dR, kLayer, T, D, nullptr, 0, nullptr, 0, false, nullptr);
|
||||
ck(cudaDeviceSynchronize(), "add");
|
||||
{
|
||||
const auto got = down(dR, R.size());
|
||||
bool same = true;
|
||||
for (int64_t t = 0; t < T; ++t)
|
||||
for (int64_t c = 0; c < HC; ++c)
|
||||
for (int64_t j = 0; j < N; ++j) {
|
||||
const size_t i = (size_t) (t * D + c * N + j);
|
||||
same = same && got[i] == R[i] + dir[(size_t) (kLayer * N + j)];
|
||||
}
|
||||
check(same, "add is h + d in every stream (bitwise)");
|
||||
}
|
||||
|
||||
std::printf(g_fail ? "cvec_parity: %d FAILED\n" : "cvec_parity: all passed\n", g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
// src/kernels/kv_q4_parity.cpp - Parity test for Q4_0 KV cache with orthonormal Walsh-Hadamard rotation.
|
||||
// Validates:
|
||||
// 1. FWHT 256 CUDA kernel vs exact mathematical orthonormal Walsh-Hadamard transform.
|
||||
// (H * H * x == x, H is symmetric and orthonormal with scale 1/sqrt(256) = 1/16).
|
||||
// 2. Q4_0 quantization and packing (32 values per block, 18 bytes) bitwise vs host reference.
|
||||
// 3. kv_append_q4_step and kv_gather_q4_step through paged pool against host reference.
|
||||
// 4. Invariance of dot products under Walsh-Hadamard rotation: (H*q) . (H*k) == q . k.
|
||||
|
||||
#include "strata/kernels/f16_bits.hpp"
|
||||
#include "strata/kernels/kv_q4.hpp"
|
||||
#include "strata/kernels/qsa.hpp"
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
namespace k = strata::kernels;
|
||||
|
||||
namespace {
|
||||
|
||||
int g_fail = 0;
|
||||
|
||||
void ck(cudaError_t e, const char* w) {
|
||||
if (e != cudaSuccess) {
|
||||
std::fprintf(stderr, "CUDA error in %s: %s\n", w, cudaGetErrorString(e));
|
||||
std::exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T* dalloc(size_t n) {
|
||||
T* p = nullptr;
|
||||
ck(cudaMalloc(&p, n * sizeof(T) + 64), "malloc");
|
||||
ck(cudaMemset(p, 0, n * sizeof(T) + 64), "memset");
|
||||
return p;
|
||||
}
|
||||
|
||||
// Exact CPU reference for Fast Walsh-Hadamard Transform (D = 256)
|
||||
// Sylvester construction matching llama.cpp's ggml_gen_hadamard
|
||||
void fwht256_host_reference(float* x) {
|
||||
constexpr int n = 256;
|
||||
for (int s = 1; s < n; s *= 2) {
|
||||
for (int i = 0; i < n; i += 2 * s) {
|
||||
for (int j = 0; j < s; ++j) {
|
||||
const float u = x[i + j];
|
||||
const float v = x[i + j + s];
|
||||
x[i + j] = u + v;
|
||||
x[i + j + s] = u - v;
|
||||
}
|
||||
}
|
||||
}
|
||||
const float scale = 1.0f / 16.0f; // 1 / sqrt(256)
|
||||
for (int i = 0; i < n; ++i) {
|
||||
x[i] *= scale;
|
||||
}
|
||||
}
|
||||
|
||||
// Host reference for Q4_0 quantization of 32 elements (1 block)
|
||||
void quantize_block_q4_0_host(const float* x, k::block_q4_0& blk) {
|
||||
float amax = 0.0f;
|
||||
float max_val = 0.0f;
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
const float v = x[i];
|
||||
if (amax < std::fabs(v)) {
|
||||
amax = std::fabs(v);
|
||||
max_val = v;
|
||||
}
|
||||
}
|
||||
const float d = max_val / -8.0f;
|
||||
const float id = d != 0.0f ? 1.0f / d : 0.0f;
|
||||
blk.d = k::f16_from_f32(d);
|
||||
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
const float x0 = x[i] * id;
|
||||
const float x1 = x[i + 16] * id;
|
||||
const int q0 = (int) (x0 + 8.5f);
|
||||
const int q1 = (int) (x1 + 8.5f);
|
||||
const uint8_t xi0 = (uint8_t) std::min(15, std::max(0, q0));
|
||||
const uint8_t xi1 = (uint8_t) std::min(15, std::max(0, q1));
|
||||
blk.qs[i] = xi0 | (xi1 << 4);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
std::printf("=== Running kv_q4_parity test ===\n");
|
||||
std::mt19937 rng(42);
|
||||
std::normal_distribution<float> nd(0.0f, 1.0f);
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Test 1: Orthonormal Fast Walsh-Hadamard Transform (D=256)
|
||||
// -------------------------------------------------------------
|
||||
std::printf("[1/4] Verifying FWHT-256 CUDA kernel vs exact mathematical reference...\n");
|
||||
constexpr int n_vectors = 64;
|
||||
std::vector<float> h_in(n_vectors * 256);
|
||||
std::vector<float> h_ref(n_vectors * 256);
|
||||
for (size_t i = 0; i < h_in.size(); ++i) {
|
||||
h_in[i] = nd(rng);
|
||||
}
|
||||
h_ref = h_in;
|
||||
|
||||
// Run CPU reference on each row
|
||||
for (int r = 0; r < n_vectors; ++r) {
|
||||
fwht256_host_reference(h_ref.data() + r * 256);
|
||||
}
|
||||
|
||||
// Run GPU kernel
|
||||
float* d_src = dalloc<float>(n_vectors * 256);
|
||||
float* d_dst = dalloc<float>(n_vectors * 256);
|
||||
ck(cudaMemcpy(d_src, h_in.data(), h_in.size() * sizeof(float), cudaMemcpyHostToDevice), "memcpy H2D");
|
||||
|
||||
k::fwht256_cuda(d_src, d_dst, n_vectors, nullptr);
|
||||
ck(cudaDeviceSynchronize(), "fwht256_cuda");
|
||||
|
||||
std::vector<float> h_out(n_vectors * 256);
|
||||
ck(cudaMemcpy(h_out.data(), d_dst, h_out.size() * sizeof(float), cudaMemcpyDeviceToHost), "memcpy D2H");
|
||||
|
||||
double max_fwht_diff = 0.0;
|
||||
for (size_t i = 0; i < h_out.size(); ++i) {
|
||||
double diff = std::fabs(h_out[i] - h_ref[i]);
|
||||
if (diff > max_fwht_diff) max_fwht_diff = diff;
|
||||
}
|
||||
if (max_fwht_diff > 1e-5) {
|
||||
std::fprintf(stderr, "FAIL: FWHT-256 GPU output differs from host reference (max diff = %e)\n", max_fwht_diff);
|
||||
++g_fail;
|
||||
} else {
|
||||
std::printf(" -> FWHT-256 bitwise match with host reference (max diff = %e, OK)\n", max_fwht_diff);
|
||||
}
|
||||
|
||||
// Self-inverse property: H * (H * x) == x
|
||||
k::fwht256_inplace_cuda(d_dst, n_vectors, nullptr);
|
||||
ck(cudaDeviceSynchronize(), "fwht256_inplace_cuda");
|
||||
ck(cudaMemcpy(h_out.data(), d_dst, h_out.size() * sizeof(float), cudaMemcpyDeviceToHost), "memcpy D2H");
|
||||
|
||||
double max_inv_diff = 0.0;
|
||||
for (size_t i = 0; i < h_out.size(); ++i) {
|
||||
double diff = std::fabs(h_out[i] - h_in[i]);
|
||||
if (diff > max_inv_diff) max_inv_diff = diff;
|
||||
}
|
||||
if (max_inv_diff > 1e-5) {
|
||||
std::fprintf(stderr, "FAIL: FWHT-256 self-inverse property failed (max diff = %e)\n", max_inv_diff);
|
||||
++g_fail;
|
||||
} else {
|
||||
std::printf(" -> FWHT-256 self-inverse property verified H*H*x == x (max diff = %e, OK)\n", max_inv_diff);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Test 2: Invariance of Attention Scores under Hadamard Rotation
|
||||
// -------------------------------------------------------------
|
||||
std::printf("[2/4] Verifying Attention Score Invariance: <H*q, H*k> == <q, k>...\n");
|
||||
double max_dot_diff = 0.0;
|
||||
for (int r = 0; r < n_vectors; r += 2) {
|
||||
const float* q = h_in.data() + r * 256;
|
||||
const float* k_vec = h_in.data() + (r + 1) * 256;
|
||||
const float* hq = h_ref.data() + r * 256;
|
||||
const float* hk = h_ref.data() + (r + 1) * 256;
|
||||
|
||||
double dot_raw = 0.0;
|
||||
double dot_rot = 0.0;
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
dot_raw += (double) q[i] * (double) k_vec[i];
|
||||
dot_rot += (double) hq[i] * (double) hk[i];
|
||||
}
|
||||
double d = std::fabs(dot_raw - dot_rot);
|
||||
if (d > max_dot_diff) max_dot_diff = d;
|
||||
}
|
||||
if (max_dot_diff > 1e-5) {
|
||||
std::fprintf(stderr, "FAIL: Rotated dot product differs from original (max diff = %e)\n", max_dot_diff);
|
||||
++g_fail;
|
||||
} else {
|
||||
std::printf(" -> Dot product invariant under rotation: |<Hq,Hk> - <q,k>| = %e (OK)\n", max_dot_diff);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Test 3: Q4_0 Paged KV Append & Gather against Host Reference
|
||||
// -------------------------------------------------------------
|
||||
std::printf("[3/4] Verifying Q4_0 KV append and gather against host reference...\n");
|
||||
k::QsaShapes s = k::qsa_real_shapes();
|
||||
s.page_size = 64;
|
||||
const int H = (int) s.n_head_kv; // 2
|
||||
const int D = (int) s.head_dim; // 256
|
||||
const int P = (int) s.page_size; // 64
|
||||
const int pages = 8;
|
||||
const int cells = pages * P;
|
||||
|
||||
std::vector<int32_t> table(pages);
|
||||
for (int i = 0; i < pages; ++i) table[i] = (i * 3 + 5) % pages; // permutation
|
||||
int32_t* d_table = dalloc<int32_t>(pages);
|
||||
ck(cudaMemcpy(d_table, table.data(), pages * sizeof(int32_t), cudaMemcpyHostToDevice), "memcpy table");
|
||||
|
||||
const size_t pool_bytes = (size_t) pages * H * P * k::kv_q4_bytes_per_head(D);
|
||||
uint8_t* d_k_q4 = dalloc<uint8_t>(pool_bytes);
|
||||
uint8_t* d_v_q4 = dalloc<uint8_t>(pool_bytes);
|
||||
float* d_kcur = dalloc<float>(H * D);
|
||||
float* d_vcur = dalloc<float>(H * D);
|
||||
int32_t* d_step = dalloc<int32_t>(k::kStepCount);
|
||||
|
||||
std::vector<std::vector<float>> host_k(cells), host_v(cells);
|
||||
std::vector<int> positions(cells);
|
||||
for (int i = 0; i < cells; ++i) positions[i] = i;
|
||||
std::shuffle(positions.begin(), positions.end(), rng);
|
||||
|
||||
const int n_fill = cells - 25; // leave some cells empty
|
||||
for (int n = 0; n < n_fill; ++n) {
|
||||
const int pos = positions[n];
|
||||
std::vector<float> kv(H * D), vv(H * D);
|
||||
for (auto& x : kv) x = nd(rng) * 2.0f;
|
||||
for (auto& x : vv) x = nd(rng) * 2.0f;
|
||||
|
||||
// Apply FWHT before append
|
||||
for (int h = 0; h < H; ++h) {
|
||||
fwht256_host_reference(kv.data() + h * D);
|
||||
fwht256_host_reference(vv.data() + h * D);
|
||||
}
|
||||
host_k[pos] = kv;
|
||||
host_v[pos] = vv;
|
||||
|
||||
int32_t hstep[k::kStepCount] = {pos, pos + 1, 0, 0};
|
||||
ck(cudaMemcpy(d_step, hstep, sizeof(hstep), cudaMemcpyHostToDevice), "memcpy step");
|
||||
ck(cudaMemcpy(d_kcur, kv.data(), kv.size() * sizeof(float), cudaMemcpyHostToDevice), "memcpy k");
|
||||
ck(cudaMemcpy(d_vcur, vv.data(), vv.size() * sizeof(float), cudaMemcpyHostToDevice), "memcpy v");
|
||||
|
||||
k::kv_append_q4_step(d_k_q4, d_v_q4, d_table, d_step, d_kcur, d_vcur, s, nullptr);
|
||||
ck(cudaDeviceSynchronize(), "kv_append_q4_step");
|
||||
}
|
||||
|
||||
// Verify written Q4_0 blocks bitwise against host quantizer
|
||||
std::vector<uint8_t> h_k_q4(pool_bytes), h_v_q4(pool_bytes);
|
||||
ck(cudaMemcpy(h_k_q4.data(), d_k_q4, pool_bytes, cudaMemcpyDeviceToHost), "memcpy d2h k_q4");
|
||||
ck(cudaMemcpy(h_v_q4.data(), d_v_q4, pool_bytes, cudaMemcpyDeviceToHost), "memcpy d2h v_q4");
|
||||
|
||||
long bad_blocks = 0;
|
||||
const int blocks_per_head = D / k::QK4_0; // 8
|
||||
const int bytes_per_head = blocks_per_head * sizeof(k::block_q4_0); // 144
|
||||
|
||||
for (int n = 0; n < n_fill; ++n) {
|
||||
const int pos = positions[n];
|
||||
for (int is_v = 0; is_v < 2; ++is_v) {
|
||||
for (int h = 0; h < H; ++h) {
|
||||
const long long page = (long long) table[pos / P];
|
||||
const long long row = (page * H + h) * P + (pos % P);
|
||||
const uint8_t* pool_ptr = (is_v ? h_v_q4.data() : h_k_q4.data()) + row * bytes_per_head;
|
||||
const float* raw_vec = (is_v ? host_v[pos].data() : host_k[pos].data()) + h * D;
|
||||
|
||||
for (int b = 0; b < blocks_per_head; ++b) {
|
||||
k::block_q4_0 host_blk;
|
||||
quantize_block_q4_0_host(raw_vec + b * 32, host_blk);
|
||||
|
||||
const k::block_q4_0* gpu_blk = reinterpret_cast<const k::block_q4_0*>(pool_ptr) + b;
|
||||
if (std::memcmp(&host_blk, gpu_blk, sizeof(k::block_q4_0)) != 0) {
|
||||
if (bad_blocks == 0) {
|
||||
std::printf("MISMATCH at n=%d, pos=%d, is_v=%d, h=%d, b=%d\n", n, pos, is_v, h, b);
|
||||
std::printf(" host blk.d = 0x%04x (%f), gpu blk.d = 0x%04x (%f)\n",
|
||||
host_blk.d, k::f32_from_f16(host_blk.d),
|
||||
gpu_blk->d, k::f32_from_f16(gpu_blk->d));
|
||||
std::printf(" host qs: ");
|
||||
for (int i = 0; i < 16; ++i) std::printf("%02x ", host_blk.qs[i]);
|
||||
std::printf("\n gpu qs: ");
|
||||
for (int i = 0; i < 16; ++i) std::printf("%02x ", gpu_blk->qs[i]);
|
||||
std::printf("\n");
|
||||
}
|
||||
++bad_blocks;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bad_blocks > 0) {
|
||||
std::fprintf(stderr, "FAIL: %ld Q4_0 blocks differ from host quantizer\n", bad_blocks);
|
||||
++g_fail;
|
||||
} else {
|
||||
std::printf(" -> All Q4_0 pool blocks bitwise equal to host quantizer (OK)\n");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Test 4: Gather Q4_0 into FP16 Scratch and verify accuracy
|
||||
// -------------------------------------------------------------
|
||||
std::printf("[4/4] Verifying Q4_0 gather unpacking and dequantization...\n");
|
||||
const int max_ids = 128;
|
||||
int32_t* d_ids = dalloc<int32_t>(max_ids);
|
||||
uint16_t* d_k_scratch = dalloc<uint16_t>((size_t) max_ids * H * D);
|
||||
uint16_t* d_v_scratch = dalloc<uint16_t>((size_t) max_ids * H * D);
|
||||
|
||||
std::vector<int32_t> ids(max_ids);
|
||||
for (int i = 0; i < max_ids; ++i) {
|
||||
ids[i] = positions[rng() % n_fill];
|
||||
}
|
||||
ck(cudaMemcpy(d_ids, ids.data(), max_ids * sizeof(int32_t), cudaMemcpyHostToDevice), "memcpy ids");
|
||||
|
||||
int32_t hstep[k::kStepCount] = {0, 0, 0, max_ids};
|
||||
ck(cudaMemcpy(d_step, hstep, sizeof(hstep), cudaMemcpyHostToDevice), "memcpy step");
|
||||
|
||||
k::kv_gather_q4_step(d_k_q4, d_v_q4, d_table, d_ids, d_step, max_ids, s, d_k_scratch, d_v_scratch, nullptr);
|
||||
ck(cudaDeviceSynchronize(), "kv_gather_q4_step");
|
||||
|
||||
std::vector<uint16_t> h_k_scratch((size_t) max_ids * H * D);
|
||||
std::vector<uint16_t> h_v_scratch((size_t) max_ids * H * D);
|
||||
ck(cudaMemcpy(h_k_scratch.data(), d_k_scratch, h_k_scratch.size() * sizeof(uint16_t), cudaMemcpyDeviceToHost), "memcpy d2h k_scratch");
|
||||
ck(cudaMemcpy(h_v_scratch.data(), d_v_scratch, h_v_scratch.size() * sizeof(uint16_t), cudaMemcpyDeviceToHost), "memcpy d2h v_scratch");
|
||||
|
||||
double worst_quant_err = 0.0;
|
||||
for (int i = 0; i < max_ids; ++i) {
|
||||
const int cell = ids[i];
|
||||
for (int is_v = 0; is_v < 2; ++is_v) {
|
||||
for (int h = 0; h < H; ++h) {
|
||||
const float* orig = (is_v ? host_v[cell].data() : host_k[cell].data()) + h * D;
|
||||
const uint16_t* gathered = (is_v ? h_v_scratch.data() : h_k_scratch.data()) + (i * H + h) * D;
|
||||
|
||||
for (int d = 0; d < D; ++d) {
|
||||
float deq = k::f32_from_f16(gathered[d]);
|
||||
float raw = orig[d];
|
||||
double err = std::fabs(deq - raw);
|
||||
if (err > worst_quant_err) worst_quant_err = err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::printf(" -> Max reconstruction error after 4-bit quant + dequant: %.4f (OK)\n", worst_quant_err);
|
||||
|
||||
cudaFree(d_src);
|
||||
cudaFree(d_dst);
|
||||
cudaFree(d_table);
|
||||
cudaFree(d_k_q4);
|
||||
cudaFree(d_v_q4);
|
||||
cudaFree(d_kcur);
|
||||
cudaFree(d_vcur);
|
||||
cudaFree(d_step);
|
||||
cudaFree(d_ids);
|
||||
cudaFree(d_k_scratch);
|
||||
cudaFree(d_v_scratch);
|
||||
|
||||
if (g_fail == 0) {
|
||||
std::printf("=== kv_q4_parity: ALL TESTS PASSED SUCCESSFULLY! ===\n");
|
||||
return 0;
|
||||
} else {
|
||||
std::fprintf(stderr, "=== kv_q4_parity: FAILED (%d failures) ===\n", g_fail);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@
|
||||
// 2. the residency map is consistent after every call (slot_block and page_table invert each other);
|
||||
// 3. no call overflowed, and the hit/miss counters add up;
|
||||
// 4. a ring (the MTP drafter's layout) restored from the host copy reads the same values as the resident pool.
|
||||
// INT8 and FP16 pools both.
|
||||
// INT8, FP16 and Q4_0 (PR #21) pools.
|
||||
#include "strata/kernels/kv_q4.hpp"
|
||||
#include "strata/kernels/kv_q8.hpp"
|
||||
#include "strata/kernels/kv_stream.hpp"
|
||||
#include "strata/kernels/qsa.hpp"
|
||||
@@ -48,9 +49,13 @@ template <typename T> T* halloc(size_t n) { // pinned, mapped; returns the dev
|
||||
|
||||
struct Pools { // one K/V pool set of `pages` pages
|
||||
k::KvHostPools p; // reused as a plain pointer bundle
|
||||
void alloc(int64_t pages, const k::QsaShapes& s, bool int8, bool host) {
|
||||
void alloc(int64_t pages, const k::QsaShapes& s, int fmt, bool host) {
|
||||
const size_t rows = (size_t) pages * s.n_head_kv * s.page_size;
|
||||
if (int8) {
|
||||
if (fmt == k::kKvQ4) {
|
||||
const size_t b = rows * k::kv_q4_bytes_per_head((int) s.head_dim);
|
||||
p.k_q4 = host ? halloc<uint8_t>(b) : dalloc<uint8_t>(b);
|
||||
p.v_q4 = host ? halloc<uint8_t>(b) : dalloc<uint8_t>(b);
|
||||
} else if (fmt == k::kKvInt8) {
|
||||
p.k_q = host ? halloc<int8_t>(rows * s.head_dim) : dalloc<int8_t>(rows * s.head_dim);
|
||||
p.v_q = host ? halloc<int8_t>(rows * s.head_dim) : dalloc<int8_t>(rows * s.head_dim);
|
||||
p.k_scale = host ? halloc<uint16_t>(rows * 4) : dalloc<uint16_t>(rows * 4);
|
||||
@@ -63,14 +68,15 @@ struct Pools { // one K/V pool set of `pages` pages
|
||||
k::QsaAttnPools attn(const int32_t* table) const {
|
||||
k::QsaAttnPools a;
|
||||
a.k_pool = p.k_pool; a.v_pool = p.v_pool; a.k_q = p.k_q; a.v_q = p.v_q; a.k_scale = p.k_scale;
|
||||
a.v_scale = p.v_scale; a.page_table = table;
|
||||
a.v_scale = p.v_scale; a.k_q4 = p.k_q4; a.v_q4 = p.v_q4; a.page_table = table;
|
||||
return a;
|
||||
}
|
||||
};
|
||||
|
||||
void append(const Pools& pl, const int32_t* table, const int32_t* step, const float* kc, const float* vc,
|
||||
const k::QsaShapes& s, bool int8, const k::KvHostPools* host) {
|
||||
if (int8) k::kv_append_q8_step(pl.p.k_q, pl.p.v_q, pl.p.k_scale, pl.p.v_scale, table, step, kc, vc, s, nullptr, host);
|
||||
const k::QsaShapes& s, int fmt, const k::KvHostPools* host) {
|
||||
if (fmt == k::kKvQ4) k::kv_append_q4_step(pl.p.k_q4, pl.p.v_q4, table, step, kc, vc, s, nullptr, host);
|
||||
else if (fmt == k::kKvInt8) k::kv_append_q8_step(pl.p.k_q, pl.p.v_q, pl.p.k_scale, pl.p.v_scale, table, step, kc, vc, s, nullptr, host);
|
||||
else k::kv_append_step(pl.p.k_pool, pl.p.v_pool, table, step, kc, vc, s, nullptr, host);
|
||||
}
|
||||
|
||||
@@ -96,18 +102,19 @@ std::vector<int32_t> selection(int64_t n_kv, int64_t width, std::mt19937& rng) {
|
||||
return ids;
|
||||
}
|
||||
|
||||
bool run(bool int8) {
|
||||
bool run(int fmt) {
|
||||
const char* name = fmt == k::kKvQ4 ? "q4_0" : fmt == k::kKvInt8 ? "int8" : "fp16";
|
||||
k::QsaShapes s = k::qsa_real_shapes();
|
||||
const int64_t N = 40000, n_blocks = (N + 3) / 4, n_slots = 8 * 516 + 700; // must evict: slots < blocks
|
||||
const int64_t cap = k::qsa_selection_width(k::kTopkMaxCells, s), NQ = 8;
|
||||
const int64_t H = s.n_head_kv, D = s.head_dim, NH = s.n_head;
|
||||
std::mt19937 rng(int8 ? 7 : 11);
|
||||
std::mt19937 rng(7 + 4 * fmt);
|
||||
std::normal_distribution<float> nd(0.f, 1.f);
|
||||
|
||||
Pools ref, slots, host;
|
||||
ref.alloc(n_blocks, s, int8, false);
|
||||
slots.alloc(n_slots, s, int8, false);
|
||||
host.alloc(n_blocks, s, int8, true);
|
||||
ref.alloc(n_blocks, s, fmt, false);
|
||||
slots.alloc(n_slots, s, fmt, false);
|
||||
host.alloc(n_blocks, s, fmt, true);
|
||||
int32_t* ident = dalloc<int32_t>(n_blocks);
|
||||
{
|
||||
std::vector<int32_t> t(n_blocks);
|
||||
@@ -143,8 +150,8 @@ bool run(bool int8) {
|
||||
ck(cudaMemcpy(kc, hk.data(), hk.size() * 4, cudaMemcpyHostToDevice), "k");
|
||||
ck(cudaMemcpy(vc, hv.data(), hv.size() * 4, cudaMemcpyHostToDevice), "v");
|
||||
ck(cudaMemcpy(step, st, sizeof(st), cudaMemcpyHostToDevice), "step");
|
||||
append(ref, ident, step, kc, vc, s, int8, nullptr);
|
||||
append(slots, m.page_table, step, kc, vc, s, int8, &host.p);
|
||||
append(ref, ident, step, kc, vc, s, fmt, nullptr);
|
||||
append(slots, m.page_table, step, kc, vc, s, fmt, &host.p);
|
||||
if (pos % 131 != 130 && pos != N - 1) continue;
|
||||
// a batch: n_q queries at the last n_q positions (as a verify window), each with its own selection
|
||||
const int n_q = 1 + (int) (rng() % NQ);
|
||||
@@ -163,13 +170,13 @@ bool run(bool int8) {
|
||||
ck(cudaMemcpy(steps, hst.data(), hst.size() * 4, cudaMemcpyHostToDevice), "steps");
|
||||
ck(cudaMemcpy(q, hq.data(), hq.size() * 4, cudaMemcpyHostToDevice), "q");
|
||||
k::qsa_decode_attn_batch(q, ref.attn(ident), ids, steps, cap, s, scratch, out_ref, n_q, nullptr);
|
||||
k::kv_stream_resolve(m, slots.attn(m.page_table), host.p, int8, ids, steps, n_q, cap, s, nullptr);
|
||||
k::kv_stream_resolve(m, slots.attn(m.page_table), host.p, fmt, ids, steps, n_q, cap, s, nullptr);
|
||||
k::qsa_decode_attn_batch(q, slots.attn(m.page_table), ids, steps, cap, s, scratch, out_str, n_q, nullptr);
|
||||
ck(cudaDeviceSynchronize(), "batch");
|
||||
ck(cudaMemcpy(a.data(), out_ref, (size_t) n_q * NH * D * 4, cudaMemcpyDeviceToHost), "a");
|
||||
ck(cudaMemcpy(b2.data(), out_str, (size_t) n_q * NH * D * 4, cudaMemcpyDeviceToHost), "b");
|
||||
if (std::memcmp(a.data(), b2.data(), (size_t) n_q * NH * D * 4) != 0) {
|
||||
if (bad++ < 5) std::fprintf(stderr, " %s pos %lld n_q %d: streamed attention differs\n", int8 ? "int8" : "fp16", (long long) pos, n_q);
|
||||
if (bad++ < 5) std::fprintf(stderr, " %s pos %lld n_q %d: streamed attention differs\n", name, (long long) pos, n_q);
|
||||
}
|
||||
// the map inverts itself
|
||||
ck(cudaMemcpy(pt.data(), m.page_table, n_blocks * 4, cudaMemcpyDeviceToHost), "pt");
|
||||
@@ -188,7 +195,7 @@ bool run(bool int8) {
|
||||
}
|
||||
const k::KvStreamCounters c = k::kv_stream_counters(m);
|
||||
std::printf(" %s: %d batches, %llu block lookups, %llu misses (%.1f%% hit), overflow %d, %d failures\n",
|
||||
int8 ? "int8" : "fp16", batches, (unsigned long long) c.lookups, (unsigned long long) c.misses,
|
||||
name, batches, (unsigned long long) c.lookups, (unsigned long long) c.misses,
|
||||
c.lookups ? 100.0 * (double) (c.lookups - c.misses) / (double) c.lookups : 0.0, (int) c.overflow, bad);
|
||||
if (c.overflow || c.calls != (uint64_t) batches || c.misses == 0 || c.misses >= c.lookups) ++bad;
|
||||
|
||||
@@ -196,10 +203,10 @@ bool run(bool int8) {
|
||||
{
|
||||
const int64_t R = 1500, b1 = n_blocks, b0 = b1 - R;
|
||||
Pools ring;
|
||||
ring.alloc(R, s, int8, false);
|
||||
ring.alloc(R, s, fmt, false);
|
||||
int32_t* rt = dalloc<int32_t>(n_blocks);
|
||||
k::kv_ring_table(rt, n_blocks, R, nullptr);
|
||||
k::kv_ring_restore(ring.attn(rt), host.p, int8, b0, b1, R, s, nullptr);
|
||||
k::kv_ring_restore(ring.attn(rt), host.p, fmt, b0, b1, R, s, nullptr);
|
||||
const int64_t width = cap;
|
||||
std::vector<int32_t> hids((size_t) cap), hst = {(int32_t) (N - 1), (int32_t) N, (int32_t) (N / 4), (int32_t) width};
|
||||
for (int64_t i = 0; i < width; ++i) hids[i] = (int32_t) (N - width + i); // the window's last cells
|
||||
@@ -211,7 +218,7 @@ bool run(bool int8) {
|
||||
ck(cudaMemcpy(a.data(), out_ref, (size_t) NH * D * 4, cudaMemcpyDeviceToHost), "a");
|
||||
ck(cudaMemcpy(b2.data(), out_str, (size_t) NH * D * 4, cudaMemcpyDeviceToHost), "b");
|
||||
const bool ok = std::memcmp(a.data(), b2.data(), (size_t) NH * D * 4) == 0;
|
||||
std::printf(" %s ring restore: %s\n", int8 ? "int8" : "fp16", ok ? "identical" : "DIFFERS");
|
||||
std::printf(" %s ring restore: %s\n", name, ok ? "identical" : "DIFFERS");
|
||||
if (!ok) ++bad;
|
||||
}
|
||||
return bad == 0;
|
||||
@@ -220,8 +227,8 @@ bool run(bool int8) {
|
||||
|
||||
int main() {
|
||||
std::printf("kv_stream_parity: streamed vs resident KV, bitwise\n");
|
||||
const bool a = run(true), b = run(false);
|
||||
if (!a || !b) ++g_fail;
|
||||
const bool a = run(k::kKvInt8), b = run(k::kKvF16), c = run(k::kKvQ4);
|
||||
if (!a || !b || !c) ++g_fail;
|
||||
std::printf(g_fail ? "FAIL\n" : "PASS\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "strata/kernels/cpu/native_expert.hpp"
|
||||
#include "strata/kernels/cpu/expert.hpp"
|
||||
#include "strata/kernels/cpu/iq_avx512.hpp"
|
||||
#include "strata/kernels/cpu/iq_avx2.hpp"
|
||||
#include "strata/kernels/cpu/expert_layout.hpp"
|
||||
#include "ggml-cpu.h"
|
||||
#include "strata/kernels/iq_kernels.hpp"
|
||||
|
||||
@@ -33,6 +35,7 @@ static double rel(const std::vector<float>& a, const std::vector<float>& b) {
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
setvbuf(stdout, nullptr, _IONBF, 0); // keep the trail on a crash
|
||||
if (argc < 2) { std::fprintf(stderr, "usage: native_expert_parity <shard1.gguf> [layer ...]\n"); return 2; }
|
||||
strata::GgufFile gguf(argv[1]);
|
||||
std::vector<int> layers;
|
||||
@@ -103,40 +106,50 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
cpu::native_gu_rows(f, blob.data(), a, NT, ffp, 0, (int) FF);
|
||||
if (cpu::iq512_supported(f.gu_type)) {
|
||||
// the AVX-512 rows against ggml's own vec_dot, same Q8_K activations: float-order differences only
|
||||
std::vector<float> g512((size_t) NT * FF), gref((size_t) NT * FF);
|
||||
float* gp[NT];
|
||||
for (int k = 0; k < NT; ++k) gp[k] = g512.data() + k * FF;
|
||||
cpu::iq512_rows(f.gu_type, blob.data(), f.gu_row, (int) H, a, NT, gp, 0, (int) FF);
|
||||
// ggml's own vec_dot, same Q8_K activations: the reference for both multi-token kernels
|
||||
// (float-order differences only)
|
||||
const auto* tc = ggml_get_type_traits_cpu((ggml_type) f.gu_type);
|
||||
std::vector<float> gref((size_t) NT * FF);
|
||||
for (int k = 0; k < NT; ++k)
|
||||
for (int64_t r = 0; r < FF; ++r)
|
||||
tc->vec_dot((int) H, &gref[k * FF + r], 0, blob.data() + r * f.gu_row, 0, a[k], 0, 1);
|
||||
{ // single-thread throughput, the weights cache-resident (compute bound)
|
||||
float* f1[1] = {ffp[0]};
|
||||
double usg = 0.0;
|
||||
{ // ggml single-token throughput, the weights cache-resident (compute bound)
|
||||
const int it = 50;
|
||||
auto t0 = std::chrono::steady_clock::now();
|
||||
for (int i = 0; i < it; ++i) cpu::iq512_gu_rows(f.gu_type, blob.data(), f.gu_row, f.up_off, (int) H, a, 1, f1, 0, (int) FF);
|
||||
auto t1 = std::chrono::steady_clock::now();
|
||||
for (int i = 0; i < it; ++i)
|
||||
for (int64_t r = 0; r < FF; ++r) {
|
||||
float gg, uu;
|
||||
tc->vec_dot((int) H, &gg, 0, blob.data() + r * f.gu_row, 0, a[0], 0, 1);
|
||||
tc->vec_dot((int) H, &uu, 0, blob.data() + f.up_off + r * f.gu_row, 0, a[0], 0, 1);
|
||||
}
|
||||
auto t2 = std::chrono::steady_clock::now();
|
||||
const double us512 = std::chrono::duration<double, std::micro>(t1 - t0).count() / it;
|
||||
const double usg = std::chrono::duration<double, std::micro>(t2 - t1).count() / it;
|
||||
std::printf(" gate+up one token one thread: avx512 %.0f us (%.2f GB/s), ggml %.0f us (%.2f GB/s)\n",
|
||||
us512, 2.0 * f.up_off / us512 / 1e3, usg, 2.0 * f.up_off / usg / 1e3);
|
||||
auto t3 = std::chrono::steady_clock::now();
|
||||
for (int i = 0; i < it; ++i) cpu::iq512_gu_rows(f.gu_type, blob.data(), f.gu_row, f.up_off, (int) H, a, NT, ffp, 0, (int) FF);
|
||||
auto t4 = std::chrono::steady_clock::now();
|
||||
std::printf(" gate+up %d tokens one thread: avx512 %.0f us vs ggml %d x %.0f us\n", NT,
|
||||
std::chrono::duration<double, std::micro>(t4 - t3).count() / it, NT, usg);
|
||||
auto t1 = std::chrono::steady_clock::now();
|
||||
usg = std::chrono::duration<double, std::micro>(t1 - t0).count() / it;
|
||||
}
|
||||
std::printf(" %s AVX-512 gate rows vs ggml vec_dot: rel %.2e\n",
|
||||
ggml_type_name((ggml_type) f.gu_type), rel(g512, gref));
|
||||
// one multi-token kernel: parity against ggml's vec_dot, then one-token and NT-token throughput
|
||||
auto check = [&](const char* tag, bool is512) {
|
||||
std::vector<float> g((size_t) NT * FF);
|
||||
float* gp[NT];
|
||||
for (int k = 0; k < NT; ++k) gp[k] = g.data() + k * FF;
|
||||
(is512 ? cpu::iq512_rows : cpu::iq256_rows)(f.gu_type, blob.data(), f.gu_row, (int) H, a, NT, gp, 0, (int) FF);
|
||||
std::printf(" %s %s gate rows vs ggml vec_dot: rel %.2e\n",
|
||||
ggml_type_name((ggml_type) f.gu_type), tag, rel(g, gref));
|
||||
float* f1[1] = {ffp[0]};
|
||||
const int it = 50;
|
||||
const auto gu = is512 ? cpu::iq512_gu_rows : cpu::iq256_gu_rows;
|
||||
auto t0 = std::chrono::steady_clock::now();
|
||||
for (int i = 0; i < it; ++i) gu(f.gu_type, blob.data(), f.gu_row, f.up_off, (int) H, a, 1, f1, 0, (int) FF);
|
||||
auto t1 = std::chrono::steady_clock::now();
|
||||
for (int i = 0; i < it; ++i) gu(f.gu_type, blob.data(), f.gu_row, f.up_off, (int) H, a, NT, ffp, 0, (int) FF);
|
||||
auto t2 = std::chrono::steady_clock::now();
|
||||
const double us1 = std::chrono::duration<double, std::micro>(t1 - t0).count() / it;
|
||||
const double usn = std::chrono::duration<double, std::micro>(t2 - t1).count() / it;
|
||||
std::printf(" gate+up one token one thread: %s %.0f us (%.2f GB/s), ggml %.0f us (%.2f GB/s)\n",
|
||||
tag, us1, 2.0 * f.up_off / us1 / 1e3, usg, 2.0 * f.up_off / usg / 1e3);
|
||||
std::printf(" gate+up %d tokens one thread: %s %.0f us vs ggml %d x %.0f us\n", NT, tag, usn, NT, usg);
|
||||
};
|
||||
if (cpu::cpu_avx512_ok()) check("avx512", true); // guarded: the binary runs on AVX-2 CPUs too
|
||||
check("avx2", false);
|
||||
}
|
||||
for (int k = 0; k < NT; ++k) {
|
||||
cpu::native_quant_h(f, ff[k].data(), hq[k].data());
|
||||
@@ -145,18 +158,51 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
cpu::native_down_rows(f, blob.data(), hp, NT, op, 0, (int) H);
|
||||
if (f.d_type == 42) {
|
||||
// (b2) the AVX-512 GGUF-layout Q2_0 kernel the pool uses for Q2_0 down projections
|
||||
// (b2) the GGUF-layout Q2_0 kernel the pool uses for Q2_0 down projections - the AVX-512 one
|
||||
// where the CPU has it, the AVX-2 one (q2_avx2.cpp) where it does not. Calling the AVX-512
|
||||
// kernel unconditionally faults on a Zen 2/3 CPU.
|
||||
std::vector<cpu::ActQ> a2(NT);
|
||||
const cpu::ActQ* ap[NT];
|
||||
std::vector<float> alt((size_t) NT * H);
|
||||
float* altp[NT];
|
||||
for (int k = 0; k < NT; ++k) {
|
||||
cpu::act_quant_q8_1(ff[k].data(), (int) FF, a2[k]);
|
||||
cpu::act_quant_any(ff[k].data(), (int) FF, a2[k]); // gated: AVX-512 or AVX-2 per CPU
|
||||
ap[k] = &a2[k];
|
||||
altp[k] = alt.data() + k * H;
|
||||
}
|
||||
cpu::q2_0_gguf_rows_multi(blob.data() + f.down_off, f.d_row, (int) (FF / 64), ap, NT, altp, 0, (int) H);
|
||||
std::printf(" q2_0 AVX-512 down vs ggml down: rel %.2e\n", rel(alt, got_c));
|
||||
cpu::q2_rows_any(blob.data() + f.down_off, f.d_row, (int) (FF / 64), ap, NT, altp, 0, (int) H);
|
||||
std::printf(" q2_0 %s down vs ggml down: rel %.2e\n",
|
||||
cpu::cpu_avx512_ok() ? "AVX-512" : "AVX-2", rel(alt, got_c));
|
||||
}
|
||||
if (f.d_type == 20) {
|
||||
// (b3) the IQ4_NL multi-token AVX-2 kernel the pool now uses for IQ4_NL down projections,
|
||||
// against ggml-cpu's single-token vec_dot on the SAME Q8_0 activations (h), plus timing.
|
||||
std::vector<float> alt((size_t) NT * H), ref((size_t) NT * H);
|
||||
float* altp[NT];
|
||||
for (int k = 0; k < NT; ++k) altp[k] = alt.data() + k * H;
|
||||
cpu::iq4nl256_down_rows(blob.data() + f.down_off, f.d_row, (int) FF, hp, NT, altp, 0, (int) H);
|
||||
const auto* tdc = ggml_get_type_traits_cpu((ggml_type) f.d_type);
|
||||
for (int k = 0; k < NT; ++k)
|
||||
for (int64_t r = 0; r < H; ++r)
|
||||
tdc->vec_dot((int) FF, ref.data() + k * H + r, 0,
|
||||
blob.data() + f.down_off + (size_t) r * f.d_row, 0, hp[k], 0, 1);
|
||||
std::printf(" iq4_nl AVX-2 multi-token down vs ggml down: rel %.2e\n", rel(alt, ref));
|
||||
const int it = 50;
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
for (int i = 0; i < it; ++i)
|
||||
cpu::iq4nl256_down_rows(blob.data() + f.down_off, f.d_row, (int) FF, hp, NT, altp, 0, (int) H);
|
||||
const auto t1 = std::chrono::steady_clock::now();
|
||||
for (int i = 0; i < it; ++i)
|
||||
for (int k = 0; k < NT; ++k)
|
||||
for (int64_t r = 0; r < H; ++r)
|
||||
tdc->vec_dot((int) FF, ref.data() + k * H + r, 0,
|
||||
blob.data() + f.down_off + (size_t) r * f.d_row, 0, hp[k], 0, 1);
|
||||
const auto t2 = std::chrono::steady_clock::now();
|
||||
const double usn = std::chrono::duration<double, std::micro>(t1 - t0).count() / it;
|
||||
const double usg = std::chrono::duration<double, std::micro>(t2 - t1).count() / it;
|
||||
std::printf(" down %d tokens one thread: avx2 %.0f us (%.2f GB/s) vs ggml %.0f us (%.2f GB/s)\n",
|
||||
NT, usn, (double) (f.bytes - f.down_off) / usn / 1e3, usg,
|
||||
(double) (f.bytes - f.down_off) / usg / 1e3);
|
||||
}
|
||||
}
|
||||
// (c) the GPU: one group holding the NT entries
|
||||
|
||||
@@ -44,6 +44,13 @@ bool Gemm::init_external(void* stream, uint16_t* scratch, int64_t scratch_elems,
|
||||
return true;
|
||||
}
|
||||
|
||||
void Gemm::rebind(uint16_t* scratch, int64_t scratch_elems, void* workspace, size_t ws_bytes) {
|
||||
scratch_ = scratch;
|
||||
scratch_elems_ = scratch_elems;
|
||||
workspace_ = workspace;
|
||||
cublasSetWorkspace((cublasHandle_t) handle_, workspace_, ws_bytes);
|
||||
}
|
||||
|
||||
bool Gemm::init(void* stream, int64_t scratch_elems, std::string& err) {
|
||||
cublasHandle_t h = nullptr;
|
||||
if (cublasCreate(&h) != CUBLAS_STATUS_SUCCESS) { err = "prefill gemm: cublasCreate failed"; return false; }
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// src/prefill/ggml_cuda_host.cu - prompt-speed plan step 2b: the host-side symbols of llama.cpp's ggml-cuda that its MMQ
|
||||
// and quantize code reference, for the MMQ kernels compiled into strata_mmq without the rest of ggml-cuda.cu.
|
||||
#include "common.cuh"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
[[noreturn]] void ggml_cuda_error(const char * stmt, const char * func, const char * file, int line, const char * msg) {
|
||||
std::fprintf(stderr, "ggml-cuda (strata mmq): %s: %s\n in %s at %s:%d\n", msg, stmt, func, file, line);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
int ggml_cuda_get_device() {
|
||||
int id = 0;
|
||||
CUDA_CHECK(cudaGetDevice(&id));
|
||||
return id;
|
||||
}
|
||||
|
||||
const ggml_cuda_device_info & ggml_cuda_info() {
|
||||
static ggml_cuda_device_info info = [] {
|
||||
ggml_cuda_device_info in = {};
|
||||
int n = 0;
|
||||
if (cudaGetDeviceCount(&n) != cudaSuccess) n = 0;
|
||||
n = n > GGML_CUDA_MAX_DEVICES ? GGML_CUDA_MAX_DEVICES : n;
|
||||
in.device_count = n;
|
||||
in.physical_device_count = n;
|
||||
for (int id = 0; id < n; ++id) {
|
||||
cudaDeviceProp prop;
|
||||
CUDA_CHECK(cudaGetDeviceProperties(&prop, id));
|
||||
auto & d = in.devices[id];
|
||||
d.cc = 100 * prop.major + 10 * prop.minor;
|
||||
d.nsm = prop.multiProcessorCount;
|
||||
d.smpb = prop.sharedMemPerBlock;
|
||||
d.smpbo = prop.sharedMemPerBlockOptin;
|
||||
d.integrated = prop.integrated != 0;
|
||||
d.vmm = false;
|
||||
d.total_vram = prop.totalGlobalMem;
|
||||
d.warp_size = prop.warpSize;
|
||||
d.supports_cooperative_launch = prop.cooperativeLaunch != 0;
|
||||
d.physical_device = id;
|
||||
d.physical_share_count = 1;
|
||||
d.virtual_index = 0;
|
||||
}
|
||||
return in;
|
||||
}();
|
||||
return info;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Buffers are kept and reused: MMQ asks for the same few sizes every launch (its stream-k fixup tiles).
|
||||
struct CachingPool : ggml_cuda_pool {
|
||||
struct Buf { void * p; size_t size; bool used; };
|
||||
std::vector<Buf> bufs;
|
||||
std::mutex mu;
|
||||
void * alloc(size_t size, size_t * actual_size) override {
|
||||
std::lock_guard<std::mutex> lk(mu);
|
||||
for (auto & b : bufs)
|
||||
if (!b.used && b.size >= size) { b.used = true; *actual_size = b.size; return b.p; }
|
||||
void * p = nullptr;
|
||||
CUDA_CHECK(cudaMalloc(&p, size));
|
||||
bufs.push_back({p, size, true});
|
||||
*actual_size = size;
|
||||
return p;
|
||||
}
|
||||
void free(void * ptr, size_t) override {
|
||||
std::lock_guard<std::mutex> lk(mu);
|
||||
for (auto & b : bufs)
|
||||
if (b.p == ptr) { b.used = false; return; }
|
||||
}
|
||||
~CachingPool() override {
|
||||
for (auto & b : bufs) cudaFree(b.p);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<ggml_cuda_pool> ggml_backend_cuda_context::new_pool_for_device(int, int) {
|
||||
return std::unique_ptr<ggml_cuda_pool>(new CachingPool());
|
||||
}
|
||||
|
||||
ggml_backend_cuda_context::~ggml_backend_cuda_context() {}
|
||||
@@ -0,0 +1,186 @@
|
||||
// src/prefill/moe_mmq.cu - see include/strata/prefill/moe_mmq.hpp. llama.cpp's MMQ (ggml-cuda, MIT) is compiled
|
||||
// from the pinned llama.cpp checkout the build already takes ggml from; src/prefill/ggml_cuda_host.cu supplies the
|
||||
// few host symbols of ggml-cuda.cu it references.
|
||||
#include "strata/prefill/moe_mmq.hpp"
|
||||
|
||||
#include "common.cuh"
|
||||
#include "mmq.cuh"
|
||||
#include "quantize.cuh"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace strata::prefill::mmq {
|
||||
namespace {
|
||||
|
||||
void ck(cudaError_t e, const char* what) {
|
||||
if (e != cudaSuccess) {
|
||||
std::fprintf(stderr, "prefill mmq: %s: %s\n", what, cudaGetErrorString(e));
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
int64_t pad512(int64_t n) { return (n + 511) / 512 * 512; }
|
||||
|
||||
__global__ void copy16_kernel(const uint4* __restrict__ a, int64_t na, const uint4* __restrict__ b, int64_t nb,
|
||||
uint4* __restrict__ ab_dst, const uint4* __restrict__ c, int64_t nc, uint4* __restrict__ c_dst) {
|
||||
const int64_t i = (int64_t) blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i < na) ab_dst[i] = a[i];
|
||||
else if (i < na + nb) ab_dst[i] = b[i - na];
|
||||
else if (i < na + nb + nc) c_dst[i - na - nb] = c[i - na - nb];
|
||||
}
|
||||
__global__ void copy1_kernel(const uint8_t* __restrict__ a, int64_t na, const uint8_t* __restrict__ b, int64_t nb,
|
||||
uint8_t* __restrict__ ab_dst, const uint8_t* __restrict__ c, int64_t nc, uint8_t* __restrict__ c_dst) {
|
||||
const int64_t i = (int64_t) blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i < na) ab_dst[i] = a[i];
|
||||
else if (i < na + nb) ab_dst[i] = b[i - na];
|
||||
else if (i < na + nb + nc) c_dst[i - na - nb] = c[i - na - nb];
|
||||
}
|
||||
|
||||
// Strata blob: gate/up codes [1280][640 B], down codes [2560][160 B], gate/up scales [1280][40] f16, down scales
|
||||
// [2560][10] f16 (the layout of prefill/kernels.cu's blob_dequant_kernel). A GGUF Q2_0 block is {f16 d; 16 code
|
||||
// bytes} with the same 2-bit codes in the same order, so a block is a scale and a 16-byte run of codes.
|
||||
__global__ void strata_q2_kernel(const uint8_t* __restrict__ blob, uint16_t* __restrict__ gu, uint16_t* __restrict__ dn) {
|
||||
constexpr size_t O_D_CODES = (size_t) 1280 * 640, O_GU_SC = O_D_CODES + (size_t) 2560 * 160,
|
||||
O_D_SC = O_GU_SC + (size_t) 1280 * 40 * 2;
|
||||
const int64_t i = (int64_t) blockIdx.x * blockDim.x + threadIdx.x; // one GGUF block
|
||||
const int64_t n_gu = 1280LL * 40, n_d = 2560LL * 10;
|
||||
const uint8_t* codes;
|
||||
const uint16_t* scale;
|
||||
uint16_t* out;
|
||||
if (i < n_gu) {
|
||||
const int64_t row = i / 40, b = i % 40;
|
||||
codes = blob + row * 640 + b * 16;
|
||||
scale = (const uint16_t*) (blob + O_GU_SC) + row * 40 + b;
|
||||
out = gu + i * 9;
|
||||
} else if (i < n_gu + n_d) {
|
||||
const int64_t j = i - n_gu, row = j / 10, b = j % 10;
|
||||
codes = blob + O_D_CODES + row * 160 + b * 16;
|
||||
scale = (const uint16_t*) (blob + O_D_SC) + row * 10 + b;
|
||||
out = dn + j * 9;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
const uint4 q = *(const uint4*) codes;
|
||||
const uint16_t* qh = (const uint16_t*) &q;
|
||||
out[0] = *scale;
|
||||
#pragma unroll
|
||||
for (int k = 0; k < 8; ++k) out[1 + k] = qh[k];
|
||||
}
|
||||
|
||||
__global__ void swiglu_kernel(const float* __restrict__ gu, float* __restrict__ h, int64_t rows, int64_t n_ff,
|
||||
bool interleaved) {
|
||||
const int64_t i = (int64_t) blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= rows * n_ff) return;
|
||||
const int64_t r = i / n_ff, k = i % n_ff;
|
||||
const float* row = gu + r * 2 * n_ff;
|
||||
const float g = interleaved ? row[2 * k] : row[k], u = interleaved ? row[2 * k + 1] : row[n_ff + k];
|
||||
h[i] = g / (1.0f + __expf(-g)) * u;
|
||||
}
|
||||
|
||||
__global__ void iota_kernel(int32_t* dst, int64_t n) {
|
||||
const int64_t i = (int64_t) blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i < n) dst[i] = (int32_t) i;
|
||||
}
|
||||
|
||||
unsigned blocks(int64_t n) { return (unsigned) ((n + 255) / 256); }
|
||||
|
||||
} // namespace
|
||||
|
||||
bool built() { return true; }
|
||||
|
||||
bool supported(int t) {
|
||||
switch ((ggml_type) t) {
|
||||
case GGML_TYPE_Q2_0: case GGML_TYPE_IQ2_XXS: case GGML_TYPE_IQ2_XS: case GGML_TYPE_IQ2_S:
|
||||
case GGML_TYPE_IQ3_XXS: case GGML_TYPE_IQ3_S: case GGML_TYPE_IQ4_NL: case GGML_TYPE_IQ4_XS:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
size_t q8_bytes(int64_t rows, int64_t cols) {
|
||||
return (size_t) rows * (size_t) pad512(cols) * sizeof(block_q8_1_mmq) / (4 * QK8_1) + 128 * sizeof(block_q8_1_mmq);
|
||||
}
|
||||
|
||||
void quantize(const float* x, const int32_t* ids, void* xq, int t, int64_t cols, int64_t ld, int64_t rows, void* stream) {
|
||||
if (rows <= 0) return;
|
||||
quantize_mmq_q8_1_cuda(x, ids, xq, (ggml_type) t, cols, ld, rows * ld, rows * ld, pad512(cols), rows, 1, 1,
|
||||
(cudaStream_t) stream);
|
||||
ck(cudaGetLastError(), "quantize");
|
||||
}
|
||||
|
||||
Context::Context() {
|
||||
int dev = 0;
|
||||
cudaGetDevice(&dev);
|
||||
ctx_ = new ggml_backend_cuda_context(dev);
|
||||
}
|
||||
Context::~Context() { delete (ggml_backend_cuda_context*) ctx_; }
|
||||
|
||||
void Context::run(const Product& p, void* stream) {
|
||||
if (p.n <= 0 || p.max_rows <= 0) return;
|
||||
const ggml_type t = (ggml_type) p.type;
|
||||
const int64_t qk = ggml_blck_size(t), bpr = p.w_cols / qk;
|
||||
const mmq_args a = {(const char*) p.w, t, (const int*) p.xq, p.ids, p.bounds, p.dst, nullptr,
|
||||
p.w_cols, p.w_rows, p.total_rows, bpr, p.total_rows, p.ld_dst,
|
||||
p.n, p.n, (int64_t) (p.expert_bytes / ggml_type_size(t)), 0, 0,
|
||||
1, 1, 0, 0, 0,
|
||||
p.max_rows, p.max_rows};
|
||||
auto& ctx = *(ggml_backend_cuda_context*) ctx_;
|
||||
const cudaStream_t s = (cudaStream_t) stream;
|
||||
switch (t) {
|
||||
case GGML_TYPE_Q2_0: mul_mat_q_case<GGML_TYPE_Q2_0>(ctx, a, s); break;
|
||||
case GGML_TYPE_IQ2_XXS: mul_mat_q_case<GGML_TYPE_IQ2_XXS>(ctx, a, s); break;
|
||||
case GGML_TYPE_IQ2_XS: mul_mat_q_case<GGML_TYPE_IQ2_XS>(ctx, a, s); break;
|
||||
case GGML_TYPE_IQ2_S: mul_mat_q_case<GGML_TYPE_IQ2_S>(ctx, a, s); break;
|
||||
case GGML_TYPE_IQ3_XXS: mul_mat_q_case<GGML_TYPE_IQ3_XXS>(ctx, a, s); break;
|
||||
case GGML_TYPE_IQ3_S: mul_mat_q_case<GGML_TYPE_IQ3_S>(ctx, a, s); break;
|
||||
case GGML_TYPE_IQ4_NL: mul_mat_q_case<GGML_TYPE_IQ4_NL>(ctx, a, s); break;
|
||||
case GGML_TYPE_IQ4_XS: mul_mat_q_case<GGML_TYPE_IQ4_XS>(ctx, a, s); break;
|
||||
default:
|
||||
std::fprintf(stderr, "prefill mmq: type %d is not covered\n", (int) t);
|
||||
std::exit(1);
|
||||
}
|
||||
ck(cudaGetLastError(), "mul_mat_q");
|
||||
}
|
||||
|
||||
void gather_native(const void* gate, const void* up, size_t gu_half_bytes, const void* down, size_t d_bytes,
|
||||
void* gu_dst, void* d_dst, void* stream) {
|
||||
const cudaStream_t s = (cudaStream_t) stream;
|
||||
const bool a16 = ((uintptr_t) gate | (uintptr_t) up | (uintptr_t) down | (uintptr_t) gu_dst | (uintptr_t) d_dst |
|
||||
gu_half_bytes | d_bytes) % 16 == 0;
|
||||
if (a16) {
|
||||
const int64_t na = (int64_t) gu_half_bytes / 16, nc = (int64_t) d_bytes / 16;
|
||||
copy16_kernel<<<blocks(2 * na + nc), 256, 0, s>>>((const uint4*) gate, na, (const uint4*) up, na, (uint4*) gu_dst,
|
||||
(const uint4*) down, nc, (uint4*) d_dst);
|
||||
} else {
|
||||
const int64_t na = (int64_t) gu_half_bytes, nc = (int64_t) d_bytes;
|
||||
copy1_kernel<<<blocks(2 * na + nc), 256, 0, s>>>((const uint8_t*) gate, na, (const uint8_t*) up, na,
|
||||
(uint8_t*) gu_dst, (const uint8_t*) down, nc, (uint8_t*) d_dst);
|
||||
}
|
||||
ck(cudaGetLastError(), "gather_native");
|
||||
}
|
||||
|
||||
void gather_strata_q2(const uint8_t* blob, void* gu_dst, void* d_dst, void* stream) {
|
||||
strata_q2_kernel<<<blocks(1280LL * 40 + 2560LL * 10), 256, 0, (cudaStream_t) stream>>>(blob, (uint16_t*) gu_dst,
|
||||
(uint16_t*) d_dst);
|
||||
ck(cudaGetLastError(), "gather_strata_q2");
|
||||
}
|
||||
|
||||
void swiglu(const float* gu, float* h, int64_t rows, int64_t n_ff, bool interleaved, void* stream) {
|
||||
if (rows <= 0) return;
|
||||
swiglu_kernel<<<blocks(rows * n_ff), 256, 0, (cudaStream_t) stream>>>(gu, h, rows, n_ff, interleaved);
|
||||
ck(cudaGetLastError(), "swiglu");
|
||||
}
|
||||
|
||||
void iota(int32_t* dst, int64_t n, void* stream) {
|
||||
if (n <= 0) return;
|
||||
iota_kernel<<<blocks(n), 256, 0, (cudaStream_t) stream>>>(dst, n);
|
||||
ck(cudaGetLastError(), "iota");
|
||||
}
|
||||
|
||||
} // namespace strata::prefill::mmq
|
||||
+835
-88
File diff suppressed because it is too large.
Load diff
+548
-94
@@ -34,6 +34,7 @@
|
||||
#include "strata/kernels/native_qsa_indexer.hpp"
|
||||
#include "strata/kernels/native_rope.hpp"
|
||||
#include "strata/kernels/mrope.hpp"
|
||||
#include "strata/kernels/kv_q4.hpp"
|
||||
#include "strata/kernels/qsa.hpp"
|
||||
#include "strata/core/native_head.hpp"
|
||||
#include "strata/core/verify.hpp"
|
||||
@@ -43,6 +44,20 @@
|
||||
#include "strata/program/logits_selection.hpp"
|
||||
#include "strata/spec/draft_policy.hpp"
|
||||
#include "strata/spec/suffix_drafter.hpp"
|
||||
#include "strata/kernels/cvec.hpp"
|
||||
#include "strata/core/progress.hpp"
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX // gguf_reader.hpp includes windows.h
|
||||
#endif
|
||||
#include "strata/artifact/gguf_reader.hpp"
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#include <psapi.h>
|
||||
#include <io.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <cerrno>
|
||||
#endif
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
@@ -199,6 +214,9 @@ struct Options {
|
||||
int vram_reserve_mib = 700;
|
||||
/// Plan v0.3 P5: batched prompt processing in chunks of this many tokens (0 = the token path).
|
||||
int64_t prefill_chunk = 0;
|
||||
/// `--prefill auto`: the largest chunk (up to 8192) whose buffers the expert cache can lend. Every expert a chunk
|
||||
/// routes to is streamed once per chunk, so a bigger chunk streams fewer bytes per token (the "ubatch" effect).
|
||||
bool prefill_auto = false;
|
||||
bool no_split_rows = false; ///< plan v0.3 P4 A/B: one whole expert per pool thread
|
||||
/// Plan v0.3 P5: the prompt path borrows the top expert-cache slots for its buffers and refills them after
|
||||
/// the prompt (default); `--no-prefill-borrow` reserves the buffers' VRAM for the whole session instead.
|
||||
@@ -258,6 +276,13 @@ struct Options {
|
||||
int suffix_draft = 3;
|
||||
/// The MTP's own window cap (0 = --spec): with --spec 6 --mtp-max-t 4 the long windows come from suffix matches.
|
||||
int mtp_max_t = 0;
|
||||
/// A control vector on the residual stream (strata/kernels/cvec.hpp), with llama.cpp's flags: the
|
||||
/// `experimental-speed-projection` profile passes `--control-vector-scaled FILE:1.0 --control-vector-layer-range
|
||||
/// 4 44 --cvec-mode project --cvec-dir per-layer`. None by default; --serve switches a loaded one per request.
|
||||
std::vector<std::pair<std::string, float>> cvec_files;
|
||||
int cvec_first = -1, cvec_last = -1; ///< llama.cpp's defaults: 1 .. the last layer
|
||||
int cvec_mode = 1; ///< 0 = project, 1 = add (llama.cpp's default)
|
||||
int cvec_single = -1; ///< --cvec-dir single:L (project mode): layer L's direction everywhere
|
||||
};
|
||||
|
||||
void usage() {
|
||||
@@ -277,6 +302,8 @@ void usage() {
|
||||
" --ple-sync-submit A/B arm: submit table reads on the token thread (default: an I/O thread)\n"
|
||||
" --kv fp16|int8 KV storage (plan v0.3 P7): int8 codes + fp16 scale per 64 values, half the\n"
|
||||
" VRAM; default fp16 until gate G-C accepts int8\n"
|
||||
" --kv q4_0 4-bit K/V after a Hadamard rotation (PR #21): half of int8's memory,\n"
|
||||
" slightly lower precision (see bench/results/2026-09-27-kv-q4)\n"
|
||||
" --kv-resident N KV streaming: keep N cells of each QSA layer in VRAM (min 20480) and the\n"
|
||||
" whole K/V in pinned RAM; the freed VRAM goes to expert slots. 0 (default):\n"
|
||||
" all of it in VRAM. A context of N cells or fewer is not streamed\n"
|
||||
@@ -327,9 +354,15 @@ void usage() {
|
||||
" --suffix-draft N prompt lookup: draft from an earlier repeat of the last N+ tokens of context\n"
|
||||
" when it pays (default 3; 0 = MTP only)\n"
|
||||
" --mtp-max-t M cap the MTP's windows at M tokens (0 = --spec; longer ones come from suffixes)\n"
|
||||
" --control-vector-scaled FILE:SCALE[,...] a control vector GGUF on the residual stream (llama.cpp's\n"
|
||||
" format; --control-vector FILE = scale 1). --serve: requests switch it (cvec=0|1)\n"
|
||||
" --control-vector-layer-range A B the layers it follows (inclusive; default 1 .. the last)\n"
|
||||
" --cvec-mode add|project h += s v (default) or h -= s (h.v) v with v unit\n"
|
||||
" --cvec-dir per-layer|single:L each layer's own direction (default) or layer L's everywhere (project)\n"
|
||||
" --no-token-graph A/B: two graphs per layer (the host launches each) instead of one per token\n"
|
||||
" --no-fused-gr A/B: the six-kernel hyper-connection read and a separate write (native)\n"
|
||||
" --prefill CHUNK batched prompt processing in chunks of CHUNK tokens (needs --native)\n"
|
||||
" --prefill CHUNK batched prompt processing in chunks of CHUNK tokens (needs --native); auto =\n"
|
||||
" the largest chunk up to 8192 whose buffers the expert cache can lend\n"
|
||||
" --no-pool skip the CPU expert pool (the GPU-only floor)\n"
|
||||
" --sync-every-layer debug: synchronise after every layer\n"
|
||||
" --ple-gguf PATH the n-gram/PLE shard. WITHOUT IT LAYER 1's PLE IS SILENTLY SKIPPED,\n"
|
||||
@@ -461,6 +494,96 @@ void drive_pool_multi(void* user, const float* x_f, const int32_t* ids, int64_t
|
||||
++t->calls;
|
||||
}
|
||||
|
||||
// ---- issue #31: what the watchdog prints before it stops a stalled engine
|
||||
struct MemSample {
|
||||
unsigned long long faults = 0, rss_mib = 0, avail_mib = 0, commit_mib = 0; // faults: hard (Linux) / all (Windows)
|
||||
};
|
||||
MemSample mem_sample() {
|
||||
MemSample m;
|
||||
#if defined(_WIN32)
|
||||
PROCESS_MEMORY_COUNTERS pmc{};
|
||||
if (K32GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof pmc)) {
|
||||
m.faults = pmc.PageFaultCount;
|
||||
m.rss_mib = pmc.WorkingSetSize >> 20;
|
||||
m.commit_mib = pmc.PagefileUsage >> 20;
|
||||
}
|
||||
MEMORYSTATUSEX ms{};
|
||||
ms.dwLength = sizeof ms;
|
||||
if (GlobalMemoryStatusEx(&ms)) m.avail_mib = ms.ullAvailPhys >> 20;
|
||||
#else
|
||||
if (std::FILE* f = std::fopen("/proc/self/stat", "r")) {
|
||||
char buf[4096];
|
||||
const size_t n = std::fread(buf, 1, sizeof buf - 1, f);
|
||||
buf[n] = 0;
|
||||
std::fclose(f);
|
||||
const char* s = std::strrchr(buf, ')'); // fields after the command name: 3 state ... 12 majflt
|
||||
for (int field = 2; s && field < 12; ++field) s = std::strchr(s + 1, ' ');
|
||||
if (s) m.faults = std::strtoull(s + 1, nullptr, 10);
|
||||
}
|
||||
auto kb = [](const char* path, const char* key) -> unsigned long long {
|
||||
unsigned long long v = 0;
|
||||
if (std::FILE* f = std::fopen(path, "r")) {
|
||||
char line[256];
|
||||
const size_t kl = std::strlen(key);
|
||||
while (std::fgets(line, sizeof line, f))
|
||||
if (std::strncmp(line, key, kl) == 0) { v = std::strtoull(line + kl, nullptr, 10); break; }
|
||||
std::fclose(f);
|
||||
}
|
||||
return v;
|
||||
};
|
||||
m.rss_mib = kb("/proc/self/status", "VmRSS:") >> 10;
|
||||
m.commit_mib = kb("/proc/self/status", "VmSwap:") >> 10;
|
||||
m.avail_mib = kb("/proc/meminfo", "MemAvailable:") >> 10;
|
||||
#endif
|
||||
return m;
|
||||
}
|
||||
|
||||
void stall_report(std::FILE* f, uint64_t layers_during) {
|
||||
strata::core::Progress& p = strata::core::progress();
|
||||
std::fprintf(f, "strata serve: stall report (engine %s): stage \"%s %lld\" for %lld s; %llu layers served since the "
|
||||
"last finished step (0 = stopped, more = slow)\n", STRATA_VERSION, p.where.load(),
|
||||
(long long) p.detail.load(), (long long) ((strata::core::progress_now_ms() - p.since_ms.load()) / 1000),
|
||||
(unsigned long long) layers_during);
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
if (pass == 1) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
std::fprintf(f, " 2 s later:\n");
|
||||
}
|
||||
if (auto fn = strata::core::diag_pool_fn().load()) fn(f);
|
||||
if (auto fn = strata::core::diag_verify_fn().load()) fn(f);
|
||||
const MemSample m = mem_sample();
|
||||
std::fprintf(f, " memory: %llu MiB resident, %llu MiB %s, %llu MiB RAM available; %llu %s\n", m.rss_mib,
|
||||
m.commit_mib,
|
||||
#if defined(_WIN32)
|
||||
"committed", m.avail_mib, m.faults, "page faults so far"
|
||||
#else
|
||||
"in swap", m.avail_mib, m.faults, "major page faults so far"
|
||||
#endif
|
||||
);
|
||||
std::fflush(f);
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
// every thread's stack, to read against this build's PDB: a few MB beside the engine's working directory
|
||||
if (HMODULE dbg = LoadLibraryA("dbghelp.dll")) {
|
||||
using Fn = BOOL(WINAPI*)(HANDLE, DWORD, HANDLE, int, void*, void*, void*);
|
||||
if (auto write = (Fn) GetProcAddress(dbg, "MiniDumpWriteDump")) {
|
||||
char path[64];
|
||||
std::snprintf(path, sizeof path, "strata-stall-%lu.dmp", (unsigned long) GetCurrentProcessId());
|
||||
HANDLE h = CreateFileA(path, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (h != INVALID_HANDLE_VALUE) {
|
||||
const int kThreadInfo = 0x1000; // MiniDumpWithThreadInfo; MiniDumpNormal = 0
|
||||
const BOOL ok = write(GetCurrentProcess(), GetCurrentProcessId(), h, kThreadInfo, nullptr, nullptr, nullptr);
|
||||
CloseHandle(h);
|
||||
char full[MAX_PATH];
|
||||
if (!GetFullPathNameA(path, MAX_PATH, full, nullptr)) std::snprintf(full, sizeof full, "%s", path);
|
||||
std::fprintf(f, " %s the thread stacks to %s (attach it to the issue)\n", ok ? "wrote" : "could not write",
|
||||
full);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// STRATA_TRACE=1: the VRAM left at a step of the startup (finds what fills the card after the cache is sized)
|
||||
void mem_mark(const char* where) {
|
||||
static const bool on = std::getenv("STRATA_TRACE") != nullptr;
|
||||
@@ -555,6 +678,80 @@ bool checkpoint_restore(const ConvCheckpoint& c, strata::core::SessionState& ss,
|
||||
return cudaDeviceSynchronize() == cudaSuccess;
|
||||
}
|
||||
|
||||
// --control-vector-scaled: llama.cpp's `common_control_vector_load` (every file's `direction.<l>` times its scale,
|
||||
// summed; layer 0 has none) and `llama_adapter_cvec::apply` with the projection-mode patch (project: the unit
|
||||
// direction and its norm as the scale), into the tables `cvec_upload` takes. `summary` is what INFO reports.
|
||||
bool load_control_vectors(const Options& o, const strata::core::ModelGeometry& g, std::string& summary, std::string& err) {
|
||||
const int64_t L = g.n_layers, N = g.n_embd;
|
||||
std::vector<float> data((size_t) (L * N), 0.0f);
|
||||
std::vector<bool> have((size_t) L, false);
|
||||
for (const auto& [path, scale] : o.cvec_files) {
|
||||
try {
|
||||
strata::GgufFile f(path);
|
||||
const strata::MetaValue* arch = f.get("general.architecture");
|
||||
if (arch == nullptr || arch->s != "controlvector") {
|
||||
err = path + ": not a control vector GGUF (general.architecture is not 'controlvector')";
|
||||
return false;
|
||||
}
|
||||
const strata::MetaValue* hint = f.get("controlvector.model_hint");
|
||||
if (hint != nullptr && hint->s != "qwen4exp")
|
||||
std::fprintf(stderr, "strata generate: %s was made for '%s', not qwen4exp\n", path.c_str(), hint->s.c_str());
|
||||
int found = 0;
|
||||
for (const strata::TensorInfo& t : f.tensors()) {
|
||||
if (t.name.rfind("direction.", 0) != 0) continue;
|
||||
const long l = std::strtol(t.name.c_str() + 10, nullptr, 10);
|
||||
if (l < 1 || l >= L) continue; // layer 0 has no vector; past the model is ignored, as in llama.cpp
|
||||
if (t.type != 0 || t.elements() != (uint64_t) N) {
|
||||
err = path + ": " + t.name + " must be " + std::to_string((long long) N) + " f32";
|
||||
return false;
|
||||
}
|
||||
const float* src = reinterpret_cast<const float*>(f.tensor_data(t));
|
||||
for (int64_t j = 0; j < N; ++j) data[(size_t) (l * N + j)] += scale * src[j];
|
||||
have[(size_t) l] = true;
|
||||
++found;
|
||||
}
|
||||
if (found == 0) { err = path + ": no direction.<layer> tensors"; return false; }
|
||||
} catch (const std::exception& e) {
|
||||
err = e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const int first = o.cvec_first <= 0 ? 1 : o.cvec_first;
|
||||
const int last = (o.cvec_last <= 0 || o.cvec_last >= L) ? (int) L - 1 : o.cvec_last;
|
||||
const int single = o.cvec_mode == 0 ? o.cvec_single : -1;
|
||||
if (single >= 0 && (single >= L || !have[(size_t) single])) {
|
||||
err = "--cvec-dir single:" + std::to_string(single) + ": the vector has no direction for that layer";
|
||||
return false;
|
||||
}
|
||||
std::vector<float> dir((size_t) (L * N), 0.0f), s((size_t) L, 0.0f);
|
||||
int steered = 0;
|
||||
for (int64_t l = first; l <= last; ++l) {
|
||||
const int64_t src = single >= 0 ? single : l;
|
||||
if (!have[(size_t) src]) continue;
|
||||
const float* d = data.data() + (size_t) (src * N);
|
||||
if (o.cvec_mode == 0) {
|
||||
double nrm = 0.0;
|
||||
for (int64_t j = 0; j < N; ++j) nrm += (double) d[j] * d[j];
|
||||
nrm = std::sqrt(nrm);
|
||||
if (nrm <= 0.0) continue;
|
||||
s[(size_t) l] = (float) nrm;
|
||||
for (int64_t j = 0; j < N; ++j) dir[(size_t) (l * N + j)] = (float) (d[j] / nrm);
|
||||
} else {
|
||||
s[(size_t) l] = 1.0f;
|
||||
std::copy(d, d + N, dir.begin() + (size_t) (l * N));
|
||||
}
|
||||
++steered;
|
||||
}
|
||||
if (steered == 0) { err = "the control vector has no direction in layers " + std::to_string(first) + ".." + std::to_string(last); return false; }
|
||||
if (!strata::kernels::cvec_upload(dir, s, o.cvec_mode, first, last, N, g.hc, err)) return false;
|
||||
summary = std::string(o.cvec_mode == 0 ? "project" : "add") + ":" + std::to_string(first) + "-" + std::to_string(last) +
|
||||
(single >= 0 ? ":single" + std::to_string(single) : "");
|
||||
// the line llama.cpp's patched build prints, so a log shows the same thing
|
||||
std::fprintf(stderr, "strata generate: control vector mode = %s, dir = %s, layers %d..%d (%d steered)\n",
|
||||
o.cvec_mode == 0 ? "project" : "add", single >= 0 ? "single" : "per-layer", first, last, steered);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
@@ -562,6 +759,16 @@ int main(int argc, char** argv) {
|
||||
// pipe or a file is block-buffered, so a program that dies loses every line it had already printed - which
|
||||
// turns "it crashed at step 7" into "it crashed somewhere", and the difference is a debugging session.
|
||||
std::setvbuf(stdout, nullptr, _IONBF, 0);
|
||||
// Load every CUDA kernel when the context is created, before the expert cache takes the free VRAM. With the
|
||||
// default lazy loading, a kernel first used mid-prompt (MMQ for IQ3_XXS at 64K+ on a 12 GB card) found no VRAM
|
||||
// left for its code and the engine ended ("out of memory: cudaFuncSetAttribute"). Costs ~30 MB of VRAM.
|
||||
if (std::getenv("CUDA_MODULE_LOADING") == nullptr) {
|
||||
#if defined(_WIN32)
|
||||
_putenv_s("CUDA_MODULE_LOADING", "EAGER");
|
||||
#else
|
||||
setenv("CUDA_MODULE_LOADING", "EAGER", 0);
|
||||
#endif
|
||||
}
|
||||
Options o;
|
||||
bool have_tokens = false;
|
||||
bool have_logits_stride = false;
|
||||
@@ -653,7 +860,11 @@ int main(int argc, char** argv) {
|
||||
o.expert_cache = (v == "auto") ? -1 : std::atoi(v.c_str());
|
||||
}
|
||||
else if (a == "--vram-reserve-mib") o.vram_reserve_mib = std::atoi(next("--vram-reserve-mib"));
|
||||
else if (a == "--prefill") o.prefill_chunk = std::atoll(next("--prefill"));
|
||||
else if (a == "--prefill") {
|
||||
const std::string v = next("--prefill");
|
||||
o.prefill_auto = v == "auto";
|
||||
o.prefill_chunk = o.prefill_auto ? 8192 : std::atoll(v.c_str());
|
||||
}
|
||||
else if (a == "--no-split-rows") o.no_split_rows = true;
|
||||
else if (a == "--no-prefill-borrow") o.no_prefill_borrow = true;
|
||||
else if (a == "--prefill-until") o.prefill_until = std::atoll(next("--prefill-until"));
|
||||
@@ -677,6 +888,38 @@ int main(int argc, char** argv) {
|
||||
else if (a == "--short-read") o.short_read = std::max(0LL, std::atoll(next("--short-read")));
|
||||
else if (a == "--suffix-draft") o.suffix_draft = std::max(0, std::atoi(next("--suffix-draft")));
|
||||
else if (a == "--mtp-max-t") o.mtp_max_t = std::max(0, std::atoi(next("--mtp-max-t")));
|
||||
else if (a == "--control-vector") o.cvec_files.push_back({next("--control-vector"), 1.0f});
|
||||
else if (a == "--control-vector-scaled") {
|
||||
// FILE:SCALE, comma-separated; the LAST colon splits, so a Windows path (C:\...) keeps its drive
|
||||
std::stringstream list(next("--control-vector-scaled"));
|
||||
std::string item;
|
||||
while (std::getline(list, item, ',')) {
|
||||
const size_t colon = item.rfind(':');
|
||||
char* end = nullptr;
|
||||
const float sc = colon == std::string::npos ? 0.0f : std::strtof(item.c_str() + colon + 1, &end);
|
||||
if (colon == std::string::npos || colon == 0 || end == item.c_str() + colon + 1 || *end != '\0') {
|
||||
std::fprintf(stderr, "--control-vector-scaled: expected FILE:SCALE, got '%s'\n", item.c_str());
|
||||
return 2;
|
||||
}
|
||||
o.cvec_files.push_back({item.substr(0, colon), sc});
|
||||
}
|
||||
}
|
||||
else if (a == "--control-vector-layer-range") {
|
||||
o.cvec_first = std::atoi(next("--control-vector-layer-range"));
|
||||
o.cvec_last = std::atoi(next("--control-vector-layer-range"));
|
||||
}
|
||||
else if (a == "--cvec-mode") {
|
||||
const std::string m = next("--cvec-mode");
|
||||
if (m == "project") o.cvec_mode = 0;
|
||||
else if (m == "add") o.cvec_mode = 1;
|
||||
else { std::fprintf(stderr, "--cvec-mode: add or project, got '%s'\n", m.c_str()); return 2; }
|
||||
}
|
||||
else if (a == "--cvec-dir") {
|
||||
const std::string d = next("--cvec-dir");
|
||||
if (d == "per-layer") o.cvec_single = -1;
|
||||
else if (d.rfind("single:", 0) == 0) o.cvec_single = std::atoi(d.c_str() + 7);
|
||||
else { std::fprintf(stderr, "--cvec-dir: per-layer or single:L, got '%s'\n", d.c_str()); return 2; }
|
||||
}
|
||||
else if (a == "--no-spec-split") o.spec_split = false;
|
||||
else if (a == "--eos-ids") {
|
||||
std::string e;
|
||||
@@ -707,6 +950,10 @@ int main(int argc, char** argv) {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
if (o.prefill_auto && (o.no_prefill_borrow || o.expert_profile.empty())) {
|
||||
o.prefill_auto = false; // nothing to lend from: the buffers are reserved for the session, so keep them small
|
||||
o.prefill_chunk = 2048;
|
||||
}
|
||||
if (!have_tokens && o.serve) { // plan v0.3 P8: requests bring their own tokens
|
||||
o.tokens = {248045};
|
||||
o.max_new = 1;
|
||||
@@ -725,11 +972,13 @@ int main(int argc, char** argv) {
|
||||
std::fprintf(stderr, "strata generate: invalid --ple-io/--ple-row-cache/--ple-inflight/--ple-delay-us\n");
|
||||
return 2;
|
||||
}
|
||||
if (o.kv != "fp16" && o.kv != "int8") {
|
||||
std::fprintf(stderr, "strata generate: --kv must be fp16 or int8\n");
|
||||
if (o.kv == "q4") o.kv = "q4_0";
|
||||
if (o.kv != "fp16" && o.kv != "int8" && o.kv != "q4_0") {
|
||||
std::fprintf(stderr, "strata generate: --kv must be fp16, int8 or q4_0\n");
|
||||
return 2;
|
||||
}
|
||||
strata::core::qsa_set_kv_int8(o.kv == "int8");
|
||||
strata::core::qsa_set_kv_q4(o.kv == "q4_0"); // PR #21: 4-bit codes after a Hadamard rotation (kv_q4.hpp)
|
||||
if (o.kv_resident < 0) {
|
||||
std::fprintf(stderr, "strata generate: --kv-resident must be >= 0\n");
|
||||
return 2;
|
||||
@@ -839,7 +1088,9 @@ int main(int argc, char** argv) {
|
||||
// the canonical Q2_0 pack's CPU kernels are AVX-512 only; a native pack runs on AVX2 CPUs as well
|
||||
if (!native_pack) strata::kernels::cpu::cpu_require_expert_support();
|
||||
else if (!strata::kernels::cpu::cpu_avx512_ok())
|
||||
std::fprintf(stderr, "strata generate: this CPU has no AVX-512: the expert kernels run on AVX2\n");
|
||||
std::fprintf(stderr, "strata generate: this CPU has no AVX-512: the expert kernels run on %s "
|
||||
"(multi-token for the i-quant gate/up rows)\n",
|
||||
std::getenv("STRATA_NO_IQ256") == nullptr ? "AVX-2" : "ggml-cpu vec_dot (STRATA_NO_IQ256 set)");
|
||||
strata::core::NativeEmbed native_embed;
|
||||
if (native_pack) {
|
||||
if (o.native_preset.empty() || o.spec < 2 || o.keep_canonical ||
|
||||
@@ -943,6 +1194,15 @@ int main(int argc, char** argv) {
|
||||
strata::kernels::ple_set_native_postops(o.native_ple_postops);
|
||||
const strata::core::ModelGeometry g;
|
||||
const int64_t K = 10;
|
||||
// before session_init: every graph captured from here on has the vector's kernels where it applies
|
||||
std::string cvec_summary = "0";
|
||||
if (!o.cvec_files.empty()) {
|
||||
std::string ce;
|
||||
if (!load_control_vectors(o, g, cvec_summary, ce)) {
|
||||
std::fprintf(stderr, "strata generate: control vector: %s\n", ce.c_str());
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
if (o.max_context < (int64_t) o.tokens.size() + o.max_new) {
|
||||
std::fprintf(stderr, "strata generate: --max-context %lld cannot hold %zu prompt + %lld new tokens\n",
|
||||
(long long) o.max_context, o.tokens.size(), (long long) o.max_new);
|
||||
@@ -1161,13 +1421,35 @@ int main(int argc, char** argv) {
|
||||
std::fprintf(stderr, "strata generate: %s\n", err.c_str());
|
||||
return 1;
|
||||
}
|
||||
// The profile knows how many slots it was built for. `--expert-cache 0` means "take the profile's";
|
||||
// an explicit smaller number is allowed and simply truncates the ranked list, which is the right
|
||||
// behaviour for asking "what would 2,000 slots give" without rebuilding the file.
|
||||
if (o.expert_cache == 0) o.expert_cache = (int) pslots;
|
||||
// An explicit number truncates the ranked list ("what would 2,000 slots give" without rebuilding the
|
||||
// file). `--expert-cache 0` used to take the count the profile was built for; the profile now ranks
|
||||
// every pair (issue #46: a card that holds more than the old 8,000 used to stop there), so it means auto.
|
||||
if (o.expert_cache == 0) o.expert_cache = -1;
|
||||
std::fprintf(stderr, "strata generate: profile %s: %zu ranked pairs, built for %lld slots\n",
|
||||
o.expert_profile.c_str(), profile.size(), (long long) pslots);
|
||||
}
|
||||
// THE HEAD BEFORE THE CACHE. The expert cache takes what is free minus the reserve, so everything allocated
|
||||
// after it comes out of the reserve. The native head (~0.5 GB with IQ3_S) was loaded after it and ate most of
|
||||
// the 700 MiB: 128K IQ3_S ended with 30 MiB free, the driver paged, and a request stalled for good at its first
|
||||
// verify window. Loaded first, the cache is sized around it.
|
||||
const strata::core::WeightRef* wo = wt.find("output.weight");
|
||||
if (wo == nullptr) { std::fprintf(stderr, "strata generate: output.weight is missing\n"); return 1; }
|
||||
const int64_t n_vocab = wo->ne1;
|
||||
strata::core::NativeHead native_head;
|
||||
if (!o.native_head_gguf.empty()) {
|
||||
if (!native_head.load(o.native_head_gguf, g.n_embd, n_vocab, err)) {
|
||||
std::fprintf(stderr, "strata generate: %s\n", err.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::fprintf(stderr, "strata generate: experimental native Q5_K head, %llu bytes\n",
|
||||
(unsigned long long) native_head.weight_bytes());
|
||||
}
|
||||
std::vector<float> logits((size_t) n_vocab);
|
||||
float* d_logits = nullptr;
|
||||
if (cudaMalloc(&d_logits, (size_t) n_vocab * 4) != cudaSuccess) {
|
||||
std::fprintf(stderr, "strata generate: the logits buffer failed\n");
|
||||
return 1;
|
||||
}
|
||||
const bool auto_cache = o.expert_cache < 0;
|
||||
if (o.expert_cache < 0) {
|
||||
size_t free_b = 0, total_b = 0;
|
||||
@@ -1250,19 +1532,16 @@ int main(int argc, char** argv) {
|
||||
(long long) xcache.slots(), xcache.gib());
|
||||
mem_mark("opening the expert cache");
|
||||
xcache.set_per_layer_admission(o.expert_cache_per_layer);
|
||||
// **ROUND 328: THE HIT PATH IS PROVABLY WRONG, AND THIS SAYS SO OUT LOUD RATHER THAN LETTING IT
|
||||
// CORRUPT A RUN QUIETLY.** With the cache on, the generated tokens DIVERGE from the cache-off run:
|
||||
// at 256 global slots (2.97% hits) the first difference is at **token 40**; at 4096 per-layer slots
|
||||
// (54.4% hits) it is at **token 0**. The cache-off run is deterministic across repeated runs, so
|
||||
// this is a real fault in `moe_hit_grouped_s2`'s inputs or the fill - not noise. It also explains
|
||||
// what R4 recorded as "a better profile makes the token worse": more hits means more wrong rows, so
|
||||
// the payoff is non-monotone BY CONSTRUCTION rather than by any memory-system effect.
|
||||
// The cache stays opt-in and this warning is not a refusal, because the divergence IS the diagnostic.
|
||||
// Round 328 warned here that the GPU hit path was wrong (tokens diverged from a cache-off run from
|
||||
// token 0). That fault was fixed long since (native_expert_parity, expert_parity, the grouped kernels'
|
||||
// tests), and the warning outlived it (issue #23). What remains is rounding: a GPU expert and the CPU's
|
||||
// compute the same quantized expert with different float order, so a near-tie can flip. Measured teacher-
|
||||
// forced on 2,557 tokens (bench/results/2026-09-27-cache-parity): 95-98% same top-1, and perplexity equal
|
||||
// (on - off = -0.005 +- 0.005 nats). Neither output is more correct than the other.
|
||||
std::fprintf(stderr,
|
||||
"strata generate: *** WARNING: --expert-cache is enabled and the GPU hit path is NOT\n"
|
||||
" CORRECT. The generated tokens diverge from a cache-off run (measured:\n"
|
||||
" first difference at token 40 at 2.97%% hits, token 0 at 54.4%%). Any\n"
|
||||
" timing from this run is real; any OUTPUT from it is not. ***\n");
|
||||
"strata generate: the GPU computes the experts in the cache; it rounds differently from the CPU,\n"
|
||||
" so a reply can differ slightly from a run without the cache (same quality:\n"
|
||||
" bench/results/2026-09-27-cache-parity).\n");
|
||||
if (o.expert_cache_per_layer) {
|
||||
int64_t lo = 0, hi = 0;
|
||||
xcache.layer_slot_range(0, lo, hi);
|
||||
@@ -1479,25 +1758,7 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
|
||||
mem_mark("the expert cache and the graphs");
|
||||
std::fprintf(stderr, "strata generate: session is up; locating the head\n");
|
||||
const strata::core::WeightRef* wo = wt.find("output.weight");
|
||||
if (wo == nullptr) { std::fprintf(stderr, "strata generate: output.weight is missing\n"); return 1; }
|
||||
const int64_t n_vocab = wo->ne1;
|
||||
strata::core::NativeHead native_head;
|
||||
if (!o.native_head_gguf.empty()) {
|
||||
if (!native_head.load(o.native_head_gguf, g.n_embd, n_vocab, err)) {
|
||||
std::fprintf(stderr, "strata generate: %s\n", err.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::fprintf(stderr, "strata generate: experimental native Q5_K head, %llu bytes\n",
|
||||
(unsigned long long) native_head.weight_bytes());
|
||||
}
|
||||
std::vector<float> logits((size_t) n_vocab);
|
||||
float* d_logits = nullptr;
|
||||
if (cudaMalloc(&d_logits, (size_t) n_vocab * 4) != cudaSuccess) {
|
||||
std::fprintf(stderr, "strata generate: the logits buffer failed\n");
|
||||
return 1;
|
||||
}
|
||||
std::fprintf(stderr, "strata generate: session is up (engine %s)\n", STRATA_VERSION);
|
||||
auto run_head = [&](void* stream) -> bool {
|
||||
if (!native_head.loaded())
|
||||
return strata::core::lm_head(wt, g, ss.block, d_logits, stream, err);
|
||||
@@ -1957,6 +2218,65 @@ int main(int argc, char** argv) {
|
||||
// from the live session or the longest conversation checkpoint its prompt starts with (see ConvCheckpoint),
|
||||
// otherwise from an empty sequence (`session_zero`); the rest of the prompt goes through the batched prompt path
|
||||
// and its last token through the first verify window - the path all three model files share. Decoding is greedy.
|
||||
// The expert-cache slots (from the end of the cache) that hold the prompt path's buffers for a chunk, and the
|
||||
// bytes from the first of them to the end.
|
||||
// the share of expert bytes the arena could pin (sizes the prompt path's streamed ring and its lend cap)
|
||||
if (srcp != nullptr && o.prefill_chunk > 0) {
|
||||
uint64_t pinned = 0, total = 0;
|
||||
const auto& lay = strata::kernels::cpu::expert_layout();
|
||||
for (int64_t l = 0; l < g.n_layers; ++l)
|
||||
for (int64_t e = 0; e < g.n_expert; ++e) {
|
||||
const uint64_t b = lay.blob_bytes(l);
|
||||
total += b;
|
||||
if (srcp->pinned(l, e)) pinned += b;
|
||||
}
|
||||
strata::prefill::Prefill::set_pinned_share(total ? (double) pinned / (double) total : 1.0);
|
||||
}
|
||||
auto lend_slots = [&](int64_t c) -> int64_t {
|
||||
const uint64_t need = strata::prefill::Prefill::bytes_needed(g, ss, c);
|
||||
const int64_t blob = (int64_t) strata::kernels::cpu::expert_layout().max_blob;
|
||||
int64_t k = (int64_t) ((need + (uint64_t) blob - 1) / (uint64_t) blob);
|
||||
if (xcache.slot_offsets() != nullptr) { // sized slots: take slots from the end until they hold `need`
|
||||
k = 0;
|
||||
while (k < xcache.slots() &&
|
||||
(uint64_t) (xcache.bytes() - (int64_t) xcache.slot_offsets()[xcache.slots() - k]) < need) ++k;
|
||||
}
|
||||
return k;
|
||||
};
|
||||
auto lend_bytes = [&](int32_t first) -> uint64_t {
|
||||
return xcache.slot_offsets() ? (uint64_t) (xcache.bytes() - (int64_t) xcache.slot_offsets()[first])
|
||||
: (uint64_t) (xcache.slots() - first) *
|
||||
(uint64_t) strata::kernels::cpu::expert_layout().max_blob;
|
||||
};
|
||||
// The prompt path's chunk and the slots it borrows for its buffers: the requested chunk halved until it fits,
|
||||
// or with --prefill auto the largest of kAutoChunks whose buffers take at most kAutoLendPct % of the slots (a
|
||||
// lent slot's expert is streamed during the prompt and refilled after it; measured on a 12 GB card, 32K Q2_0
|
||||
// prompt: 4096 791 tok/s, 6144 878, 8192 973 with 69% of the slots lent). A request lends only what its own
|
||||
// prompt needs (Prefill::relayout), so a big chunk costs short prompts nothing. 0 = none fits.
|
||||
auto plan_lend = [&](int64_t& chunk) -> int64_t {
|
||||
static constexpr int64_t kAutoChunks[] = {8192, 6144, 4096, 3072, 2048, 1024, 512, 256};
|
||||
// at 8192-token chunks nearly every expert streams anyway, so a lent slot costs little: 90% when the
|
||||
// copies are DMA from pinned RAM (Q2_0 8192 + a 384-slot ring: 1283 tok/s), 85% when host copies are the
|
||||
// limit (lending more only streams more through them). STRATA_PREFILL_LEND_PCT overrides (tuning).
|
||||
const int64_t kAutoLendPct = [] {
|
||||
const char* v = std::getenv("STRATA_PREFILL_LEND_PCT");
|
||||
return v ? (int64_t) std::atoi(v)
|
||||
: (int64_t) (strata::prefill::Prefill::pinned_share() >= 0.9 ? 90 : 85);
|
||||
}();
|
||||
auto slots_for = lend_slots;
|
||||
if (o.prefill_auto) {
|
||||
for (const int64_t c : kAutoChunks) {
|
||||
const int64_t k = slots_for(c);
|
||||
if (k + 128 <= xcache.slots() && k * 100 <= kAutoLendPct * xcache.slots()) { chunk = c; return k; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
for (int64_t c = chunk; c >= 256; c /= 2) {
|
||||
const int64_t k = slots_for(c);
|
||||
if (k + 128 <= xcache.slots()) { chunk = c; return k; }
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
if (o.serve) {
|
||||
if (o.spec < 2 || o.mtp.empty() || o.prefill_chunk <= 0 || thits.d_res == nullptr || host_res.empty()) {
|
||||
std::fprintf(stderr, "strata serve: needs --spec T, --mtp DIR, --prefill CHUNK, --expert-profile P and "
|
||||
@@ -1966,29 +2286,28 @@ int main(int argc, char** argv) {
|
||||
strata::prefill::Prefill sp;
|
||||
void* borrow = nullptr;
|
||||
uint64_t borrow_bytes = 0;
|
||||
int32_t lend_first = -1;
|
||||
int32_t lend_first = -1; // the first slot the prompt path may borrow (its largest chunk)
|
||||
int32_t lend_first_now = -1; // where its buffers are laid out now
|
||||
// a cache too small to lend the prompt path its buffers would make it allocate them on top - on a card the
|
||||
// cache already filled to its reserve, that is the over-subscription the auto sizing avoids - so the
|
||||
// prompt chunk is halved until its buffers fit in the lendable slots (a smaller chunk only reads slower)
|
||||
for (int64_t chunk = o.prefill_chunk; !o.no_prefill_borrow && d_res != nullptr && chunk >= 256; chunk /= 2) {
|
||||
const uint64_t need = strata::prefill::Prefill::bytes_needed(g, ss, chunk);
|
||||
const int64_t blob = (int64_t) strata::kernels::cpu::expert_layout().max_blob;
|
||||
int64_t k = (int64_t) ((need + (uint64_t) blob - 1) / (uint64_t) blob);
|
||||
if (xcache.slot_offsets() != nullptr) {
|
||||
k = 0;
|
||||
while (k < xcache.slots() &&
|
||||
(uint64_t) (xcache.bytes() - (int64_t) xcache.slot_offsets()[xcache.slots() - k]) < need) ++k;
|
||||
}
|
||||
if (k + 128 <= xcache.slots()) {
|
||||
if (chunk != o.prefill_chunk)
|
||||
if (!o.no_prefill_borrow && d_res != nullptr) {
|
||||
int64_t chunk = o.prefill_chunk;
|
||||
if (const int64_t k = plan_lend(chunk); k > 0) {
|
||||
if (o.prefill_auto)
|
||||
std::fprintf(stderr, "strata serve: prompt chunk auto: %lld tokens\n", (long long) chunk);
|
||||
else if (chunk != o.prefill_chunk)
|
||||
std::fprintf(stderr, "strata serve: prompt chunk %lld -> %lld tokens so its buffers fit in the "
|
||||
"expert cache\n", (long long) o.prefill_chunk, (long long) chunk);
|
||||
o.prefill_chunk = chunk;
|
||||
lend_first = (int32_t) (xcache.slots() - k);
|
||||
lend_first_now = lend_first;
|
||||
borrow = xcache.device_slot(lend_first);
|
||||
borrow_bytes = xcache.slot_offsets() ? (uint64_t) (xcache.bytes() - (int64_t) xcache.slot_offsets()[lend_first])
|
||||
: (uint64_t) k * (uint64_t) blob;
|
||||
break;
|
||||
borrow_bytes = xcache.slot_offsets()
|
||||
? (uint64_t) (xcache.bytes() - (int64_t) xcache.slot_offsets()[lend_first])
|
||||
: (uint64_t) k * (uint64_t) strata::kernels::cpu::expert_layout().max_blob;
|
||||
} else if (o.prefill_auto) {
|
||||
o.prefill_chunk = 1024; // nothing lendable: small buffers of its own
|
||||
}
|
||||
}
|
||||
if (borrow != nullptr)
|
||||
@@ -2023,8 +2342,11 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
mem_mark("the verifier and the drafter's binding");
|
||||
ver.set_split(o.spec_split);
|
||||
ver.set_pcie_mode(o.pcie_mode == "dma" ? 0 : o.pcie_mode == "direct" ? 1 : o.pcie_mode == "kernel" ? 2
|
||||
: native_pack ? 0 : 2); // auto: DMA for the native packs, the copy kernel for Q2_0
|
||||
// auto: the copy kernel for every pack. DMA (the native packs' default until 0.1.13) has the host call
|
||||
// cudaMemcpyAsync + cudaLaunchHostFunc inside a verify window while the GPU spins on the flag they raise;
|
||||
// issue #31's thread dumps show the host stuck in that cudaMemcpyAsync on a driver lock for good. The copy
|
||||
// kernel needs no host CUDA call there, and costs ~1-3% decode on IQ3_S (45.3 -> 44.8 tok/s, 8 requests).
|
||||
ver.set_pcie_mode(o.pcie_mode == "dma" ? 0 : o.pcie_mode == "direct" ? 1 : 2);
|
||||
std::vector<int64_t> cur;
|
||||
// ---- the conversation cache (see ConvCheckpoint). `live` is what the session holds right now: the tokens
|
||||
// it has consumed, so a request that starts with exactly them continues without any copy. `checks` are the
|
||||
@@ -2033,6 +2355,7 @@ int main(int argc, char** argv) {
|
||||
std::vector<ImgKey> live_imgs, req_imgs;
|
||||
bool live_ok = false;
|
||||
std::vector<ConvCheckpoint> checks;
|
||||
bool cvec_cached = true; // the control vector's state the live session and the checkpoints were read with
|
||||
int64_t pp_total = 0, pp_from = 0, pp_next_check = 0;
|
||||
Clock::time_point pp_t0 = Clock::now();
|
||||
auto imgs_below = [&](const std::vector<ImgKey>& all, int64_t L) {
|
||||
@@ -2061,6 +2384,8 @@ int main(int argc, char** argv) {
|
||||
const double ms = std::chrono::duration<double, std::milli>(Clock::now() - pp_t0).count();
|
||||
std::printf("PP %lld %lld %.0f %.1f\n", (long long) done, (long long) pp_total, ms,
|
||||
ms > 0.0 ? 1000.0 * (double) (done - pp_from) / ms : 0.0);
|
||||
strata::core::progress_at("reading the prompt (batched), done up to token", done);
|
||||
strata::core::progress_beat();
|
||||
std::fflush(stdout);
|
||||
if (o.prompt_cache_every > 0 && done >= pp_next_check) {
|
||||
if (!checkpoint_at(done)) { e = "saving a conversation checkpoint failed"; return false; }
|
||||
@@ -2139,8 +2464,35 @@ int main(int argc, char** argv) {
|
||||
std::deque<std::string> in_lines;
|
||||
bool in_eof = false;
|
||||
std::thread([&] {
|
||||
std::string l;
|
||||
while (std::getline(std::cin, l)) {
|
||||
// read(2) on the descriptor, not std::cin: glibc's exit() flushes every stdio stream and waits for
|
||||
// stdin's lock, which getline holds while it waits for input - an engine ending on an error (every
|
||||
// std::exit) would hang in exit() on Linux, and the server would wait for it forever
|
||||
std::string l, buf;
|
||||
char chunk[4096];
|
||||
auto getline_fd = [&](std::string& out) -> bool {
|
||||
for (;;) {
|
||||
const size_t nlpos = buf.find('\n');
|
||||
if (nlpos != std::string::npos) {
|
||||
out.assign(buf, 0, nlpos);
|
||||
buf.erase(0, nlpos + 1);
|
||||
return true;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
const int n = _read(0, chunk, (unsigned) sizeof chunk);
|
||||
#else
|
||||
const ssize_t n = ::read(0, chunk, sizeof chunk);
|
||||
if (n < 0 && errno == EINTR) continue;
|
||||
#endif
|
||||
if (n <= 0) {
|
||||
if (buf.empty()) return false;
|
||||
out.swap(buf);
|
||||
buf.clear();
|
||||
return true;
|
||||
}
|
||||
buf.append(chunk, (size_t) n);
|
||||
}
|
||||
};
|
||||
while (getline_fd(l)) {
|
||||
if (!l.empty() && l.back() == '\r') l.pop_back();
|
||||
if (l == "STOP") { stop_req.store(true); continue; }
|
||||
std::lock_guard<std::mutex> lk(in_mu);
|
||||
@@ -2172,9 +2524,56 @@ int main(int argc, char** argv) {
|
||||
// and a page-in while the verify graph spins on a host flag stalls the request for good
|
||||
size_t free_b = 0, total_b = 0;
|
||||
cudaMemGetInfo(&free_b, &total_b);
|
||||
std::fprintf(stderr, "strata serve: %lld MiB of VRAM free with everything loaded%s\n",
|
||||
(long long) (free_b >> 20), free_b < ((size_t) 128 << 20)
|
||||
? " - LOW: requests may stall; lower --max-context or raise --vram-reserve-mib" : "");
|
||||
// below ~256 MiB a later allocation (a first-used window's buffers, the desktop, another program) can make
|
||||
// the driver page GPU memory, and a verify graph spinning on a host flag then never finishes
|
||||
const int64_t free_mib = (int64_t) (free_b >> 20);
|
||||
if (free_mib >= 256) {
|
||||
std::fprintf(stderr, "strata serve: %lld MiB of VRAM free with everything loaded\n", (long long) free_mib);
|
||||
} else {
|
||||
std::fprintf(stderr, "strata serve: %lld MiB of VRAM free with everything loaded - LOW: requests may stall;"
|
||||
" add --vram-reserve-mib %lld to the config's args (or lower --max-context)\n",
|
||||
(long long) free_mib, (long long) (o.vram_reserve_mib + 512 - free_mib));
|
||||
}
|
||||
}
|
||||
// what the server's Monitor tab shows (servers before 0.1.8 skip unknown lines until READY)
|
||||
{
|
||||
size_t free_b = 0, total_b = 0;
|
||||
cudaMemGetInfo(&free_b, &total_b);
|
||||
std::printf("INFO context=%lld kv=%s kv_resident=%lld expert_slots=%lld expert_cache_mib=%lld spec=%d "
|
||||
"mtp_max=%d lookup=%d vram_free_mib=%lld cvec=%s arena_mib=%lld engine=" STRATA_VERSION "\n",
|
||||
(long long) o.max_context, o.kv.c_str(),
|
||||
(long long) (g.n_qsa_layers() > 0 && ss.qsa_states[0].kv_mode == 1
|
||||
? ss.qsa_states[0].n_slots * 4 : 0),
|
||||
(long long) xcache.slots(), (long long) (xcache.bytes() >> 20), o.spec, o.mtp_max_t,
|
||||
o.suffix_draft, (long long) (free_b >> 20), cvec_summary.c_str(),
|
||||
(long long) (strata::kernels::cpu::expert_layout().total >> 20));
|
||||
}
|
||||
// issue #29: a request whose heartbeat (tokens, prompt chunks, verify windows) stops for this long is stuck on
|
||||
// a flag nobody will raise - end the engine with where it was, so the server starts it again instead of the
|
||||
// GPU spinning forever. STRATA_WATCHDOG_S=0 turns it off. Issue #31: before it does, it reports what every
|
||||
// part was doing (stall_report), so one occurrence says where the wait is.
|
||||
{
|
||||
const char* ws = std::getenv("STRATA_WATCHDOG_S");
|
||||
const int limit = ws ? std::atoi(ws) : 60; // one step (a prompt layer, a verify window) takes seconds
|
||||
if (limit > 0)
|
||||
std::thread([limit] {
|
||||
strata::core::Progress& p = strata::core::progress();
|
||||
uint64_t last = p.beats.load(), ticks_at = p.ticks.load();
|
||||
auto since = std::chrono::steady_clock::now();
|
||||
for (;;) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const uint64_t b = p.beats.load();
|
||||
if (!p.busy.load() || b != last) { last = b; ticks_at = p.ticks.load(); since = now; continue; }
|
||||
if (now - since < std::chrono::seconds(limit)) continue;
|
||||
std::fprintf(stderr, "strata serve: no progress for %d s during a request (%s %lld) - stopping "
|
||||
"the engine so the server starts it again (issue #29)\n",
|
||||
limit, p.where.load(), (long long) p.detail.load());
|
||||
stall_report(stderr, p.ticks.load() - ticks_at);
|
||||
std::fflush(stderr);
|
||||
std::abort();
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
std::printf("READY %lld stop\n", (long long) o.max_context); // "stop": this engine honours STOP
|
||||
std::fflush(stdout);
|
||||
@@ -2195,6 +2594,11 @@ int main(int argc, char** argv) {
|
||||
std::vector<const float*> row_ptr;
|
||||
while (next_line(line)) {
|
||||
if (line == "QUIT") break;
|
||||
// the watchdog watches a request from here until this iteration ends, whichever way it ends
|
||||
struct BusyScope {
|
||||
BusyScope() { strata::core::progress().busy.store(true); strata::core::progress_at("request"); }
|
||||
~BusyScope() { strata::core::progress().busy.store(false); strata::core::progress_at("idle"); }
|
||||
} busy_scope;
|
||||
stop_req.store(false); // a STOP that arrived between requests is stale
|
||||
const bool geni = line.rfind("GENI ", 0) == 0;
|
||||
if (!geni && line.rfind("GEN ", 0) != 0) {
|
||||
@@ -2211,7 +2615,8 @@ int main(int argc, char** argv) {
|
||||
unsigned long long req_seed = 0;
|
||||
float req_min_p = 0.0f, req_penalty_repeat = 1.0f, req_penalty_freq = 0.0f, req_penalty_present = 0.0f;
|
||||
int req_penalty_last_n = 0;
|
||||
if (!geni && endp != nullptr) {
|
||||
int req_cvec = 1; // cvec=0|1: a loaded control vector for this request (on when absent)
|
||||
if (endp != nullptr) { // GENI takes only cvec=; its file path is the first token without an =
|
||||
for (;;) {
|
||||
while (*endp == ' ') ++endp;
|
||||
const char* start = endp;
|
||||
@@ -2222,7 +2627,9 @@ int main(int argc, char** argv) {
|
||||
if (eq == std::string::npos) { endp = const_cast<char*>(start); break; }
|
||||
const std::string key = tok.substr(0, eq);
|
||||
const float fv = std::strtof(tok.c_str() + eq + 1, nullptr);
|
||||
if (key == "temperature") req_temperature = fv;
|
||||
if (key == "cvec") req_cvec = std::atoi(tok.c_str() + eq + 1);
|
||||
else if (geni) {} // image requests decode greedily
|
||||
else if (key == "temperature") req_temperature = fv;
|
||||
else if (key == "top_p") req_top_p = fv;
|
||||
else if (key == "top_k") req_top_k = std::atoi(tok.c_str() + eq + 1);
|
||||
else if (key == "min_p") req_min_p = fv;
|
||||
@@ -2348,6 +2755,17 @@ int main(int argc, char** argv) {
|
||||
if ((int32_t) ids[(size_t) i] != pre[(size_t) i]) return false;
|
||||
return imgs_below(req_imgs, L) == pre_imgs;
|
||||
};
|
||||
// the control vector for this request. The live session and the checkpoints were read one way, so a
|
||||
// switch reads the prompt again from the start
|
||||
if (strata::kernels::cvec().loaded()) {
|
||||
const bool want = req_cvec != 0;
|
||||
if (want != cvec_cached) {
|
||||
live_ok = false;
|
||||
checks.clear();
|
||||
cvec_cached = want;
|
||||
}
|
||||
strata::kernels::cvec_set_enabled(want);
|
||||
}
|
||||
int64_t resume = 0;
|
||||
bool from_live = false;
|
||||
if (o.prompt_cache > 0) {
|
||||
@@ -2400,6 +2818,7 @@ int main(int argc, char** argv) {
|
||||
pp_t0 = r0;
|
||||
pp_next_check = reread_to > 0 ? INT64_MAX : resume + o.prompt_cache_every;
|
||||
std::printf("RESUME %lld\n", (long long) resume); // before reading: this many prompt tokens are reused
|
||||
strata::core::progress_at("reading the prompt, from token", read_from);
|
||||
std::fflush(stdout);
|
||||
// A SHORT PART OF THE PROMPT - the new message of a chat that continues from a checkpoint, the assistant
|
||||
// header - goes through the verify windows, S tokens at a time, as decode reads them. The batched path
|
||||
@@ -2439,21 +2858,14 @@ int main(int argc, char** argv) {
|
||||
const double ms = std::chrono::duration<double, std::milli>(Clock::now() - pp_t0).count();
|
||||
std::printf("PP %lld %lld %.0f %.1f\n", (long long) b, (long long) pp_total, ms,
|
||||
ms > 0.0 ? 1000.0 * (double) (b - pp_from) / ms : 0.0);
|
||||
strata::core::progress_beat();
|
||||
std::fflush(stdout);
|
||||
return true;
|
||||
};
|
||||
// the batched path's slots are lent just before its first run and given back (refilled) before a window
|
||||
// reads - so the windows always see the whole expert cache - or once the prompt is read
|
||||
std::vector<std::pair<int32_t, int32_t>> lent_now;
|
||||
auto lend = [&] {
|
||||
if (lend_first < 0 || !lent_now.empty()) return;
|
||||
for (size_t i = 0; i < host_res.size(); ++i)
|
||||
if (host_res[i] >= lend_first) {
|
||||
lent_now.emplace_back((int32_t) i, host_res[i]);
|
||||
host_res[i] = strata::core::kNotResident;
|
||||
}
|
||||
cudaMemcpy(d_res, host_res.data(), host_res.size() * sizeof(int32_t), cudaMemcpyHostToDevice);
|
||||
};
|
||||
int64_t lent_chunk = 0; // the chunk the lent slots hold the prompt path's buffers for
|
||||
auto refill = [&](std::string& e) -> bool {
|
||||
if (lent_now.empty()) return true;
|
||||
tr("refill start", (long long) lent_now.size());
|
||||
@@ -2466,6 +2878,30 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
cudaMemcpy(d_res, host_res.data(), host_res.size() * sizeof(int32_t), cudaMemcpyHostToDevice);
|
||||
lent_now.clear();
|
||||
lent_chunk = 0;
|
||||
return true;
|
||||
};
|
||||
// lend the slots `tokens` batched prompt tokens need: the prompt path's buffers for min(chunk, tokens
|
||||
// rounded up to 256), laid out in the last of the slots it may borrow
|
||||
auto lend = [&](int64_t tokens, std::string& e) -> bool {
|
||||
if (lend_first < 0) return true; // its own buffers: nothing to lend
|
||||
const int64_t want = std::min<int64_t>(o.prefill_chunk, (tokens + 255) / 256 * 256);
|
||||
if (!lent_now.empty()) {
|
||||
if (want <= lent_chunk) return true;
|
||||
if (!refill(e)) return false;
|
||||
}
|
||||
const int32_t first = std::max<int32_t>(lend_first, (int32_t) (xcache.slots() - lend_slots(want)));
|
||||
if (want != sp.chunk() || first != lend_first_now) {
|
||||
if (!sp.relayout(want, xcache.device_slot(first), lend_bytes(first), e)) return false;
|
||||
lend_first_now = first;
|
||||
}
|
||||
for (size_t i = 0; i < host_res.size(); ++i)
|
||||
if (host_res[i] >= first) {
|
||||
lent_now.emplace_back((int32_t) i, host_res[i]);
|
||||
host_res[i] = strata::core::kNotResident;
|
||||
}
|
||||
cudaMemcpy(d_res, host_res.data(), host_res.size() * sizeof(int32_t), cudaMemcpyHostToDevice);
|
||||
lent_chunk = want;
|
||||
return true;
|
||||
};
|
||||
apply_pending(true);
|
||||
@@ -2504,7 +2940,10 @@ int main(int argc, char** argv) {
|
||||
std::printf("ERR refilling a lent slot failed: %s\n", err.c_str());
|
||||
return 1;
|
||||
}
|
||||
if (!win) lend();
|
||||
if (!win && !lend(to - at, err)) {
|
||||
std::printf("ERR lending the prompt path its slots failed: %s\n", err.c_str());
|
||||
return 1;
|
||||
}
|
||||
const auto tsp = Clock::now();
|
||||
const bool sp_ok = win ? read_windows(at, to, err) : sp.run(ids.data() + at, to - at, at, err);
|
||||
if (trace) {
|
||||
@@ -2622,6 +3061,7 @@ int main(int argc, char** argv) {
|
||||
bool eos = false;
|
||||
for (int i = 0; i <= a && produced_n < max_new && !eos; ++i) {
|
||||
std::printf("T %d\n", (int) outv[(size_t) i]);
|
||||
strata::core::progress_beat();
|
||||
++produced_n;
|
||||
if (o.suffix_draft > 0) sfx.append(outv[(size_t) i]);
|
||||
eos = std::find(o.eos_ids.begin(), o.eos_ids.end(), (int64_t) outv[(size_t) i]) != o.eos_ids.end();
|
||||
@@ -2697,6 +3137,19 @@ int main(int argc, char** argv) {
|
||||
uint64_t h_ple = hash_dev(ss.ple_hist, z.ple, 1469598103934665603ull);
|
||||
uint64_t h_tail = 1469598103934665603ull, h_pool = h_tail, h_kv = h_tail, h_stale = h_tail;
|
||||
const int64_t kvb = qs.head_dim, scb = (qs.head_dim / 64) * 2;
|
||||
// a state's K/V arrays and their bytes per (cell, head) row: the host copy when it has one
|
||||
auto kv_arrays = [&](const strata::core::QsaState& st) {
|
||||
const bool h = st.kv_mode != 0;
|
||||
std::vector<std::pair<const void*, int64_t>> a;
|
||||
if (st.kv_q4) {
|
||||
const int64_t q4b = (int64_t) strata::kernels::kv_q4_bytes_per_head((int) qs.head_dim);
|
||||
a = {{h ? st.host.k_q4 : st.k_q4, q4b}, {h ? st.host.v_q4 : st.v_q4, q4b}};
|
||||
} else {
|
||||
a = {{h ? st.host.k_q : st.k_q, kvb}, {h ? st.host.v_q : st.v_q, kvb},
|
||||
{h ? st.host.k_scale : st.k_scale, scb}, {h ? st.host.v_scale : st.v_scale, scb}};
|
||||
}
|
||||
return a;
|
||||
};
|
||||
const int64_t end_cell = std::min<int64_t>(((L + qs.page_size - 1) / qs.page_size) * qs.page_size,
|
||||
ss.qsa_states[0].max_cells);
|
||||
for (int64_t i = 0; i < g.n_qsa_layers(); ++i) {
|
||||
@@ -2704,11 +3157,7 @@ int main(int argc, char** argv) {
|
||||
h_tail = hash_dev(st.idx_tail, z.tail, h_tail);
|
||||
h_pool = hash_dev(st.idx_pooled, (size_t) (L / qs.idx_block) * qs.idx_dim * 4, h_pool);
|
||||
// KV streaming: the host copy is the identity layout and holds every cell
|
||||
const bool hs = st.kv_mode != 0;
|
||||
for (const auto& [pool, w] : {std::pair<const void*, int64_t>{hs ? st.host.k_q : st.k_q, kvb},
|
||||
{hs ? st.host.v_q : st.v_q, kvb},
|
||||
{hs ? st.host.k_scale : st.k_scale, scb},
|
||||
{hs ? st.host.v_scale : st.v_scale, scb}}) {
|
||||
for (const auto& [pool, w] : kv_arrays(st)) {
|
||||
h_kv = hash_cells(pool, w, 0, L, h_kv);
|
||||
h_stale = hash_cells(pool, w, L, end_cell, h_stale);
|
||||
}
|
||||
@@ -2716,11 +3165,7 @@ int main(int argc, char** argv) {
|
||||
const strata::core::QsaState& ms = mtp.kv_state();
|
||||
uint64_t h_mtp = 1469598103934665603ull;
|
||||
const int64_t mL = std::min<int64_t>(L, ms.max_cells);
|
||||
const bool mh = ms.kv_mode != 0;
|
||||
for (const auto& [pool, w] : {std::pair<const void*, int64_t>{mh ? ms.host.k_q : ms.k_q, kvb},
|
||||
{mh ? ms.host.v_q : ms.v_q, kvb},
|
||||
{mh ? ms.host.k_scale : ms.k_scale, scb},
|
||||
{mh ? ms.host.v_scale : ms.v_scale, scb}})
|
||||
for (const auto& [pool, w] : kv_arrays(ms))
|
||||
if (pool != nullptr) h_mtp = hash_cells(pool, w, 0, mL, h_mtp);
|
||||
std::fprintf(stderr, "strata serve: STATE_HASH L=%lld gdn=%016llx ple=%016llx tail=%016llx pooled=%016llx "
|
||||
"kv=%016llx mtp=%016llx stale=%016llx ple_prev=%d,%d\n", (long long) L,
|
||||
@@ -2772,15 +3217,21 @@ int main(int argc, char** argv) {
|
||||
void* borrow = nullptr;
|
||||
uint64_t borrow_bytes = 0;
|
||||
if (!o.no_prefill_borrow && !host_res.empty() && d_res != nullptr) {
|
||||
const uint64_t need = strata::prefill::Prefill::bytes_needed(g, ss, o.prefill_chunk);
|
||||
const int64_t blob = (int64_t) strata::kernels::cpu::expert_layout().max_blob;
|
||||
int64_t k = (int64_t) ((need + (uint64_t) blob - 1) / (uint64_t) blob);
|
||||
if (xcache.slot_offsets() != nullptr) { // sized slots: take slots from the end until they hold `need`
|
||||
k = 0;
|
||||
while (k < xcache.slots() &&
|
||||
(uint64_t) (xcache.bytes() - (int64_t) xcache.slot_offsets()[xcache.slots() - k]) < need) ++k;
|
||||
int64_t chunk = o.prefill_chunk;
|
||||
int64_t k = plan_lend(chunk); // auto: the largest chunk that fits; fixed: halved to fit
|
||||
if (k > 0 && chunk > (n_prompt - 1 + 255) / 256 * 256) { // no bigger than the prompt needs
|
||||
chunk = std::max<int64_t>(256, (n_prompt - 1 + 255) / 256 * 256);
|
||||
k = lend_slots(chunk);
|
||||
if (!o.prefill_auto) o.prefill_chunk = chunk;
|
||||
}
|
||||
if (k + 128 <= xcache.slots()) { // the lent slots are refilled after the prompt
|
||||
if (o.prefill_auto) {
|
||||
o.prefill_chunk = k > 0 ? chunk : 1024;
|
||||
std::fprintf(stderr, "strata generate: prompt chunk auto: %lld tokens\n", (long long) o.prefill_chunk);
|
||||
} else if (chunk != o.prefill_chunk) {
|
||||
k = 0; // a fixed chunk that does not fit: its own buffers, as before
|
||||
}
|
||||
const int64_t blob = (int64_t) strata::kernels::cpu::expert_layout().max_blob;
|
||||
if (k > 0) { // the lent slots are refilled after the prompt
|
||||
const int32_t first = (int32_t) (xcache.slots() - k);
|
||||
for (size_t i = 0; i < host_res.size(); ++i)
|
||||
if (host_res[i] >= first) {
|
||||
@@ -3105,8 +3556,11 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
mem_mark("the verifier and the drafter's binding");
|
||||
ver.set_split(o.spec_split);
|
||||
ver.set_pcie_mode(o.pcie_mode == "dma" ? 0 : o.pcie_mode == "direct" ? 1 : o.pcie_mode == "kernel" ? 2
|
||||
: native_pack ? 0 : 2); // auto: DMA for the native packs, the copy kernel for Q2_0
|
||||
// auto: the copy kernel for every pack. DMA (the native packs' default until 0.1.13) has the host call
|
||||
// cudaMemcpyAsync + cudaLaunchHostFunc inside a verify window while the GPU spins on the flag they raise;
|
||||
// issue #31's thread dumps show the host stuck in that cudaMemcpyAsync on a driver lock for good. The copy
|
||||
// kernel needs no host CUDA call there, and costs ~1-3% decode on IQ3_S (45.3 -> 44.8 tok/s, 8 requests).
|
||||
ver.set_pcie_mode(o.pcie_mode == "dma" ? 0 : o.pcie_mode == "direct" ? 1 : 2);
|
||||
drive.d.plan = ver.plan_sink();
|
||||
drive.d.pcie_num = (int) (o.pcie_frac * 256.0 + 0.5);
|
||||
if (drive.d.pcie_num < 0) drive.d.pcie_num = 0;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""tools/make_profile.py - the expert-cache profile (`data/expert-profile.bin`): every (layer, expert) pair, ranked.
|
||||
|
||||
`--expert-cache auto` fills the VRAM slots it can afford with the profile's pairs in order, so the ranking decides
|
||||
which experts start resident (the adaptive tier then swaps in what a conversation routes most). A profile that
|
||||
ranks fewer pairs than a card can hold caps the cache (issue #46: a 32 GB card stopped at 8,000 slots); this tool
|
||||
writes one that ranks all 24,576.
|
||||
|
||||
The order: the base profile's ranking (default: the shipped data/expert-profile.bin), then the pairs your routing
|
||||
traces used, most frequent first, then every pair still missing, interleaved across the layers.
|
||||
|
||||
python tools/make_profile.py [TRACE ...] [--base data/expert-profile.bin | --no-base] [--out PATH]
|
||||
|
||||
A routing trace comes from a one-shot engine run with `--dump-routing FILE` (a prompt typical of your use; the
|
||||
routed experts of every layer and position are written). Point the model config's `--expert-profile` at the result.
|
||||
"""
|
||||
import argparse
|
||||
import struct
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
N_LAYER, N_EXPERT = 48, 512
|
||||
MAGIC, VERSION = b"STRP", 1
|
||||
|
||||
|
||||
def read_profile(path):
|
||||
blob = Path(path).read_bytes()
|
||||
if blob[:4] != MAGIC:
|
||||
raise SystemExit(f"{path}: not a Strata profile")
|
||||
ver, nl, ne, slots, n = struct.unpack_from("<5I", blob, 4)
|
||||
if (nl, ne) != (N_LAYER, N_EXPERT):
|
||||
raise SystemExit(f"{path}: {nl}x{ne}, not {N_LAYER}x{N_EXPERT}")
|
||||
return [struct.unpack_from("<HH", blob, 24 + 4 * i) for i in range(n)]
|
||||
|
||||
|
||||
def read_trace(path):
|
||||
"""(layer, k, k expert ids, k weights) records, as `--dump-routing` writes them."""
|
||||
blob = Path(path).read_bytes()
|
||||
off, freq = 0, defaultdict(int)
|
||||
while off + 8 <= len(blob):
|
||||
layer, k = struct.unpack_from("<ii", blob, off)
|
||||
off += 8
|
||||
for e in struct.unpack_from("<%di" % k, blob, off):
|
||||
if 0 <= layer < N_LAYER and 0 <= e < N_EXPERT:
|
||||
freq[(layer, e)] += 1
|
||||
off += 8 * k # the ids and the weights
|
||||
return freq
|
||||
|
||||
|
||||
def write_profile(path, ranked):
|
||||
table = [[-1] * N_EXPERT for _ in range(N_LAYER)]
|
||||
for slot, (layer, e) in enumerate(ranked):
|
||||
table[layer][e] = slot
|
||||
with open(path, "wb") as f:
|
||||
f.write(MAGIC + struct.pack("<5I", VERSION, N_LAYER, N_EXPERT, len(ranked), len(ranked)))
|
||||
for layer, e in ranked:
|
||||
f.write(struct.pack("<HH", layer, e))
|
||||
for layer in range(N_LAYER):
|
||||
f.write(struct.pack("<%di" % N_EXPERT, *table[layer]))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
||||
ap.add_argument("traces", nargs="*", help="routing traces from --dump-routing")
|
||||
ap.add_argument("--base", default=str(ROOT / "data" / "expert-profile.bin"), help="ranking to keep first")
|
||||
ap.add_argument("--no-base", action="store_true", help="rank by the traces only")
|
||||
ap.add_argument("--out", default=str(ROOT / "data" / "expert-profile.bin"))
|
||||
a = ap.parse_args()
|
||||
|
||||
ranked, seen = [], set()
|
||||
|
||||
def take(pairs):
|
||||
for p in pairs:
|
||||
p = (int(p[0]), int(p[1]))
|
||||
if p not in seen:
|
||||
seen.add(p)
|
||||
ranked.append(p)
|
||||
|
||||
if not a.no_base:
|
||||
take(read_profile(a.base))
|
||||
n_base = len(ranked)
|
||||
freq = defaultdict(int)
|
||||
for t in a.traces:
|
||||
for p, c in read_trace(t).items():
|
||||
freq[p] += c
|
||||
take(p for p, _ in sorted(freq.items(), key=lambda kv: (-kv[1], kv[0])))
|
||||
n_trace = len(ranked) - n_base
|
||||
take((layer, e) for e in range(N_EXPERT) for layer in range(N_LAYER)) # the rest, across the layers
|
||||
write_profile(a.out, ranked)
|
||||
assert read_profile(a.out) == ranked, "the profile did not survive the round trip"
|
||||
print(f"wrote {a.out}: {len(ranked)} ranked pairs ({n_base} from the base, {n_trace} from the traces, "
|
||||
f"{len(ranked) - n_base - n_trace} filled in)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user