mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 14:21:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10409135dd | ||
|
|
03939f1c06 | ||
|
|
75e6efeb77 | ||
|
|
fa59b3f3a9 | ||
|
|
eef241178c | ||
|
|
7980d3719f | ||
|
|
0425829226 | ||
|
|
481b69246f | ||
|
|
99b40973c6 | ||
|
|
e227683eb9 | ||
|
|
d62579d482 | ||
|
|
717971d33b | ||
|
|
d92c2a3034 | ||
|
|
ed01b4de03 | ||
|
|
ddfb98a6ea | ||
|
|
c81819a87c | ||
|
|
4832dccd09 | ||
|
|
78db15b630 | ||
|
|
cfc73be8b9 | ||
|
|
2f8d9be954 | ||
|
|
c9077a7669 | ||
|
|
a402eefdd1 |
No files matched your search
+8
-7
@@ -8,7 +8,7 @@
|
||||
# below refuses anything older: silently building for a pre-Ampere arch would produce a binary that runs
|
||||
# and is wrong about its own performance, which is the worst of both outcomes.
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
project(strata VERSION 0.1.25 LANGUAGES CXX)
|
||||
project(strata VERSION 0.1.28 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
|
||||
@@ -62,14 +62,15 @@ if(STRATA_ENABLE_CUDA)
|
||||
endif()
|
||||
foreach(_a IN LISTS CMAKE_CUDA_ARCHITECTURES)
|
||||
string(REGEX REPLACE "-.*$" "" _base "${_a}") # 120-real / 120-virtual are still 120
|
||||
# Plan v0.3 P6: developed and measured on sm_120 (RTX 50); the kernels need sm_80 or newer (tf32 mma in
|
||||
# the attention scorer, bf16 math), so RTX 30 (86) and RTX 40 (89) build too. Older cards are refused.
|
||||
# Turing port: developed and measured on sm_120 (RTX 50); the tf32 mma in the QSA scorer has a
|
||||
# portable fp32-FMA fallback, so RTX 20 (75), 30 (86), RTX 40 (89) and RTX 50 (120) build too.
|
||||
# Pre-sm_75 is refused.
|
||||
if(_base STREQUAL "native" OR _base STREQUAL "all" OR _base STREQUAL "all-major")
|
||||
continue()
|
||||
endif()
|
||||
if(_base LESS 80 AND NOT STRATA_EXPERIMENTAL_SM75) # the A/B of a Turing card as a layer-split stage
|
||||
if(_base LESS 75)
|
||||
message(FATAL_ERROR
|
||||
"Strata needs an NVIDIA GPU of compute capability 8.0 or newer (RTX 30 / 40 / 50 series), but "
|
||||
"Strata needs an NVIDIA GPU of compute capability 7.5 or newer (RTX 20 / 30 / 40 / 50 series), but "
|
||||
"CMAKE_CUDA_ARCHITECTURES is '${CMAKE_CUDA_ARCHITECTURES}'.")
|
||||
endif()
|
||||
endforeach()
|
||||
@@ -664,7 +665,7 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
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)
|
||||
foreach(_t q2_0 iq2_xxs iq2_xs iq2_s iq3_xxs iq3_s iq4_nl iq4_xs q8_0) # q8_0: the drafter's matrices (E-9)
|
||||
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}
|
||||
@@ -692,7 +693,7 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
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)
|
||||
foreach(_t q2_0 iq2_xxs iq2_xs iq2_s iq3_xxs iq3_s iq4_nl iq4_xs q8_0)
|
||||
list(APPEND _strata_mmq_sources "${_strata_mmq}/template-instances/mmq-instance-${_t}.cu")
|
||||
endforeach()
|
||||
set_source_files_properties(${_strata_mmq_sources} PROPERTIES LANGUAGE HIP)
|
||||
|
||||
@@ -25,15 +25,15 @@ 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** | 90 tokens/s | 67 tokens/s | 1,840 tokens/s |
|
||||
| **IQ2_XS** | 74 tokens/s | 60 tokens/s | 1,800 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 46 tokens/s | 1,550 tokens/s |
|
||||
| **IQ3_S** | 52 tokens/s | 41 tokens/s | 1,500 tokens/s |
|
||||
| **Coder** (IQ1_M) | 51 tokens/s | 44 tokens/s | 1,870 tokens/s |
|
||||
| **Q2_0** | 93 tokens/s | 74 tokens/s | 2,170 tokens/s |
|
||||
| **IQ2_XS** | 79 tokens/s | 63 tokens/s | 2,090 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 49 tokens/s | 1,750 tokens/s |
|
||||
| **IQ3_S** | 53 tokens/s | 46 tokens/s | 1,620 tokens/s |
|
||||
| **Coder** (IQ1_M) | 55 tokens/s | 43 tokens/s | 2,180 tokens/s |
|
||||
|
||||
- **Writes answers** = how fast the reply appears (tokens per second).
|
||||
- **Reads your prompt** = how fast it takes in what you send (long documents, code, chat history), measured on a
|
||||
32K-token prompt; a 4K prompt reads at 890-1,520 tokens/s. A 32K prompt takes about 17 seconds with Q2_0.
|
||||
32K-token prompt; a 4K prompt reads at 910-1,580 tokens/s. A 32K prompt takes about 15 seconds with Q2_0.
|
||||
|
||||
A card with more VRAM is faster, because more of the model fits on the GPU: an RTX 3090 (24 GB) should do roughly
|
||||
100-140 tokens per second. All measurements, long-context numbers and estimates for other cards are in the
|
||||
@@ -47,7 +47,7 @@ asks whether to share the model across them (recommended when two can). An insta
|
||||
its next start. Or choose yourself: `START-HERE.bat --gpus 0,2` (both, remembered), `--gpus all`, or `--gpu 0` (one
|
||||
card, this start only). Each card keeps the experts of its own layers, and prompts flow through the cards in a
|
||||
pipeline: on an RTX 5080 + RTX 3090 prompts were read 18-20% faster than on the 5080 alone, decoding on par.
|
||||
Every card must be an RTX 30 series or newer with 8 GB or more. See [docs/MULTI_GPU.md](docs/MULTI_GPU.md).
|
||||
Every card must be an RTX 20 series or newer with 8 GB or more. See [docs/MULTI_GPU.md](docs/MULTI_GPU.md).
|
||||
|
||||
## Which model should I pick?
|
||||
|
||||
@@ -90,7 +90,8 @@ yet). Details: [AMD HIP](docs/AMD_HIP.md).
|
||||
|
||||
## 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),
|
||||
**You need:** an NVIDIA RTX 20, 30, 40 or 50 card with 12 GB of VRAM or more (RTX 20 since 0.1.27), enough RAM for the size you pick (above;
|
||||
a big GPU makes up for less RAM - the [low-RAM mode](docs/DETAILS.md)),
|
||||
~80 GB of free disk space (an SSD makes the first start much faster), and Windows 10/11 or Linux. The only thing you
|
||||
install yourself is a current **NVIDIA driver** ([nvidia.com/drivers](https://www.nvidia.com/drivers) or the NVIDIA
|
||||
App). Everything else - Python, the engine, the model - is set up for you.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# Speed with engine 0.1.26
|
||||
|
||||
RTX 5070 12 GB (PCIe 5.0 x16), Ryzen 5 7600, 64 GB DDR5-5200, Windows 10. The ready-made 0.1.26 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, images
|
||||
off, no timing marks. The same code-agent prompts as `2026-09-29-speed-0122`. Every run: [`matrix.json`](matrix.json).
|
||||
|
||||
## Prompt (tokens/s)
|
||||
|
||||
| Model | 1K | 4K | 32K | 64K | 128K |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| **Q2_0** | 536 | 1,299 | 2,171 | 2,126 | 2,107 |
|
||||
| **IQ2_XS** | 534 | 1,256 | 2,092 | 1,754 | 1,752 |
|
||||
| **IQ3_XXS** | 482 | 1,007 | 1,745 | 1,609 | 1,602 |
|
||||
| **IQ3_S** | 427 | 913 | 1,624 | 1,640 | 1,443 |
|
||||
| **Coder** | 656 | 1,583 | 2,177 | 2,236 | 2,208 |
|
||||
|
||||
## Output (tokens/s)
|
||||
|
||||
| Model | 1K | 4K | 32K | 64K | 128K |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| **Q2_0** | 87.3 | 93.0 | 81.8 | 76.2 | 73.7 |
|
||||
| **IQ2_XS** | 79.6 | 78.6 | 76.3 | 63.7 | 62.7 |
|
||||
| **IQ3_XXS** | 61.9 | 61.6 | 58.5 | 57.2 | 49.0 |
|
||||
| **IQ3_S** | 52.4 | 53.3 | 48.3 | 46.3 | 45.5 |
|
||||
| **Coder** | 58.9 | 55.1 | 54.9 | 53.2 | 43.0 |
|
||||
|
||||
- Against 0.1.22 (`2026-09-29-speed-0122`), prompts at 32K-128K are 8-28% faster: Q2_0 1,844 -> 2,171 at 32K and
|
||||
1,682 -> 2,107 at 128K, the Coder 1,779 -> 2,208 at 128K. The gains come from:
|
||||
- 0.1.24's QSA selection on tensor cores;
|
||||
- 0.1.25's grouping tables in mapped memory and fused norms;
|
||||
- 0.1.26's batched draft-layer pass.
|
||||
- 262K was not run again (0.1.22: Q2_0 1,304, IQ2_XS 1,181 tokens/s).
|
||||
- Output speed is the same decode as before. One run per cell moves several percent with the text (the share of
|
||||
accepted drafts, `spec_accept` in `matrix.json`). For example, the Coder's 128K answer accepted 55% of its drafts
|
||||
and its 64K answer 71%.
|
||||
@@ -0,0 +1,352 @@
|
||||
[
|
||||
{
|
||||
"model": "Q2_0",
|
||||
"tier": "1k",
|
||||
"prompt_tokens": 1017,
|
||||
"exit": 0,
|
||||
"wall_s": 23.9,
|
||||
"decode_tok_s": 87.33,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 536.42,
|
||||
"ttft_s": 3.4,
|
||||
"spec_accept": 0.681,
|
||||
"tokens_per_round": 2.72,
|
||||
"vram_slots": 4174
|
||||
},
|
||||
{
|
||||
"model": "Q2_0",
|
||||
"tier": "4k",
|
||||
"prompt_tokens": 3562,
|
||||
"exit": 0,
|
||||
"wall_s": 27.6,
|
||||
"decode_tok_s": 92.97,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 1299.44,
|
||||
"ttft_s": 4.21,
|
||||
"spec_accept": 0.838,
|
||||
"tokens_per_round": 3.23,
|
||||
"vram_slots": 4121
|
||||
},
|
||||
{
|
||||
"model": "Q2_0",
|
||||
"tier": "32k",
|
||||
"prompt_tokens": 31566,
|
||||
"exit": 0,
|
||||
"wall_s": 40.8,
|
||||
"decode_tok_s": 81.75,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 2170.85,
|
||||
"ttft_s": 16.02,
|
||||
"spec_accept": 0.788,
|
||||
"tokens_per_round": 2.99,
|
||||
"vram_slots": 3832
|
||||
},
|
||||
{
|
||||
"model": "Q2_0",
|
||||
"tier": "64k",
|
||||
"prompt_tokens": 64162,
|
||||
"exit": 0,
|
||||
"wall_s": 57.3,
|
||||
"decode_tok_s": 76.23,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 2126.03,
|
||||
"ttft_s": 32.06,
|
||||
"spec_accept": 0.707,
|
||||
"tokens_per_round": 2.8,
|
||||
"vram_slots": 3778
|
||||
},
|
||||
{
|
||||
"model": "Q2_0",
|
||||
"tier": "128k",
|
||||
"prompt_tokens": 128478,
|
||||
"exit": 0,
|
||||
"wall_s": 87.7,
|
||||
"decode_tok_s": 73.73,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 2106.85,
|
||||
"ttft_s": 62.48,
|
||||
"spec_accept": 0.679,
|
||||
"tokens_per_round": 2.71,
|
||||
"vram_slots": 3695
|
||||
},
|
||||
{
|
||||
"model": "IQ2_XS",
|
||||
"tier": "1k",
|
||||
"prompt_tokens": 1017,
|
||||
"exit": 0,
|
||||
"wall_s": 42.2,
|
||||
"decode_tok_s": 79.62,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 533.83,
|
||||
"ttft_s": 3.51,
|
||||
"spec_accept": 0.774,
|
||||
"tokens_per_round": 2.89,
|
||||
"vram_slots": 4359
|
||||
},
|
||||
{
|
||||
"model": "IQ2_XS",
|
||||
"tier": "4k",
|
||||
"prompt_tokens": 3562,
|
||||
"exit": 0,
|
||||
"wall_s": 40.6,
|
||||
"decode_tok_s": 78.6,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 1256.02,
|
||||
"ttft_s": 4.31,
|
||||
"spec_accept": 0.81,
|
||||
"tokens_per_round": 3.29,
|
||||
"vram_slots": 4308
|
||||
},
|
||||
{
|
||||
"model": "IQ2_XS",
|
||||
"tier": "32k",
|
||||
"prompt_tokens": 31566,
|
||||
"exit": 0,
|
||||
"wall_s": 53.6,
|
||||
"decode_tok_s": 76.34,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 2092.02,
|
||||
"ttft_s": 16.57,
|
||||
"spec_accept": 0.85,
|
||||
"tokens_per_round": 3.2,
|
||||
"vram_slots": 4029
|
||||
},
|
||||
{
|
||||
"model": "IQ2_XS",
|
||||
"tier": "64k",
|
||||
"prompt_tokens": 64162,
|
||||
"exit": 0,
|
||||
"wall_s": 78.3,
|
||||
"decode_tok_s": 63.69,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 1753.65,
|
||||
"ttft_s": 38.08,
|
||||
"spec_accept": 0.636,
|
||||
"tokens_per_round": 2.59,
|
||||
"vram_slots": 3977
|
||||
},
|
||||
{
|
||||
"model": "IQ2_XS",
|
||||
"tier": "128k",
|
||||
"prompt_tokens": 128478,
|
||||
"exit": 0,
|
||||
"wall_s": 115.1,
|
||||
"decode_tok_s": 62.72,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 1751.79,
|
||||
"ttft_s": 74.85,
|
||||
"spec_accept": 0.696,
|
||||
"tokens_per_round": 2.6,
|
||||
"vram_slots": 3896
|
||||
},
|
||||
{
|
||||
"model": "IQ3_XXS",
|
||||
"tier": "1k",
|
||||
"prompt_tokens": 1017,
|
||||
"exit": 0,
|
||||
"wall_s": 46.9,
|
||||
"decode_tok_s": 61.94,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 482.04,
|
||||
"ttft_s": 3.67,
|
||||
"spec_accept": 0.845,
|
||||
"tokens_per_round": 3.57,
|
||||
"vram_slots": 3279
|
||||
},
|
||||
{
|
||||
"model": "IQ3_XXS",
|
||||
"tier": "4k",
|
||||
"prompt_tokens": 3562,
|
||||
"exit": 0,
|
||||
"wall_s": 47.4,
|
||||
"decode_tok_s": 61.64,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 1007.48,
|
||||
"ttft_s": 5.03,
|
||||
"spec_accept": 0.801,
|
||||
"tokens_per_round": 3.19,
|
||||
"vram_slots": 3242
|
||||
},
|
||||
{
|
||||
"model": "IQ3_XXS",
|
||||
"tier": "32k",
|
||||
"prompt_tokens": 31566,
|
||||
"exit": 0,
|
||||
"wall_s": 65.6,
|
||||
"decode_tok_s": 58.49,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 1745.16,
|
||||
"ttft_s": 19.63,
|
||||
"spec_accept": 0.789,
|
||||
"tokens_per_round": 2.85,
|
||||
"vram_slots": 3007
|
||||
},
|
||||
{
|
||||
"model": "IQ3_XXS",
|
||||
"tier": "64k",
|
||||
"prompt_tokens": 64162,
|
||||
"exit": 0,
|
||||
"wall_s": 88.5,
|
||||
"decode_tok_s": 57.21,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 1609.31,
|
||||
"ttft_s": 41.45,
|
||||
"spec_accept": 0.74,
|
||||
"tokens_per_round": 2.87,
|
||||
"vram_slots": 2963
|
||||
},
|
||||
{
|
||||
"model": "IQ3_XXS",
|
||||
"tier": "128k",
|
||||
"prompt_tokens": 128478,
|
||||
"exit": 0,
|
||||
"wall_s": 127.0,
|
||||
"decode_tok_s": 48.95,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 1602.33,
|
||||
"ttft_s": 81.71,
|
||||
"spec_accept": 0.643,
|
||||
"tokens_per_round": 2.37,
|
||||
"vram_slots": 2899
|
||||
},
|
||||
{
|
||||
"model": "IQ3_S",
|
||||
"tier": "1k",
|
||||
"prompt_tokens": 1017,
|
||||
"exit": 0,
|
||||
"wall_s": 54.2,
|
||||
"decode_tok_s": 52.4,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 426.6,
|
||||
"ttft_s": 4.03,
|
||||
"spec_accept": 0.822,
|
||||
"tokens_per_round": 3.17,
|
||||
"vram_slots": 2617
|
||||
},
|
||||
{
|
||||
"model": "IQ3_S",
|
||||
"tier": "4k",
|
||||
"prompt_tokens": 3562,
|
||||
"exit": 0,
|
||||
"wall_s": 58.9,
|
||||
"decode_tok_s": 53.3,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 912.99,
|
||||
"ttft_s": 5.58,
|
||||
"spec_accept": 0.85,
|
||||
"tokens_per_round": 3.38,
|
||||
"vram_slots": 2579
|
||||
},
|
||||
{
|
||||
"model": "IQ3_S",
|
||||
"tier": "32k",
|
||||
"prompt_tokens": 31566,
|
||||
"exit": 0,
|
||||
"wall_s": 72.8,
|
||||
"decode_tok_s": 48.31,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 1624.07,
|
||||
"ttft_s": 21.07,
|
||||
"spec_accept": 0.827,
|
||||
"tokens_per_round": 3.0,
|
||||
"vram_slots": 2383
|
||||
},
|
||||
{
|
||||
"model": "IQ3_S",
|
||||
"tier": "64k",
|
||||
"prompt_tokens": 64162,
|
||||
"exit": 0,
|
||||
"wall_s": 93.3,
|
||||
"decode_tok_s": 46.25,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 1640.34,
|
||||
"ttft_s": 40.77,
|
||||
"spec_accept": 0.693,
|
||||
"tokens_per_round": 2.56,
|
||||
"vram_slots": 2346
|
||||
},
|
||||
{
|
||||
"model": "IQ3_S",
|
||||
"tier": "128k",
|
||||
"prompt_tokens": 128478,
|
||||
"exit": 0,
|
||||
"wall_s": 144.6,
|
||||
"decode_tok_s": 45.51,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 1443.21,
|
||||
"ttft_s": 90.72,
|
||||
"spec_accept": 0.697,
|
||||
"tokens_per_round": 2.73,
|
||||
"vram_slots": 2291
|
||||
},
|
||||
{
|
||||
"model": "Coder",
|
||||
"tier": "1k",
|
||||
"prompt_tokens": 1017,
|
||||
"exit": 0,
|
||||
"wall_s": 37.0,
|
||||
"decode_tok_s": 58.91,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 656.36,
|
||||
"ttft_s": 2.48,
|
||||
"spec_accept": 0.743,
|
||||
"tokens_per_round": 2.91,
|
||||
"vram_slots": 2649
|
||||
},
|
||||
{
|
||||
"model": "Coder",
|
||||
"tier": "4k",
|
||||
"prompt_tokens": 3562,
|
||||
"exit": 0,
|
||||
"wall_s": 18.3,
|
||||
"decode_tok_s": 55.1,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 1583.1,
|
||||
"ttft_s": 3.09,
|
||||
"spec_accept": 0.713,
|
||||
"tokens_per_round": 2.88,
|
||||
"vram_slots": 2614
|
||||
},
|
||||
{
|
||||
"model": "Coder",
|
||||
"tier": "32k",
|
||||
"prompt_tokens": 31566,
|
||||
"exit": 0,
|
||||
"wall_s": 30.4,
|
||||
"decode_tok_s": 54.89,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 2177.17,
|
||||
"ttft_s": 15.33,
|
||||
"spec_accept": 0.731,
|
||||
"tokens_per_round": 2.75,
|
||||
"vram_slots": 2417
|
||||
},
|
||||
{
|
||||
"model": "Coder",
|
||||
"tier": "64k",
|
||||
"prompt_tokens": 64162,
|
||||
"exit": 0,
|
||||
"wall_s": 44.8,
|
||||
"decode_tok_s": 53.22,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 2236.49,
|
||||
"ttft_s": 29.51,
|
||||
"spec_accept": 0.709,
|
||||
"tokens_per_round": 2.68,
|
||||
"vram_slots": 2382
|
||||
},
|
||||
{
|
||||
"model": "Coder",
|
||||
"tier": "128k",
|
||||
"prompt_tokens": 128478,
|
||||
"exit": 0,
|
||||
"wall_s": 75.9,
|
||||
"decode_tok_s": 43.04,
|
||||
"decoded": 256,
|
||||
"prefill_tok_s": 2208.27,
|
||||
"ttft_s": 59.02,
|
||||
"spec_accept": 0.553,
|
||||
"tokens_per_round": 2.23,
|
||||
"vram_slots": 2324
|
||||
}
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
+34
-15
@@ -12,7 +12,7 @@ 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, engine 0.1.22 (prompts) / 0.1.14 (output) with the settings setup writes
|
||||
RTX 5070 **12 GB**, Ryzen 5 7600 (6 cores), 64 GB DDR5-5200, Windows, engine 0.1.26 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.
|
||||
@@ -21,24 +21,27 @@ measured with Swift 1.5's IQ2_XS, which runs at the original's speed.
|
||||
|
||||
| Model | 1K | 4K | 32K | 64K | 128K | 262K |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| **Q2_0** | 519 | 1,226 | 1,844 | 1,836 | 1,682 | 1,304 |
|
||||
| **IQ2_XS** | 524 | 1,196 | 1,799 | 1,611 | 1,495 | 1,181* |
|
||||
| **IQ3_XXS** | 472 | 974 | 1,555 | 1,449 | 1,386 | - |
|
||||
| **IQ3_S** | 419 | 893 | 1,499 | 1,285 | 1,245 | - |
|
||||
| **Coder** | 660 | 1,522 | 1,871 | 1,938 | 1,779 | 1,034** |
|
||||
| **Q2_0** | 536 | 1,299 | 2,171 | 2,126 | 2,107 | 1,304† |
|
||||
| **IQ2_XS** | 534 | 1,256 | 2,092 | 1,754 | 1,752 | 1,181*† |
|
||||
| **IQ3_XXS** | 482 | 1,007 | 1,745 | 1,609 | 1,602 | - |
|
||||
| **IQ3_S** | 427 | 913 | 1,624 | 1,640 | 1,443 | - |
|
||||
| **Coder** | 656 | 1,583 | 2,177 | 2,236 | 2,208 | 1,034** |
|
||||
|
||||
Engine 0.1.22 (the prompt path of 0.1.23 is the same); `bench/results/2026-09-29-speed-0122`. \* measured with
|
||||
images on (the image encoder's VRAM reserve leaves fewer experts cached). \*\* not measured again: 0.1.14.
|
||||
Engine 0.1.26; `bench/results/2026-09-29-speed-0126`. At 32K-128K that is 8-28% faster than 0.1.22. † not measured
|
||||
again: 0.1.22. \* measured with images on (the image encoder's VRAM reserve leaves fewer experts cached). \*\* not
|
||||
measured again: 0.1.14.
|
||||
|
||||
### Output (tokens/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 | - |
|
||||
| **Coder** | 53.3 | 50.6 | 53.3 | 50.8 | 44.0 | 42.8 |
|
||||
| **Q2_0** | 87.3 | 93.0 | 81.8 | 76.2 | 73.7 | 60.3† |
|
||||
| **IQ2_XS** | 79.6 | 78.6 | 76.3 | 63.7 | 62.7 | 52.8† |
|
||||
| **IQ3_XXS** | 61.9 | 61.6 | 58.5 | 57.2 | 49.0 | - |
|
||||
| **IQ3_S** | 52.4 | 53.3 | 48.3 | 46.3 | 45.5 | - |
|
||||
| **Coder** | 58.9 | 55.1 | 54.9 | 53.2 | 43.0 | 42.8† |
|
||||
|
||||
Engine 0.1.26, the same runs. † not measured again: 0.1.14.
|
||||
|
||||
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,
|
||||
@@ -67,6 +70,22 @@ VRAM. RTX 3090, the Coder at 198K context: 99 instead of 85 tokens/s output, the
|
||||
slower. It does not stream its KV cache (KV streaming is on by default from 64K), so it pays off mostly on large
|
||||
cards at long contexts.
|
||||
|
||||
**The draft layer's tokens (0.1.27, `--draft-vocab`):** the MTP draft layer can only propose tokens from a subset
|
||||
of the vocabulary (`mtp/rt/draft_vocab.bin`). Since 0.1.27 the subset includes every Chinese, Japanese and Korean
|
||||
token (106,299 ids), so answers in those languages are 15-38% faster (Q2_0, RTX 5070). Its head takes ~180 MiB of
|
||||
VRAM, which the expert cache leaves free for it (0.1.28). `START-HERE.bat --setup --draft-vocab en` keeps the
|
||||
English/code subset from before (40,525 ids, ~110 MiB less VRAM, English answers 1-2% faster; CJK answers get
|
||||
almost no drafts). `tools/draft_vocab.py` builds and inspects subsets.
|
||||
|
||||
**Low-RAM mode (engine 0.1.26, chosen by setup):** normally all of a model's experts are copied into RAM (23-50 GB,
|
||||
pinned) and the GPU holds a copy of the most-used ones. On a PC whose RAM cannot hold them beside the system (the
|
||||
experts plus ~10 GB), setup instead maps them from one file in the model's folder (`--mmap-experts`, the pack's
|
||||
`experts.bin`, +23-50 GB of disk). The OS file cache holds what the GPU does not, and it can give that memory back.
|
||||
On the Coder the engine's committed memory drops from 36 to ~13 GB, with the same answers. With a big GPU (an RTX
|
||||
5090 holds all of the Coder's experts, most of Q2_0's) it runs at nearly the usual speed. With a small one, most
|
||||
experts come from the SSD and it is much slower (setup says so). `START-HERE.bat --setup --low-ram on|off` overrides
|
||||
the choice.
|
||||
|
||||
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).
|
||||
|
||||
@@ -157,7 +176,7 @@ You need **only an NVIDIA driver** (version 580 or newer; update it with the NVI
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| GPU | NVIDIA **RTX 30, 40 or 50 series**, **12 GB VRAM or more** (8 GB runs, slowly). Measured on an RTX 5070; RTX 30/40 are untested. |
|
||||
| GPU | NVIDIA **RTX 20, 30, 40 or 50 series**, **12 GB VRAM or more** (8 GB runs, slowly). Measured on an RTX 5070 and an RTX 3090; RTX 20 (Turing, since 0.1.27) was tested by a contributor on an RTX 2070. |
|
||||
| RAM | **64 GB** recommended (see the table above). |
|
||||
| CPU | x86-64 with AVX2 (any Intel/AMD desktop CPU from the last ~8 years). AVX-512 (Ryzen 7000/9000) is a bit faster. |
|
||||
| Disk | ~70-80 GB free for the model, ~6 GB for the MTP layer (+1 GB with images). **Q2_0 on an AVX-512 CPU** also writes a one-time ~40 GB copy of its experts for the fast CPU kernel. An NVMe SSD is strongly recommended. |
|
||||
@@ -169,7 +188,7 @@ elsewhere) finds them and sets itself up the same way. The place is remembered p
|
||||
`~/.config/strata/settings.json`); `--data-dir` chooses another. Installs from before 0.1.16 are moved there by the next
|
||||
start (a rename on the same drive; files on another drive are used where they are).
|
||||
Python 3.12 if you have none (for your user account, no admin), a private Python environment, NVIDIA's CUDA libraries
|
||||
(from pip, ~0.4 GB), the ready-made Strata engine for RTX 30/40/50, the model and the MTP draft layer. If no
|
||||
(from pip, ~0.4 GB), the ready-made Strata engine for RTX 20/30/40/50, the model and the MTP draft layer. If no
|
||||
ready-made engine fits your PC, it offers to install the build tools (Visual Studio Build Tools + CUDA Toolkit on
|
||||
Windows, `build-essential` + CUDA on Ubuntu) and compiles the engine for your GPU (asks first; 20-40 minutes once).
|
||||
|
||||
|
||||
+4
-4
@@ -18,7 +18,7 @@ Strata can use it:
|
||||
```
|
||||
Your NVIDIA GPUs:
|
||||
GPU 0: NVIDIA GeForce RTX 5080, 16 GB VRAM - can be used
|
||||
GPU 1: NVIDIA GeForce RTX 2080 Ti, 22 GB VRAM - not supported - older than the RTX 30 series (compute capability 7.5; Strata needs 8.0 or newer)
|
||||
GPU 1: NVIDIA GeForce GTX 1080 Ti, 11 GB VRAM - not supported - older than the RTX 20 series (compute capability 6.1; Strata needs 7.5 or newer)
|
||||
GPU 2: NVIDIA GeForce RTX 3090, 24 GB VRAM - can be used
|
||||
...
|
||||
1) GPU 0 (NVIDIA GeForce RTX 5080, 16 GB) + GPU 2 (NVIDIA GeForce RTX 3090, 24 GB) together (recommended)
|
||||
@@ -41,7 +41,7 @@ now on; the answer is kept.
|
||||
```
|
||||
|
||||
**Not supported** (setup says so and names the cards that can be used instead):
|
||||
- a card older than the RTX 30 series (compute capability below 8.0: RTX 20, GTX 16/10);
|
||||
- a card older than the RTX 20 series (compute capability below 7.5: GTX 10 and older);
|
||||
- a card with less than 8 GB of VRAM, together with others (each card holds a copy of the dense weights and its
|
||||
own prompt buffers);
|
||||
- AMD and Intel GPUs, and a mix of NVIDIA with them.
|
||||
@@ -96,8 +96,8 @@ into the card that owns the layer.
|
||||
capped to leave room for them.
|
||||
- On Windows only 8 GiB of the expert arena is pinned (more, mapped into two GPU contexts, leaves WDDM refusing
|
||||
allocations); the PCIe share covers those layers.
|
||||
- Every card needs compute capability 8.0 (RTX 30 or newer). A Turing card (RTX 20, sm_75) builds only with the
|
||||
experimental `-DSTRATA_EXPERIMENTAL_SM75=ON`.
|
||||
- Every card needs compute capability 7.5 (RTX 20 or newer). The pre-sm_80 QSA scorer path is fp32 FMAs, so a
|
||||
Turing card runs the same kernels instead of the tensor-core prompt attention.
|
||||
|
||||
## Measured
|
||||
|
||||
|
||||
@@ -53,6 +53,9 @@ public:
|
||||
/// KV streaming: refill the ring of the drafter's window from its host copy for a sequence that continues at
|
||||
/// `upto` (a conversation-cache resume). No-op unless the drafter's K/V is a ring.
|
||||
void kv_restore(int64_t upto);
|
||||
/// The VRAM bind() will allocate for a native head of `head_row_bytes` per vocabulary row: the draft logits and
|
||||
/// the draft head over rt/draft_vocab.bin's subset. The expert cache is sized before bind(), so it reserves this.
|
||||
uint64_t bind_bytes(uint64_t head_row_bytes, int64_t n_vocab) const;
|
||||
/// The main model's embedding and head, and the verify window's final residuals (T rows, hc*n_embd each).
|
||||
bool bind(const WeightTable& wt, const NativeHead* head, const float* window_R, std::string& err);
|
||||
|
||||
@@ -73,6 +76,19 @@ public:
|
||||
double ms_draft = 0, ms_prefill = 0;
|
||||
int64_t rounds = 0;
|
||||
|
||||
/// E-9: the prompt path computes this layer's prompt K/V in batches (Prefill::draft_kv): its tensors, its K/V
|
||||
/// state, the first cell a round can still read, its device, and a wait for its own stream.
|
||||
const float* tensor_f32(const char* name) const { return f32(name); }
|
||||
const uint16_t* tensor_bf16(const char* name) const { return bf16(name); }
|
||||
const void* tensor_q8(const char* name) const { return q8(name); }
|
||||
QsaState& kv_state_rw() { return st_; }
|
||||
int64_t first_needed() const { return (window_ > 0 && prompt_len_ > 0) ? prompt_len_ - window_ - 64 : 0; }
|
||||
int device() const { return device_; }
|
||||
bool idle(std::string& err) {
|
||||
if (cs_ && cudaStreamSynchronize(cs_) != cudaSuccess) { err = "mtp: its stream failed"; return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
bool record_forward(int T, int step_row0, cudaStream_t cs, std::string& err);
|
||||
bool capture_prefill(int T, std::string& err);
|
||||
|
||||
@@ -34,8 +34,19 @@ struct PrefillStats {
|
||||
double ms_ple = 0;
|
||||
};
|
||||
|
||||
} // namespace strata::prefill
|
||||
namespace strata::core { class MtpDrafter; }
|
||||
namespace strata::prefill {
|
||||
|
||||
class Prefill {
|
||||
public:
|
||||
/// E-9: the draft layer's K/V for prompt cells [cell0, cell0 + n) from their final residual rows `R_rows`
|
||||
/// (device) and `next_tokens` (host: the token at cell+1), in batches through this path's GEMMs and its idle
|
||||
/// scratch - call it from on_chunk. false with `err` empty: not applicable here (a ring or hybrid K/V, another
|
||||
/// device, too little scratch; STRATA_MTP_BATCH=0), the caller runs the drafter's own pass. Not bit-identical to
|
||||
/// that pass (FP16 GEMMs instead of Q8_1 activations): the drafts may differ, never the target's tokens' logits.
|
||||
bool draft_kv(core::MtpDrafter& mtp, const float* R_rows, const int32_t* next_tokens, int64_t n, int64_t cell0,
|
||||
std::string& err);
|
||||
Prefill();
|
||||
~Prefill();
|
||||
Prefill(const Prefill&) = delete;
|
||||
|
||||
+64
-8
@@ -242,6 +242,61 @@ CALL_START = "<tool_call>"
|
||||
CALL_END = "</tool_call>"
|
||||
|
||||
|
||||
PARAM_END = "</parameter>"
|
||||
FUNC_END = "</function>"
|
||||
|
||||
|
||||
def param_end(text: str, final: bool = False) -> int:
|
||||
"""Where a parameter value in `text` ends: the first `</parameter>` followed (after whitespace) by the next
|
||||
`<parameter=` or `</function>` - the same text inside a value (a file that documents the call format, #210) is
|
||||
part of the value. -1: none yet; -2: a candidate whose follower has not arrived (streaming; `final` accepts it)."""
|
||||
at = text.find(PARAM_END)
|
||||
while at >= 0:
|
||||
after = text[at + len(PARAM_END):].lstrip()
|
||||
if after.startswith(("<parameter=", FUNC_END)):
|
||||
return at
|
||||
if not after or "<parameter=".startswith(after) or FUNC_END.startswith(after):
|
||||
return at if final else -2
|
||||
at = text.find(PARAM_END, at + 1)
|
||||
return -1
|
||||
|
||||
|
||||
def call_end(text: str) -> int:
|
||||
"""Where a tool call's body ends (#210): the `</tool_call>` after the call's own `</function>`, found by walking
|
||||
its parameters with param_end, so a value may contain either tag. -1: not complete yet. A body that is not in
|
||||
the call format ends at the first `</tool_call>`, as before."""
|
||||
s = text.lstrip()
|
||||
pos = len(text) - len(s)
|
||||
if not s.startswith("<function="):
|
||||
return text.find(CALL_END) if not "<function=".startswith(s) else -1
|
||||
gt = text.find(">", pos)
|
||||
if gt < 0:
|
||||
return -1
|
||||
pos = gt + 1
|
||||
while True:
|
||||
rest = text[pos:]
|
||||
s = rest.lstrip()
|
||||
pos += len(rest) - len(s)
|
||||
if s.startswith("<parameter="):
|
||||
gt = text.find(">", pos)
|
||||
if gt < 0:
|
||||
return -1
|
||||
end = param_end(text[gt + 1:])
|
||||
if end < 0:
|
||||
return -1
|
||||
pos = gt + 1 + end + len(PARAM_END)
|
||||
elif s.startswith(FUNC_END):
|
||||
rest = text[pos + len(FUNC_END):]
|
||||
s = rest.lstrip()
|
||||
if s.startswith(CALL_END):
|
||||
return pos + len(FUNC_END) + len(rest) - len(s)
|
||||
return -1 if CALL_END.startswith(s) else text.find(CALL_END, pos)
|
||||
elif not s or "<parameter=".startswith(s) or FUNC_END.startswith(s):
|
||||
return -1
|
||||
else:
|
||||
return text.find(CALL_END, pos)
|
||||
|
||||
|
||||
def parse_tool_call(body: str, schema: dict | None = None) -> ToolCall:
|
||||
"""`<function=NAME>\\n<parameter=P>\\nVALUE\\n</parameter>...</function>` -> ToolCall. Values are JSON-decoded
|
||||
when the tool's schema says the parameter is not a string (or, without a schema, when they parse as JSON
|
||||
@@ -257,9 +312,9 @@ def parse_tool_call(body: str, schema: dict | None = None) -> ToolCall:
|
||||
rest = rest[rest.index("<parameter=") + len("<parameter="):]
|
||||
pname = rest[:rest.index(">")]
|
||||
rest = rest[rest.index(">") + 1:]
|
||||
end = rest.find("</parameter>")
|
||||
end = param_end(rest, final=True)
|
||||
value = rest[:end] if end >= 0 else rest
|
||||
rest = rest[end + len("</parameter>"):] if end >= 0 else ""
|
||||
rest = rest[end + len(PARAM_END):] if end >= 0 else ""
|
||||
if value.startswith("\n"):
|
||||
value = value[1:]
|
||||
if value.endswith("\n"):
|
||||
@@ -351,16 +406,17 @@ class OutputParser:
|
||||
self.sp += 1
|
||||
rest = rest[1:]
|
||||
self.sval_started = True
|
||||
end = rest.find("</parameter>")
|
||||
end = param_end(rest)
|
||||
if end >= 0:
|
||||
value = rest[:end]
|
||||
if value.endswith("\n"):
|
||||
value = value[:-1]
|
||||
args(json.dumps(value)[1:-1] + '"')
|
||||
self.sp += end + len("</parameter>")
|
||||
self.sp += end + len(PARAM_END)
|
||||
self.ss = "between"
|
||||
continue
|
||||
safe = len(rest) - self._hold(rest, ("</parameter>",))
|
||||
# an undecided </parameter> (-2) is held from its start, like a tag still arriving
|
||||
safe = rest.find(PARAM_END) if end == -2 else len(rest) - self._hold(rest, (PARAM_END,))
|
||||
if safe > 0 and rest[safe - 1] == "\n": # may be the trailing newline before </parameter>
|
||||
safe -= 1
|
||||
if safe > 0:
|
||||
@@ -368,7 +424,7 @@ class OutputParser:
|
||||
self.sp += safe
|
||||
return out
|
||||
elif self.ss == "raw":
|
||||
end = rest.find("</parameter>")
|
||||
end = param_end(rest)
|
||||
if end < 0:
|
||||
return out
|
||||
value = rest[:end]
|
||||
@@ -381,7 +437,7 @@ class OutputParser:
|
||||
except ValueError:
|
||||
v = value
|
||||
args(json.dumps(v, ensure_ascii=False))
|
||||
self.sp += end + len("</parameter>")
|
||||
self.sp += end + len(PARAM_END)
|
||||
self.ss = "between"
|
||||
else:
|
||||
return out
|
||||
@@ -449,7 +505,7 @@ class OutputParser:
|
||||
self.buf = self.buf[i + len(CALL_START):]
|
||||
self.state = "call"
|
||||
else:
|
||||
i = self.buf.find(CALL_END)
|
||||
i = call_end(self.buf)
|
||||
if self.stream_tools:
|
||||
if i >= 0:
|
||||
whole, self.buf = self.buf, self.buf[:i] # scan only the body
|
||||
|
||||
+43
-11
@@ -24,6 +24,7 @@ import argparse
|
||||
import collections
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import codecs
|
||||
import json
|
||||
import os
|
||||
@@ -49,6 +50,7 @@ from serve.mcp import McpCancelled, hub_from_config # noqa: E402
|
||||
|
||||
IM_END = "<|im_end|>"
|
||||
IMAGE_PAD = "<|image_pad|>"
|
||||
VISION_START = "<|vision_start|>"
|
||||
CTX_SLACK = 8 # `strata --serve` rejects prompt + max_new + 8 > context: keep the same margin here
|
||||
# The live tok/s is a rate over a window, not a mean since the first token: a mean reads ~1/elapsed at the first
|
||||
# token (the Monitor showed five-digit numbers) and then undershoots for the first second of every answer.
|
||||
@@ -215,6 +217,11 @@ class StrataEngine:
|
||||
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.")
|
||||
rc = self.proc.poll()
|
||||
last = next((x.strip() for x in reversed(tail.splitlines()) if x.strip().startswith(("strata", "ERR"))), "")
|
||||
if rc is not None and rc >= 0 and last: # it ended by itself: its own last words say why (#215)
|
||||
return (f"The engine exited (code {rc}). Its last log line: {last} - if that does not explain it, please "
|
||||
"report it at github.com/Niko1221/Strata/issues with the log.")
|
||||
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).")
|
||||
@@ -720,17 +727,24 @@ class Service:
|
||||
raise ValueError("this server was started without the vision encoder (run setup again and choose "
|
||||
"'vision'), so it cannot read images")
|
||||
pad = self.tok.encode(IMAGE_PAD, parse_special=True)[0]
|
||||
start = self.tok.encode(VISION_START, parse_special=True)[0]
|
||||
# Encode only while the engine is idle: the engine and the image encoder (a separate process) must not
|
||||
# run on the GPU at the same time - an encode during a running request left that request stuck at
|
||||
# "reading the prompt" with CPU and GPU busy, for good (reproduced). So encoding takes its turn in the
|
||||
# same FIFO as the requests.
|
||||
with self.fifo:
|
||||
encoded = [self.vision.encode(src) for src in images]
|
||||
# one <|image_pad|> per image -> one per image token. Only the markers the template writes for an image
|
||||
# (right after <|vision_start|>) are images: the same text inside a message (an agent reading these docs,
|
||||
# #150) is kept as plain text, or it took an image's place and the counts no longer matched.
|
||||
literal = self.tok.encode(IMAGE_PAD, parse_special=False)
|
||||
out, k = [], 0
|
||||
for t in ids: # one <|image_pad|> per image -> one per image token
|
||||
if t == pad and k < len(encoded):
|
||||
for j, t in enumerate(ids):
|
||||
if t == pad and j > 0 and ids[j - 1] == start and k < len(encoded):
|
||||
out += [pad] * encoded[k][1]
|
||||
k += 1
|
||||
elif t == pad:
|
||||
out += literal
|
||||
else:
|
||||
out.append(t)
|
||||
if k != len(encoded):
|
||||
@@ -906,6 +920,8 @@ class Service:
|
||||
if os.environ.get("STRATA_DEBUG") and raw_ids:
|
||||
print(f"[strata] raw: {self.tok.decode(raw_ids)!r}", flush=True)
|
||||
self.status["busy"] = False
|
||||
self.status.pop("tail", None) # #212: the answer's end is not kept once it is done
|
||||
self.status.pop("tool", None)
|
||||
for ev in parser.finish():
|
||||
yield "event", ev
|
||||
yield "done", {"finish": finish, "completion_tokens": n, "reused": (timings or {}).get("cache_n", 0),
|
||||
@@ -1237,7 +1253,7 @@ def make_handler(svc: Service):
|
||||
return True
|
||||
auth = self.headers.get("Authorization", "")
|
||||
given = auth[7:].strip() if auth.lower().startswith("bearer ") else self.headers.get("x-api-key", "")
|
||||
if given == svc.api_key:
|
||||
if hmac.compare_digest(given.encode(), svc.api_key.encode()): # #213: constant-time
|
||||
return True
|
||||
self._json(401, {"error": {"type": "authentication_error", "message": "missing or wrong API key"}})
|
||||
return False
|
||||
@@ -1302,6 +1318,8 @@ def make_handler(svc: Service):
|
||||
self._json(200, {"status": "ok", "max_context": svc.engine.max_context, "model": svc.model,
|
||||
"images": svc.vision is not None, "api_key": bool(svc.api_key)})
|
||||
elif path == "/status":
|
||||
if not self._authorized(): # #212: it shows the end of the last answer
|
||||
return
|
||||
with svc.status_lock:
|
||||
s = dict(svc.status)
|
||||
now = time.time()
|
||||
@@ -1493,6 +1511,10 @@ class Server(ThreadingHTTPServer):
|
||||
# either one (a forgotten second start of run-<model>.bat). Without it the second start fails loudly instead.
|
||||
allow_reuse_address = os.name != "nt"
|
||||
|
||||
def handle_error(self, request, client_address):
|
||||
if not isinstance(sys.exc_info()[1], ConnectionError): # a client that hangs up needs no stack trace
|
||||
super().handle_error(request, client_address)
|
||||
|
||||
|
||||
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
|
||||
@@ -1716,6 +1738,12 @@ def main() -> int:
|
||||
model_name=cfg.get("model_name", "qwen3.8-flash-next"), vision=vision,
|
||||
sampling_defaults=sampling_defaults,
|
||||
fit_max_tokens=a.fit_max_tokens or cfg.get("fit_max_tokens") is True)
|
||||
if ("STRATA_API_KEY" in os.environ and not os.environ["STRATA_API_KEY"].strip()) or any(x == "--api-key" and i + 1 < len(sys.argv) and not sys.argv[i + 1].strip() or x.strip() == "--api-key="
|
||||
for i, x in enumerate(sys.argv)):
|
||||
# #213: an empty key would switch authentication off without a word
|
||||
print("[strata] an API key was given but it is empty: set a key, or leave --api-key / STRATA_API_KEY out",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
svc.api_key = a.api_key or cfg.get("api_key", "")
|
||||
svc.gpu_index = (gpu_list(cfg) or [0])[0] # the Monitor reads the card the engine runs on (issue #51)
|
||||
svc.gpu_indices = gpu_list(cfg) # ... or every card of a layer split (issue #112)
|
||||
@@ -1760,15 +1788,19 @@ def main() -> int:
|
||||
import webbrowser
|
||||
webbrowser.open(f"http://{'127.0.0.1' if a.host in ('0.0.0.0', '') else a.host}:{a.port}/")
|
||||
try:
|
||||
threading.Event().wait()
|
||||
while True:
|
||||
time.sleep(1) # Windows never delivers Ctrl+C to an untimed Event.wait()
|
||||
except KeyboardInterrupt:
|
||||
httpd.shutdown()
|
||||
if hasattr(engine, "close"):
|
||||
engine.close()
|
||||
if vision:
|
||||
vision.close()
|
||||
if hub is not None:
|
||||
hub.close()
|
||||
print("\n[strata] stopping (Ctrl+C again to end the engine at once) ...", flush=True)
|
||||
closers = [httpd.shutdown, getattr(engine, "close", None), vision.close if vision else None,
|
||||
hub.close if hub is not None else None]
|
||||
for close in filter(None, closers):
|
||||
try:
|
||||
close()
|
||||
except KeyboardInterrupt: # a second Ctrl+C: don't wait for the engine to free its memory
|
||||
if getattr(engine, "proc", None):
|
||||
engine.proc.kill()
|
||||
print("[strata] stopped", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -167,6 +167,90 @@ class FitMaxTokens(unittest.TestCase):
|
||||
self.assertIn("no room to answer", b["error"]["message"])
|
||||
|
||||
|
||||
class ImageMarkers(unittest.TestCase):
|
||||
"""#150: the text "<|image_pad|>" inside a message is text, not an image's place."""
|
||||
|
||||
class FakeVision:
|
||||
def __init__(self, d):
|
||||
self.dir = Path(d)
|
||||
self.rows = self.dir / "img.sve"
|
||||
self.rows.write_bytes(b"rows")
|
||||
|
||||
def encode(self, source):
|
||||
return self.rows, 3
|
||||
|
||||
def test_literal_marker_with_an_image(self):
|
||||
import tempfile
|
||||
tok = ByteTokenizer()
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
svc = Service(MockEngine(tok, "ok", max_context=CTX), tok, ChatTemplate(ROOT / "serve/chat_template.jinja"),
|
||||
vision=self.FakeVision(d))
|
||||
pad = tok.encode("<|image_pad|>", parse_special=True)[0]
|
||||
for text in ("the docs say <|image_pad|> marks an image", "plain"):
|
||||
with self.subTest(text=text):
|
||||
msgs = [{"role": "user", "content": [{"type": "text", "text": text},
|
||||
{"type": "image", "source": "x.png"}]}]
|
||||
ids, _, _ = svc.prepare(msgs, None, {})
|
||||
self.assertEqual(ids.count(pad), 3) # the image's three rows, nothing else
|
||||
self.assertIn("<|image_pad|> marks" if "docs" in text else "plain", tok.decode(ids))
|
||||
svc.embeddings.path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
class StatusNeedsTheKey(unittest.TestCase):
|
||||
"""#212: /status shows the end of the answer being written, so it needs the key like /v1/*."""
|
||||
|
||||
def test_status(self):
|
||||
tok = ByteTokenizer()
|
||||
svc = Service(MockEngine(tok, "ok", max_context=CTX), tok, ChatTemplate(ROOT / "serve/chat_template.jinja"))
|
||||
svc.api_key = "k3y"
|
||||
httpd = serve(svc, port=0)
|
||||
base = f"http://127.0.0.1:{httpd.server_address[1]}/status"
|
||||
try:
|
||||
with self.assertRaises(urllib.error.HTTPError) as e:
|
||||
urllib.request.urlopen(base, timeout=10)
|
||||
self.assertEqual(e.exception.code, 401)
|
||||
e.exception.close()
|
||||
req = urllib.request.Request(base, headers={"Authorization": "Bearer k3y"})
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
self.assertEqual(r.status, 200)
|
||||
self.assertNotIn("tail", json.loads(r.read()))
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
httpd.server_close()
|
||||
|
||||
|
||||
class ToolCallTerminators(unittest.TestCase):
|
||||
"""#210: a value that contains </parameter> or </tool_call> (a file documenting the call format) is kept whole."""
|
||||
CONTENT = ("Close each value with </parameter> and the call with </function></tool_call>.\n"
|
||||
"<parameter=x>\nnot a parameter\n</parameter>\nend")
|
||||
SCHEMA = [{"name": "write", "parameters": {"properties": {"path": {"type": "string"},
|
||||
"content": {"type": "string"}}}}]
|
||||
|
||||
def run_parser(self, stream_tools, step):
|
||||
from serve.frontend import OutputParser
|
||||
text = ("</think>\n\n<tool_call>\n<function=write>\n<parameter=path>\ndoc.md\n</parameter>\n"
|
||||
f"<parameter=content>\n{self.CONTENT}\n</parameter>\n</function>\n</tool_call>")
|
||||
p = OutputParser(thinking=True, tools=self.SCHEMA, stream_tools=stream_tools)
|
||||
evs = []
|
||||
for i in range(0, len(text), step):
|
||||
evs += p.feed(text[i:i + step])
|
||||
evs += p.finish()
|
||||
return evs
|
||||
|
||||
def test_values_keep_the_terminators(self):
|
||||
for stream_tools in (False, True):
|
||||
for step in (1, 7, 10_000):
|
||||
with self.subTest(stream_tools=stream_tools, step=step):
|
||||
evs = self.run_parser(stream_tools, step)
|
||||
calls = [e.call for e in evs if e.kind == "tool_call"]
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertEqual(calls[0].arguments, {"path": "doc.md", "content": self.CONTENT})
|
||||
self.assertFalse([e for e in evs if e.kind == "content" and e.text.strip()])
|
||||
if stream_tools:
|
||||
streamed = "".join(e.text for e in evs if e.kind == "tool_args")
|
||||
self.assertEqual(json.loads(streamed), {"path": "doc.md", "content": self.CONTENT})
|
||||
|
||||
|
||||
class ClientShapes(unittest.TestCase):
|
||||
"""What real clients send: Claude Code posts /v1/messages?beta=true (issue #55) and puts hook context into the
|
||||
conversation as a mid-conversation system message (issue #56); some OpenAI clients send a late developer message."""
|
||||
|
||||
@@ -13,7 +13,7 @@ What the first run does (each step is skipped when it is already done):
|
||||
1. checks your PC: NVIDIA GPU and driver, RAM, CPU, free disk space
|
||||
2. asks the questions
|
||||
3. installs the Python packages it needs into .venv (numpy, jinja2, ..., and NVIDIA's CUDA libraries)
|
||||
4. gets the Strata engine: a ready-made build for RTX 30/40/50 cards (no compiler needed); if none fits your PC,
|
||||
4. gets the Strata engine: a ready-made build for RTX 20/30/40/50 cards (no compiler needed); if none fits your PC,
|
||||
it installs the build tools (asks first) and compiles the engine for your GPU
|
||||
5. downloads the model from Hugging Face (resumable), and the vision encoder if you want images
|
||||
6. prepares the model for Strata and fetches the MTP draft layer (~5 GB, from the original Qwen checkpoint)
|
||||
@@ -58,11 +58,13 @@ 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, 25) # v0.1.25: faster prompts (grouping off the copy engine, fused hyper-connection kernels), AMD HIP backend, --kv k8v4; v0.1.24: long prompts faster (QSA select on tensor cores); v0.1.23: image requests honor sampling, 8 GB cards start, batched verify window; v0.1.22: faster prompts (tensor-core attention), multi-GPU across images/steering/KV streaming; v0.1.21: multi-GPU layer split (--gpus); v0.1.20: system-prompt checkpoint, PCIe probe, hit rate; v0.1.19: penalties
|
||||
MIN_ENGINE = (0, 1, 28) # v0.1.28: the expert cache reserves the draft head, a cancelled request no longer fails the next; v0.1.27: RTX 20 (sm_75) in the ready-made engine, the HIP build without CUDA headers; v0.1.26: the draft layer's prompt pass in batches; v0.1.25: faster prompts (grouping off the copy engine, fused hyper-connection kernels), AMD HIP backend, --kv k8v4; v0.1.24: long prompts faster (QSA select on tensor cores); v0.1.23: image requests honor sampling, 8 GB cards start, batched verify window; v0.1.22: faster prompts (tensor-core attention), multi-GPU across images/steering/KV streaming; v0.1.21: multi-GPU layer split (--gpus); v0.1.20: system-prompt checkpoint, PCIe probe, hit rate; v0.1.19: penalties
|
||||
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},
|
||||
# the original model only for now: Swift 1.5's Q2_0 files split one layer's experts across the two shards, which
|
||||
# the pack tool (tools/iq_pack.py) cannot prepare yet (#171)
|
||||
"Q2_0": {"about": "2-bit, the fastest", "download_gb": 66.4, "ram_gb": 48, "arena_gb": 34.0, "families": ("qwen",)},
|
||||
"IQ2_XS": {"about": "2-bit i-quant, a little better quality, close in speed", "download_gb": 68.0, "ram_gb": 48,
|
||||
"arena_gb": 35.5},
|
||||
"IQ3_XXS": {"about": "3-bit i-quant, better quality, slower (more CPU work per token)", "download_gb": 75.8,
|
||||
@@ -221,7 +223,7 @@ def cpu_info():
|
||||
name, avx2, avx512 = platform.processor() or "unknown CPU", False, False
|
||||
if WIN:
|
||||
pf = ctypes.windll.kernel32.IsProcessorFeaturePresent
|
||||
avx2 = bool(pf(40)) # PF_AVX2_INSTRUCTIONS_AVAILABLE
|
||||
avx2 = bool(pf(40)) or _cpuid_avx2() # PF_AVX2_INSTRUCTIONS_AVAILABLE, else the CPU itself (#159)
|
||||
n = out(["powershell", "-NoProfile", "-Command", "(Get-CimInstance Win32_Processor).Name"]).strip()
|
||||
name = n or name
|
||||
avx512 = bool(pf(41)) and _cpuid_avx512_full()
|
||||
@@ -259,6 +261,47 @@ def _cpuid_avx512_full() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _run_stub(code: bytes, *args) -> None:
|
||||
"""Runs a few bytes of x64 machine code (Windows calling convention: the arguments in rcx, rdx)."""
|
||||
k32 = ctypes.windll.kernel32
|
||||
k32.VirtualAlloc.restype = ctypes.c_void_p
|
||||
k32.VirtualFree.argtypes = (ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32)
|
||||
buf = k32.VirtualAlloc(None, len(code), 0x3000, 0x40)
|
||||
if not buf:
|
||||
raise OSError("VirtualAlloc failed")
|
||||
try:
|
||||
ctypes.memmove(buf, code, len(code))
|
||||
ctypes.CFUNCTYPE(None, *[ctypes.c_void_p] * len(args))(buf)(*args)
|
||||
finally:
|
||||
k32.VirtualFree(buf, 0, 0x8000)
|
||||
|
||||
|
||||
def _cpuid_avx2() -> bool:
|
||||
"""AVX2 asked from the CPU (CPUID leaf 7 EBX bit 5), with the OS saving the YMM registers (OSXSAVE + XCR0):
|
||||
Windows' IsProcessorFeaturePresent(PF_AVX2) says no on some PCs whose CPU has it (a Ryzen 9 3950X, #159)."""
|
||||
try:
|
||||
def cpuid(leaf):
|
||||
regs = (ctypes.c_uint32 * 4)()
|
||||
_run_stub(bytes([0x53, 0x49, 0x89, 0xC8, 0x89, 0xD0, 0x31, 0xC9, 0x0F, 0xA2, # push rbx; r8=rcx; eax=edx; ecx=0; cpuid
|
||||
0x41, 0x89, 0x00, 0x41, 0x89, 0x58, 0x04, 0x41, 0x89, 0x48, 0x08, # [r8]=eax, [r8+4]=ebx, [r8+8]=ecx
|
||||
0x41, 0x89, 0x50, 0x0C, 0x5B, 0xC3]), # [r8+12]=edx; pop rbx
|
||||
ctypes.addressof(regs), leaf)
|
||||
return list(regs)
|
||||
if cpuid(0)[0] < 7:
|
||||
return False
|
||||
ecx1 = cpuid(1)[2]
|
||||
if not (ecx1 >> 27) & 1 or not (ecx1 >> 28) & 1: # OSXSAVE, AVX
|
||||
return False
|
||||
xcr0 = (ctypes.c_uint32 * 2)()
|
||||
_run_stub(bytes([0x49, 0x89, 0xC8, 0x31, 0xC9, 0x0F, 0x01, 0xD0, # r8=rcx; ecx=0; xgetbv
|
||||
0x41, 0x89, 0x00, 0x41, 0x89, 0x50, 0x04, 0xC3]), ctypes.addressof(xcr0))
|
||||
if xcr0[0] & 6 != 6: # the OS saves XMM and YMM
|
||||
return False
|
||||
return bool((cpuid(7)[1] >> 5) & 1)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def gpus():
|
||||
"""Every NVIDIA GPU, numbered as nvidia-smi numbers them (by PCI bus, the order the engine is told to use)."""
|
||||
s = out(["nvidia-smi", "--query-gpu=index,name,memory.total,compute_cap,driver_version",
|
||||
@@ -285,8 +328,8 @@ def cc(g) -> str:
|
||||
|
||||
def gpu_problem(g, together=False):
|
||||
"""Why Strata cannot use this card, in plain words (None: it can)."""
|
||||
if int(g["arch"]) < 80:
|
||||
return (f"not supported - older than the RTX 30 series (compute capability {cc(g)}; Strata needs 8.0 or "
|
||||
if int(g["arch"]) < 75:
|
||||
return (f"not supported - older than the RTX 20 series (compute capability {cc(g)}; Strata needs 7.5 or "
|
||||
"newer)")
|
||||
if together and g["vram_gb"] < SPLIT_MIN_VRAM_GB - 0.5:
|
||||
return (f"not supported together with other GPUs - {g['vram_gb']:.0f} GB of VRAM (a card sharing the model "
|
||||
@@ -349,7 +392,7 @@ def check_gpus(sel, found, what="") -> None:
|
||||
ones = " or ".join(f"--gpu {x['index']}" for x in single)
|
||||
both = "--gpus " + ",".join(str(x["index"]) for x in can) if can else ""
|
||||
hint = ((f"use these together: {both}" + (f" (or one card: {ones})" if not together else "")) if can else
|
||||
f"use one card: {ones}" if single else "Strata needs an NVIDIA RTX 30 series or newer card")
|
||||
f"use one card: {ones}" if single else "Strata needs an NVIDIA RTX 20 series or newer card")
|
||||
fail(f"GPU {i}{'' if g is None else ' (' + g['name'] + ')'} {what}cannot be used: {p}", hint)
|
||||
|
||||
|
||||
@@ -384,7 +427,7 @@ def choose_gpus(a, found) -> list:
|
||||
single = sorted([g for g in found if gpu_problem(g) is None], key=lambda x: (-round(x["vram_gb"]), x["index"]))
|
||||
if not single:
|
||||
gpu_table(found)
|
||||
fail("none of your GPUs can run Strata", "it needs an NVIDIA RTX 30 series or newer (compute capability 8.0+)")
|
||||
fail("none of your GPUs can run Strata", "it needs an NVIDIA RTX 20 series or newer (compute capability 7.5+)")
|
||||
can = together_ok(found)
|
||||
if not can:
|
||||
return [single[0]["index"]]
|
||||
@@ -566,6 +609,18 @@ def download(url, dst: Path, what=None):
|
||||
ok(f"{what or dst.name} downloaded")
|
||||
|
||||
|
||||
def whole_shard(s: Path) -> bool:
|
||||
"""A shard as long as its own tensor directory says (check_shards' test, without stopping setup)."""
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
from gguf_reader import GGUFFile
|
||||
try:
|
||||
g = GGUFFile(s)
|
||||
return s.stat().st_size >= g.data_start + max((t.offset + (t.expected_bytes() or 0) for t in g.tensors),
|
||||
default=0)
|
||||
except (OSError, ValueError, struct.error):
|
||||
return False
|
||||
|
||||
|
||||
def check_shards(shards):
|
||||
"""Every shard present and whole, or setup stops naming the file and the numbers. Whole means as long as
|
||||
its own tensor directory says (the header is read, the data is not): a truncated copy (--gguf-dir, a .part
|
||||
@@ -597,7 +652,9 @@ def get_llama_cpp():
|
||||
tmp = ROOT / "third_party" / "_unpack"
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
with zipfile.ZipFile(z) as f:
|
||||
f.extractall(tmp)
|
||||
# llama.cpp's own web UI (tools/ui) is not used, and its deep paths passed Windows' 260-character limit in a
|
||||
# folder like Downloads\Strata-main\Strata-main (#206)
|
||||
f.extractall(tmp, [m for m in f.namelist() if "/tools/ui/" not in m])
|
||||
top = next(tmp.iterdir())
|
||||
shutil.rmtree(llama, ignore_errors=True)
|
||||
# PR #63: on Windows a rename can fail with PermissionError while an antivirus scanner still holds a file of the
|
||||
@@ -773,7 +830,16 @@ def get_prebuilt(url_base, gpu, vision, updating=False) -> Path | None:
|
||||
if info.exists() and (eng / EXE).exists():
|
||||
meta = json.loads(info.read_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:
|
||||
if meta.get("source") == "local": # compiled here: build_engine checks its source and cards
|
||||
return None
|
||||
have = [int(a) for a in meta.get("archs", [])]
|
||||
miss = [int(x) for x in gpu.get("archs", [gpu["arch"]])
|
||||
if have and int(x) not in have and not (meta.get("ptx") and int(x) > max(have))]
|
||||
if miss: # a card it has no code for (#128): compiled here instead
|
||||
warn(f"the installed engine is built for {', '.join(str(a) for a in have)}; your GPU is "
|
||||
f"{', '.join(str(x) for x in miss)}: compiling instead")
|
||||
return None
|
||||
if ver >= MIN_ENGINE:
|
||||
ok("ready-made engine already installed")
|
||||
return eng
|
||||
say(f" Updating the ready-made engine ({meta.get('version')} -> {'.'.join(map(str, MIN_ENGINE))} or newer) ...")
|
||||
@@ -898,7 +964,8 @@ def update_installed_engine(url_base) -> None:
|
||||
def install_build_tools(gpu, yes):
|
||||
"""The compiler and the CUDA toolkit, installed for the user (asks once). Returns (nvcc, vcvars)."""
|
||||
nvcc, cuda_v = find_nvcc()
|
||||
need_cuda = (12, 8) if max(int(x) for x in gpu.get("archs", [gpu["arch"]])) >= 120 else (12, 0)
|
||||
# RTX 50 (sm_120): CUDA 13.0 - an engine built with 12.8 crashed in the prompt path on Linux (#220)
|
||||
need_cuda = (13, 0) if max(int(x) for x in gpu.get("archs", [gpu["arch"]])) >= 120 else (12, 0)
|
||||
vcvars = find_vcvars() if WIN else None
|
||||
have_cc = vcvars is not None if WIN else shutil.which("g++") is not None
|
||||
missing = []
|
||||
@@ -1004,16 +1071,24 @@ def build_engine(gpu, vision, yes, llama) -> Path:
|
||||
want_vision = vision != "none"
|
||||
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
|
||||
archs = sorted({int(x) for x in gpu.get("archs", [gpu["arch"]])}) # every card the model runs on
|
||||
built = {int(x) for x in meta.get("archs", [])}
|
||||
# a card the engine has no code for (a GPU added with --gpus, #128) needs a compile even when the source is the
|
||||
# same; the compile keeps the generations it was built for
|
||||
new_arch = local and not set(archs) <= built
|
||||
engine_ok = local and (eng / EXE).exists() and meta.get("src") == src and not new_arch
|
||||
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)
|
||||
archs = sorted({int(x) for x in gpu.get("archs", [gpu["arch"]])}) # every card the model runs on
|
||||
if local:
|
||||
archs = sorted(built | set(archs))
|
||||
nvcc, vcvars = install_build_tools({**gpu, "archs": archs}, yes)
|
||||
cuda_archs = ";".join(str(x) for x in archs)
|
||||
if not engine_ok:
|
||||
say(" The engine's source changed: compiling it again (only what changed, a few minutes) ..."
|
||||
say(" Compiling the engine for " + ", ".join(f"sm_{x}" for x in archs) + " (a card it had no code for; "
|
||||
"10-20 minutes, once) ..." if new_arch else
|
||||
" 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={cuda_archs}",
|
||||
@@ -1043,6 +1118,26 @@ def build_engine(gpu, vision, yes, llama) -> Path:
|
||||
DATA_ITEMS = ("models", "packs", "mtp")
|
||||
|
||||
|
||||
LOW_RAM_HEADROOM_GB = 10 # RAM beside the experts: the OS, the engine's other buffers, the server
|
||||
|
||||
|
||||
def low_ram_needed(model, ram) -> bool:
|
||||
"""The model's experts do not fit this PC's RAM with room left for the rest: they are then mapped from the pack's
|
||||
experts.bin instead of copied into RAM (the low-RAM mode)."""
|
||||
return ram < MODELS[model]["arena_gb"] + LOW_RAM_HEADROOM_GB
|
||||
|
||||
|
||||
def low_ram_gpu_share(model, vram_gb) -> float:
|
||||
"""About how much of the model's experts the GPU holds (VRAM minus ~5 GB for the dense weights, KV, buffers)."""
|
||||
return max(0.0, min(1.0, (vram_gb - 5) / MODELS[model]["arena_gb"]))
|
||||
|
||||
|
||||
def low_ram_fits(model, ram, vram_gb) -> bool:
|
||||
"""In the low-RAM mode: the experts the GPU does not hold fit the RAM left beside the rest (as file cache)."""
|
||||
arena = MODELS[model]["arena_gb"]
|
||||
return ram - 6 + max(0.0, vram_gb - 5) >= arena
|
||||
|
||||
|
||||
def settings_path() -> Path:
|
||||
if WIN:
|
||||
return Path(os.environ.get("APPDATA") or Path.home() / "AppData" / "Roaming") / "Strata" / "settings.json"
|
||||
@@ -1165,7 +1260,18 @@ def data_folder(requested: str | None) -> tuple:
|
||||
warn(f"cannot use {dest} for the model files ({e}): keeping them in {ROOT}")
|
||||
dest = ROOT
|
||||
elsewhere = []
|
||||
for folder in [ROOT, *other_installs(settings)]:
|
||||
# #198: the data folder remembered before (a --data-dir to a new place) is a source too, and so is a Strata-data
|
||||
# folder nested in any of them (an install that kept its models one level down)
|
||||
sources = [ROOT, *other_installs(settings)]
|
||||
if settings.get("data_dir") and Path(settings["data_dir"]) != dest:
|
||||
sources.append(Path(settings["data_dir"]))
|
||||
sources += [f / "Strata-data" for f in list(sources) if (f / "Strata-data") != dest]
|
||||
seen = set()
|
||||
for folder in sources:
|
||||
key = os.path.normcase(str(folder))
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
if folder == dest or not has_data(folder):
|
||||
continue
|
||||
if not same_drive(folder, dest):
|
||||
@@ -1338,12 +1444,21 @@ def upgrade_config(cfg_path: Path, cfg: dict) -> dict:
|
||||
|
||||
|
||||
def start(cfg_path: Path, port: int | None, gpu: int | list | None = None, open_browser=True, yes=False,
|
||||
layer_split=None) -> int:
|
||||
layer_split=None, keep=None) -> int:
|
||||
"""keep: settings given on this start that the model keeps from now on (--host, --api-key, --draft-vocab)."""
|
||||
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")
|
||||
keep = {k: v for k, v in (keep or {}).items() if v is not None}
|
||||
if keep and any(cfg.get(k) != v for k, v in keep.items()): # #179: a --host/--api-key on a start was ignored
|
||||
cfg.update(keep)
|
||||
cfg_path.write_text(json.dumps(cfg, indent=1), encoding="utf-8")
|
||||
ok("saved for this model: " + ", ".join("api key" if k == "api_key" else f"{k.replace('_', ' ')} {v}"
|
||||
for k, v in keep.items()))
|
||||
cfg_path.touch() # the most recently used model
|
||||
if "--mtp" in cfg["args"][:-1]:
|
||||
refresh_draft_vocab(Path(cfg["args"][cfg["args"].index("--mtp") + 1]), cfg.get("draft_vocab", "cjk"))
|
||||
cmd = [sys.executable, str(ROOT / "serve" / "server.py"), "--engine", "strata", "--config", str(cfg_path),
|
||||
"--port", str(port or cfg.get("port", 8080))]
|
||||
if cfg.get("backend") == "hip": # AMD: one card, numbered as HIP numbers them
|
||||
@@ -1378,11 +1493,13 @@ def start(cfg_path: Path, port: int | None, gpu: int | list | None = None, open_
|
||||
elif isinstance(use, list):
|
||||
check_gpus(use, found, "(chosen for this model) ")
|
||||
byid = {g["index"]: g for g in found}
|
||||
cfg = ensure_engine_for([byid[i] for i in use], cfg_path, cfg, yes)
|
||||
ok("GPUs: " + " + ".join(gpu_name(byid[i]) for i in use) + f" together (layers split {cfg.get('layer_split') or 'auto'})")
|
||||
elif found:
|
||||
g = next((x for x in found if x["index"] == use), None) if use is not None else max(
|
||||
found, key=lambda x: (round(x["vram_gb"]), -x["index"]))
|
||||
if g is not None:
|
||||
cfg = ensure_engine_for([g], cfg_path, cfg, yes)
|
||||
ok("GPU: " + gpu_name(g))
|
||||
if open_browser:
|
||||
cmd.append("--open")
|
||||
@@ -1403,13 +1520,60 @@ def start(cfg_path: Path, port: int | None, gpu: int | list | None = None, open_
|
||||
return subprocess.call(cmd)
|
||||
|
||||
|
||||
# the draft subsets setup copied before (sha256): replaced by the current one, a subset made by hand is kept
|
||||
OLD_DRAFT_VOCABS = {"369151522226a5edaa5f12cfd1e2ae7db8f4fbdbd222f3dcf327dced9597fb25"} # to 0.1.26: 27 Han tokens
|
||||
|
||||
|
||||
DRAFT_VOCABS = {"cjk": "draft_vocab.bin", "en": "draft_vocab_en.bin"}
|
||||
|
||||
|
||||
def refresh_draft_vocab(rt: Path, choice: str = "cjk") -> None:
|
||||
"""The draft layer's token subset in the MTP folder: `cjk` (data/draft_vocab.bin, since 0.1.27, #137) or `en`
|
||||
(data/draft_vocab_en.bin, the English/code subset before it: ~110 MiB less VRAM, English answers 1-2% faster).
|
||||
Copied when missing or when a shipped subset other than the chosen one is there; a subset made by hand is kept."""
|
||||
new, dst = ROOT / "data" / DRAFT_VOCABS.get(choice, "draft_vocab.bin"), rt / "draft_vocab.bin"
|
||||
if not new.exists() or not rt.is_dir():
|
||||
return
|
||||
if dst.exists():
|
||||
old = hashlib.sha256(dst.read_bytes()).hexdigest()
|
||||
shipped = OLD_DRAFT_VOCABS | {hashlib.sha256((ROOT / "data" / f).read_bytes()).hexdigest()
|
||||
for f in DRAFT_VOCABS.values() if (ROOT / "data" / f).exists()}
|
||||
if old not in shipped or old == hashlib.sha256(new.read_bytes()).hexdigest():
|
||||
return
|
||||
ok("draft layer: the token subset " + ("with Chinese, Japanese and Korean" if choice == "cjk" else
|
||||
"for English and code (less VRAM)"))
|
||||
shutil.copyfile(new, dst)
|
||||
|
||||
|
||||
def ensure_engine_for(cards, cfg_path: Path, cfg: dict, yes: bool) -> dict:
|
||||
"""The installed engine must have code for every card the model starts on: a card added later (--gpus with an
|
||||
older or newer generation, #128) or a new GPU in the PC otherwise stops the start with 'no kernel image'. Such a
|
||||
card gets the engine compiled for all of them, before the start."""
|
||||
missing = [g for g in cards if not engine_runs_on(g)]
|
||||
if not missing:
|
||||
return cfg
|
||||
info = ROOT / "engine" / "BUILD.json"
|
||||
meta = json.loads(info.read_text())
|
||||
say()
|
||||
say(" The installed engine has no code for " + ", ".join(f"{g['name']} (sm_{g['arch']})" for g in missing) +
|
||||
": it is compiled for " + ("these cards" if len(cards) > 1 else "it") + " now.")
|
||||
main = gpu_info(cards[0]["index"])
|
||||
archs = sorted({int(x) for x in meta.get("archs", [])} | {int(g["arch"]) for g in cards})
|
||||
vision = meta.get("vision") or ("gpu" if (ROOT / "engine" / VEXE).exists() else "none")
|
||||
build_engine({**main, "archs": archs}, vision, yes, get_llama_cpp())
|
||||
dirs = json.loads(info.read_text()).get("cuda_dirs") or []
|
||||
cfg["lib_dirs"] = dirs + [d for d in cfg.get("lib_dirs") or [] if d not in dirs]
|
||||
cfg_path.write_text(json.dumps(cfg, indent=1), encoding="utf-8")
|
||||
return cfg
|
||||
|
||||
|
||||
def write_run_script(model, cfg_path, port):
|
||||
serve = [sys.executable, str(ROOT / "serve" / "server.py"), "--engine", "strata", "--config", str(cfg_path),
|
||||
"--port", str(port), "--open"]
|
||||
if WIN:
|
||||
script = ROOT / f"run-{model.lower()}.bat"
|
||||
script.write_text("@echo off\r\ntitle Strata " + model + "\r\ncd /d \"" + str(ROOT) + "\"\r\n" +
|
||||
" ".join(f'"{x}"' for x in serve) + "\r\npause\r\n", encoding="utf-8")
|
||||
" ".join(f'"{x}"' for x in serve) + "\r\nif errorlevel 1 pause\r\n", encoding="utf-8")
|
||||
else:
|
||||
script = ROOT / f"run-{model.lower()}.sh"
|
||||
script.write_text("#!/bin/sh\ncd \"" + str(ROOT) + "\"\nexec " + " ".join(f'"{x}"' for x in serve) + "\n",
|
||||
@@ -1456,6 +1620,12 @@ def main() -> int:
|
||||
ap.add_argument("--check", action="store_true", help="only check this PC and exit")
|
||||
ap.add_argument("--calibrate", action="store_true",
|
||||
help="tune the engine's settings for this PC (about 5-10 minutes), then start the model")
|
||||
ap.add_argument("--draft-vocab", choices=list(DRAFT_VOCABS),
|
||||
help="the draft layer's tokens: cjk = with Chinese, Japanese and Korean (default), en = English "
|
||||
"and code only (~110 MiB less VRAM, English answers 1-2%% faster)")
|
||||
ap.add_argument("--low-ram", choices=["auto", "on", "off"], default="auto",
|
||||
help="map the model's experts from its folder instead of copying them into RAM (for a PC with a big "
|
||||
"GPU and little RAM); auto: when the experts would not fit the RAM")
|
||||
ap.add_argument("--backend", choices=["cuda", "hip"],
|
||||
help="cuda = NVIDIA (default), hip = AMD RX 7900 XT/XTX on Linux (experimental; chosen by itself "
|
||||
"when the PC has no NVIDIA card Strata can use)")
|
||||
@@ -1512,29 +1682,44 @@ def main() -> int:
|
||||
say(f" {i}) {json.loads(c.read_text(encoding='utf-8-sig')).get('model_name', c.stem)}")
|
||||
pick_cfg = have[int(ask("Tune which one?", [str(i) for i in range(1, len(have) + 1)], "1", a.yes)) - 1]
|
||||
calibrate_config(pick_cfg)
|
||||
return 0 if a.no_start else start(pick_cfg, a.port, run_gpu, yes=a.yes, layer_split=a.layer_split)
|
||||
return 0 if a.no_start else start(pick_cfg, a.port, run_gpu, yes=a.yes, layer_split=a.layer_split,
|
||||
keep={"host": a.host, "api_key": a.api_key, "draft_vocab": a.draft_vocab})
|
||||
if have and not (a.setup or a.model or a.family or a.check or a.no_start):
|
||||
if not a.build:
|
||||
update_installed_engine(a.prebuilt)
|
||||
if len(have) == 1:
|
||||
return start(have[0], a.port, run_gpu, yes=a.yes, layer_split=a.layer_split)
|
||||
return start(have[0], a.port, run_gpu, yes=a.yes, layer_split=a.layer_split,
|
||||
keep={"host": a.host, "api_key": a.api_key, "draft_vocab": a.draft_vocab})
|
||||
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], a.port, run_gpu, yes=a.yes, layer_split=a.layer_split)
|
||||
return start(have[pick - 1], a.port, run_gpu, yes=a.yes, layer_split=a.layer_split,
|
||||
keep={"host": a.host, "api_key": a.api_key, "draft_vocab": a.draft_vocab})
|
||||
|
||||
# ---- 1. the PC
|
||||
step(1, "checking your PC")
|
||||
found = gpus()
|
||||
amd = [] if WIN else amd_gpus()
|
||||
hip = a.backend == "hip" or (a.backend is None and not any(gpu_problem(g) is None for g in found)
|
||||
and any(amd_problem(g) is None for g in amd))
|
||||
nv_ok = any(gpu_problem(g) is None for g in found)
|
||||
amd_ok = [g for g in amd if amd_problem(g) is None]
|
||||
hip = a.backend == "hip" or (a.backend is None and not nv_ok and bool(amd_ok))
|
||||
if a.backend is None and nv_ok and amd_ok:
|
||||
# both kinds of card: asked (a first run on such a PC used to take NVIDIA without mentioning the Radeon)
|
||||
say()
|
||||
say(" This PC has NVIDIA and AMD cards Strata can use:")
|
||||
say(" 1) NVIDIA: " + ", ".join(f"{g['name']} ({g['vram_gb']:.0f} GB)" for g in found if gpu_problem(g) is None)
|
||||
+ " (recommended)")
|
||||
say(" 2) AMD: " + ", ".join(f"{g['name']} ({g['vram_gb']:.0f} GB)" for g in amd_ok)
|
||||
+ " (experimental: compiled here, one GPU, no images - docs/AMD_HIP.md)")
|
||||
hip = ask("Which cards?", ["1", "2"], "1", a.yes or a.check) == "2"
|
||||
if a.check and not hip:
|
||||
say(" (the AMD card: ./setup.sh --backend hip)")
|
||||
if hip: # AMD (experimental): one card, compiled here
|
||||
if WIN:
|
||||
fail("Strata's AMD backend runs on Linux only", "use an NVIDIA RTX 30 series or newer card on Windows")
|
||||
fail("Strata's AMD backend runs on Linux only", "use an NVIDIA RTX 20 series or newer card on Windows")
|
||||
say(" Your AMD GPUs:" if amd else " No AMD GPU found (the amdgpu driver's KFD topology is empty).")
|
||||
for g in amd:
|
||||
say(f" GPU {g['index']}: {g['name']}, {g['vram_gb']:.0f} GB VRAM - " + (amd_problem(g) or "can be used"))
|
||||
@@ -1578,13 +1763,15 @@ def main() -> int:
|
||||
ram = ram_gb()
|
||||
cpu, avx2, avx512 = cpu_info()
|
||||
need = min(d["ram_gb"] for d in MODELS.values())
|
||||
if ram < need - 4 and not a.check:
|
||||
low_ok = low_ram_fits("IQ1_M", ram, gpu["vram_gb"]) and a.low_ram != "off" # the smallest model, mapped
|
||||
if ram < need - 4 and not a.check and not low_ok:
|
||||
# every model keeps ALL its experts in RAM (23+ GB); VRAM only holds a copy of the most-used ones, so a
|
||||
# bigger GPU does not lower this
|
||||
fail(f"RAM: {ram:.0f} GB - the smallest model (the Coder) needs about {need} GB",
|
||||
"Strata keeps all of the model's experts in RAM (23-50 GB, whatever the GPU) and the GPU holds a copy "
|
||||
"of the most-used ones: it needs 32 GB of RAM or more (48 GB for the full model)")
|
||||
ok(f"RAM: {ram:.0f} GB" if ram >= need - 4 else f"RAM: {ram:.0f} GB (less than the {need} GB the smallest model needs)")
|
||||
ok(f"RAM: {ram:.0f} GB" if ram >= need - 4 else f"RAM: {ram:.0f} GB (less than the {need} GB the smallest model needs)"
|
||||
+ ("; the GPU's VRAM makes up for it (the low-RAM mode)" if ram < need - 4 and low_ok else ""))
|
||||
pf = page_file_gb()
|
||||
if pf is not None and pf < 4:
|
||||
warn(f"Windows' page file is {pf:.1f} GB: the graphics card's memory needs room there too (issue #60), so "
|
||||
@@ -1597,6 +1784,9 @@ def main() -> int:
|
||||
say()
|
||||
for m, d in MODELS.items():
|
||||
verdict = "fits" if ram >= d["ram_gb"] else "tight" if ram >= d["ram_gb"] - 8 else "does not fit"
|
||||
if low_ram_needed(m, ram) and low_ram_fits(m, ram, gpu["vram_gb"]) and a.low_ram != "off":
|
||||
verdict = (f"fits in the low-RAM mode (the GPU holds ~{100 * low_ram_gpu_share(m, gpu['vram_gb']):.0f}% "
|
||||
"of its experts)")
|
||||
say(f" {m:8s} needs ~{d['ram_gb']} GB RAM: {verdict}")
|
||||
say("\nThis PC can run Strata. Run it again without --check to install.")
|
||||
return 0
|
||||
@@ -1622,10 +1812,23 @@ def main() -> int:
|
||||
for i, m in enumerate(names, 1):
|
||||
d = MODELS[m]
|
||||
fit = "" if ram >= d["ram_gb"] else f" <- needs {d['ram_gb']} GB RAM, you have {ram:.0f}"
|
||||
if low_ram_needed(m, ram) and low_ram_fits(m, ram, gpu["vram_gb"]) and a.low_ram != "off":
|
||||
fit = f" <- fits in the low-RAM mode (the GPU holds ~{100 * low_ram_gpu_share(m, gpu['vram_gb']):.0f}%)"
|
||||
say(f" {i}) {m:8s} {d['about']}; download {d['download_gb']:.0f} GB, uses ~{d['arena_gb']:.0f} GB of RAM{fit}")
|
||||
rec = str(names.index("IQ3_XXS") + 1) if ram >= 60 and "IQ3_XXS" in names else "1"
|
||||
model = a.model or names[int(ask("Which size?", [str(i) for i in range(1, len(names) + 1)], rec, a.yes)) - 1]
|
||||
if ram < MODELS[model]["ram_gb"] - 4:
|
||||
low_ram = a.low_ram == "on" or (a.low_ram == "auto" and low_ram_needed(model, ram))
|
||||
if low_ram and multi:
|
||||
warn("the low-RAM mode runs on one GPU: using " + gpu_name(gpu) + " only")
|
||||
multi, sel, chosen = [], [gpu["index"]], [gpu]
|
||||
if low_ram:
|
||||
share = low_ram_gpu_share(model, gpu["vram_gb"])
|
||||
ok(f"low-RAM mode: {model}'s experts ({MODELS[model]['arena_gb']:.0f} GB) are read from the model folder "
|
||||
f"through the OS file cache instead of a copy in RAM ({ram:.0f} GB); the GPU holds ~{100 * share:.0f}% of them")
|
||||
if share < 0.6:
|
||||
warn("most of the experts are read from the SSD while it answers: expect it to be much slower than with "
|
||||
"enough RAM (a faster SSD and a smaller size help)")
|
||||
elif ram < MODELS[model]["ram_gb"] - 4:
|
||||
# #125: a warning and a question, not a stop: the user may accept paging (asked, "no" by default, so an
|
||||
# unattended --yes install still stops here)
|
||||
need_gb, arena = MODELS[model]["ram_gb"], MODELS[model]["arena_gb"]
|
||||
@@ -1706,9 +1909,13 @@ def main() -> int:
|
||||
models_dir, shards = cand[0].parent, cand
|
||||
ok(f"model files found in {models_dir}")
|
||||
break
|
||||
for s in shards: # #173: a whole file copied in by hand has no finish mark
|
||||
if s.exists() and not done(s) and whole_shard(s):
|
||||
mark(s, "whole (checked against its own tensor directory)")
|
||||
have_model = all(s.exists() and (done(s) or a.gguf_dir) for s in shards)
|
||||
need = (0 if a.gguf_dir or have_model else MODELS[model]["download_gb"]) + 8 + \
|
||||
(40 if model == "Q2_0" and avx512 and family == "qwen" else 0) + (1 if vision != "none" else 0)
|
||||
(40 if model == "Q2_0" and avx512 and family == "qwen" else 0) + (1 if vision != "none" else 0) + \
|
||||
(MODELS[model]["arena_gb"] + 1 if low_ram and not (model == "Q2_0" and avx512 and family == "qwen") else 0)
|
||||
if free_gb(models_dir) < need:
|
||||
fail(f"not enough free disk space in {models_dir}: need ~{need:.0f} GB", "use --models-dir on a bigger drive")
|
||||
|
||||
@@ -1726,6 +1933,12 @@ def main() -> int:
|
||||
if vision != "none" and not (eng / VEXE).exists():
|
||||
warn("the ready-made engine has no image encoder: compiling it")
|
||||
eng = None
|
||||
elif vision == "gpu": # the encoder can cover fewer cards than the engine (RTX 20)
|
||||
m = json.loads((eng / "BUILD.json").read_text())
|
||||
va = [int(x) for x in m.get("vision_archs", m.get("archs", []))]
|
||||
if va and int(gpu["arch"]) not in va and not (m.get("ptx") and int(gpu["arch"]) > max(va)):
|
||||
warn(f"the ready-made image encoder has no code for your GPU (sm_{gpu['arch']}): compiling it")
|
||||
eng = None
|
||||
if eng is None:
|
||||
eng = build_engine_hip(gpu, llama) if hip else build_engine(gpu, vision, a.yes, llama)
|
||||
meta = json.loads((eng / "BUILD.json").read_text())
|
||||
@@ -1779,6 +1992,10 @@ def main() -> int:
|
||||
elif not (pack / "native_experts.txt").exists() or not (pack / "tokenizer" / "vocab.json").exists():
|
||||
# every tensor as the GGUF stores it; the experts are read from the GGUF at start (seconds to build)
|
||||
run([sys.executable, str(ROOT / "tools" / "iq_pack.py"), "--gguf", str(shards[0]), "--out", str(pack)], env=env)
|
||||
if low_ram and not (pack / "experts.bin").exists():
|
||||
say(f" Writing the experts into one file for the low-RAM mode (one time, {MODELS[model]['arena_gb']:.0f} GB) ...")
|
||||
run([sys.executable, str(ROOT / "tools" / "iq_pack.py"), "--gguf", str(shards[0]), "--out", str(pack),
|
||||
"--experts-bin"], env=env)
|
||||
ok(f"model prepared: {pack}")
|
||||
mtp = (find_in(roots, "mtp/rt/experts.bin") or data / "mtp/rt/experts.bin").parent.parent
|
||||
rt = mtp / "rt"
|
||||
@@ -1790,8 +2007,7 @@ def main() -> int:
|
||||
"--out", str(mtp / "mtp-q2_0.gguf")], env=env)
|
||||
run([sys.executable, str(ROOT / "tools" / "mtp_rt.py"), "--gguf", str(mtp / "mtp-q2_0.gguf"), "--out", str(rt)],
|
||||
env=env)
|
||||
if not (rt / "draft_vocab.bin").exists():
|
||||
shutil.copyfile(ROOT / "data" / "draft_vocab.bin", rt / "draft_vocab.bin")
|
||||
refresh_draft_vocab(rt, a.draft_vocab or "cjk")
|
||||
ok(f"MTP draft layer: {rt}")
|
||||
|
||||
# ---- 7. the start script
|
||||
@@ -1807,6 +2023,8 @@ def main() -> int:
|
||||
"--max-context", str(ctx)]
|
||||
if ctx > 8192:
|
||||
args += ["--kv", kv]
|
||||
if low_ram:
|
||||
args += ["--mmap-experts"] # the experts from the pack's experts.bin, not copied into RAM
|
||||
# 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 with 8-bit KV (1.7 GB at 128K), 7.5 KB with 4-bit, so only when it fits.
|
||||
@@ -1849,6 +2067,8 @@ def main() -> int:
|
||||
cfg["host"] = a.host
|
||||
if a.api_key:
|
||||
cfg["api_key"] = a.api_key
|
||||
if a.draft_vocab:
|
||||
cfg["draft_vocab"] = a.draft_vocab
|
||||
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"]}
|
||||
|
||||
+8
-7
@@ -33,7 +33,7 @@ DeviceInfo device_info(int ordinal) {
|
||||
#if defined(STRATA_USE_HIP)
|
||||
throw CudaError("no HIP device is present; this backend targets gfx1100 wave32", -1);
|
||||
#else
|
||||
throw CudaError("no CUDA device is present; Strata needs an NVIDIA GPU of compute capability 8.0 or newer", -1);
|
||||
throw CudaError("no CUDA device is present; Strata needs an NVIDIA GPU (RTX 20 series or newer)", -1);
|
||||
#endif
|
||||
}
|
||||
if (ordinal < 0 || ordinal >= count) {
|
||||
@@ -60,19 +60,20 @@ DeviceInfo device_info(int ordinal) {
|
||||
check(cudaDriverGetVersion(&d.driver_version), "cudaDriverGetVersion");
|
||||
check(cudaRuntimeGetVersion(&d.runtime_version), "cudaRuntimeGetVersion");
|
||||
|
||||
// The kernels need sm_80 or newer (tf32 mma in the attention scorer, bf16 math) - the same floor the
|
||||
// arch guard in CMakeLists enforces at build time (RTX 30 / 40 / 50). Anything older is caught here,
|
||||
// because a binary can be carried to a machine with an older card and would otherwise silently take
|
||||
// whatever path the driver chose. The HIP backend is validated on gfx1100 (wave32) only.
|
||||
// The engine supports compute capability 7.5 and newer (Turing: the QSA scorer's tf32 mma has a portable
|
||||
// fp32-FMA fallback below sm_80, the tensor-core prompt kernels refuse and fall back). Compiling for a
|
||||
// supported arch is enforced by CMake; RUNNING on an older card is caught here, because a binary can be carried
|
||||
// to a machine with an older card and would otherwise silently take whatever path the driver chose. The HIP
|
||||
// backend is validated on gfx1100 (wave32) only.
|
||||
#if defined(STRATA_USE_HIP)
|
||||
if (std::strncmp(p.gcnArchName, "gfx1100", 7) != 0 || p.warpSize != 32) {
|
||||
throw CudaError("HIP backend requires validated gfx1100 wave32 hardware", -1);
|
||||
}
|
||||
#else
|
||||
if (d.cc_major < 8) {
|
||||
if (d.cc_major * 10 + d.cc_minor < 75) {
|
||||
throw CudaError("device " + d.name + " reports compute capability " + std::to_string(d.cc_major) +
|
||||
"." + std::to_string(d.cc_minor) +
|
||||
"; Strata needs an NVIDIA GPU of compute capability 8.0 or newer (RTX 30 / 40 / 50)",
|
||||
"; Strata needs compute capability 7.5 or newer (RTX 20 / 30 / 40 / 50 series)",
|
||||
-1);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -292,6 +292,19 @@ bool MtpDrafter::load(const std::string& rt_dir, const ModelGeometry& g, Session
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t MtpDrafter::bind_bytes(uint64_t head_row_bytes, int64_t n_vocab) const {
|
||||
uint64_t bytes = head_logits_ ? 0 : (uint64_t) max_t_ * (uint64_t) n_vocab * sizeof(float);
|
||||
if (dhead_ == nullptr) {
|
||||
if (FILE* f = std::fopen((rt_dir_ + "/draft_vocab.bin").c_str(), "rb")) {
|
||||
std::fseek(f, 0, SEEK_END);
|
||||
const long size = std::ftell(f);
|
||||
std::fclose(f);
|
||||
if (size >= 4 && size % 4 == 0) bytes += (uint64_t) (size / 4) * head_row_bytes + (uint64_t) size;
|
||||
}
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
bool MtpDrafter::bind(const WeightTable& wt, const NativeHead* head, const float* window_R, std::string& err) {
|
||||
const OnDevice on_device(device_);
|
||||
wt_ = &wt;
|
||||
|
||||
+4
-1
@@ -148,7 +148,8 @@ bool Verifier::init(const WeightTable& wt, const ModelGeometry& g, SessionState&
|
||||
return false;
|
||||
}
|
||||
if (hits.d_res == nullptr || hits.cache_base == nullptr || hits.blob <= 0) {
|
||||
err = "verify: needs the profile-filled VRAM expert tier (--expert-profile and --expert-cache)";
|
||||
err = "verify: needs the profile-filled VRAM expert tier (--expert-profile and --expert-cache); with "
|
||||
"--expert-cache auto, no VRAM was left for it: lower --max-context, use --kv k8v4 or images on the CPU";
|
||||
return false;
|
||||
}
|
||||
std::string why;
|
||||
@@ -850,8 +851,10 @@ bool Verifier::capture(int T, std::string& err) {
|
||||
if (ty == cudaGraphNodeTypeKernel) {
|
||||
cudaKernelNodeParams kp{};
|
||||
if (cudaGraphKernelNodeGetParams(nd, &kp) == cudaSuccess) {
|
||||
#if CUDART_VERSION >= 12030 // cudaFuncGetName arrived in CUDA 12.3
|
||||
const char* fn = nullptr;
|
||||
if (cudaFuncGetName(&fn, kp.func) == cudaSuccess && fn) name = fn;
|
||||
#endif
|
||||
}
|
||||
} else if (ty == cudaGraphNodeTypeMemcpy) name = "memcpy";
|
||||
else if (ty == cudaGraphNodeTypeMemset) name = "memset";
|
||||
|
||||
@@ -325,19 +325,40 @@ void fused_gr_read_multi(const FusedGrArgs* a, int n_tok, float* xn_scratch, voi
|
||||
// the shared-memory opt-in is a per-DEVICE setting: once per device, not once per process (a layer split
|
||||
// runs this kernel on two cards)
|
||||
static bool attr[64] = {};
|
||||
static int chunk[64] = {}; // Turing port: tokens the down kernel may carry in one launch on this card
|
||||
int dev = 0;
|
||||
cudaGetDevice(&dev);
|
||||
if (dev < 0 || dev >= 64 || !attr[dev]) {
|
||||
if (dev >= 0 && dev < 64 && !attr[dev]) {
|
||||
// at most what the card allows (Turing: 64 KB - enough for windows of up to 6 tokens)
|
||||
int optin = 0;
|
||||
cudaDeviceGetAttribute(&optin, cudaDevAttrMaxSharedMemoryPerBlockOptin, dev);
|
||||
int want = (int) (kFusedGrMaxT * TILE * sizeof(float));
|
||||
if (optin > 0 && want > optin) want = optin;
|
||||
cudaFuncSetAttribute(gr_down_multi_kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, want);
|
||||
cudaGetLastError();
|
||||
if (dev >= 0 && dev < 64) attr[dev] = true;
|
||||
cudaGetLastError(); // drop any error the attempt left behind
|
||||
// Turing port: the down kernel stages n_tok*TILE floats of dynamic shared memory - 80 KB at the full
|
||||
// 8 tokens. A card whose opt-in is below that (Turing: 64 KB, so 7+ tokens fail to launch as
|
||||
// "invalid argument") processes the tokens in slices that fit; a card that reports no opt-in gets what
|
||||
// fits the 48 KB default (4 tokens of the CUDA tile; all 8 of HIP's smaller tile). The down kernel's
|
||||
// outputs (lo, inject_out) are strictly per-token, so the chunk boundaries are safe, and the up kernel
|
||||
// below still sees every token of the batch in one launch.
|
||||
const int capacity = (optin > 0 ? optin : 48 * 1024) / (int) (TILE * sizeof(float));
|
||||
chunk[dev] = capacity < 1 ? 1 : (capacity > kFusedGrMaxT ? kFusedGrMaxT : capacity);
|
||||
attr[dev] = true;
|
||||
}
|
||||
const int chunk_tok = (dev >= 0 && dev < 64 && chunk[dev]) ? chunk[dev] : kFusedGrMaxT;
|
||||
if (chunk_tok >= n_tok) {
|
||||
gr_down_multi_kernel<<<DOWN_BLOCKS + 1, THREADS, (size_t) n_tok * TILE * sizeof(float), st>>>(m);
|
||||
} else {
|
||||
for (int c0 = 0; c0 < n_tok; c0 += chunk_tok) {
|
||||
const int ct = n_tok - c0 < chunk_tok ? n_tok - c0 : chunk_tok;
|
||||
GrMulti c{};
|
||||
c.xn = xn_scratch + (size_t) c0 * D;
|
||||
c.T = ct;
|
||||
for (int k = 0; k < ct; ++k) c.a[k] = a[c0 + k];
|
||||
gr_down_multi_kernel<<<DOWN_BLOCKS + 1, THREADS, (size_t) ct * TILE * sizeof(float), st>>>(c);
|
||||
}
|
||||
}
|
||||
gr_down_multi_kernel<<<DOWN_BLOCKS + 1, THREADS, (size_t) n_tok * TILE * sizeof(float), st>>>(m);
|
||||
if (stamp_buf) gpu_stamp(stamp_buf, stamp_i0 + 1, stream);
|
||||
gr_up_multi_kernel<<<UPM_BLOCKS, THREADS, 0, st>>>(m);
|
||||
const cudaError_t e = cudaGetLastError();
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#include <cuda_fp16.h>
|
||||
#include <cuda_runtime.h>
|
||||
#include <math_constants.h>
|
||||
#include <cmath>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -117,7 +117,7 @@ __global__ void __launch_bounds__(THREADS) prompt_attn_kernel(const float* __res
|
||||
S.qh[h][d] = hi;
|
||||
S.ql[h][d] = __float2half_rn(x - __half2float(hi));
|
||||
}
|
||||
if (t < 16) { S.mrow[t] = -CUDART_INF_F; S.lsum[t] = 0.0f; }
|
||||
if (t < 16) { S.mrow[t] = -INFINITY; S.lsum[t] = 0.0f; }
|
||||
|
||||
float acc[8][4];
|
||||
#pragma unroll
|
||||
@@ -244,17 +244,17 @@ __global__ void __launch_bounds__(THREADS) prompt_attn_kernel(const float* __res
|
||||
sc[3] = fmaf(tg[3], s1, sc[3]);
|
||||
}
|
||||
const int c = cb + 2 * tig;
|
||||
S.s[gid][c] = c < nh ? sc[0] * qdown : -CUDART_INF_F;
|
||||
S.s[gid][c + 1] = c + 1 < nh ? sc[1] * qdown : -CUDART_INF_F;
|
||||
S.s[gid + 8][c] = c < nh ? sc[2] * qdown : -CUDART_INF_F;
|
||||
S.s[gid + 8][c + 1] = c + 1 < nh ? sc[3] * qdown : -CUDART_INF_F;
|
||||
S.s[gid][c] = c < nh ? sc[0] * qdown : -INFINITY;
|
||||
S.s[gid][c + 1] = c + 1 < nh ? sc[1] * qdown : -INFINITY;
|
||||
S.s[gid + 8][c] = c < nh ? sc[2] * qdown : -INFINITY;
|
||||
S.s[gid + 8][c + 1] = c + 1 < nh ? sc[3] * qdown : -INFINITY;
|
||||
}
|
||||
__syncthreads();
|
||||
// online softmax: row t/8, 4 cells per thread, 8 threads per row (lanes 8r..8r+7 of a warp)
|
||||
{
|
||||
constexpr int PER = CH / 8;
|
||||
const int r = t >> 3, sub = t & 7;
|
||||
float x[PER], mx = -CUDART_INF_F;
|
||||
float x[PER], mx = -INFINITY;
|
||||
#pragma unroll
|
||||
for (int j = 0; j < PER; ++j) { x[j] = S.s[r][sub * PER + j]; mx = fmaxf(mx, x[j]); }
|
||||
#pragma unroll
|
||||
@@ -264,7 +264,7 @@ __global__ void __launch_bounds__(THREADS) prompt_attn_kernel(const float* __res
|
||||
float sum = 0.0f;
|
||||
#pragma unroll
|
||||
for (int j = 0; j < PER; ++j) {
|
||||
const float e = x[j] == -CUDART_INF_F ? 0.0f : exp2f(x[j] - m_new);
|
||||
const float e = x[j] == -INFINITY ? 0.0f : exp2f(x[j] - m_new);
|
||||
S.s[r][sub * PER + j] = e;
|
||||
sum += e;
|
||||
}
|
||||
@@ -272,7 +272,7 @@ __global__ void __launch_bounds__(THREADS) prompt_attn_kernel(const float* __res
|
||||
for (int o = 1; o < 8; o <<= 1) sum += __shfl_xor_sync(0xffffffffu, sum, o);
|
||||
__syncwarp();
|
||||
if (sub == 0) {
|
||||
const float a = m_old == -CUDART_INF_F ? 0.0f : exp2f(m_old - m_new);
|
||||
const float a = m_old == -INFINITY ? 0.0f : exp2f(m_old - m_new);
|
||||
S.alpha[r] = a;
|
||||
S.lsum[r] = fmaf(S.lsum[r], a, sum);
|
||||
S.mrow[r] = m_new;
|
||||
@@ -419,7 +419,7 @@ __global__ void __launch_bounds__(THREADS) prompt_attn_i8_kernel(const float* __
|
||||
#pragma unroll
|
||||
for (int o = 16; o > 0; o >>= 1) qm = fmaxf(qm, __shfl_xor_sync(0xffffffffu, qm, o));
|
||||
if (lane == 0) S.qmax[warp] = qm;
|
||||
if (t < 16) { S.mrow[t] = -CUDART_INF_F; S.lsum[t] = 0.0f; }
|
||||
if (t < 16) { S.mrow[t] = -INFINITY; S.lsum[t] = 0.0f; }
|
||||
__syncthreads();
|
||||
qm = fmaxf(fmaxf(S.qmax[0], S.qmax[1]), fmaxf(S.qmax[2], S.qmax[3]));
|
||||
int qe = 0;
|
||||
@@ -511,12 +511,12 @@ __global__ void __launch_bounds__(THREADS) prompt_attn_i8_kernel(const float* __
|
||||
// online softmax over the four groups' sum (fixed order): row t/8, 4 cells per thread
|
||||
{
|
||||
const int r = t >> 3, sub = t & 7;
|
||||
float x[4], mx = -CUDART_INF_F;
|
||||
float x[4], mx = -INFINITY;
|
||||
#pragma unroll
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
const int c = sub * 4 + j;
|
||||
x[j] = c0 + c < n ? (((S.part[0][r][c] + S.part[1][r][c]) + S.part[2][r][c]) + S.part[3][r][c]) * qdown
|
||||
: -CUDART_INF_F;
|
||||
: -INFINITY;
|
||||
mx = fmaxf(mx, x[j]);
|
||||
}
|
||||
#pragma unroll
|
||||
@@ -526,7 +526,7 @@ __global__ void __launch_bounds__(THREADS) prompt_attn_i8_kernel(const float* __
|
||||
float sum = 0.0f;
|
||||
#pragma unroll
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
const float e = x[j] == -CUDART_INF_F ? 0.0f : exp2f(x[j] - m_new);
|
||||
const float e = x[j] == -INFINITY ? 0.0f : exp2f(x[j] - m_new);
|
||||
S.p[r][sub * 4 + j] = e;
|
||||
sum += e;
|
||||
}
|
||||
@@ -534,7 +534,7 @@ __global__ void __launch_bounds__(THREADS) prompt_attn_i8_kernel(const float* __
|
||||
for (int o = 1; o < 8; o <<= 1) sum += __shfl_xor_sync(0xffffffffu, sum, o);
|
||||
__syncwarp();
|
||||
if (sub == 0) {
|
||||
const float a = m_old == -CUDART_INF_F ? 0.0f : exp2f(m_old - m_new);
|
||||
const float a = m_old == -INFINITY ? 0.0f : exp2f(m_old - m_new);
|
||||
S.alpha[r] = a;
|
||||
S.lsum[r] = fmaf(S.lsum[r], a, sum);
|
||||
S.mrow[r] = m_new;
|
||||
|
||||
@@ -93,6 +93,7 @@ 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:
|
||||
case GGML_TYPE_Q8_0: // the draft layer's dense matrices (E-9)
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -141,6 +142,7 @@ void Context::run(const Product& p, void* stream) {
|
||||
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;
|
||||
case GGML_TYPE_Q8_0: mul_mat_q_case<GGML_TYPE_Q8_0>(ctx, a, s); break;
|
||||
default:
|
||||
std::fprintf(stderr, "prefill mmq: type %d is not covered\n", (int) t);
|
||||
std::exit(1);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// src/prefill/prefill.cpp - see include/strata/prefill/prefill.hpp.
|
||||
#include "strata/prefill/prefill.hpp"
|
||||
#include "strata/core/mtp.hpp"
|
||||
#include "strata/core/progress.hpp"
|
||||
#include "strata/core/on_device.hpp"
|
||||
|
||||
@@ -673,6 +674,151 @@ bool Prefill::relayout(int64_t chunk, void* borrow, uint64_t borrow_bytes, std::
|
||||
}
|
||||
|
||||
int64_t Prefill::chunk() const { return impl_->T; }
|
||||
|
||||
bool Prefill::draft_kv(core::MtpDrafter& mtp, const float* R_rows, const int32_t* next_tokens, int64_t n, int64_t cell0,
|
||||
std::string& err) {
|
||||
Impl& m = *impl_;
|
||||
static const bool off = [] { const char* v = std::getenv("STRATA_MTP_BATCH"); return v != nullptr && v[0] == '0'; }();
|
||||
core::QsaState& st = mtp.kv_state_rw();
|
||||
if (off || n <= 0 || m.g == nullptr || m.region == nullptr || st.kv_mode != 0 || st.kv_hybrid ||
|
||||
mtp.device() != m.device)
|
||||
return false;
|
||||
const auto t0 = Clock::now();
|
||||
const core::ModelGeometry& g = *m.g;
|
||||
const int64_t Nn = g.n_embd, HCN = g.hc * g.n_embd, KV = g.n_head_kv * g.head_dim;
|
||||
constexpr int kQ8_0 = 8; // GGML_TYPE_Q8_0
|
||||
const float* w_ne = mtp.tensor_f32("pre_fc_norm_embedding.weight");
|
||||
const void* w_fe = mtp.tensor_q8("fc_embedding.weight");
|
||||
const float* w_nh = mtp.tensor_f32("pre_fc_norm_hidden.weight");
|
||||
const void* w_fh = mtp.tensor_q8("fc_hidden.weight");
|
||||
const float* w_hn = mtp.tensor_f32("attn_hyper_connection.hc_norm.weight");
|
||||
const uint16_t* w_dn = mtp.tensor_bf16("attn_hyper_connection.input_mix_weight_down.weight");
|
||||
const uint16_t* w_up = mtp.tensor_bf16("attn_hyper_connection.input_mix_weight_up.weight");
|
||||
const void* w_k = mtp.tensor_q8("self_attn.k_proj.weight");
|
||||
const void* w_v = mtp.tensor_q8("self_attn.v_proj.weight");
|
||||
const float* w_kn = mtp.tensor_f32("self_attn.k_norm.weight");
|
||||
if (!w_ne || !w_fe || !w_nh || !w_fh || !w_hn || !w_dn || !w_up || !w_k || !w_v || !w_kn) return false;
|
||||
const core::NativeEmbed* nemb = core::native_embed();
|
||||
const core::WeightRef* wemb = nemb ? nullptr : m.wt->find("token_embd.weight");
|
||||
if (!nemb && (wemb == nullptr || wemb->codebook_iq4nl || wemb->ne0 != g.n_embd || wemb->group_elems <= 0 ||
|
||||
(wemb->code_bits != 2 && wemb->code_bits != 4 && wemb->code_bits != 8)))
|
||||
return false;
|
||||
// the cells the drafter's window can still reach
|
||||
const int64_t r0 = std::max<int64_t>(0, mtp.first_needed() - cell0);
|
||||
if (r0 >= n) return true;
|
||||
// per row: emb/e2 (N), en16 (N half), hn/h2/Rm/gated (HCN), hn16/xn16 (HCN half), lo (LR) + lo16, grs, mixed (N) +
|
||||
// mixed_h, K and V (KV each), the token id
|
||||
// E-9: the drafter's Q8_0 matrices through Q8_1 x Q8_0 MMQ - its own pass's integer dot products (mmvq), so
|
||||
// its K/V stay close to what the drafter computes itself; STRATA_MTP_BATCH_F16=1: FP16 GEMMs (the A/B)
|
||||
static const bool f16_only = [] { const char* v = std::getenv("STRATA_MTP_BATCH_F16"); return v && v[0] == '1'; }();
|
||||
const bool q8 = !f16_only && mmq::built() && mmq::supported(kQ8_0);
|
||||
const uint64_t per_row = 4 * (2 * Nn + 4 * HCN + LR + HC + Nn + 2 * KV + 1) + 2 * (Nn + 2 * HCN + LR + Nn) + 64 +
|
||||
(q8 ? (uint64_t) mmq::q8_bytes(g.hc, Nn) + 4 * g.hc : 0);
|
||||
const int64_t B = std::min<int64_t>(n - r0, (int64_t) (m.region_bytes / per_row) & ~(int64_t) 63);
|
||||
if (B < 64) return false;
|
||||
uint8_t* q = m.region;
|
||||
auto carve = [&](size_t bytes) { void* p = q; q += (bytes + 255) & ~(size_t) 255; return p; };
|
||||
float* emb = (float*) carve((size_t) B * Nn * 4);
|
||||
float* e2 = (float*) carve((size_t) B * Nn * 4);
|
||||
uint16_t* en16 = (uint16_t*) carve((size_t) B * Nn * 2);
|
||||
float* hn = (float*) carve((size_t) B * HCN * 4);
|
||||
float* h2 = (float*) carve((size_t) B * HCN * 4);
|
||||
float* Rm = (float*) carve((size_t) B * HCN * 4);
|
||||
float* gated = (float*) carve((size_t) B * HCN * 4);
|
||||
uint16_t* hn16 = (uint16_t*) carve((size_t) B * HCN * 2);
|
||||
uint16_t* xn16 = (uint16_t*) carve((size_t) B * HCN * 2);
|
||||
float* lo = (float*) carve((size_t) B * LR * 4);
|
||||
uint16_t* lo16 = (uint16_t*) carve((size_t) B * LR * 2);
|
||||
float* grs = (float*) carve((size_t) B * HC * 4);
|
||||
float* mixed = (float*) carve((size_t) B * Nn * 4);
|
||||
uint16_t* mixed_h = (uint16_t*) carve((size_t) B * Nn * 2);
|
||||
float* Kc = (float*) carve((size_t) B * KV * 4);
|
||||
float* Vc = (float*) carve((size_t) B * KV * 4);
|
||||
int32_t* tok = (int32_t*) carve((size_t) B * 4);
|
||||
void* xq = q8 ? carve(mmq::q8_bytes(B * g.hc, Nn)) : nullptr;
|
||||
int32_t* ident = q8 ? (int32_t*) carve((size_t) B * g.hc * 4) : nullptr;
|
||||
int32_t* bnd = q8 ? (int32_t*) carve(16) : nullptr;
|
||||
if ((uint64_t) (q - m.region) > m.region_bytes) return false;
|
||||
if (!mtp.idle(err)) return false; // the drafter's own stream (its graph uploads) before this writes its K/V
|
||||
strata::kernels::QsaShapes s = strata::kernels::qsa_real_shapes();
|
||||
s.n_head = g.n_head; s.n_head_kv = g.n_head_kv; s.head_dim = g.head_dim; s.idx_n_head = g.idx_q_heads;
|
||||
s.idx_dim = g.idx_key_dim;
|
||||
std::vector<int32_t> tk((size_t) B);
|
||||
if (q8) {
|
||||
if (!m.mmq_ctx) m.mmq_ctx = std::make_unique<mmq::Context>();
|
||||
mmq::iota(ident, B * g.hc, m.cs);
|
||||
}
|
||||
// y[rows, n_out] = x[rows, k] . w^T for a Q8_0 matrix: MMQ from the FP32 rows (bounds slot 0: rows, 1: rows*hc)
|
||||
auto proj = [&](const float* x, const uint16_t* x16, const void* w, float* y, int64_t rows, int64_t n_out,
|
||||
int64_t k, int slot) {
|
||||
if (!q8) { m.gemm.native(x16, kQ8_0, w, y, rows, n_out, k); return; }
|
||||
mmq::quantize(x, nullptr, xq, kQ8_0, k, k, rows, m.cs);
|
||||
mmq::Product p;
|
||||
p.w = w; p.type = kQ8_0; p.w_rows = n_out; p.w_cols = k; p.expert_bytes = mmq::matrix_bytes(kQ8_0, n_out, k);
|
||||
p.n = 1; p.xq = xq; p.bounds = bnd + 2 * slot; p.ids = ident; p.total_rows = rows; p.max_rows = rows;
|
||||
p.dst = y; p.ld_dst = n_out;
|
||||
m.mmq_ctx->run(p, m.cs);
|
||||
};
|
||||
for (int64_t b0 = r0; b0 < n; b0 += B) {
|
||||
const int64_t nb = std::min(B, n - b0), c0 = cell0 + b0;
|
||||
for (int64_t i = 0; i < nb; ++i) tk[(size_t) i] = next_tokens[b0 + i];
|
||||
const int32_t bh[4] = {0, (int32_t) nb, 0, (int32_t) (nb * g.hc)};
|
||||
if (q8 && cudaMemcpyAsync(bnd, bh, sizeof bh, cudaMemcpyHostToDevice, m.cs) != cudaSuccess) {
|
||||
err = "prefill: the draft bounds' upload failed";
|
||||
return false;
|
||||
}
|
||||
if (cudaMemcpyAsync(tok, tk.data(), (size_t) nb * 4, cudaMemcpyHostToDevice, m.cs) != cudaSuccess) {
|
||||
err = "prefill: the draft tokens' upload failed";
|
||||
return false;
|
||||
}
|
||||
// the input branches: the next token's embedding, and this cell's final residual rows
|
||||
if (nemb) {
|
||||
nemb->gather_dev(tok, nb, emb, m.cs);
|
||||
} else {
|
||||
const auto* codes = (const uint8_t*) wemb->data;
|
||||
const auto* scales = (const float*) (codes + wemb->codes_bytes);
|
||||
const auto* offsets = wemb->has_offset ? (const float*) (codes + wemb->codes_bytes + wemb->scales_bytes)
|
||||
: nullptr;
|
||||
strata::kernels::embedding_gather_dev(codes, scales, offsets, tok, (int) nb, wemb->ne0, wemb->code_bits,
|
||||
wemb->code_bias, wemb->group_elems,
|
||||
(uint64_t) (wemb->ne0 / (8 / wemb->code_bits)),
|
||||
(uint64_t) (wemb->ne0 / wemb->group_elems), emb, m.cs);
|
||||
}
|
||||
rms_rows(emb, w_ne, nb, Nn, Nn, EPS, m.cs);
|
||||
if (!q8) to_f16(emb, en16, nb * Nn, m.cs);
|
||||
proj(emb, en16, w_fe, e2, nb, Nn, Nn, 0);
|
||||
cudaMemcpyAsync(hn, R_rows + (size_t) b0 * HCN, (size_t) nb * HCN * 4, cudaMemcpyDeviceToDevice, m.cs);
|
||||
rms_rows(hn, w_nh, nb, HCN, HCN, EPS, m.cs);
|
||||
if (!q8) to_f16(hn, hn16, nb * HCN, m.cs);
|
||||
proj(hn, hn16, w_fh, h2, nb * g.hc, Nn, Nn, 1); // every stream through fc_hidden
|
||||
strata::kernels::add_streams_broadcast(h2, e2, Rm, Nn, (int) g.hc, (int) nb, m.cs);
|
||||
// the attention hyper-connection's read (its mixed input only: this pass writes nothing back)
|
||||
gr_norm_rs(Rm, w_hn, EPS, grs, xn16, nb, m.cs);
|
||||
m.gemm.bf16(xn16, w_dn, lo, nb, LR, HCN);
|
||||
gr_silu(lo, lo16, nb, m.cs);
|
||||
m.gemm.bf16(lo16, w_up, gated, nb, HCN, LR);
|
||||
gr_mix_r(Rm, grs, w_hn, gated, mixed, nullptr, nb, m.cs, mixed_h);
|
||||
// K and V into the drafter's cache, as the prompt path's QSA layers append theirs
|
||||
proj(mixed, mixed_h, w_k, Kc, nb, KV, Nn, 0);
|
||||
proj(mixed, mixed_h, w_v, Vc, nb, KV, Nn, 0);
|
||||
rms_rows(Kc, w_kn, nb * g.n_head_kv, g.head_dim, g.head_dim, EPS, m.cs);
|
||||
rope(Kc, nb, g.n_head_kv, g.head_dim, KV, c0, (float) strata::kernels::qsa_freq_base(), m.cs);
|
||||
if (st.kv_q4) {
|
||||
strata::kernels::fwht256_inplace_cuda(Kc, nb * g.n_head_kv, m.cs);
|
||||
strata::kernels::fwht256_inplace_cuda(Vc, nb * g.n_head_kv, m.cs);
|
||||
strata::kernels::kv_append_q4(st.k_q4, st.v_q4, st.page_table, c0, nb, Kc, Vc, s, m.cs, &st.host);
|
||||
} else {
|
||||
kv_append(Kc, Vc, nb, c0, st.page_table, s.page_size, st.kv_int8 ? nullptr : st.k_pool,
|
||||
st.kv_int8 ? nullptr : st.v_pool, st.k_q, st.v_q, st.k_scale, st.v_scale, m.cs, &st.host);
|
||||
}
|
||||
}
|
||||
if (cudaStreamSynchronize(m.cs) != cudaSuccess) {
|
||||
err = std::string("prefill: the draft layer's K/V: ") + cudaGetErrorString(cudaGetLastError());
|
||||
return false;
|
||||
}
|
||||
mtp.ms_prefill += ms_since(t0);
|
||||
return true;
|
||||
}
|
||||
void Prefill::set_pinned_share(double share) { g_pinned_share = share; }
|
||||
double Prefill::pinned_share() { return g_pinned_share; }
|
||||
|
||||
@@ -781,6 +927,7 @@ struct PfTimer {
|
||||
} // namespace
|
||||
|
||||
bool Prefill::run(const int64_t* tokens, int64_t n, int64_t pos0, std::string& err) {
|
||||
err.clear();
|
||||
Impl& m = *impl_;
|
||||
const core::OnDevice on_device(m.device);
|
||||
const core::ModelGeometry& g = *m.g;
|
||||
|
||||
+33
-10
@@ -81,6 +81,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -1763,9 +1764,14 @@ int main(int argc, char** argv) {
|
||||
strata::core::ArenaExpertSource arena_src;
|
||||
strata::core::ExpertSource* srcp = nullptr;
|
||||
if (o.mmap_experts) {
|
||||
if (native_pack) { // FileExpertSource maps the canonical pack's experts.bin; a native pack has none
|
||||
std::fprintf(stderr, "strata generate: --mmap-experts needs a canonical pack (experts.bin); %s is a native "
|
||||
"(IQ) pack, whose experts are loaded into the arena\n", o.pack.c_str());
|
||||
// FileExpertSource maps the pack's experts.bin: a canonical pack has it; a native (IQ) pack has it when
|
||||
// built with `tools/iq_pack.py --experts-bin` (the per-layer blob sizes of its layout, PR #121). The low-RAM
|
||||
// mode: the experts come from the file through the OS cache instead of a pinned copy in RAM, for a PC whose
|
||||
// GPU holds most of them but whose RAM cannot hold them all.
|
||||
if (native_pack && !std::filesystem::exists(std::filesystem::path(o.pack) / "experts.bin")) {
|
||||
std::fprintf(stderr, "strata generate: --mmap-experts needs the pack's experts.bin; %s is a native (IQ) pack "
|
||||
"built without it: python tools/iq_pack.py --gguf <shard 1> --out %s --experts-bin\n",
|
||||
o.pack.c_str(), o.pack.c_str());
|
||||
return 2;
|
||||
}
|
||||
if (!src.open(o.pack, g.n_layers, g.n_expert, err)) {
|
||||
@@ -1987,12 +1993,20 @@ int main(int argc, char** argv) {
|
||||
// (with borrowing - the default with a profile - the prompt path lends cache slots instead)
|
||||
const bool borrow = !o.no_prefill_borrow && !o.expert_profile.empty();
|
||||
const int64_t prefill_mib = (o.prefill_chunk > 0 && !borrow) ? 160 + (o.prefill_chunk * 680) / 1024 : 0;
|
||||
const int64_t reserve = ((int64_t) o.vram_reserve_mib + prefill_mib) << 20;
|
||||
// the draft layer's head and logits are allocated when it binds, after this: 0.1.27's CJK subset made them
|
||||
// ~110-180 MiB larger, and out of the reserve they left 16 GB cards below the stall line (#199)
|
||||
const int64_t mtp_bind = (!o.mtp.empty() && native_head.loaded())
|
||||
? (int64_t) mtp.bind_bytes(native_head.row_bytes(), n_vocab) : 0;
|
||||
const int64_t reserve = (((int64_t) o.vram_reserve_mib + prefill_mib) << 20) + mtp_bind;
|
||||
int64_t slots = ((int64_t) free_b - reserve) / (int64_t) strata::kernels::cpu::expert_layout().max_blob;
|
||||
if (!profile.empty()) slots = std::min<int64_t>(slots, (int64_t) profile.size());
|
||||
o.expert_cache = (int) std::max<int64_t>(slots, 0);
|
||||
std::fprintf(stderr, "strata generate: expert cache auto: %.2f GiB free, %d MiB reserved -> %d slots\n",
|
||||
(double) free_b / 1073741824.0, o.vram_reserve_mib, o.expert_cache);
|
||||
std::fprintf(stderr, "strata generate: expert cache auto: %.2f GiB free, %d MiB reserved (+%lld MiB for the "
|
||||
"draft head) -> %d slots\n",
|
||||
(double) free_b / 1073741824.0, o.vram_reserve_mib, (long long) (mtp_bind >> 20), o.expert_cache);
|
||||
if (o.expert_cache == 0) // the verify window cannot start without it (#174): say what makes room
|
||||
std::fprintf(stderr, "strata generate: no VRAM is left for the expert cache: lower --max-context, use "
|
||||
"--kv k8v4, run images on the CPU, or close other programs that use the GPU\n");
|
||||
} else if (multi_gpu && o.expert_cache > 0) {
|
||||
// a layer split's prompt path has its own buffers (it borrows no slots): an explicit cache size leaves room
|
||||
// for them and the reserve, or the first prompt fails with "device buffers ... do not fit"
|
||||
@@ -2058,6 +2072,7 @@ int main(int argc, char** argv) {
|
||||
// STRATA_TEST_CACHE_FAIL=N: the first N opens fail as an out-of-commit cudaMalloc does (tests the retry)
|
||||
int fake_fails = std::getenv("STRATA_TEST_CACHE_FAIL") ? std::atoi(std::getenv("STRATA_TEST_CACHE_FAIL")) : 0;
|
||||
int failed = 0;
|
||||
int zero_reads = 0;
|
||||
for (int attempt = 0;; ++attempt) {
|
||||
bool ok = false;
|
||||
if (fake_fails > 0) {
|
||||
@@ -2101,9 +2116,11 @@ int main(int argc, char** argv) {
|
||||
cudaMemGetInfo(&free_b, &total_b);
|
||||
const int64_t want = (int64_t) o.vram_reserve_mib << 20;
|
||||
if ((int64_t) free_b >= want - (64ll << 20)) break;
|
||||
// short by (want - free); a figure of 0 only says "at least", so then give back a quarter as well
|
||||
// short by (want - free); a figure of 0 only says "at least": the first two such reads give back 1 GiB
|
||||
// each (under WDDM the free figure read before the allocation runs ~0.7 GiB high), later ones a quarter
|
||||
int64_t give = want - (int64_t) free_b + (64ll << 20);
|
||||
if (free_b < ((size_t) 16 << 20)) give = std::max<int64_t>(give, xcache.bytes() / 4);
|
||||
if (free_b < ((size_t) 16 << 20))
|
||||
give = std::max<int64_t>(give, ++zero_reads <= 2 ? 1ll << 30 : xcache.bytes() / 4);
|
||||
const int64_t keep_bytes = xcache.bytes() - give;
|
||||
std::fprintf(stderr, "strata generate: only %lld MiB free once the slots are written (reserve %d MiB); "
|
||||
"shrinking the expert cache\n", (long long) (free_b >> 20), o.vram_reserve_mib);
|
||||
@@ -3266,7 +3283,9 @@ int main(int argc, char** argv) {
|
||||
sp.on_chunk = [&](const float* R_rows, int64_t T, int64_t p0, std::string& e) -> bool {
|
||||
std::vector<int32_t> nxt((size_t) T);
|
||||
for (int64_t t = 0; t < T; ++t) nxt[(size_t) t] = (int32_t) cur[(size_t) (p0 + t + 1)];
|
||||
if (!mtp.prefill(R_rows, nxt.data(), T, p0, e)) return false;
|
||||
// E-9: batched through the prompt path when it can (one GPU: a layer split's drafter is on the last stage)
|
||||
const bool batched = !multi_gpu && sp.draft_kv(mtp, R_rows, nxt.data(), T, p0, e);
|
||||
if (!e.empty() || (!batched && !mtp.prefill(R_rows, nxt.data(), T, p0, e))) return false;
|
||||
// progress for the server window: PP <position reached> <prompt tokens> <ms> <fresh tokens/s>
|
||||
const int64_t done = p0 + T;
|
||||
const double ms = std::chrono::duration<double, std::milli>(Clock::now() - pp_t0).count();
|
||||
@@ -3551,6 +3570,7 @@ int main(int argc, char** argv) {
|
||||
~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
|
||||
err.clear();
|
||||
const bool geni = line.rfind("GENI ", 0) == 0;
|
||||
if (!geni && line.rfind("GEN ", 0) != 0) {
|
||||
std::printf("ERR expected: GEN <max_new> <id,id,...> or GENI <max_new> <file> <id,id,...>\n");
|
||||
@@ -3836,6 +3856,7 @@ int main(int argc, char** argv) {
|
||||
};
|
||||
// tokens [a, b) through the windows: commit all of them, then give the draft layer their residuals
|
||||
auto read_windows = [&](int64_t a, int64_t b, std::string& e) -> bool {
|
||||
strata::core::progress_at("reading the prompt (verify windows), from token", a); // #217: not "batched"
|
||||
// every token is committed and the picks are discarded: no head sampling (see set_head_sampling)
|
||||
struct NoHeadSampling {
|
||||
strata::core::Verifier& v;
|
||||
@@ -3968,6 +3989,7 @@ int main(int argc, char** argv) {
|
||||
int64_t at = read_from;
|
||||
for (const int64_t to : {reread_to, root_at, turn_at, n - 1}) {
|
||||
if (to <= at) continue;
|
||||
err.clear();
|
||||
const bool win = windows_ok(at, to);
|
||||
if (win && !refill(err)) {
|
||||
std::printf("ERR refilling a lent slot failed: %s\n", err.c_str());
|
||||
@@ -4371,7 +4393,8 @@ int main(int argc, char** argv) {
|
||||
// cell i pairs R_i with the token at i + 1 (every such token is in the prompt)
|
||||
std::vector<int32_t> nxt((size_t) T);
|
||||
for (int64_t t = 0; t < T; ++t) nxt[(size_t) t] = (int32_t) o.tokens[(size_t) (p0 + t + 1)];
|
||||
return mtp.prefill(R_rows, nxt.data(), T, p0, e);
|
||||
if (prefill.draft_kv(mtp, R_rows, nxt.data(), T, p0, e)) return true; // E-9
|
||||
return e.empty() && mtp.prefill(R_rows, nxt.data(), T, p0, e);
|
||||
};
|
||||
}
|
||||
const Clock::time_point tp0 = Clock::now();
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""The draft head's token subset (rt/draft_vocab.bin): the vocabulary rows the MTP draft layer may propose.
|
||||
|
||||
The engine reads rt/draft_vocab.bin (int32 token ids) when the draft layer binds and computes the draft logits over
|
||||
those rows only: a smaller head is faster and takes less VRAM, but a token outside the subset can never be drafted.
|
||||
The shipped subset was built from English and code and had 27 of the vocabulary's 55,328 Han tokens, so an answer in
|
||||
Chinese drafted almost nothing (#137). This adds whole scripts to a subset:
|
||||
|
||||
python tools/draft_vocab.py --gguf <model>-00001-of-0000N.gguf --base data/draft_vocab.bin --add cjk \
|
||||
--out data/draft_vocab.bin
|
||||
|
||||
--add takes han, kana, hangul, cjk_punct, or cjk (all four). The base ids keep their order; the added ones follow in
|
||||
id order. --stats prints what a subset holds.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from array import array
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import strata_tokenizer as ST # noqa: E402
|
||||
|
||||
SCRIPTS = {
|
||||
"han": [(0x3400, 0x4DBF), (0x4E00, 0x9FFF), (0xF900, 0xFAFF), (0x20000, 0x2FA1F)],
|
||||
"kana": [(0x3040, 0x30FF), (0x31F0, 0x31FF), (0xFF66, 0xFF9F)],
|
||||
"hangul": [(0x1100, 0x11FF), (0x3130, 0x318F), (0xAC00, 0xD7AF)],
|
||||
"cjk_punct": [(0x3000, 0x303F), (0xFF00, 0xFF65), (0xFFA0, 0xFFEF)],
|
||||
}
|
||||
GROUPS = {"cjk": ["han", "kana", "hangul", "cjk_punct"]}
|
||||
|
||||
|
||||
def scripts_of(text: str) -> set[str]:
|
||||
found = set()
|
||||
for ch in text:
|
||||
c = ord(ch)
|
||||
for name, ranges in SCRIPTS.items():
|
||||
if any(a <= c <= b for a, b in ranges):
|
||||
found.add(name)
|
||||
return found
|
||||
|
||||
|
||||
def token_text(tok, i: int) -> str | None:
|
||||
"""A token's text, or None when its bytes are not whole UTF-8 characters (a piece of one) or it is special."""
|
||||
try:
|
||||
return tok.token_bytes(i).decode("utf-8")
|
||||
except (KeyError, UnicodeDecodeError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--gguf", required=True, help="the model's (first) GGUF file: its vocabulary")
|
||||
ap.add_argument("--base", help="the subset to extend (int32 ids)")
|
||||
ap.add_argument("--add", default="", help="comma list: han, kana, hangul, cjk_punct, cjk")
|
||||
ap.add_argument("--out", help="where to write the new subset")
|
||||
ap.add_argument("--stats", action="store_true", help="print what the base (and the new) subset holds")
|
||||
a = ap.parse_args()
|
||||
|
||||
tok = ST.Tokenizer.from_gguf(a.gguf)
|
||||
n = len(tok.tokens)
|
||||
kinds = [scripts_of(t) if (t := token_text(tok, i)) is not None else set() for i in range(n)]
|
||||
base = list(array("i", Path(a.base).read_bytes())) if a.base else []
|
||||
if any(i < 0 or i >= n for i in base):
|
||||
sys.exit(f"{a.base} has ids outside the vocabulary ({n})")
|
||||
|
||||
want = set()
|
||||
for x in filter(None, (s.strip() for s in a.add.split(","))):
|
||||
names = GROUPS.get(x, [x])
|
||||
if any(nm not in SCRIPTS for nm in names):
|
||||
sys.exit(f"--add {x}: expected one of {', '.join([*SCRIPTS, *GROUPS])}")
|
||||
want.update(names)
|
||||
have = set(base)
|
||||
added = [i for i in range(n) if i not in have and kinds[i] & want]
|
||||
ids = base + added
|
||||
|
||||
def stats(label, sel):
|
||||
counts = {nm: sum(1 for i in sel if nm in kinds[i]) for nm in SCRIPTS}
|
||||
full = {nm: sum(1 for k in kinds if nm in k) for nm in SCRIPTS}
|
||||
print(f"{label}: {len(sel)} ids; " + ", ".join(f"{nm} {counts[nm]}/{full[nm]}" for nm in SCRIPTS))
|
||||
|
||||
if a.stats or not a.out:
|
||||
if base:
|
||||
stats("base", base)
|
||||
if added:
|
||||
stats("new", ids)
|
||||
if a.out:
|
||||
Path(a.out).write_bytes(array("i", ids).tobytes())
|
||||
print(f"wrote {a.out}: {len(ids)} ids ({len(added)} added)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user