mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 22:59:00 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c62d5e3a15 | ||
|
|
7aa9a584aa | ||
|
|
987ebd442f | ||
|
|
e4031fc4d0 | ||
|
|
dec8f1366c | ||
|
|
6f7f763ce5 | ||
|
|
f93ec8a924 | ||
|
|
5322f1c2bd | ||
|
|
4cf0d8cd38 | ||
|
|
33bbbca442 | ||
|
|
38f56619cc | ||
|
|
1a26492064 | ||
|
|
bee469c462 | ||
|
|
6a7d750578 | ||
|
|
417765b8d8 | ||
|
|
5c25767fcd | ||
|
|
ea5dbffa7f | ||
|
|
a3a1103c13 | ||
|
|
ba3d28b7e3 | ||
|
|
0dba618d34 | ||
|
|
5ff7da4a70 | ||
|
|
1b6c67122a | ||
|
|
7851a8dab6 | ||
|
|
4148c51257 | ||
|
|
bd3c5cd4cb | ||
|
|
99866a47a2 | ||
|
|
73e0a3b6ed | ||
|
|
9144da7b1a | ||
|
|
a833fb46f3 | ||
|
|
32806f82aa | ||
|
|
bc694233bf | ||
|
|
dd3f9ec82f | ||
|
|
47cbb76690 | ||
|
|
ab083a693f | ||
|
|
ad4755c249 | ||
|
|
b60fbb54f7 | ||
|
|
8f13b8c4ad | ||
|
|
0b1808f072 | ||
|
|
cfa7f737cc | ||
|
|
b3de587e03 | ||
|
|
7f6bc33081 | ||
|
|
1ead709790 | ||
|
|
dd63f88fdd | ||
|
|
2a9ccfe3ea | ||
|
|
dd471d3140 | ||
|
|
4a75fa2b62 | ||
|
|
3282c61343 | ||
|
|
209dcadb3f | ||
|
|
e66a445892 | ||
|
|
68718c33fa | ||
|
|
ea52e55cd7 | ||
|
|
08e8770c6e | ||
|
|
7146d6983f | ||
|
|
01ab6b51bb | ||
|
|
957038a1cd | ||
|
|
d340dc8dd1 | ||
|
|
ffaa6ad78c | ||
|
|
4006305476 | ||
|
|
97e6f1fe19 | ||
|
|
33bd74182b | ||
|
|
4d12701181 | ||
|
|
aed382885e | ||
|
|
274257630b | ||
|
|
d96873fc56 | ||
|
|
86bed1c92f | ||
|
|
203df526f1 | ||
|
|
b6fc410ee5 | ||
|
|
0a76d62e68 | ||
|
|
c0e245c8be | ||
|
|
16e08b50cd | ||
|
|
1fb3e9a326 | ||
|
|
1f8f2fa8ad | ||
|
|
3a76d5eaf6 | ||
|
|
01ef876562 | ||
|
|
4746bfb165 | ||
|
|
0b1fbf4012 | ||
|
|
6923421a61 | ||
|
|
09dc5c52b3 | ||
|
|
5eaee81a43 | ||
|
|
d936db6547 | ||
|
|
ac3e0e17b5 | ||
|
|
b3bc477512 | ||
|
|
db729163ce | ||
|
|
19b5f873f2 | ||
|
|
0122eceb4c | ||
|
|
8401c21f9d | ||
|
|
9c2121cf5c | ||
|
|
1cfffa2ede | ||
|
|
a83252a7b7 | ||
|
|
d8056fbe00 | ||
|
|
6fb110e557 | ||
|
|
28c4543e48 | ||
|
|
e1e7e32680 | ||
|
|
94fc01c6fd | ||
|
|
b0ba459514 | ||
|
|
2fef430202 | ||
|
|
7a9245b6c0 | ||
|
|
368bcb7a26 | ||
|
|
5ad0135c00 | ||
|
|
cdb5470c70 | ||
|
|
87970c8ae0 | ||
|
|
777d11f8a3 | ||
|
|
dcc79c5230 | ||
|
|
db0606b4f8 | ||
|
|
4bfd66686e | ||
|
|
5c00af8a1f | ||
|
|
78859bbcb1 | ||
|
|
9b5965c3c1 | ||
|
|
836dd2d86e | ||
|
|
6d7255f205 | ||
|
|
97bbd76c92 | ||
|
|
36e798f4c5 | ||
|
|
1510e6d6d2 | ||
|
|
4772f0cd9a | ||
|
|
7aafac422c | ||
|
|
62b042e843 | ||
|
|
9d8337844e | ||
|
|
714438ec19 | ||
|
|
d3af2d42ad | ||
|
|
1a865fb3db | ||
|
|
a0e7f5160d | ||
|
|
dc149ef003 | ||
|
|
9f1d0ad041 | ||
|
|
1064ff7783 | ||
|
|
301a09f100 | ||
|
|
4f88a2fa8b | ||
|
|
1f05fdc283 | ||
|
|
a8fba1f5b7 | ||
|
|
2f0d6a1c15 | ||
|
|
42aee7a349 | ||
|
|
3782008944 | ||
|
|
05cd12e7a2 | ||
|
|
0c94851835 | ||
|
|
21f950b4b4 | ||
|
|
3ccfd5aed3 | ||
|
|
51567de1a6 | ||
|
|
f0a34fa9bf | ||
|
|
84aa2d7d57 | ||
|
|
1d9b4c48e9 | ||
|
|
2072510a47 | ||
|
|
0ea3e455fd | ||
|
|
a9f154e82b | ||
|
|
766b54afb1 | ||
|
|
faa1be268c | ||
|
|
47a1047750 | ||
|
|
ee05114846 | ||
|
|
d8c94b45c8 | ||
|
|
236adb3987 | ||
|
|
3484572236 | ||
|
|
e0cc4e859e | ||
|
|
cea3057e2c | ||
|
|
5e9995d296 | ||
|
|
095464152c | ||
|
|
2cf3fd181f | ||
|
|
06f212aefe | ||
|
|
4d9d9cefd3 | ||
|
|
f34ab1c8f5 | ||
|
|
280885fda0 | ||
|
|
ae2657a19d | ||
|
|
d4f15adf87 | ||
|
|
dbdec0bb9e | ||
|
|
d7cb41ba87 | ||
|
|
c4e975cc7a | ||
|
|
c15a90b28c | ||
|
|
870aedd9e1 | ||
|
|
36f6663c25 | ||
|
|
1bf946eafa | ||
|
|
5e8f47cfff | ||
|
|
18200cffaf | ||
|
|
cf059188ca | ||
|
|
b119f40f08 | ||
|
|
8d5be82566 | ||
|
|
74e1f86442 | ||
|
|
a6fd788567 | ||
|
|
32f0c44f08 | ||
|
|
93dd9b421d | ||
|
|
92a867be17 | ||
|
|
a3d6298297 | ||
|
|
b82d1d4567 | ||
|
|
3b8dc8fa35 | ||
|
|
d76b50681a | ||
|
|
77287f13ec | ||
|
|
95563ff292 | ||
|
|
5c8bcf4307 | ||
|
|
4fdb563a34 | ||
|
|
9d3c8c3664 | ||
|
|
061c542010 | ||
|
|
f2bf7a3ce3 | ||
|
|
6124c35e39 | ||
|
|
8475011ac1 | ||
|
|
89253935db | ||
|
|
e318f71e75 | ||
|
|
bf8d88b9dd | ||
|
|
7356aa1404 | ||
|
|
11d337537c | ||
|
|
478964ecaa | ||
|
|
09524eb479 | ||
|
|
949f77c0a8 | ||
|
|
7e0f719903 | ||
|
|
f7570e23e2 | ||
|
|
d56565ce39 | ||
|
|
da3df90eb8 | ||
|
|
16ccaf6150 | ||
|
|
11a72289d2 | ||
|
|
43f32b3046 | ||
|
|
3fcb2106d0 | ||
|
|
20a63cf0f0 | ||
|
|
72a2b77057 | ||
|
|
cfa197e21a | ||
|
|
22fb183cf6 | ||
|
|
01d6f7b995 | ||
|
|
07d5ecfacb | ||
|
|
b18292d702 | ||
|
|
d71132738e | ||
|
|
84c7f43358 | ||
|
|
012bea51f8 | ||
|
|
5596cb2c5b | ||
|
|
587a474433 | ||
|
|
35c0c9a8ae | ||
|
|
c7a0cef13a | ||
|
|
ea73eab442 | ||
|
|
5ef90cb96b | ||
|
|
3e1141edf0 | ||
|
|
293044e561 | ||
|
|
5af539691d | ||
|
|
0e72aff563 | ||
|
|
b72f56e02a | ||
|
|
77794e3400 | ||
|
|
b5aa1d94e0 | ||
|
|
2f9c6fc35c | ||
|
|
c3326979fc | ||
|
|
b2dbd9420d | ||
|
|
7a80b3a96b | ||
|
|
d52b73dc65 | ||
|
|
0c5b157fbd | ||
|
|
c697e859e9 | ||
|
|
2ea46ded73 | ||
|
|
20bba22af7 | ||
|
|
1493781e91 | ||
|
|
46c152a131 | ||
|
|
78b99a6605 | ||
|
|
55a557bb6d | ||
|
|
ed1e5546c2 | ||
|
|
bc6c32555b | ||
|
|
a9686a900a | ||
|
|
b90c8a2172 | ||
|
|
979639e9cd | ||
|
|
756c00fbf6 | ||
|
|
9258e4086e | ||
|
|
09c18d3e98 | ||
|
|
58f244ab1e | ||
|
|
39c5534ed3 | ||
|
|
2a220c1d1c | ||
|
|
87a755bf1e | ||
|
|
3939193dd3 | ||
|
|
864afc7421 | ||
|
|
d50f6a77ed | ||
|
|
5729a6e50a | ||
|
|
48c8e85843 | ||
|
|
4daa83e75f | ||
|
|
5cfe96ee6a | ||
|
|
845941f522 | ||
|
|
e3ed743f21 | ||
|
|
c7980a3fe4 | ||
|
|
1a937829a5 | ||
|
|
773fc4e228 | ||
|
|
732e4bfb60 | ||
|
|
52ee842dbf | ||
|
|
b0e897b41f | ||
|
|
6a51355051 | ||
|
|
8f7d53ccd5 | ||
|
|
2cb47c34c2 | ||
|
|
232fa357fd | ||
|
|
828fc80004 | ||
|
|
1fca685d6b | ||
|
|
cb32d8758d | ||
|
|
fa370e46d1 | ||
|
|
2e7af09a86 | ||
|
|
b4814a9fb1 | ||
|
|
d551ab0282 | ||
|
|
43a328d8b6 | ||
|
|
e6fd914616 | ||
|
|
694e711708 | ||
|
|
8544420180 | ||
|
|
f436dba81b | ||
|
|
552c56f7a7 | ||
|
|
e3d8823e6c | ||
|
|
9a398bd5bf | ||
|
|
4e06c84b91 | ||
|
|
f262b81b28 | ||
|
|
56ab127c87 | ||
|
|
5ed8ae03be | ||
|
|
756380301b | ||
|
|
314a6ecf69 | ||
|
|
8477811197 | ||
|
|
1b667c7abb | ||
|
|
4acede655a | ||
|
|
1ece7efd5c | ||
|
|
2a207dbfcc | ||
|
|
b6dcb23ec6 | ||
|
|
100a2374fb | ||
|
|
8810e00c09 | ||
|
|
050752f5b9 | ||
|
|
97c091c5be | ||
|
|
6b2a530393 | ||
|
|
e527bfefc1 | ||
|
|
9e3fe0461c | ||
|
|
90359513e3 | ||
|
|
8d3c976166 | ||
|
|
dde3250cdb | ||
|
|
958774b4ce | ||
|
|
f29ff5a59a | ||
|
|
34810a3bb3 | ||
|
|
5dce2f63e9 | ||
|
|
c2675c60d1 | ||
|
|
700def65a2 | ||
|
|
58e5d7851e | ||
|
|
7c77f388dd | ||
|
|
42a982e033 | ||
|
|
3e51b1c18a | ||
|
|
9dbb13ee89 | ||
|
|
860e384db7 | ||
|
|
193529239c | ||
|
|
9f45341966 | ||
|
|
da9cf6354f | ||
|
|
0c34a7de3c | ||
|
|
97637e2a14 | ||
|
|
782a8381de | ||
|
|
c763c12ac5 | ||
|
|
00e7626771 | ||
|
|
ba9e818c42 | ||
|
|
1a80ccc981 | ||
|
|
0b2201ce44 | ||
|
|
c486f17251 | ||
|
|
611530d992 | ||
|
|
33bf441cd4 | ||
|
|
154c2990ed | ||
|
|
4735e381ad | ||
|
|
7022bd25d8 | ||
|
|
5d969adde9 | ||
|
|
cb931e7010 | ||
|
|
b5b4a97c0a | ||
|
|
45cbb451e7 | ||
|
|
15f1fe0548 | ||
|
|
5f6d49e959 | ||
|
|
2cc55f6165 | ||
|
|
446a45f750 | ||
|
|
1d21a0f60a | ||
|
|
9c1e61a6f3 | ||
|
|
45faf57702 | ||
|
|
680724b87a | ||
|
|
a445418e5e | ||
|
|
baf261deca | ||
|
|
6bfcfa777f | ||
|
|
ca903c124a | ||
|
|
883d0000a7 | ||
|
|
d692839177 | ||
|
|
aaf3aa3a7c | ||
|
|
dc61a065e3 | ||
|
|
e46b149ae5 | ||
|
|
18c6075562 | ||
|
|
a798d03858 | ||
|
|
bb60de0e8e | ||
|
|
910af91838 | ||
|
|
bcb2133a28 | ||
|
|
f0f056b9bb |
No files matched your search
@@ -66,7 +66,7 @@ body:
|
||||
attributes:
|
||||
label: Strata version
|
||||
description: "In the app: Settings > About. In the Terminal: `strata --version`. From source: the About tab of the web page, or `engine` in `GET /v1/status`, or the git commit."
|
||||
placeholder: "Strata 0.2.7 · engine 0.1.52"
|
||||
placeholder: "Strata 0.2.9 · engine 0.1.55"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
||||
@@ -89,3 +89,9 @@ node_modules/
|
||||
# screenshots and checkpoints from the Mac rebuild live outside git (StrataSplit\evidence-archive)
|
||||
/apps/host/docs/evidence/
|
||||
/apps/host/docs/checkpoints/
|
||||
|
||||
# DGX Spark build outputs and local venv (docs/DGX_SPARK_LINUX.md)
|
||||
/out-spark/
|
||||
/build-spark/
|
||||
/build-vision-spark/
|
||||
/.venv-build/
|
||||
@@ -1,6 +1,6 @@
|
||||
# AGENTS.md
|
||||
|
||||
Version 0.2.7, engine 0.1.52. The public model families are DeepSeekV4.1-Flash-1017,
|
||||
Version 0.2.9, engine 0.1.55. The public model families are DeepSeekV4.1-Flash-1017,
|
||||
Qwen3.8-Flash-Next and Qwen3.6-35B-A3B. DeepSeek V4.1-Flash runs in the app and in the Terminal
|
||||
(`strata launch --family deepseek41 --model ist-das-2.00bpw`; sets in
|
||||
[docs/MODELS.md](docs/MODELS.md#deepseek-v41-flash)). Read **[docs/AI_REFERENCE.md](docs/AI_REFERENCE.md)** for model availability, APIs, hardware limits and
|
||||
@@ -15,7 +15,7 @@ Strata has three parts:
|
||||
|
||||
- **The app** (`apps/desktop`, the Tauri shell; `apps/host`, the Node host and the web UI). It is for people who do
|
||||
not want a terminal. One installer per system ([latest release](https://github.com/Niko1221/Strata/releases/latest)):
|
||||
`Strata-0.2.7-windows-x64-setup.exe`, `Strata-0.2.7-linux-x86_64.AppImage`, `Strata-0.2.7-macos-arm64.dmg`.
|
||||
`Strata-0.2.9-windows-x64-setup.exe`, `Strata-0.2.9-linux-x86_64.AppImage`, `Strata-0.2.9-macos-arm64.dmg`.
|
||||
The Windows and Linux installers carry no engine: the app downloads the engine for the graphics card on first
|
||||
start, or installs a file the user already has. The Mac dmg keeps its small Metal engine inside.
|
||||
- **The Terminal** (`strata.py`, `strata_cli/`, `strata_control/`): the `strata` command. It sets up a model, serves
|
||||
|
||||
+86
-2
@@ -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.52 LANGUAGES CXX)
|
||||
project(strata VERSION 0.1.55 LANGUAGES CXX)
|
||||
# EXPERIMENTAL: the Intel Arc engine, the SYCL port in sycl/ (PR #423; icpx + oneMKL, docs/INTEL_ARC.md). It is its
|
||||
# own project that takes the unported sources from src/; with this on, nothing below is configured.
|
||||
option(STRATA_ENABLE_SYCL "EXPERIMENTAL: build the Intel Arc (SYCL) engine from sycl/ - docs/INTEL_ARC.md" OFF)
|
||||
@@ -92,6 +92,31 @@ if(STRATA_HIP_GFX906)
|
||||
endif()
|
||||
option(STRATA_ENABLE_HIP "Build the HIP targets for wave32 AMD GPUs (gfx1100, gfx1201; gfx1012/gfx1030 unvalidated)" OFF)
|
||||
option(STRATA_PREFILL_MMQ "Build the optional GGML MMQ prefill path for HIP" OFF)
|
||||
# The rows one verify / batch window holds (include/strata/kernels/window_limits.hpp): 8, the 0.2.7 window, or 16 (four pipelined
|
||||
# groups of 16 slots = 64 slots on a four-card split, docs/BATCHING.md). Only the CUDA and HIP engines are built from
|
||||
# these sources with a wide window; the SYCL and Metal builds never read this variable and keep 8. A window of up to
|
||||
# 8 rows runs the kernels it always ran, whichever is chosen (the wide instantiations are extra code). "auto" = 16 for
|
||||
# the HIP engines (a HIP code object loads on first use, so the extra kernels cost nothing until a wide window forms)
|
||||
# and 8 for CUDA, which loads every kernel at start (CUDA_MODULE_LOADING=EAGER: the wide kernels would take VRAM from
|
||||
# the expert cache of every card); -DSTRATA_WINDOW_MAX_T=16 builds the wide CUDA engine.
|
||||
set(STRATA_WINDOW_MAX_T "auto" CACHE STRING "rows of a verify / batch window: 8, 16 or auto (HIP 16, CUDA 8)")
|
||||
set_property(CACHE STRATA_WINDOW_MAX_T PROPERTY STRINGS auto 8 16)
|
||||
if(NOT STRATA_WINDOW_MAX_T STREQUAL "auto" AND NOT STRATA_WINDOW_MAX_T STREQUAL "8" AND NOT STRATA_WINDOW_MAX_T STREQUAL "16")
|
||||
message(FATAL_ERROR "STRATA_WINDOW_MAX_T must be auto, 8 or 16 (got '${STRATA_WINDOW_MAX_T}')")
|
||||
endif()
|
||||
if(STRATA_ENABLE_HIP OR STRATA_ENABLE_CUDA)
|
||||
if(STRATA_WINDOW_MAX_T STREQUAL "auto")
|
||||
if(STRATA_ENABLE_HIP)
|
||||
set(_strata_window_max_t 16)
|
||||
else()
|
||||
set(_strata_window_max_t 8)
|
||||
endif()
|
||||
else()
|
||||
set(_strata_window_max_t ${STRATA_WINDOW_MAX_T})
|
||||
endif()
|
||||
add_compile_definitions(STRATA_VERIFY_MAX_T=${_strata_window_max_t})
|
||||
message(STATUS "Strata: verify / batch windows hold up to ${_strata_window_max_t} rows")
|
||||
endif()
|
||||
# MMQ prompt kernels for Q4_K / Q5_K / Q5_1 / Q6_K (Unsloth's UD-Q4/Q5/Q6_K_XL experts; on HIP also the dense GGUF
|
||||
# projections, STRATA_DENSE_MMQ). On by default since MODEL-UPDATE Phase 1: on CUDA they are their own library
|
||||
# (strata_kq, loaded only for a model with these formats: the engine loads every kernel of its own binary at start,
|
||||
@@ -131,6 +156,11 @@ set(_strata_tests_default OFF)
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/CMakeLists.txt AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/bench/micro)
|
||||
set(_strata_tests_default ON)
|
||||
endif()
|
||||
if(NOT STRATA_CPU_X86)
|
||||
# arm64 Linux (DGX Spark): much of the suite drives the x86 CPU kernels, which this build does not have. A default, not a
|
||||
# prohibition: -DSTRATA_BUILD_TESTS=ON still builds what builds (classifying the tests by capability is a follow-up).
|
||||
set(_strata_tests_default OFF)
|
||||
endif()
|
||||
option(STRATA_BUILD_TESTS "Build the test suite" ${_strata_tests_default})
|
||||
option(STRATA_BUILD_CONVERSATION_TESTS "Build standalone conversation cache tests" OFF)
|
||||
option(STRATA_WERROR "Treat compiler warnings as errors" OFF)
|
||||
@@ -1046,6 +1076,10 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
add_test(NAME hip_gdn_chunked_off COMMAND hip_gdn_chunked --off)
|
||||
add_test(NAME hip_gdn_chunked_default COMMAND hip_gdn_chunked --default)
|
||||
list(APPEND _strata_hip_tests hip_gdn_chunked hip_gdn_chunked_off hip_gdn_chunked_default)
|
||||
# GDN prompt stage bench (gates + conv + recurrence, per-piece times and output/state hashes at 4K/16K/32K); a measurement
|
||||
# tool, run by hand with the STRATA_GDN_* knobs under test - no add_test
|
||||
add_executable(hip_gdn_stage_bench tests/hip/gdn_stage_bench.cpp)
|
||||
target_link_libraries(hip_gdn_stage_bench PRIVATE strata_prefill strata_hip_runtime)
|
||||
# #313: the RDNA3 / RDNA3.5 WMMA dense GEMMs (opt-in STRATA_WMMA_GEMM=1) against a reference
|
||||
add_executable(hip_prefill_wmma_gemm_parity tests/hip/prefill_wmma_gemm_parity.cpp)
|
||||
target_include_directories(hip_prefill_wmma_gemm_parity PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/prefill")
|
||||
@@ -1068,6 +1102,9 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
add_test(NAME hip_router_fast COMMAND hip_router_fast)
|
||||
list(APPEND _strata_hip_tests hip_router_fast)
|
||||
# S6: the RDNA4 matrix-core prompt attention (STRATA_HIP_WMMA) against the FP32 kernel and FP64; skips off gfx12
|
||||
# the prompt attention on a recorded selection (STRATA_QSA_DUMP), for A/B of kernel variants (STRATA_QSA_UNION=1)
|
||||
add_executable(hip_pa_bench tests/cuda/pa_bench.cpp)
|
||||
target_link_libraries(hip_pa_bench PRIVATE strata_kernels strata_hip_runtime)
|
||||
add_executable(hip_prompt_attn_wmma src/kernels/qsa_prompt_attn_parity.cpp)
|
||||
target_link_libraries(hip_prompt_attn_wmma PRIVATE strata_kernels strata_hip_runtime)
|
||||
add_test(NAME hip_prompt_attn_wmma COMMAND hip_prompt_attn_wmma 32768 2048 3)
|
||||
@@ -1248,6 +1285,20 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
# "not on this card" for the tc variant
|
||||
add_test(NAME gdn_rec_parity_selftest_frag COMMAND gdn_rec_parity --selftest-frag)
|
||||
set_tests_properties(gdn_rec_parity_selftest_frag PROPERTIES SKIP_RETURN_CODE 77)
|
||||
# the production-linked GDN parity / benchmark harness (no kernel copies: it calls gdn_gates, gdn_conv and
|
||||
# gdn_recurrence of strata_prefill through the GdnProbe of include/strata/prefill/gdn_probe.hpp, against an FP64
|
||||
# oracle from tests/gdn/gdn_fixture.hpp). Usage: docs/GDN_CONTRACT.md. The smoke test is small and CPU-cheap.
|
||||
execute_process(COMMAND git -C ${CMAKE_CURRENT_SOURCE_DIR} rev-parse --short=9 HEAD OUTPUT_VARIABLE _gdn_sha
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
|
||||
if(NOT _gdn_sha)
|
||||
set(_gdn_sha unknown)
|
||||
endif()
|
||||
add_executable(gdn_harness src/prefill/gdn_harness.cu)
|
||||
target_include_directories(gdn_harness PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests/gdn)
|
||||
target_compile_definitions(gdn_harness PRIVATE GDN_HARNESS_SHA="${_gdn_sha}")
|
||||
target_link_libraries(gdn_harness PRIVATE strata_prefill)
|
||||
add_test(NAME gdn_harness_smoke COMMAND gdn_harness --T 400 --chunk 160 --cuts 37 --paths auto,keyhead,pipe --ref-heads 0,17)
|
||||
set_tests_properties(gdn_harness_smoke PROPERTIES TIMEOUT 300)
|
||||
# the prompt path's BF16 products against cuBLAS's own BF16 GEMM, within fp32 rounding (sm_7x runs them on the
|
||||
# FP16 tensor cores; GPU, synthetic, no model; --bench adds timings)
|
||||
add_executable(gemm_bf16_parity src/prefill/gemm_bf16_parity.cu)
|
||||
@@ -1608,6 +1659,13 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(pinned_shared_test PRIVATE strata_core ${_strata_gpu_runtime_target})
|
||||
add_test(NAME pinned_shared_test COMMAND pinned_shared_test)
|
||||
endif()
|
||||
if(WIN32) # the replicas' shared expert arena as a named section (src/core/pinned.cu): two arenas, a child process, the last handle
|
||||
add_executable(pinned_shared_win_test src/core/pinned_shared_win_test.cpp)
|
||||
target_include_directories(pinned_shared_win_test PRIVATE include)
|
||||
target_link_libraries(pinned_shared_win_test PRIVATE strata_core ${_strata_gpu_runtime_target})
|
||||
add_test(NAME pinned_shared_win_test COMMAND pinned_shared_win_test)
|
||||
set_tests_properties(pinned_shared_win_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 180)
|
||||
endif()
|
||||
|
||||
# Plan v0.3 P2: the n-gram table read straight from the SSD. The selftest needs only the CPU and a temp file.
|
||||
add_executable(ple_reader_test src/ngram/ple_reader_test.cpp)
|
||||
@@ -1628,6 +1686,11 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_include_directories(ple_residency_test PRIVATE include)
|
||||
add_test(NAME ple_residency_test COMMAND ple_residency_test)
|
||||
|
||||
# #4474: the automatic expert cache's slot rule (the late allocation priced); header-only, needs nothing
|
||||
add_executable(auto_cache_test src/core/auto_cache_test.cpp)
|
||||
target_include_directories(auto_cache_test PRIVATE include)
|
||||
add_test(NAME auto_cache_test COMMAND auto_cache_test)
|
||||
|
||||
# 0.2.7 (#3911): the qwen35 host sampler's penalties on a short (draft-vocab) logit row; header-only, CPU
|
||||
add_executable(qwen35_sampling_test src/qwen35/sampling_test.cpp)
|
||||
target_include_directories(qwen35_sampling_test PRIVATE include)
|
||||
@@ -1690,6 +1753,9 @@ if(STRATA_BUILD_TESTS)
|
||||
add_executable(arch_defaults_test src/core/arch_defaults_test.cpp src/core/arch_defaults.cpp)
|
||||
target_include_directories(arch_defaults_test PRIVATE include)
|
||||
add_test(NAME arch_defaults_test COMMAND arch_defaults_test)
|
||||
add_executable(nv_defaults_test src/core/nv_defaults_test.cpp src/core/nv_defaults.cpp) # the sm_121 (GB10) table and its two off switches
|
||||
target_include_directories(nv_defaults_test PRIVATE include)
|
||||
add_test(NAME nv_defaults_test COMMAND nv_defaults_test)
|
||||
add_executable(kva_test src/prefill/kva_test.cpp) # KVA: the switch, the automatic tail, the reasons a read is turned away
|
||||
target_include_directories(kva_test PRIVATE include)
|
||||
add_test(NAME kva_test COMMAND kva_test)
|
||||
@@ -1728,6 +1794,10 @@ if(STRATA_BUILD_TESTS OR STRATA_BUILD_CONVERSATION_TESTS)
|
||||
add_executable(adapt_decay_test src/program/adapt_decay_test.cpp)
|
||||
target_include_directories(adapt_decay_test PRIVATE include)
|
||||
add_test(NAME adapt_decay_test COMMAND adapt_decay_test)
|
||||
# the window limits (8 rows, or 16 in a wide CUDA / HIP build) and the pipelined-window probe (header-only, CPU)
|
||||
add_executable(window_limits_test src/core/window_limits_test.cpp)
|
||||
target_include_directories(window_limits_test PRIVATE include)
|
||||
add_test(NAME window_limits_test COMMAND window_limits_test)
|
||||
# which window-size graph is freed when the next one does not fit (#3702; header-only, CPU)
|
||||
add_executable(window_evict_test src/core/window_evict_test.cpp)
|
||||
target_include_directories(window_evict_test PRIVATE include)
|
||||
@@ -1844,7 +1914,14 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
set(GGML_BACKEND_DL OFF CACHE BOOL "" FORCE)
|
||||
set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
if(STRATA_PORTABLE)
|
||||
if(STRATA_PORTABLE AND NOT STRATA_CPU_X86)
|
||||
# arm64 (DGX Spark: Cortex-X925 + A725): ggml-cpu for any Armv8.2 CPU with dot product, FP16 and int8 matmul instead of the build
|
||||
# host's, so the engine built here does not depend on which core the compiler ran on. Without it GGML_NATIVE OFF would leave
|
||||
# ggml-cpu at plain armv8-a, and a native pack's CPU experts would lose the NEON dot products.
|
||||
set(GGML_NATIVE OFF CACHE BOOL "" FORCE)
|
||||
set(STRATA_ARM64_ARCH "armv8.2-a+dotprod+fp16+i8mm" CACHE STRING "arm64: ggml-cpu's -march for a STRATA_PORTABLE build")
|
||||
set(GGML_CPU_ARM_ARCH "${STRATA_ARM64_ARCH}" CACHE STRING "" FORCE)
|
||||
elseif(STRATA_PORTABLE)
|
||||
set(GGML_NATIVE OFF CACHE BOOL "" FORCE)
|
||||
foreach(_f GGML_AVX GGML_AVX2 GGML_FMA GGML_F16C GGML_BMI2)
|
||||
set(${_f} ON CACHE BOOL "" FORCE)
|
||||
@@ -1897,6 +1974,10 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
src/kernels/cpu/kq_avx2.cpp src/kernels/cpu/kq_avx1.cpp src/kernels/cpu/q2_avx1.cpp
|
||||
src/kernels/cpu/iq_avx1.cpp src/kernels/cpu/nvfp4_avx512.cpp src/kernels/cpu/nvfp4_avx2.cpp
|
||||
src/kernels/cpu/tq_cpu.cpp src/kernels/cpu/tq_avx2.cpp src/kernels/cpu/tq_avx512.cpp)
|
||||
else()
|
||||
# the trellis dispatcher alone (the DeepSeek engine's CPU lane and Gyro): tq_cpu.cpp runs its scalar rows when the x86 units are
|
||||
# not compiled (the Metal build does the same)
|
||||
target_sources(strata_kernels_cpu PRIVATE src/kernels/cpu/tq_cpu.cpp)
|
||||
endif()
|
||||
target_include_directories(strata_kernels_cpu PRIVATE third_party/ggml)
|
||||
if(MSVC)
|
||||
@@ -1949,6 +2030,8 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
target_link_libraries(dequant_q5_1_test PRIVATE strata_artifact ggml-base)
|
||||
add_test(NAME dequant_q5_1_test COMMAND dequant_q5_1_test)
|
||||
endif()
|
||||
# the x86 kernels' parity programs and benches (to the matching endif below): not on arm64, which has none of those kernels
|
||||
if(STRATA_CPU_X86)
|
||||
# the router lookahead's dot on each rung this CPU has (AVX2, the older CPUs' AVX1) against a double reference
|
||||
add_executable(router_dot_parity src/kernels/cpu/router_dot_parity.cpp)
|
||||
target_link_libraries(router_dot_parity PRIVATE strata_kernels_cpu)
|
||||
@@ -2009,6 +2092,7 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
add_test(NAME q8k_quant_parity COMMAND q8k_quant_parity)
|
||||
add_test(NAME q8k_quant_parity_avx COMMAND ${CMAKE_COMMAND} -E env STRATA_FORCE_ISA=avx $<TARGET_FILE:q8k_quant_parity>)
|
||||
endif()
|
||||
endif() # STRATA_CPU_X86
|
||||
set(_strata_mmq_types q2_0 iq2_xxs iq2_xs iq2_s iq3_xxs iq3_s iq4_nl iq4_xs q8_0)
|
||||
# APR: the agention llama.cpp fork has MMQ instances for its trellis expert types (f62fdead0); any other ggml
|
||||
# tree leaves them to the FP16 prompt path
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ Unsicher, was passt? [Wähle ein Modell für deinen Computer](docs/MODELS.md#pic
|
||||
|
||||
Installiere die App, wähle ein Modell und leg mit dem Chat los. Es gibt einen Installer pro System. Beim ersten Start lädt die App die Engine für deine Grafikkarte herunter. [Hilfe zum Installer](docs/INSTALL.md#the-app).
|
||||
|
||||
<sub>Dateien: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<sub>Dateien: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Strata-Desktop-App mit Chat und Live-Codevorschau nebeneinander">
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ Medido con el conjunto de 2.00 bpw en Strata 0.2.5. «Salida» es la velocidad d
|
||||
|
||||
Instala la aplicación, elige un modelo y empieza a chatear. Hay un instalador por sistema. En el primer arranque, la aplicación descarga el motor para tu tarjeta gráfica. [Ayuda con el instalador](docs/INSTALL.md#the-app).
|
||||
|
||||
<sub>Archivos: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<sub>Archivos: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Aplicación de escritorio de Strata con un chat y una vista previa del código en vivo, una junto a otra">
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ Vous ne savez pas ce qui tient ? [Choisir un modèle pour votre ordinateur](docs
|
||||
|
||||
Installez l'application, choisissez un modèle et commencez à discuter. Il y a un seul installateur par système. Au premier lancement, l'application télécharge le moteur de votre carte graphique. [Aide pour l'installateur](docs/INSTALL.md#the-app).
|
||||
|
||||
<sub>Fichiers: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<sub>Fichiers: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Application de bureau Strata avec une discussion et un aperçu du code en direct côte à côte">
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ Strata 0.2.5 で 2.00 bpw のセットを使って測定しました。「出力
|
||||
|
||||
アプリをインストールし、モデルを選んで、チャットを始めましょう。インストーラーはシステムごとに1つです。初回起動時に、アプリがお使いのグラフィックスカード用のエンジンをダウンロードします。[インストーラーのヘルプ](docs/INSTALL.md#the-app)。
|
||||
|
||||
<sub>ファイル: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<sub>ファイル: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="チャットとライブのコードプレビューを並べて表示した Strata デスクトップアプリ">
|
||||
|
||||
@@ -63,8 +63,8 @@ disk space.
|
||||
| 2.00 bpw | 257 GB | about 265 GB |
|
||||
| 2.25 bpw | 274 GB | about 280 GB |
|
||||
|
||||
Runs on NVIDIA RTX 30, 40 and 50 series (Windows and Linux), AMD Radeon AI PRO R9700 (Linux), and AMD Ryzen AI Max — Strix Halo
|
||||
(Windows). Setup downloads the files and the DeepSeek engine, then starts the model.
|
||||
Runs on NVIDIA RTX 30, 40 and 50 series (Windows and Linux), AMD Radeon AI PRO R9700 (Linux), AMD Ryzen AI Max — Strix Halo
|
||||
(Windows), and Apple Silicon Macs with 64 GB or more, text only ([Mac](docs/DEEPSEEK_MAC.md)). Setup downloads the files and the DeepSeek engine, then starts the model.
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
@@ -99,7 +99,7 @@ Not sure what fits? [Choose a model for your computer](docs/MODELS.md#pick-by-me
|
||||
Install the app, choose a model, and start chatting. There is one installer for each system. On first start the app
|
||||
downloads the engine for your graphics card. [Help with the installer](docs/INSTALL.md#the-app).
|
||||
|
||||
<sub>Files: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<sub>Files: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Strata desktop app with a chat and live code preview side by side">
|
||||
@@ -158,6 +158,7 @@ Follow the prompts to download and start a model. [Terminal guide](docs/TERMINAL
|
||||
| **Intel Arc · Linux** | [Intel setup](docs/INTEL_ARC.md) |
|
||||
| **Apple Silicon · M1 or newer** | [Mac setup](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max · Strix Halo** | [Strix Halo setup](docs/STRIX_HALO.md) |
|
||||
| **NVIDIA DGX Spark · Linux** | [DGX Spark setup](docs/DGX_SPARK_LINUX.md) |
|
||||
|
||||
<a id="using-it"></a>
|
||||
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ Nie wiesz, co się zmieści? [Wybierz model do swojego komputera](docs/MODELS.md
|
||||
Zainstaluj aplikację, wybierz model i zacznij rozmawiać. Dla każdego systemu jest jeden instalator. Przy pierwszym
|
||||
uruchomieniu aplikacja pobiera silnik dla twojej karty graficznej. [Pomoc z instalatorem](docs/INSTALL.md#the-app).
|
||||
|
||||
<sub>Pliki: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<sub>Pliki: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Aplikacja Strata na komputer: czat i podgląd kodu na żywo obok siebie">
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ Não sabe o que cabe? [Escolha um modelo para o seu computador](docs/MODELS.md#p
|
||||
|
||||
Instale o aplicativo, escolha um modelo e comece a conversar. Há um instalador por sistema. Na primeira vez que abre, o aplicativo baixa o motor da sua placa de vídeo. [Ajuda com o instalador](docs/INSTALL.md#the-app).
|
||||
|
||||
<sub>Arquivos: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<sub>Arquivos: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Aplicativo de desktop do Strata com um chat e uma prévia do código ao vivo lado a lado">
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
|
||||
安装应用,选择模型,就可以开始聊天。每个系统只有一个安装程序。首次启动时,应用会下载适合你显卡的引擎。[安装程序帮助](docs/INSTALL.md#the-app)。
|
||||
|
||||
<sub>文件: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
<sub>文件: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Strata 桌面应用,聊天和实时代码预览并排显示">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "strata-desktop",
|
||||
"productName": "Strata",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.9",
|
||||
"private": true,
|
||||
"description": "Strata Desktop - local AI, tuned to one model.",
|
||||
"scripts": {
|
||||
|
||||
Generated
+1
-1
@@ -3625,7 +3625,7 @@ checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
|
||||
|
||||
[[package]]
|
||||
name = "strata-desktop"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"serde",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strata-desktop"
|
||||
version = "0.2.7"
|
||||
version = "0.2.9"
|
||||
description = "Strata Desktop — local AI, tuned to one model."
|
||||
edition = "2021"
|
||||
rust-version = "1.77"
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "strata",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "strata",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.9",
|
||||
"dependencies": {
|
||||
"html-to-image": "^1.11.13",
|
||||
"katex": "^0.19.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "strata",
|
||||
"private": true,
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.9",
|
||||
"type": "module",
|
||||
"description": "Strata \u2014 local AI, tuned to one model. Web shell + engine host.",
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Throwaway: serves the demo host unchanged, except POST /api/chat/stream, which streams a long scripted reply (NDJSON frames like the real host).
|
||||
import http from 'node:http';
|
||||
const UP = Number(process.env.UP ?? 18791), PORT = Number(process.env.PORT ?? 18790);
|
||||
const TOK = Number(process.env.REPLY_TOKENS ?? 12000), REASON = Number(process.env.REASON_TOKENS ?? 3000), RATE = Number(process.env.RATE ?? 400);
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
const para = i => `### Section ${i}\n\nThis is paragraph ${i} with **bold**, *italic*, \`inline code\` and a link [x](https://example.com/${i}). The measured value is about ${i} units and the results agree with the earlier runs, which is what we expected from the model.
|
||||
|
||||
More prose follows here so that a paragraph looks like real writing and not like a stress pattern.\n\n- item one of ${i}\n- item two of ${i}\n\n\`\`\`python\ndef f_${i}(a, b):\n total = a + b * ${i}\n return total\n\`\`\`\n\n| a | b |\n|---|---|\n| ${i} | ${i * 2} |\n\n`;
|
||||
http.createServer(async (req, res) => {
|
||||
if (req.method === 'POST' && req.url.startsWith('/api/chat/stream')) {
|
||||
let raw = ''; for await (const c of req) raw += c;
|
||||
res.writeHead(200, { 'content-type': 'application/x-ndjson' });
|
||||
const w = o => res.write(JSON.stringify(o) + '\n');
|
||||
w({ type: 'start', model: 'Mock', contextMax: 131072, requestId: 'r' + Date.now() });
|
||||
let sent = 0, i = 0;
|
||||
const per = Math.max(1, Math.round(RATE / 50));
|
||||
while (sent < REASON) { w({ type: 'reasoning', text: 'thinking about step ' + (i++) + ' of the problem. '.repeat(1) }); sent += 9; if (sent % per < 9) await sleep(20); }
|
||||
sent = 0; i = 0;
|
||||
while (sent < TOK) { const p = para(i++); for (let k = 0; k < p.length; k += 24) { w({ type: 'delta', text: p.slice(k, k + 24) }); sent += 6; if ((sent / 6) % Math.max(1, Math.round(per / 2)) === 0) await sleep(10); } w({ type: 'stats', tokensOut: sent, tps: 90 }); }
|
||||
w({ type: 'done', tokensIn: 100, tokensOut: sent, tps: 90, durationMs: 1000, usageSource: 'engine' });
|
||||
return res.end();
|
||||
}
|
||||
const up = http.request({ host: '127.0.0.1', port: UP, path: req.url, method: req.method, headers: req.headers }, r => { res.writeHead(r.statusCode, r.headers); r.pipe(res); });
|
||||
up.on('error', () => { res.writeHead(502); res.end(); });
|
||||
req.pipe(up);
|
||||
}).listen(PORT, '127.0.0.1', () => console.log('proxy on', PORT));
|
||||
@@ -0,0 +1,57 @@
|
||||
// Measures the chat page's heap, DOM size and main-thread time per streamed reply. Needs a throwaway host (STRATA_REAL_ENGINE=0, port 18791), scripts/chat-growth-proxy.mjs (port 18790) and Chrome.
|
||||
import { chromium } from 'playwright-core';
|
||||
import { deflateSync } from 'node:zlib';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { execSync } from 'node:child_process';
|
||||
const BASE = process.env.BASE ?? 'http://127.0.0.1:18790';
|
||||
const TURNS = Number(process.env.TURNS ?? 6), PICS = Number(process.env.PICS ?? 3), MON = process.env.MON !== '0';
|
||||
const crc = (() => { const t = new Int32Array(256).map((_, n) => { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; return c; }); return b => { let c = -1; for (const x of b) c = t[(c ^ x) & 255] ^ (c >>> 8); return (c ^ -1) >>> 0; }; })();
|
||||
function png(w, h) {
|
||||
const chunk = (type, data) => { const len = Buffer.alloc(4); len.writeUInt32BE(data.length); const body = Buffer.concat([Buffer.from(type), data]); const c = Buffer.alloc(4); c.writeUInt32BE(crc(body)); return Buffer.concat([len, body, c]); };
|
||||
const raw = Buffer.alloc((w * 3 + 1) * h); for (let y = 0; y < h; y++) { raw[y * (w * 3 + 1)] = 0; randomBytes(w * 3).copy(raw, y * (w * 3 + 1) + 1); }
|
||||
const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 2;
|
||||
return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk('IHDR', ihdr), chunk('IDAT', deflateSync(raw, { level: 1 })), chunk('IEND', Buffer.alloc(0))]);
|
||||
}
|
||||
const browser = await chromium.launch({ executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe', headless: true, args: ['--enable-precise-memory-info', '--js-flags=--expose-gc'] });
|
||||
const ctx = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
const cdp = await ctx.newCDPSession(page);
|
||||
await cdp.send('Performance.enable');
|
||||
await fetch(BASE + '/api/model-providers', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ kind: 'custom', provider: null, name: 'Mock model', baseUrl: 'http://127.0.0.1:18898/v1', protocol: 'openai-chat', apiKey: '', models: [{ id: 'mock-model', name: 'Mock model' }] }) });
|
||||
await page.goto(BASE + '/'); await page.waitForTimeout(1500);
|
||||
const skip = page.getByText('Go straight to the app'); await skip.waitFor({ timeout: 10000 }).catch(() => {}); if (await skip.count()) { await skip.click(); await page.waitForTimeout(800); }
|
||||
async function metrics(tag) {
|
||||
await cdp.send('HeapProfiler.collectGarbage').catch(() => {});
|
||||
const m = Object.fromEntries((await cdp.send('Performance.getMetrics')).metrics.map(x => [x.name, x.value]));
|
||||
const dom = await cdp.send('Memory.getDOMCounters').catch(() => ({}));
|
||||
const sess = await page.evaluate(() => { try { return (localStorage.getItem('strata.sessions.v1') ?? '').length; } catch { return -1; } });
|
||||
console.log(`${tag.padEnd(22)} heap=${(m.JSHeapUsedSize / 1048576).toFixed(0)}MB total=${(m.JSHeapTotalSize / 1048576).toFixed(0)}MB nodes=${dom.nodes ?? m.Nodes} listeners=${dom.jsEventListeners ?? '-'} docs=${m.Documents} ls=${(sess / 1e6).toFixed(2)}M`);
|
||||
return m.JSHeapUsedSize;
|
||||
}
|
||||
await page.locator('.chat-model-picker').first().click(); await page.locator('.model-picker-api').first().click(); await page.waitForTimeout(500);
|
||||
await metrics('start');
|
||||
const ta = page.locator('.composer textarea');
|
||||
for (let t = 1; t <= TURNS; t++) {
|
||||
if (t <= PICS) {
|
||||
const b64 = png(900, 900).toString('base64');
|
||||
await page.evaluate(b64 => { const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0)); const d = new DataTransfer(); d.items.add(new File([bytes], 'image.png', { type: 'image/png' })); document.querySelector('.composer textarea').dispatchEvent(new ClipboardEvent('paste', { clipboardData: d, bubbles: true, cancelable: true })); }, b64);
|
||||
await page.waitForTimeout(600);
|
||||
}
|
||||
await ta.fill('turn ' + t + ' write a long answer');
|
||||
await page.getByRole('button', { name: 'Send' }).click();
|
||||
const t0 = Date.now();
|
||||
let peak = 0; const cpu0 = (await cdp.send('Performance.getMetrics')).metrics.find(x => x.name === 'TaskDuration').value;
|
||||
for (;;) {
|
||||
await page.waitForTimeout(2000);
|
||||
const m = Object.fromEntries((await cdp.send('Performance.getMetrics')).metrics.map(x => [x.name, x.value]));
|
||||
peak = Math.max(peak, m.JSHeapUsedSize);
|
||||
const busy = await page.getByRole('button', { name: /Stop/ }).count();
|
||||
if (!busy || Date.now() - t0 > 240000) break;
|
||||
}
|
||||
const cpu1 = (await cdp.send('Performance.getMetrics')).metrics.find(x => x.name === 'TaskDuration').value;
|
||||
console.log(` turn ${t}: ${(Date.now() - t0) / 1000}s peak-heap=${(peak / 1048576).toFixed(0)}MB main-thread-busy=${(cpu1 - cpu0).toFixed(1)}s`);
|
||||
await metrics('after turn ' + t);
|
||||
}
|
||||
if (MON) { await page.getByText('Monitor', { exact: true }).first().click().catch(() => console.log('no monitor link')); await page.waitForTimeout(25000); await metrics('monitor 25s'); }
|
||||
try { console.log(execSync('powershell -NoProfile -c "(Get-Process chrome | Measure-Object WorkingSet64 -Sum).Sum/1MB"').toString().trim(), 'MB chrome working set total'); } catch {}
|
||||
await browser.close();
|
||||
@@ -62,6 +62,18 @@ export function parsePcieThroughput(raw) {
|
||||
}
|
||||
async function read(path) { try {return (await readFile(path,'utf8')).trim();} catch {return '';} }
|
||||
async function list(path) {try{return await readdir(path);}catch{return [];}}
|
||||
/** A server's remote-console chip (ASPEED), a virtual display or a framebuffer is not a graphics card: no maker we know and no memory of its own. */
|
||||
export function isComputeAdapter(vendor,vramBytes) { return vendor!=='unknown'||vramBytes>0; }
|
||||
/**
|
||||
* The temperature of an amdgpu card from its hwmon inputs [{label, milli}]: the edge sensor (what nvtop and rocm-smi show), else the
|
||||
* junction, else the first one that has a plausible value. Cards such as the Instinct line have no edge sensor. 0 is no reading.
|
||||
*/
|
||||
export function pickGpuTemperature(inputs) {
|
||||
const ok=inputs.filter(i=>i.milli!==null&&i.milli>0&&i.milli<=150000);
|
||||
const first=re=>ok.find(i=>re.test(i.label??''));
|
||||
const chosen=first(/edge|^gpu/i)??ok.find(i=>!i.label)??first(/junction|hotspot/i)??ok.find(i=>!/mem/i.test(i.label??''));
|
||||
return chosen?chosen.milli/1000:null;
|
||||
}
|
||||
export async function detectDrmGpus() {
|
||||
const result=[];
|
||||
for(const card of (await list('/sys/class/drm')).filter(n=>/^card\d+$/.test(n))) {
|
||||
@@ -71,6 +83,7 @@ export async function detectDrmGpus() {
|
||||
const vendor={'0x1002':'amd','0x8086':'intel','0x10de':'nvidia'}[vendorId]??'unknown';
|
||||
const name=await probe('lspci',['-s',busId]);
|
||||
const bytes=number(await read(`${device}/mem_info_vram_total`));
|
||||
if(!isComputeAdapter(vendor,bytes))continue;
|
||||
result.push({id:`drm:${busId}`,index:result.length,busId,vendor,name:name.replace(/^[^ ]+\s+[^:]+:\s*/,'')||`${vendor.toUpperCase()} GPU`,vramGb:bytes===null?0:round(bytes/1024**3,1),unified:vendor==='intel'&&!(bytes>0),backend:'vulkan',sensorPath:device});
|
||||
}
|
||||
return result;
|
||||
@@ -89,7 +102,7 @@ export async function readGpuTelemetry(gpus, { transfer: wantTransfer = true, ru
|
||||
if(gpu.vendor==='nvidia')reading={...reading,pcieRxMbs:transfer.get(gpu.sensorIndex??gpu.index)??null};
|
||||
if(gpu.luid&&!nvidia.has(gpu.id)) {
|
||||
const counters=windowsGpuReading(gpu.luid);
|
||||
if(counters)reading={...reading,utilizationPct:counters.utilizationPct,memoryUsedGb:round(counters.memoryUsedGb,2)};
|
||||
if(counters)reading={...reading,utilizationPct:counters.utilizationPct,memoryUsedGb:round(counters.memoryUsedGb,2),temperatureC:counters.temperatureC??null};
|
||||
}
|
||||
if(gpu.sensorPath&&!nvidia.has(gpu.id)) {
|
||||
let temperature=null,power=null,powerLimit=null,fanPwm=null,clockHz=null;
|
||||
@@ -99,12 +112,9 @@ export async function readGpuTelemetry(gpus, { transfer: wantTransfer = true, ru
|
||||
powerLimit ??= number(await read(`${folder}/power1_cap`));
|
||||
fanPwm ??= number(await read(`${folder}/pwm1`)); // 0-255, the fan's duty as the amdgpu driver reports it
|
||||
clockHz ??= number(await read(`${folder}/freq1_input`)); // the graphics clock in Hz
|
||||
for(const input of inputs) {
|
||||
const label=await read(`${folder}/${input.replace('_input','_label')}`);
|
||||
if(label&&!/edge|gpu/i.test(label))continue;
|
||||
const milli=number(await read(`${folder}/${input}`));
|
||||
if(milli!==null&&milli>=0&&milli<=150000){temperature=milli/1000;break;}
|
||||
}
|
||||
const sensors=[];
|
||||
for(const input of inputs)sensors.push({label:await read(`${folder}/${input.replace('_input','_label')}`),milli:number(await read(`${folder}/${input}`))});
|
||||
temperature=pickGpuTemperature(sensors);
|
||||
if(temperature!==null)break;
|
||||
}
|
||||
const utilization=number(await read(`${gpu.sensorPath}/gpu_busy_percent`)),bytes=number(await read(`${gpu.sensorPath}/mem_info_vram_used`));
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
const MEASURES=['temperatureC','utilizationPct','memoryUsedGb','powerW','powerLimitW','pcieGen','pcieGenMax','pcieWidth','pcieRxMbs'];
|
||||
/** The engine's numbers win; a number the engine leaves out comes from the host's own reading of the same card. */
|
||||
export function mergeOwnReading(engine,own) {
|
||||
if(!own)return engine;
|
||||
const merged={...own,...Object.fromEntries(Object.entries(engine).filter(([,v])=>v!==null&&v!==undefined))};
|
||||
const engineGave=MEASURES.some(k=>typeof engine[k]==='number');
|
||||
return {...merged,source:engineGave?'engine':own.source};
|
||||
}
|
||||
|
||||
/** Translate the original Strata /metrics dashboard without inventing missing sensors. */
|
||||
export function originalDashboard(raw, hardware, fallback) {
|
||||
const live=raw.live,engine=raw.engine,hw=raw.hardware??{},history=raw.history??{},cache=raw.conversation_cache,totals=raw.totals;
|
||||
const finite=n=>typeof n==='number'&&Number.isFinite(n);
|
||||
const gb=n=>finite(n)?n/1024**3:null;
|
||||
const devices=hardware?.gpus??[];
|
||||
// The host's own sensor readings (nvidia-smi, amdgpu sysfs, the Windows GPU counters) fill in what the engine does not report:
|
||||
// an AMD card on Windows has no engine reader at all, and the engine never gives the fan or the clock (#3727).
|
||||
const own=new Map((fallback?.hardware?.gpus??[]).map(r=>[r.id,r]));
|
||||
const readings=(hw.gpus??(devices.length===1?[{index:devices[0].index,util:hw.gpu_util,mem_used:hw.gpu_mem_used,temp:hw.gpu_temp,power:hw.gpu_power,power_limit:hw.gpu_power_limit,pcie_gen:hw.gpu_pcie_gen,pcie_gen_max:hw.gpu_pcie_gen_max,pcie_width:hw.gpu_pcie_width,pcie_rx_mb:hw.gpu_pcie_rx_mb}]:[])).map(g=>{
|
||||
const device=devices.find(d=>d.index===g.index);
|
||||
return {id:device?.id??`${device?.vendor??'unknown'}:${g.index}`,source:'engine',sampledAt:new Date().toISOString(),temperatureC:g.temp??null,utilizationPct:g.util??null,memoryUsedGb:gb(g.mem_used),powerW:g.power??null,powerLimitW:g.power_limit??null,pcieGen:g.pcie_gen??null,pcieGenMax:g.pcie_gen_max??null,pcieWidth:g.pcie_width??null,pcieRxMbs:g.pcie_rx_mb??null};
|
||||
const id=device?.id??`${device?.vendor??'unknown'}:${g.index}`;
|
||||
return mergeOwnReading({id,source:'engine',sampledAt:new Date().toISOString(),temperatureC:g.temp??null,utilizationPct:g.util??null,memoryUsedGb:gb(g.mem_used),powerW:g.power??null,powerLimitW:g.power_limit??null,pcieGen:g.pcie_gen??null,pcieGenMax:g.pcie_gen_max??null,pcieWidth:g.pcie_width??null,pcieRxMbs:g.pcie_rx_mb??null},own.get(id));
|
||||
});
|
||||
const last=raw.requests?.[0],used=live.state!=='idle'&&live.state!=='unloaded'?(live.prompt_tokens??0)+(live.generated??0):(last?.prompt_tokens??0)+(last?.output_tokens??0);
|
||||
const maximum=engine.max_context??fallback.context.maxTokens;
|
||||
@@ -20,7 +33,7 @@ export function originalDashboard(raw, hardware, fallback) {
|
||||
generation:{done:live.generated??null,total:live.max_tokens??null},
|
||||
queue:{running:live.running??(['reading','generating'].includes(live.state)?1:0),waiting:live.queued??0},context:{usedTokens:used,maxTokens:maximum,pct:maxPct(used,maximum)},
|
||||
expertCache:{used:engine.expert_slots??0,total:engine.experts??0,memoryGb:finite(engine.expert_cache_mib)?engine.expert_cache_mib/1024:null,source:finite(engine.expert_slots)?'engine':'none'},
|
||||
hardware:{...fallback.hardware,gpus:readings.length?readings:fallback.hardware.gpus,gpuLoad:hw.gpu_util??null,vramUsedGb:gb(hw.gpu_mem_used),vramTotalGb:gb(hw.gpu_mem_total)??fallback.hardware.vramTotalGb,gpuTempC:hw.gpu_temp??null,powerW:hw.gpu_power??null,powerLimitW:hw.gpu_power_limit??null,pcieRxMbs:hw.gpu_pcie_rx_mb??null,cpuLoad:hw.cpu??fallback.hardware.cpuLoad,ramUsedGb:gb(hw.ram_used)??fallback.hardware.ramUsedGb,ramTotalGb:gb(hw.ram_total)??fallback.hardware.ramTotalGb,diskReadGbs:finite(hw.disk_read_mb)?hw.disk_read_mb/1024:null,diskWriteMbs:hw.disk_write_mb??null},
|
||||
hardware:{...fallback.hardware,gpus:readings.length?readings:fallback.hardware.gpus,gpuLoad:hw.gpu_util??fallback.hardware?.gpuLoad??null,vramUsedGb:gb(hw.gpu_mem_used)??fallback.hardware?.vramUsedGb??null,vramTotalGb:gb(hw.gpu_mem_total)??fallback.hardware.vramTotalGb,gpuTempC:hw.gpu_temp??fallback.hardware?.gpuTempC??null,powerW:hw.gpu_power??fallback.hardware?.powerW??null,powerLimitW:hw.gpu_power_limit??fallback.hardware?.powerLimitW??null,pcieRxMbs:hw.gpu_pcie_rx_mb??fallback.hardware?.pcieRxMbs??null,cpuLoad:hw.cpu??fallback.hardware.cpuLoad,ramUsedGb:gb(hw.ram_used)??fallback.hardware.ramUsedGb,ramTotalGb:gb(hw.ram_total)??fallback.hardware.ramTotalGb,diskReadGbs:finite(hw.disk_read_mb)?hw.disk_read_mb/1024:null,diskWriteMbs:hw.disk_write_mb??null},
|
||||
history:samples.length?samples:fallback.history,requests,
|
||||
totals:totals?{...fallback.totals,requests:totals.requests??0,tokensIn:totals.prompt_tokens??0,tokensOut:totals.output_tokens??0,cachedTokens:totals.reused??0,uncachedTokens:(totals.prompt_tokens??0)-(totals.reused??0),promptMs:totals.prompt_ms,decodeMs:totals.decode_ms,since:totals.since?new Date(totals.since*1000).toISOString():fallback.totals.since}:fallback.totals,
|
||||
conversationCache:cache?{source:'engine',enabled:cache.enabled,parked:cache.parked,slots:cache.slots,memoryGb:gb(cache.bytes),budgetGb:finite(cache.budget_mib)?cache.budget_mib/1024:null,parks:cache.parks,restores:cache.restores,evictions:cache.evictions,lastEvent:cache.last_event,lastAt:cache.last_at,lastTokens:cache.last_tokens,requests:cache.requests,requestsReused:cache.requests_reused,lastPrompt:cache.last_prompt,lastReused:cache.last_reused}:fallback.conversationCache,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { detect, cachedHardware } from './hardware.mjs';
|
||||
import { readGpuTelemetry, gpuMemoryTotals } from './device-probes.mjs';
|
||||
import { createSystemSampler } from './system-telemetry.mjs';
|
||||
import { sensorPace, WATCH_MS } from './sensor-pace.mjs';
|
||||
import { readTemperatures } from './temperature-telemetry.mjs';
|
||||
import { readTemperatures, stopTemperatures } from './temperature-telemetry.mjs';
|
||||
import { addEnergy, energyWh } from './energy-meter.mjs';
|
||||
import { state as servingState } from './serving.mjs';
|
||||
import { originalDashboard } from './monitor-dashboard.mjs';
|
||||
@@ -127,11 +127,14 @@ function tick() {
|
||||
if(!sensorsPending&&Date.now()-sensorsAt>=pace.intervalMs){
|
||||
sensorsPending=true;sensorsAt=Date.now();
|
||||
const hardware=cachedHardware()??hwCache;
|
||||
void Promise.all([readGpuTelemetry(hardware?.gpus??[],{transfer:pace.transfer}),sampleSystem(hardware?.processors??[]),(process.env.STRATA_REAL_ENGINE==='1'?readTemperatures():Promise.resolve(temperatures)).catch(()=>({cpuTempC:null,ssdTempC:null}))])
|
||||
void Promise.all([readGpuTelemetry(hardware?.gpus??[],{transfer:pace.transfer}),sampleSystem(hardware?.processors??[]),(process.env.STRATA_REAL_ENGINE==='1'&&Date.now()<watchedUntil?readTemperatures():Promise.resolve(temperatures)).catch(()=>({cpuTempC:null,ssdTempC:null}))])
|
||||
.then(([readings,system,temps])=>{state.hardware.gpus=readings;systemReading=system;temperatures=temps;})
|
||||
.catch(()=>{state.hardware.gpus=[];systemReading=null;})
|
||||
.finally(()=>{sensorsPending=false;});
|
||||
}
|
||||
/* #4661: the drive-temperature reader asks every physical drive for its temperature. Only while the Monitor is looked at, never from the first second of the app
|
||||
(on some PCs the storage queries stall the whole system); it ends a couple of minutes after the Monitor was last seen. */
|
||||
if(Date.now()>watchedUntil+120000)stopTemperatures();
|
||||
addEnergy(hardwareSnapshot().powerW,Date.now());
|
||||
const hw = state.hardware;
|
||||
const busy = state.phase === 'generating' || state.phase === 'reading' || state.phase === 'thinking';
|
||||
|
||||
@@ -13,6 +13,7 @@ import { USER_KEY, capture as captureUserArgs, merge as mergeUserArgs, setFlag a
|
||||
import { resolvePython, venvPython } from './python-resolver.mjs';
|
||||
import os from 'node:os';
|
||||
import { runtimeVision } from './native-vision.mjs';
|
||||
import { dsEngineArgs, dsUserArgs, dsArgsNote, withoutVisionFlag } from './native-ds-args.mjs';
|
||||
import { componentsDirectory, installerEnvironment, controlEnvironment, sourceServerEnvironment } from './control-runtime.mjs';
|
||||
import { createEngineStep } from './engine-step.mjs';
|
||||
import { dataFolderFromPick, isDriveRoot, makeFolder, rememberDataFolder, syncDataFolder, terminalSettingsFile } from './model-folder.mjs';
|
||||
@@ -223,6 +224,49 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
const DS_EXPERTS_MIN=[0,1,50];
|
||||
function dsEngineVersion(config){const m=/engine-dsv41[\\/](\d+(?:\.\d+)+)-/.exec(String(config?.exe??''))||/engine-dsv41[\\/](\d+(?:\.\d+)+)-/.exec(String((config?.lib_dirs??[])[0]??''));return m?m[1].split('.').map(Number):null;}
|
||||
function dsExpertsSupported(config){const v=dsEngineVersion(config);if(!v)return false;for(let i=0;i<DS_EXPERTS_MIN.length;i++){const a=v[i]??0,b=DS_EXPERTS_MIN[i];if(a!==b)return a>b;}return true;}
|
||||
/* Control's ceiling for one start wait, read from the Control this app actually found rather than guessed from its version. The
|
||||
app starts a model with `strata runtime start --wait`, and that wait is checked three times before the engine is reached: by
|
||||
management.runtime_action, by the operation record written before the start (journal), and by runtime.start itself. 27cbbfe6
|
||||
raised only the runtime's, so a longer wait died with `start wait must be finite seconds in 0..600` and then with
|
||||
`direct runtime intent has an invalid timeout`. The app therefore asks for the longer wait only when a Control's own sources
|
||||
say all three agree, and keeps today's 600 s whenever one of them still names a ceiling of its own. Reading the installed
|
||||
Control to learn what it supports is what the field list and the /v1/responses capability already do. */
|
||||
const START_WAIT_CEILING=600;
|
||||
let ceilingCache={dir:'',time:0,value:START_WAIT_CEILING};
|
||||
function followsRuntimeCeiling(text,marker){
|
||||
/* The marker picks the one line that bounds the wait (`"wait_s": (` in the record's range table, `"start wait"` in the CLI
|
||||
validator), because both files name those keys elsewhere. A line that still spells out 600 is a ceiling of its own. */
|
||||
const line=(String(text).split(/\r?\n/).find(l=>l.includes(marker))??'');
|
||||
return /runtime\.MAX_START_WAIT|start_wait_max/.test(line)&&!/\b600\b/.test(line);
|
||||
}
|
||||
function startWaitCeiling(){
|
||||
const dir=control?.control?.dir??'';if(!dir)return START_WAIT_CEILING;
|
||||
const files=['management.py','journal.py','runtime.py'].map(name=>join(dir,'strata_control',name));
|
||||
let time=0;
|
||||
try{time=Math.max(...files.map(f=>statSync(f).mtimeMs));}catch{return START_WAIT_CEILING;}
|
||||
if(ceilingCache.dir===dir&&ceilingCache.time===time)return ceilingCache.value;
|
||||
let value=START_WAIT_CEILING;
|
||||
try{
|
||||
const [management,journal]=files.slice(0,2).map(f=>readFileSync(f,'utf8'));
|
||||
// both gates must hand the ceiling to the runtime, or the wait would be refused on the way to it
|
||||
if(followsRuntimeCeiling(management,'"start wait"')&&followsRuntimeCeiling(journal,'"wait_s": (')){
|
||||
const declared=Number(readFileSync(files[2],'utf8').match(/^MAX_START_WAIT\s*=\s*(\d+)/m)?.[1]);
|
||||
if(Number.isFinite(declared)&&declared>value)value=declared;
|
||||
}
|
||||
}catch{value=START_WAIT_CEILING;}
|
||||
ceilingCache={dir,time,value};return value;
|
||||
}
|
||||
/* How long a start waits for this model, in the same shape as `strata launch`'s figure (#3949 #3459 #4191): ten minutes, about eight
|
||||
more seconds per GB above 40 GB, never more than this Control accepts. A model nobody can size keeps the 300 s it always had.
|
||||
The run config's engine_ready_s is the floor, not the ceiling: the server reads that number and ends the engine when it runs out,
|
||||
so the start must never give up before the server does (setup writes 3600 for DeepSeek V4.1-Flash). */
|
||||
function startWaitFor(config,entry,model){
|
||||
const ceiling=startWaitCeiling(),ready=Number(config?.engine_ready_s)||0;
|
||||
const known=Number(entry?.diskGb),fromDisk=Number.isFinite(known)&&known>0?known:0;
|
||||
const gb=fromDisk||Number((snapshot.catalog?.families??[]).flatMap(f=>f.sizes??[]).find(s=>s.id===model)?.download_gb)||0;
|
||||
const sized=gb>40?Math.max(300,600+(gb-40)*8):300;
|
||||
return Math.trunc(Math.max(300,Math.min(ceiling,Math.max(ready,sized))));
|
||||
}
|
||||
function buildConfig(args,config){
|
||||
const vision=runtimeVision(config,args.vision);
|
||||
const visionPreferences=args.vision!==undefined?{ui_vision_enabled:args.vision!=='no',ui_vision_device:args.vision==='no'?config.ui_vision_device??'yes':args.vision}:{};
|
||||
@@ -234,7 +278,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
const resources=args.resourceProfile&&!ownEngine(config)?resourceSettings({...base0,[USER_KEY]:mine,vision},snapshot.hardware,args.resourceProfile,modelArena(args.model),values0):null;
|
||||
const cfg={...config,...visionPreferences,...(resources?{...resources,ui_resource_profile:args.resourceProfile}:{}),host:'127.0.0.1',open_browser:false,...validateNativeSettings(args.values??{}),vision};
|
||||
/* The app's suggested cards only fill a gap: a model whose settings file already names its GPUs ("gpu": [1, 0], with "gpu_order": "as_given") keeps them (#3256). A card the person picked in the screen always wins. */
|
||||
const namedInFile=(Array.isArray(config.gpu)?config.gpu.length>0:config.gpu!==undefined&&config.gpu!==null&&config.gpu!=='')||!!config.gpu_roles;
|
||||
const namedInFile=((Array.isArray(config.gpu)?config.gpu.length>0:config.gpu!==undefined&&config.gpu!==null&&config.gpu!=='')||!!config.gpu_roles)&&!setupPickedOneCard(config,snapshot.hardware);
|
||||
/* Roles chosen in the screen (Main / Helper / Off per card, gpu-roles.mjs): the card list is the roles' mirror - Helpers first, the Main card LAST, Off cards left out - with "gpu_roles" beside it. Every card Off is allowed: the server says plainly what the engine can do. */
|
||||
if(args.gpuRoles!==undefined){const make=snapshot.hardware?.engine_vendor,cards=(snapshot.hardware?.gpus??[]).filter(g=>!make||!g.vendor||g.vendor===make),named=parseRoles(args.gpuRoles);if(!named)throw new Error('Choose Main, Helper or Off for each graphics card, with one Main.');const known=new Set(cards.map(g=>g.index));if(Object.keys(named).some(k=>!known.has(Number(k))))throw new Error('Choose detected graphics cards.');const roles=Object.fromEntries(cards.map(g=>[g.index,named[g.index]??'off'])),m=mirrorRoles(roles,cards);for(const k of ['gpu','gpu_order','gpu_none','gpu_roles','gpu_cards','hip_ordinal'])delete cfg[k];Object.assign(cfg,m);const count=Array.isArray(m.gpu)?m.gpu.length:m.gpu_none?0:1;cfg.layer_split=count>1?validateNativeSettings({layer_split:args.layerSplit??'auto'}).layer_split:'auto';if(cfg.layer_split!=='auto'&&cfg.layer_split.split(',').length!==count-1)throw new Error('Choose one layer split point between each pair of GPUs.');}
|
||||
else if(args.gpus!==undefined&&!(args.gpusFromRecommendation&&namedInFile)){const detected=new Set((snapshot.hardware?.gpus??[]).map(g=>g.index));if(!Array.isArray(args.gpus)||!args.gpus.length||new Set(args.gpus).size!==args.gpus.length||args.gpus.some(g=>!Number.isInteger(g)||!detected.has(g)))throw new Error('Choose distinct detected GPUs. CPU-only text inference is not supported by this native adapter.');cfg.gpu=args.gpus.length===1?args.gpus[0]:args.gpus;for(const k of ['gpu_roles','gpu_cards','gpu_none'])delete cfg[k];delete cfg.hip_ordinal;cfg.layer_split=args.gpus.length>1?validateNativeSettings({layer_split:args.layerSplit??'auto'}).layer_split:'auto';if(cfg.layer_split!=='auto'&&cfg.layer_split.split(',').length!==args.gpus.length-1)throw new Error('Choose one layer split point between each pair of GPUs.');}
|
||||
@@ -243,18 +287,22 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
if(cfg.vision){if(cfg.vision_tokens!=null)cfg.vision={...cfg.vision,max_tokens:cfg.vision_tokens};if(cfg.vision_min_tokens!=null)cfg.vision={...cfg.vision,min_tokens:cfg.vision_min_tokens};}cfg.mcp={...(cfg.mcp??{}),...(cfg.mcp_timeout_s!=null?{timeout_s:cfg.mcp_timeout_s}:{}),...(cfg.mcp_max_rounds!=null?{max_rounds:cfg.mcp_max_rounds}:{}),...(cfg.mcp_max_result_chars!=null?{max_result_chars:cfg.mcp_max_result_chars}:{})};
|
||||
/* The model's own choices win over what setup wrote into the arguments: "Draft layer (MTP)" off leaves --mtp out of every start, and a picture reading that is off leaves --vision out (setup writes both again whenever it runs; the saved settings are applied here, on every load). */
|
||||
if((cfg.mtp===false||cfg.drafting==='off')&&Array.isArray(cfg.args))cfg.args=withoutDraftLayer(cfg.args);
|
||||
if(!cfg.vision&&Array.isArray(cfg.args))cfg.args=cfg.args.filter(a=>a!=='--vision');
|
||||
/* The Qwen engine's --vision stands alone, so it goes (with a picture word after it, if an older file has one). The DeepSeek engine's is a pair, `--vision on|off`, that setup wrote and the engine reads: it stays whole (taking out only the name left `off` behind: `--eos 1 off`, "unknown option off", #4358 #4359 #4505 #4507). */
|
||||
if(!cfg.vision&&!ownEngine(config)&&Array.isArray(cfg.args))cfg.args=withoutVisionFlag(cfg.args);
|
||||
for(const arg of cfg.args??[])if(arg.endsWith('.gguf')&&!existsSync(resolve(repo(),arg)))throw new Error('An engine argument names a missing GGUF. Restore the saved arguments or repair the files.');
|
||||
/* The app's own choice of request slots (4, fewer where the card has no room) is part of the configuration, so a tune measures the engine a load will start: slots take VRAM from the expert cache. */
|
||||
const parallelChoice=packaged&&!ownEngine(config)?chooseParallel({config:cfg,values:args.values??{},hardware:snapshot.hardware?.gpus?.length?snapshot.hardware:{gpus:hardwareProbe()??[]}}):null;
|
||||
let parallelNote=null;
|
||||
let parallelNote=null,dsNote='';
|
||||
/* The agent asks for long answers (a thinking model can use most of the window): let the engine clamp the output to what is left of the context instead of refusing the request. */
|
||||
if(packaged&&!ownEngine(config)&&cfg.fit_max_tokens===undefined)cfg.fit_max_tokens=true;
|
||||
if(parallelChoice){const applied=applyParallel(cfg,parallelChoice);for(const k of Object.keys(cfg))if(!(k in applied.cfg))delete cfg[k];Object.assign(cfg,applied.cfg);parallelNote=applied.note;}
|
||||
/* DeepSeek's Speed choice: the engine decides by itself (--experts auto: 5 when the cards total 16 GB or less, else 6), so nothing is passed unless the person chose 5 or 6. */
|
||||
if(ownEngine(config)){const list=[];const old=Array.isArray(cfg.args)?cfg.args:[];for(let i=0;i<old.length;i++){if(old[i]==='--experts'){i++;continue;}list.push(old[i]);}if(dsExpertsSupported(config)&&(cfg.ds_experts==='5'||cfg.ds_experts==='6'))list.push('--experts',cfg.ds_experts);else if(!dsExpertsSupported(config)&&old.includes('--experts'))list.splice(0,list.length,...old);cfg.args=list;delete cfg.ds_experts;}
|
||||
if(ownEngine(config)){const list=[];const old=Array.isArray(cfg.args)?cfg.args:[];for(let i=0;i<old.length;i++){if(old[i]==='--experts'){i++;continue;}list.push(old[i]);}if(dsExpertsSupported(config)&&(cfg.ds_experts==='5'||cfg.ds_experts==='6'))list.push('--experts',cfg.ds_experts);else if(!dsExpertsSupported(config)&&old.includes('--experts'))list.splice(0,list.length,...old);cfg.args=list;delete cfg.ds_experts;
|
||||
/* Only the options strata-dsv41 takes go to it (#4504): --vram-reserve-mib is its --reserve-mib, and --pool-workers and the other Qwen-engine names are left out, in the arguments and in the person's saved options. */
|
||||
const built=dsEngineArgs(cfg.args);cfg.args=built.args;dsNote=dsArgsNote(built);
|
||||
if(Array.isArray(cfg[USER_KEY])){const kept=dsUserArgs(cfg[USER_KEY]);if(kept.length)cfg[USER_KEY]=kept;else delete cfg[USER_KEY];}}
|
||||
else delete cfg.ds_experts;
|
||||
return {cfg,resources,visionPreferences,parallelChoice,parallelNote};
|
||||
return {cfg,resources,visionPreferences,parallelChoice,parallelNote,dsNote};
|
||||
}
|
||||
/* The DFlash2 drafter, before a load that uses it: Control says whether the choice needs it (and the file is not here), downloads it pinned and checked, and packs it, with plain progress in the job.
|
||||
Never stops the load: a drafter that cannot be had leaves the MTP running, and the job says so. The choice itself is remembered in the model's settings file. */
|
||||
@@ -279,7 +327,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
await refresh(true);if(job.state!=='running')return;const {entry,path:configFile}=recordFile(args.model);const config=checkedSettingsConfig(readFileSync(configFile,'utf8'));if(packaged){const engine=await control.engine();if(!engine.usable)throw new Error(ENGINE_NOT_RECOGNISED+(engine.notice?` ${engine.notice}`:''));}if(!entry.ready){const error=missingFilesError(args.model,entry.missing);append(args.model,error.details[0]);throw error;}
|
||||
if(externalRunning().length)throw new Error('A Strata server is already running outside this UI. Stop it before loading here, or connect its API in Settings → Models.');
|
||||
if(args.multiple&&instances.size>=8)throw new Error('Unload an instance before adding another. Up to eight native instances can run here.');
|
||||
const {cfg,resources,visionPreferences,parallelChoice,parallelNote:chosenNote}=buildConfig(args,config);
|
||||
const {cfg,resources,visionPreferences,parallelChoice,parallelNote:chosenNote,dsNote}=buildConfig(args,config);
|
||||
let parallelNote=chosenNote;
|
||||
await prepareDrafter(job,args,cfg,configFile);if(job.state!=='running')return;
|
||||
const fingerprint=createHash('sha256').update(JSON.stringify({...cfg,api_key:undefined,port:undefined})).digest('hex');
|
||||
@@ -300,12 +348,19 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
/* Another model on the same graphics card: the engine takes the card's free memory, so a second one usually finds none. Loading goes on (recommend, never force); if it fails, the reason is said. */
|
||||
const sharedWith=instance.gpus.length?[...instances.values()].find(i=>i.id!==id&&i.status!=='error'&&i.model!==args.model&&i.gpus.some(g=>instance.gpus.includes(g))):null;
|
||||
const sharedSentence=sharedWith?`GPU ${instance.gpus.find(g=>sharedWith.gpus.includes(g))+1} is already used by ${sharedWith.label}, so this model did not fit there. Load it on another GPU, or unload ${sharedWith.label} first.`:null;
|
||||
let proc=null,started=null,retried=null;
|
||||
let proc=null,started=null,retried=null,startWait=null;
|
||||
if(packaged){
|
||||
/* How long this start waits, and the server's own engine wait kept above it. The server ends the engine when its engine wait runs
|
||||
out (900 s, or the config's engine_ready_s), so a start that waits longer than that can never win: as in `strata launch`, the
|
||||
server gets a wait above the start's. It is only ever raised, never lowered, so a config that already names a longer one
|
||||
(setup's 3600 for DeepSeek V4.1-Flash) is left alone. */
|
||||
startWait=startWaitFor(config,entry,args.model);
|
||||
if(startWait+60>Math.max(900,Number(cfg.engine_ready_s)||0))cfg.engine_ready_s=startWait+60;
|
||||
/* The engine starts under Control's owner runtime, not as our child: a saved profile per slot, then `runtime start`. */
|
||||
const used=new Set([...instances.values()].map(i=>i.profile));let slot=1;while(used.has('app-'+slot))slot++;instance.profile='app-'+slot;
|
||||
instances.set(id,instance);if(!defaultId||!args.multiple||wasDefault)defaultId=id;emit();
|
||||
const startWith=value=>{writeFileSync(configPath,JSON.stringify(value,null,2),{mode:0o600});return control.stop(instance.profile).catch(()=>{}).then(()=>control.saveProfile(instance.profile,configPath)).then(()=>control.start(instance.profile,{waitSeconds:ownEngine(config)?Math.min(600,Math.max(300,Number(config.engine_ready_s)||300)):300}));};
|
||||
if(dsNote)append(id,dsNote);
|
||||
const startWith=value=>{writeFileSync(configPath,JSON.stringify(value,null,2),{mode:0o600});return control.stop(instance.profile).catch(()=>{}).then(()=>control.saveProfile(instance.profile,configPath)).then(()=>control.start(instance.profile,{waitSeconds:startWait}));};
|
||||
started=startWith(cfg);
|
||||
started.then(()=>append(id,'Strata Control reports the engine is ready.'),error=>{append(id,error.message);});
|
||||
try{await started;}catch(firstError){let error=firstError;
|
||||
@@ -397,16 +452,19 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
function writeModelConfig(path,next){const mine=mergeUserArgs(captureUserArgs(readJson(path)),next[USER_KEY]??[]);next=stampedUserArgs(next,mine);copyFileSync(path,path+'.bak');chmodSync(path+'.bak',0o600);const temp=path+'.ui-tmp';writeFileSync(temp,JSON.stringify(next,null,2)+'\n',{mode:0o600});renameSync(temp,path);}
|
||||
async function tune(job,args){
|
||||
await refresh(true);if(job.state!=='running')return;
|
||||
const chosen=record(args.model);resourceSettings(chosen.config,snapshot.hardware,args.profile,modelArena(args.model));
|
||||
const chosen=record(args.model);
|
||||
/* The resource preference writes the Qwen engine's options (--pool-workers, --vram-reserve-mib); the DeepSeek engine takes neither (#4504), so its settings file is left as it is. */
|
||||
const resourceFor=config=>ownEngine(config)?{}:resourceSettings(config,snapshot.hardware,args.profile,modelArena(args.model));
|
||||
resourceFor(chosen.config);
|
||||
if(!chosen.entry.ready)throw new Error('Prepare the model before tuning.');
|
||||
if(externalRunning().length)throw new Error('Stop the native server outside this UI before tuning.');
|
||||
if(busy(requests().filter(r=>instances.has(r.instanceId))))throw new Error('Finish requests before tuning.');
|
||||
for(const id of [...instances.keys()])await unload(id);
|
||||
if(job.state!=='running')return;
|
||||
const initialValues=resourceSettings(chosen.config,snapshot.hardware,args.profile,modelArena(args.model));writeModelConfig(chosen.path,{...chosen.config,...initialValues,ui_resource_profile:args.profile});
|
||||
const initialValues=resourceFor(chosen.config);writeModelConfig(chosen.path,{...chosen.config,...initialValues,ui_resource_profile:args.profile});
|
||||
if(args.calibrate!==false&&state().capabilities.calibration){job.summary='Measuring the best engine settings for this computer…';emit();await program(job,args);}
|
||||
if(job.state!=='running')return;
|
||||
const current=record(args.model),values=resourceSettings(current.config,snapshot.hardware,args.profile,modelArena(args.model));
|
||||
const current=record(args.model),values=resourceFor(current.config);
|
||||
const next={...current.config,...values,ui_resource_profile:args.profile};
|
||||
writeModelConfig(current.path,next);
|
||||
job.summary='Applying your resource preference and loading the model…';emit();
|
||||
@@ -535,3 +593,13 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
async function localPreview(path){if(!packaged)throw new Error('Checking local model files is part of the Strata app.');if(typeof path!=='string'||!path.trim()||[...path].some(c=>c.charCodeAt(0)<32))throw new Error('Choose a folder.');return control.importPlan(path.trim());}
|
||||
return {state:publicState,tuneStatus,tuneEffective,tuneOff,tunePresets:{list:()=>autotuner.list(),action:(kind,id,extra)=>autotuner.action(kind,id,extra),undoFor:async args=>autotuner.undoFor(tuneConfig(args).path)},refresh,localPreview,configure,setDownloadDirectory,planModelsFolder,startModelsFolder,interruptedMove,finishInterruptedMove,leaveInterruptedMove,plan,settings,save,settingsFile,openSettingsFile,operation,cancel,unload,resolveInstance,poll,logs:()=>logs.slice(),benchmarkReport:()=>readJson(join(directory,'native-benchmark.json'),null),setDefault(id){defaultId=resolveInstance(id).id;emit();return publicState();},hasLive:()=>[...instances.values()].some(i=>i.status==='ready'),readyInstances:()=>[...instances.values()].filter(i=>i.status==='ready'),defaultInstanceId:()=>defaultId,close};
|
||||
}
|
||||
/* A DeepSeek config on AMD written by setup 0.2.8 or earlier names ONE card and marks it "asked", though setup picked it itself (strata_control/gpu_pick.py): it must not stop the app's recommended cards from applying. A card setup named for itself is the first of the usable cards, largest memory first; another card number was the person's. Roles and a "gpus_by" marker are always honoured. */
|
||||
export function setupPickedOneCard(config,hardware){
|
||||
if(!config||config.arch!=='deepseek41'||config.backend!=='hip'||config.gpus_by||config.gpu_roles||!config.gpus_asked)return false;
|
||||
const one=Array.isArray(config.gpu)?(config.gpu.length===1?config.gpu[0]:null):config.gpu;if(!Number.isInteger(Number(one))||one===null||one==='')return false;
|
||||
const cards=(hardware?.gpus??[]).filter(g=>(g.vendor==='amd')&&g.usable!==false),shares=g=>!!(g.uma||g.integrated||g.unified_gb),vram=g=>Number(g.vram_gb)||0;
|
||||
const real=cards.some(g=>!shares(g)&&vram(g)>=4),keep=cards.filter(g=>!(shares(g)&&real)&&vram(g)>=8);
|
||||
if(process.platform==='win32'||keep.length<2)return false;
|
||||
const first=[...keep].sort((a,b)=>Math.round(vram(b))-Math.round(vram(a))||a.index-b.index)[0];
|
||||
return Number(one)===first.index;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/** The engine options of the DeepSeek-V4.1 engine (strata-dsv41), and the two ways the app used to hand it options it does not take.
|
||||
*
|
||||
* strata-dsv41 is its own program (src/dsv41/ds4_main.cpp) with its own short option list, and it exits with "unknown option X" (code 2)
|
||||
* on anything else. The Qwen engine takes other names (--vram-reserve-mib, --pool-workers, --resident-budget-gib, ...), and the app's
|
||||
* settings, resource profile and tune presets write those into a model's "args" and "user_args". Before the load starts, the arguments
|
||||
* of a DeepSeek model are built from what the engine takes (#4504): a name it spells differently is renamed, anything else is left out
|
||||
* with its value and said in the job's log. test/native-ds-args.test.mjs reads ds4_main.cpp and fails when this list and the engine drift.
|
||||
*/
|
||||
|
||||
/** Options that take one value. */
|
||||
export const DS_VALUE_FLAGS = new Set([
|
||||
'--pack', '--gpus', '--max-context', '--reserve-mib', '--vram-experts', '--cpu-threads', '--expert-profile', '--ssd-cache', '--ssd-policy',
|
||||
'--eos', '--dspark', '--dspark-conf', '--dspark-block', '--dspark-depth', '--experts', '--tp', '--prefill-chunk', '--threads', '--vision',
|
||||
'--mmap-experts', '--layer-split',
|
||||
]);
|
||||
/** Options that stand alone. */
|
||||
export const DS_SWITCH_FLAGS = new Set(['--no-expert-cache', '--profile-readonly', '--verbose', '-v']);
|
||||
/** The Qwen engine's names for settings the DeepSeek engine has under another name (same meaning). */
|
||||
export const DS_RENAMED = { '--vram-reserve-mib': '--reserve-mib' };
|
||||
|
||||
const isFlag = text => typeof text === 'string' && text.startsWith('-') && text.length > 1 && /^-{1,2}[A-Za-z]/.test(text);
|
||||
/** The words that follow `flag` at `at` up to the next option: its value (one word, or none for a switch at the end of the list). */
|
||||
function valueSpan(list, at) {
|
||||
return at + 1 < list.length && !isFlag(list[at + 1]) ? 1 : 0;
|
||||
}
|
||||
|
||||
/** `args` for strata-dsv41: accepted options kept (a renamed one under the engine's name, unless the list already has that name),
|
||||
* everything else left out together with its value. Returns {args, dropped: [{flag, value}], renamed: [{from, to}]}. */
|
||||
export function dsEngineArgs(args) {
|
||||
const list = (Array.isArray(args) ? args : []).map(String);
|
||||
const out = [], dropped = [], renamed = [];
|
||||
const has = new Set(list.filter(isFlag));
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i];
|
||||
if (!isFlag(item)) { out.push(item); continue; } // a stray word: kept as it was (the engine says what it is)
|
||||
const own = DS_RENAMED[item];
|
||||
if (own) {
|
||||
const n = valueSpan(list, i), value = n ? list[i + 1] : null;
|
||||
i += n;
|
||||
if (has.has(own)) { dropped.push({ flag: item, value, because: `${own} is set` }); continue; }
|
||||
out.push(own, ...(value === null ? [] : [value]));
|
||||
renamed.push({ from: item, to: own });
|
||||
continue;
|
||||
}
|
||||
const takes = DS_VALUE_FLAGS.has(item), alone = DS_SWITCH_FLAGS.has(item);
|
||||
if (takes || alone) {
|
||||
out.push(item);
|
||||
if (takes && i + 1 < list.length && !isFlag(list[i + 1])) out.push(list[++i]);
|
||||
continue;
|
||||
}
|
||||
const n = valueSpan(list, i);
|
||||
dropped.push({ flag: item, value: n ? list[i + 1] : null });
|
||||
i += n;
|
||||
}
|
||||
return { args: out, dropped, renamed };
|
||||
}
|
||||
|
||||
/** The person's own options of a DeepSeek model: unaccepted ones leave the list (so the settings file heals itself); the app's
|
||||
* --vram-reserve-mib stays, because the Settings field writes it under that name and it is renamed at every load. */
|
||||
export function dsUserArgs(user) {
|
||||
const list = (Array.isArray(user) ? user : []).map(String);
|
||||
const out = [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i];
|
||||
if (!isFlag(item)) { out.push(item); continue; }
|
||||
const n = valueSpan(list, i);
|
||||
const keep = DS_VALUE_FLAGS.has(item) || DS_SWITCH_FLAGS.has(item) || item in DS_RENAMED;
|
||||
if (keep) out.push(item, ...list.slice(i + 1, i + 1 + n));
|
||||
i += n;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** One line for the job's log about what was changed, or '' when nothing was. */
|
||||
export function dsArgsNote({ dropped = [], renamed = [] }) {
|
||||
const parts = [];
|
||||
if (renamed.length) parts.push(`${renamed.map(r => `${r.from} is passed as ${r.to}`).join(', ')}`);
|
||||
if (dropped.length) parts.push(`left out because the DeepSeek engine does not take them: ${dropped.map(d => [d.flag, d.value].filter(v => v !== null && v !== undefined).join(' ')).join(', ')}`);
|
||||
return parts.length ? `DeepSeek engine options: ${parts.join('; ')}.` : '';
|
||||
}
|
||||
|
||||
/** `args` without `flag` and the picture word that follows it (the Qwen engine's --vision stands alone; an older config may carry a value). */
|
||||
const PICTURE_WORDS = new Set(['on', 'off', 'yes', 'no', 'gpu', 'cpu', 'none']);
|
||||
export function withoutVisionFlag(args) {
|
||||
const list = (Array.isArray(args) ? args : []).map(String), out = [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i] === '--vision') { if (i + 1 < list.length && PICTURE_WORDS.has(list[i + 1].toLowerCase())) i++; continue; }
|
||||
out.push(list[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
import { once } from 'node:events';
|
||||
export async function nativeGateway({req,res,path,body,control,serving,fetcher=fetch}){
|
||||
let instance;try{instance=control.resolveInstance(body.model);}catch(e){res.writeHead(400,{'content-type':'application/json'});res.end(JSON.stringify({error:{message:e.message,type:'invalid_request_error',param:'model',code:null}}));return;}
|
||||
const abort=new AbortController(),started=Date.now();res.on('close',()=>{if(!res.writableEnded)abort.abort();});
|
||||
const abort=new AbortController(),started=Date.now();let sawDone=false;/* #4690: a client that hangs up after `data: [DONE]` (the OpenAI SDK does) has the whole answer: that is not a cancel. */res.on('close',()=>{if(!res.writableEnded&&!sawDone)abort.abort();});
|
||||
const entry=serving.open({client:req.headers['user-agent']?.slice(0,100)??'API client',endpoint:path.slice(3),model:instance.apiModelId,instanceId:instance.id,gpuIds:instance.gpus.map(String),cancel:()=>abort.abort()});
|
||||
let tokensIn=null,tokensOut=null,tps=null,promptTps=null;
|
||||
const observe=value=>{const usage=value.usage??value.response?.usage;if(usage){tokensIn=usage.prompt_tokens??usage.input_tokens??tokensIn;tokensOut=usage.completion_tokens??usage.output_tokens??tokensOut;}if(value.timings){tps=value.timings.predicted_per_second??tps;promptTps=value.timings.prompt_per_second??promptTps;}};
|
||||
@@ -11,7 +11,7 @@ export async function nativeGateway({req,res,path,body,control,serving,fetcher=f
|
||||
if(!response.ok){const content=await response.text();serving.finish(entry.id,{durationMs:Date.now()-started,error:`Engine returned ${response.status}`});res.writeHead(response.status,{'content-type':response.headers.get('content-type')??'application/json'});res.end(content);return;}
|
||||
serving.mark(entry.id,{state:'generating'});
|
||||
res.writeHead(response.status,{'content-type':response.headers.get('content-type')??'application/json','cache-control':'no-cache','x-strata-request-id':entry.id,'x-accel-buffering':'no'});
|
||||
if(body.stream===true){let buffer='';const decoder=new TextDecoder();for await(const chunk of response.body){buffer+=decoder.decode(chunk,{stream:true});let end;while((end=buffer.indexOf('\n'))>=0){const line=buffer.slice(0,end);buffer=buffer.slice(end+1);if(line.startsWith('data:'))try{observe(JSON.parse(line.slice(5)));}catch{}}if(!res.write(chunk))await once(res,'drain',{signal:abort.signal});}}
|
||||
if(body.stream===true){let buffer='';const decoder=new TextDecoder();for await(const chunk of response.body){buffer+=decoder.decode(chunk,{stream:true});let end;while((end=buffer.indexOf('\n'))>=0){const line=buffer.slice(0,end);buffer=buffer.slice(end+1);if(line.startsWith('data:')){if(line.slice(5).trim()==='[DONE]')sawDone=true;else try{observe(JSON.parse(line.slice(5)));}catch{}}}if(!res.destroyed&&!res.write(chunk))await once(res,'drain',{signal:abort.signal});}}
|
||||
else{const text=await response.text();try{observe(JSON.parse(text));}catch{}res.write(text);}
|
||||
serving.mark(entry.id,{tokensIn,tokensOut,tps,promptTps,usageSource:tokensIn===null||tokensOut===null?'unavailable':'reported'});serving.finish(entry.id,{tokensOut,durationMs:Date.now()-started});res.end();
|
||||
}catch(e){serving.finish(entry.id,{tokensOut,durationMs:Date.now()-started,error:abort.signal.aborted?'Request cancelled.':e.message});if(!res.headersSent){res.writeHead(502,{'content-type':'application/json'});res.end(JSON.stringify({error:{message:e.message,type:'server_error'}}));}else res.end();}
|
||||
|
||||
@@ -32,7 +32,8 @@ export const ENV_FLAG=/^(?:STRATA|DS4)_[A-Z0-9_]+$/;
|
||||
* The speed features of 0.2.7 that have a switch in Expert settings. A switch has a default ('on' or 'off'); outside it, it writes exactly the
|
||||
* environment entries `env` into the model's settings file ("env"); back in its default it removes them, so a default launch carries none of them.
|
||||
* "Faster long prompts" (Qwen KVA) is OFF by default (owner decision, 26 October): STRATA_KVA=1 turns it on, and an earlier test build's STRATA_KVA=0 reads as off.
|
||||
* The other two are on by default. The names are the engine switch contract (phaseA-tests/notes-027-plan.md); strata_control/speed_switches.py says the
|
||||
* "Faster answers on 12 GB NVIDIA cards" (residency-biased routing) is OFF by default since 0.2.8 (owner decision, 29 October; #4402): STRATA_ROUTE_PRIOR=1.5
|
||||
* turns it on, and the STRATA_ROUTE_PRIOR=0 that 0.2.7's Off wrote is the default state now. The other two are on by default. The names are the engine switch contract (phaseA-tests/notes-027-plan.md); strata_control/speed_switches.py says the
|
||||
* same for the Terminal (tests/terminal/test_speed_switches.py compares the two).
|
||||
*/
|
||||
export const SPEED_SWITCHES=[
|
||||
@@ -40,8 +41,8 @@ export const SPEED_SWITCHES=[
|
||||
hint:'Reads very long prompts faster by approximating part of the work. Off by default: answers about long documents can miss or mix up details.',
|
||||
default:'off',env:{STRATA_KVA:'1'}},
|
||||
{id:'faster_12gb_nvidia',label:'Faster answers on 12 GB NVIDIA cards',group:'Speed',
|
||||
hint:'Prefers the parts of the model already on the graphics card, which makes answers much faster on 12 GB cards. Turn off if answers look worse than before.',
|
||||
default:'on',env:{STRATA_ROUTE_PRIOR:'0'}},
|
||||
hint:'Prefers the parts of the model already on the graphics card, which makes answers faster on 12 GB cards. Off by default: in long agent sessions it can repeat itself and give weaker results. Turn on if you mostly chat and want the speed.',
|
||||
default:'off',env:{STRATA_ROUTE_PRIOR:'1.5'}},
|
||||
{id:'faster_radeon_9000',label:'Faster answers on Radeon RX 9000 / AI PRO R9700',group:'Speed',
|
||||
hint:'Shares the work between the graphics card and the processor in a way that makes answers faster on these cards. Turn off to go back to the previous setting, which is slower.',
|
||||
default:'on',env:{STRATA_PCIE_FRAC_DEFAULT:'old'}},
|
||||
|
||||
@@ -122,3 +122,8 @@ export async function readTemperatures({ platform = process.platform, now = Date
|
||||
}
|
||||
return { cpuTempC: null, ssdTempC: null };
|
||||
}
|
||||
|
||||
/** Ends the Windows drive-temperature loop (the Monitor has not been open for a while); the next read starts it again. */
|
||||
export function stopTemperatures() {
|
||||
if (windowsReader) { windowsReader.stop(); windowsReader = null; }
|
||||
}
|
||||
@@ -3,7 +3,11 @@
|
||||
* - size: the DirectX adapter list in the registry (HKLM\SOFTWARE\Microsoft\DirectX) has a 64-bit DedicatedVideoMemory and the adapter
|
||||
* LUID. Win32_VideoController.AdapterRAM is 32 bits and reads 0 or 4 GB for a 16 GB card (#3727, #3836).
|
||||
* - load and memory in use: the Windows GPU performance counters (the ones Task Manager shows), read by one long-lived PowerShell and
|
||||
* matched to the card by LUID. Temperature, power and the PCIe link need the vendor's own library, so they stay "not available".
|
||||
* matched to the card by LUID.
|
||||
* - temperature: the display driver's own D3DKMT adapter performance data (gdi32.dll, no vendor library, no administrator), asked for the
|
||||
* same LUID. An AMD 7900 XTX gave the same value as ADLX at idle and under load (noctrex, #3727); the card answers in tenths of a degree.
|
||||
* A driver that does not answer leaves the temperature "not available". Power, fan and the PCIe link stay "not available": D3DKMT gave
|
||||
* 0 for power and for the PCIe rate on that card.
|
||||
*/
|
||||
import { createPowerShellLoop, endWithHost } from './windows-poll.mjs';
|
||||
import { utf8Script } from './powershell-utf8.mjs';
|
||||
@@ -55,20 +59,30 @@ export function windowsOtherGpus(videoRows, dx) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** One counter line: "G <luid> <load percent> <dedicated bytes in use>". */
|
||||
export function parseGpuCounterLine(line) {
|
||||
const m = /^G\s+(luid_0x[0-9a-f]+_0x[0-9a-f]+)\s+(\d+(?:\.\d+)?)\s+(\d+)\s*$/i.exec(String(line));
|
||||
return m ? { luid: m[1].toLowerCase(), utilizationPct: Math.max(0, Math.min(100, Number(m[2]))), memoryUsedGb: Number(m[3]) / 1024 ** 3 } : null;
|
||||
/** The temperature D3DKMT reports in tenths of a degree -> whole degrees, or null when the driver gave nothing believable. */
|
||||
export function d3dkmtTemperatureC(deci) {
|
||||
const c = Number(deci) / 10;
|
||||
return Number.isFinite(c) && c > 0 && c <= 150 ? Math.round(c) : null;
|
||||
}
|
||||
|
||||
/** One counter line: "G <luid> <load percent> <dedicated bytes in use> [<temperature in tenths of a degree>]". */
|
||||
export function parseGpuCounterLine(line) {
|
||||
const m = /^G\s+(luid_0x[0-9a-f]+_0x[0-9a-f]+)\s+(\d+(?:\.\d+)?)\s+(\d+)(?:\s+(-?\d+))?\s*$/i.exec(String(line));
|
||||
return m ? { luid: m[1].toLowerCase(), utilizationPct: Math.max(0, Math.min(100, Number(m[2]))), memoryUsedGb: Number(m[3]) / 1024 ** 3, temperatureC: d3dkmtTemperatureC(m[4]) } : null;
|
||||
}
|
||||
|
||||
/** The C# that asks D3DKMT (KMTQAITYPE_ADAPTERPERFDATA = 62, 64-byte D3DKMT_ADAPTER_PERFDATA, Temperature at byte 56) for one LUID; -1 = no answer. */
|
||||
export const GPU_TEMP_CSHARP = 'using System;using System.Runtime.InteropServices;public static class StrataGpuTemp{[StructLayout(LayoutKind.Sequential)]struct Q{public uint L;public int H;public uint A;}[StructLayout(LayoutKind.Sequential)]struct Info{public uint A;public int T;public IntPtr P;public uint S;}[DllImport("gdi32.dll")]static extern int D3DKMTOpenAdapterFromLuid(ref Q q);[DllImport("gdi32.dll")]static extern int D3DKMTCloseAdapter(ref uint a);[DllImport("gdi32.dll")]static extern int D3DKMTQueryAdapterInfo(ref Info i);public static int Read(int high,uint low){Q q=new Q();q.L=low;q.H=high;if(D3DKMTOpenAdapterFromLuid(ref q)!=0)return -1;IntPtr buf=Marshal.AllocHGlobal(64);try{for(int k=0;k<64;k++)Marshal.WriteByte(buf,k,0);Info i=new Info();i.A=q.A;i.T=62;i.P=buf;i.S=64;if(D3DKMTQueryAdapterInfo(ref i)!=0)return -1;return Marshal.ReadInt32(buf,56);}finally{Marshal.FreeHGlobal(buf);uint a=q.A;D3DKMTCloseAdapter(ref a);}}}';
|
||||
|
||||
export function createWindowsGpuReader({ spawnFn, intervalSec = 4, maxStarts = 3, now = Date.now } = {}) {
|
||||
const readings = new Map();
|
||||
const script = utf8Script(
|
||||
`$t=$false;try{Add-Type -TypeDefinition '${GPU_TEMP_CSHARP}';$t=$true}catch{};` +
|
||||
`while($true){` +
|
||||
`$e=@(Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine);$m=@(Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUAdapterMemory);` +
|
||||
`$u=@{};foreach($r in $e){if($r.Name -match '(luid_0x[0-9A-Fa-f]+_0x[0-9A-Fa-f]+)_phys_\\d+_eng_\\d+_engtype_(\\w+)'){$k=$matches[1].ToLower()+'|'+$matches[2];$u[$k]=[double]$u[$k]+[double]$r.UtilizationPercentage}};` +
|
||||
`$b=@{};foreach($k in $u.Keys){$l=$k.Split('|')[0];if(-not $b.ContainsKey($l) -or $u[$k] -gt $b[$l]){$b[$l]=$u[$k]}};` +
|
||||
`foreach($r in $m){if($r.Name -match '(luid_0x[0-9A-Fa-f]+_0x[0-9A-Fa-f]+)'){$l=$matches[1].ToLower();[Console]::Out.WriteLine('G '+$l+' '+[math]::Round([double]$b[$l],0)+' '+$r.DedicatedUsage)}};` +
|
||||
`foreach($r in $m){if($r.Name -match '(luid_0x[0-9A-Fa-f]+_0x[0-9A-Fa-f]+)'){$l=$matches[1].ToLower();$d=-1;if($t){try{$p=$l.Split('_');$d=[StrataGpuTemp]::Read([Convert]::ToInt32($p[1].Substring(2),16),[Convert]::ToUInt32($p[2].Substring(2),16))}catch{}};[Console]::Out.WriteLine('G '+$l+' '+[math]::Round([double]$b[$l],0)+' '+$r.DedicatedUsage+' '+$d)}};` +
|
||||
`[Console]::Out.Flush();Start-Sleep -Seconds ${intervalSec}}`);
|
||||
const loop = createPowerShellLoop({ script, spawnFn, maxStarts, onLine: line => {
|
||||
const parsed = parseGpuCounterLine(line);
|
||||
|
||||
@@ -96,7 +96,7 @@ export function SettingsUpdates() {
|
||||
</>;
|
||||
}
|
||||
|
||||
/** "Strata 0.2.7 · Engine 0.1.52": the app's own version first, the engine's beside it (About). */
|
||||
/** "Strata 0.2.9 · Engine 0.1.55": the app's own version first, the engine's beside it (About). */
|
||||
export function AboutVersions() {
|
||||
const native = useNative();
|
||||
const app = appVersionLabel(useStore(s => s.setup?.app.version), native?.version);
|
||||
|
||||
@@ -1775,6 +1775,23 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
|
||||
const started = performance.now();
|
||||
|
||||
const imageSlots=photoHistorySlots(prior,pictures.length);
|
||||
/* Text and reasoning arrive token by token (90 a second or more). Drawing the whole answer again for each one made the page do more work for every
|
||||
word the longer the answer grew (measured: 15.7 s of main-thread time for 24 s of a 40,000-token answer), until the page stopped answering
|
||||
and ran out of memory. The pieces are gathered and applied together about ten times a second; any other frame, the end and a failure
|
||||
apply what is waiting first, so the order the person sees is the order it was written. */
|
||||
const waiting: ['reasoning' | 'delta', string][] = [];
|
||||
let waitingTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const applyReasoning = (text: string) => patchAssistant((m) => ({ ...m, reasoning: (m.reasoning ?? '') + text, reasoningStartedAt: m.reasoningStartedAt ?? Date.now() }));
|
||||
const applyDelta = (text: string) => patchAssistant((m) => { const content=tidyAnswer(m.content+text); if (m.reasoningStartedAt && m.reasoningMs === undefined) m = { ...m, reasoningMs: Date.now() - m.reasoningStartedAt }; const count=Math.max(1,Math.ceil((content.length+(m.reasoning?.length??0))/4)); return {...m,content,...(m.blocks?{blocks:appendText(m.blocks,text)}:{}),tokens:m.usageSource==='estimated'&&m.tps?m.tokens:count,inputTokens:m.inputTokens??Math.ceil((body.length+prior.reduce((n,p)=>n+p.content.length,0))/4),usageSource:'estimated'}; });
|
||||
const flushWaiting = () => {
|
||||
if (waitingTimer !== null) { clearTimeout(waitingTimer); waitingTimer = null; }
|
||||
for (const [kind, text] of waiting.splice(0)) if (kind === 'delta') applyDelta(text); else applyReasoning(text);
|
||||
};
|
||||
const queueText = (kind: 'reasoning' | 'delta', text: string) => {
|
||||
const last = waiting[waiting.length - 1];
|
||||
if (last && last[0] === kind) last[1] += text; else waiting.push([kind, text]);
|
||||
waitingTimer ??= setTimeout(flushWaiting, 90);
|
||||
};
|
||||
try {
|
||||
await streamPost(
|
||||
planStart ? '/api/plan/start-goal' : '/api/chat/stream',
|
||||
@@ -1801,6 +1818,8 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
|
||||
// A line from the app itself carries `key` (the English sentence): shown in the person's language.
|
||||
if (frame.type === 'delta' && typeof frame.key === 'string') frame = { ...frame, text: tr(frame.key, Array.isArray(frame.values) ? frame.values : undefined) };
|
||||
const type = frame.type as string;
|
||||
if (type === 'delta' || type === 'reasoning') { queueText(type, String(frame.text ?? '')); return; }
|
||||
flushWaiting();
|
||||
if (type === 'start' && frame.agent === true) { const turn = liveTurns.get(sessionId); if (turn) turn.agent = true; }
|
||||
if (type === 'queued') {
|
||||
patchAssistant(m => ({ ...m, requestId: String(frame.requestId) }));
|
||||
@@ -1837,7 +1856,7 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
|
||||
} else if (type === 'plan-step') {
|
||||
/* the whole plan follows in a plan frame; the live status already says which step is running */
|
||||
} else if (type === 'reasoning') {
|
||||
patchAssistant((m) => ({ ...m, reasoning: (m.reasoning ?? '') + String(frame.text ?? ''), reasoningStartedAt: m.reasoningStartedAt ?? Date.now() }));
|
||||
/* applied through queueText above */
|
||||
} else if (type === 'notice') {
|
||||
// The host says why this chat is not using the agent. One toast per reason per page load.
|
||||
const code = String(frame.code ?? 'notice');
|
||||
@@ -1852,7 +1871,7 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
|
||||
} else if (type === 'agent') {
|
||||
patchSession(sessionId, s => ({ ...s, agents: updateAgent(s.agents ?? [], frame.agent) }));
|
||||
} else if (type === 'delta') {
|
||||
patchAssistant((m) => { const content=tidyAnswer(m.content+String(frame.text??'')); if (m.reasoningStartedAt && m.reasoningMs === undefined) m = { ...m, reasoningMs: Date.now() - m.reasoningStartedAt }; const count=Math.max(1,Math.ceil((content.length+(m.reasoning?.length??0))/4)); return {...m,content,...(m.blocks?{blocks:appendText(m.blocks,String(frame.text??''))}:{}),tokens:m.usageSource==='estimated'&&m.tps?m.tokens:count,inputTokens:m.inputTokens??Math.ceil((body.length+prior.reduce((n,p)=>n+p.content.length,0))/4),usageSource:'estimated'}; });
|
||||
/* applied through queueText above */
|
||||
} else if (type === 'deliverable') {
|
||||
const file = readDeliverable(frame);
|
||||
if (file) patchAssistant(m => ({ ...m, deliverables: mergeDeliverable(m.deliverables, file) }));
|
||||
@@ -1914,6 +1933,7 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
|
||||
turnAbort.signal,
|
||||
);
|
||||
} catch (err) {
|
||||
flushWaiting();
|
||||
if ((err as Error).name !== 'AbortError') {
|
||||
patchAssistant((m) => ({
|
||||
...m,
|
||||
@@ -1925,6 +1945,7 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
|
||||
patchAssistant((m) => ({ ...m, streaming: false }));
|
||||
}
|
||||
} finally {
|
||||
flushWaiting();
|
||||
// A cancelled or failed turn is still worth keeping.
|
||||
flushSessions();
|
||||
clearStatus(sessionId);
|
||||
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "Braucht etwa {0} GB freien Speicherplatz. Was nicht in den Speicher passt, wird von Ihrer SSD gelesen.",
|
||||
"fx81": "{0} RAM + VRAM, dazu etwa {1} GB Speicherplatz",
|
||||
"fx82": "Schnellere Antworten auf NVIDIA-Karten mit 12 GB",
|
||||
"fx83": "Bevorzugt die Teile des Modells, die schon auf der Grafikkarte liegen. Das macht Antworten auf Karten mit 12 GB deutlich schneller. Schalten Sie die Option aus, wenn die Antworten schlechter aussehen als vorher."
|
||||
"fx83": "Bevorzugt die Teile des Modells, die schon auf der Grafikkarte liegen. Das macht Antworten auf Karten mit 12 GB schneller. Standardmäßig aus: In langen Agent-Sitzungen kann sich das Modell wiederholen und schwächere Ergebnisse liefern. Schalten Sie die Option ein, wenn Sie vor allem chatten und die Geschwindigkeit wollen."
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "Needs about {0} GB of free disk space. What does not fit in memory is read from your SSD.",
|
||||
"fx81": "{0} RAM + VRAM, plus about {1} GB of disk",
|
||||
"fx82": "Faster answers on 12 GB NVIDIA cards",
|
||||
"fx83": "Prefers the parts of the model already on the graphics card, which makes answers much faster on 12 GB cards. Turn off if answers look worse than before."
|
||||
"fx83": "Prefers the parts of the model already on the graphics card, which makes answers faster on 12 GB cards. Off by default: in long agent sessions it can repeat itself and give weaker results. Turn on if you mostly chat and want the speed."
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "Necesita unos {0} GB de espacio libre en disco. Lo que no cabe en memoria se lee de tu SSD.",
|
||||
"fx81": "{0} de RAM + VRAM, más unos {1} GB de disco",
|
||||
"fx82": "Respuestas más rápidas en tarjetas NVIDIA de 12 GB",
|
||||
"fx83": "Prefiere las partes del modelo que ya están en la tarjeta gráfica, lo que hace las respuestas mucho más rápidas en tarjetas de 12 GB. Desactívalo si las respuestas parecen peores que antes."
|
||||
"fx83": "Prefiere las partes del modelo que ya están en la tarjeta gráfica, lo que hace las respuestas más rápidas en tarjetas de 12 GB. Desactivado por defecto: en sesiones largas de agente puede repetirse y dar resultados más débiles. Actívalo si sobre todo chateas y quieres la velocidad."
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "Il faut environ {0} Go d’espace disque libre. Ce qui ne tient pas en mémoire est lu depuis votre SSD.",
|
||||
"fx81": "{0} de RAM + VRAM, plus environ {1} Go de disque",
|
||||
"fx82": "Réponses plus rapides sur les cartes NVIDIA de 12 Go",
|
||||
"fx83": "Privilégie les parties du modèle déjà présentes sur la carte graphique, ce qui accélère nettement les réponses sur les cartes de 12 Go. Désactivez-le si les réponses semblent moins bonnes qu’avant."
|
||||
"fx83": "Privilégie les parties du modèle déjà présentes sur la carte graphique, ce qui accélère les réponses sur les cartes de 12 Go. Désactivé par défaut : dans de longues sessions d’agent, le modèle peut se répéter et donner de moins bons résultats. Activez-le si vous discutez surtout et voulez la vitesse."
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "約 {0} GB の空きディスク容量が必要です。メモリに入らない分は SSD から読み込みます。",
|
||||
"fx81": "RAM + VRAM {0}、ほかにディスク約 {1} GB",
|
||||
"fx82": "12 GB の NVIDIA カードでの高速な応答",
|
||||
"fx83": "グラフィックスカード上にすでにあるモデルの部分を優先して使うため、12 GB のカードで応答がとても速くなります。以前より回答が悪くなったと感じたらオフにしてください。"
|
||||
"fx83": "グラフィックスカード上にすでにあるモデルの部分を優先して使うため、12 GB のカードで応答が速くなります。初期設定ではオフです。長いエージェント作業では、同じ内容を繰り返したり、結果が弱くなったりすることがあります。主にチャットで使い、速度が欲しい場合はオンにしてください。"
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "약 {0}GB의 여유 디스크 공간이 필요합니다. 메모리에 들어가지 않는 부분은 SSD에서 읽습니다.",
|
||||
"fx81": "RAM + VRAM {0}, 디스크 약 {1}GB 추가",
|
||||
"fx82": "12GB NVIDIA 카드에서 더 빠른 응답",
|
||||
"fx83": "이미 그래픽 카드에 올라와 있는 모델 부분을 우선 사용해 12GB 카드에서 응답이 훨씬 빨라집니다. 답변이 이전보다 나빠 보이면 끄세요."
|
||||
"fx83": "이미 그래픽 카드에 올라와 있는 모델 부분을 우선 사용해 12GB 카드에서 응답이 더 빨라집니다. 기본값은 꺼짐입니다. 긴 에이전트 세션에서는 같은 말을 반복하거나 결과가 나빠질 수 있습니다. 주로 대화에 쓰고 속도를 원하면 켜세요."
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "Precisa de cerca de {0} GB de espaço livre em disco. O que não cabe na memória é lido do seu SSD.",
|
||||
"fx81": "{0} de RAM + VRAM, mais cerca de {1} GB de disco",
|
||||
"fx82": "Respostas mais rápidas em placas NVIDIA de 12 GB",
|
||||
"fx83": "Prefere as partes do modelo que já estão na placa de vídeo, o que deixa as respostas bem mais rápidas em placas de 12 GB. Desative se as respostas parecerem piores do que antes."
|
||||
"fx83": "Prefere as partes do modelo que já estão na placa de vídeo, o que deixa as respostas mais rápidas em placas de 12 GB. Desativado por padrão: em sessões longas de agente ele pode se repetir e dar resultados mais fracos. Ative se você usa principalmente o chat e quer a velocidade."
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "Нужно около {0} ГБ свободного места на диске. То, что не помещается в память, читается с вашего SSD.",
|
||||
"fx81": "{0} RAM + VRAM, плюс около {1} ГБ на диске",
|
||||
"fx82": "Более быстрые ответы на видеокартах NVIDIA с 12 ГБ",
|
||||
"fx83": "Отдаёт предпочтение частям модели, которые уже находятся на видеокарте, поэтому ответы на картах с 12 ГБ становятся намного быстрее. Отключите, если ответы стали выглядеть хуже, чем раньше."
|
||||
"fx83": "Отдаёт предпочтение частям модели, которые уже находятся на видеокарте, поэтому ответы на картах с 12 ГБ становятся быстрее. По умолчанию выключено: в длинных сессиях агента модель может повторяться и давать более слабые результаты. Включите, если вы в основном общаетесь в чате и хотите скорость."
|
||||
}
|
||||
@@ -81,5 +81,5 @@
|
||||
"fx80": "大约需要 {0} GB 可用磁盘空间。内存放不下的部分会从你的 SSD 读取。",
|
||||
"fx81": "{0} 内存 + 显存,另需约 {1} GB 磁盘空间",
|
||||
"fx82": "12 GB NVIDIA 显卡上更快的回答",
|
||||
"fx83": "优先使用已经在显卡上的模型部分,使 12 GB 显卡上的回答快很多。如果回答看起来比以前差,请关闭。"
|
||||
"fx83": "优先使用已经在显卡上的模型部分,使 12 GB 显卡上的回答更快。默认关闭:在长时间的智能体会话中,它可能重复内容并给出较弱的结果。如果你主要用来聊天并想要速度,请开启。"
|
||||
}
|
||||
@@ -33,7 +33,7 @@ import { useLiveStatus } from '../lib/live-status';
|
||||
import { askQuestion, askSub, askIcon } from '../lib/ask';
|
||||
import { artifactFromUrl, type PreviewArtifact } from '../lib/artifacts';
|
||||
/** Chat — talk to the one loaded model. */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Markdown } from '../components/Markdown';
|
||||
import { Btn, Icon, IconBtn, ErrorNotice, type IconName } from '../components/ui';
|
||||
import { composerCap, scrollBehavior } from '../lib/responsive';
|
||||
@@ -188,6 +188,16 @@ export function ChatScreen() {
|
||||
if (item)
|
||||
openPreview(item);
|
||||
}
|
||||
/* The message list is memoised (#4640 family: Out of Memory in a long chat): a streamed token changes one message, so the others must not
|
||||
re-render. These handlers keep one identity for the life of the screen and always call the newest functions above. */
|
||||
const latestHandlers = useRef({ editPrompt, openPreview, openTimeSkip, openLink });
|
||||
latestHandlers.current = { editPrompt, openPreview, openTimeSkip, openLink };
|
||||
const messageHandlers = useMemo(() => ({
|
||||
onEditPrompt: (text: string) => latestHandlers.current.editPrompt(text),
|
||||
onPreview: (a: PreviewArtifact) => latestHandlers.current.openPreview(a),
|
||||
onOpenTimeSkip: (id: string) => latestHandlers.current.openTimeSkip(id),
|
||||
onOpenLink: (url: string) => latestHandlers.current.openLink(url),
|
||||
}), []);
|
||||
const scroller = useRef<HTMLDivElement>(null);
|
||||
const focusMode = useStore((s) => s.focus);
|
||||
/** Per-conversation scroll offsets, so returning to one restores the view. */
|
||||
@@ -445,7 +455,7 @@ export function ChatScreen() {
|
||||
|
||||
<div className="chat__inner">
|
||||
{tr(session.messages.length === 0 ? (session.statsAvailable === false ? <p className="small muted">{tr("Editing from an earlier message. Update the prompt below.")}</p> :
|
||||
<UsageDashboard loaded={!!loaded}/>) : (session.messages.map((m, i) => (<Message key={m.id} message={m} isLast={i === session.messages.length - 1} onEditPrompt={editPrompt} onPreview={openPreview} onOpenTimeSkip={openTimeSkip} timeSkipDone={!!allTimeSkipRuns.find(r => r.id === m.timeSkipId)?.completedAt} timeSkipReal={!!allTimeSkipRuns.find(r => r.id === m.timeSkipId)?.job} timeSkipRun={allTimeSkipRuns.find(r => r.id === m.timeSkipId)} onOpenLink={openLink}/>))))}
|
||||
<UsageDashboard loaded={!!loaded}/>) : (session.messages.map((m, i) => (<Message key={m.id} message={m} isLast={i === session.messages.length - 1} onEditPrompt={messageHandlers.onEditPrompt} onPreview={messageHandlers.onPreview} onOpenTimeSkip={messageHandlers.onOpenTimeSkip} timeSkipDone={!!allTimeSkipRuns.find(r => r.id === m.timeSkipId)?.completedAt} timeSkipReal={!!allTimeSkipRuns.find(r => r.id === m.timeSkipId)?.job} timeSkipRun={allTimeSkipRuns.find(r => r.id === m.timeSkipId)} onOpenLink={messageHandlers.onOpenLink}/>))))}
|
||||
<QueuedMessages chatId={session.id}/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -611,7 +621,8 @@ function Composer({ draft, setDraft, loaded, streaming, onSubmit, onKey, session
|
||||
</div>) : null)}
|
||||
</div>);
|
||||
}
|
||||
function Message({ message: m, isLast, onEditPrompt, onPreview, onOpenLink, onOpenTimeSkip, timeSkipDone, timeSkipReal, timeSkipRun, }: {
|
||||
const Message = memo(MessageView);
|
||||
function MessageView({ message: m, isLast, onEditPrompt, onPreview, onOpenLink, onOpenTimeSkip, timeSkipDone, timeSkipReal, timeSkipRun, }: {
|
||||
message: ChatMessage;
|
||||
isLast: boolean;
|
||||
onEditPrompt: (text: string) => void;
|
||||
|
||||
@@ -37,7 +37,7 @@ test('streaming: a tag that arrives in pieces is removed once it is complete', (
|
||||
|
||||
test('the store applies it to the answer, the ordered pieces and the Markdown export', async () => {
|
||||
const store = await readFile(new URL('../src/lib/store.ts', import.meta.url), 'utf8');
|
||||
assert.match(store, /tidyAnswer\(m\.content\+String\(frame\.text/);
|
||||
assert.match(store, /tidyAnswer\(m\.content\+text\)/); // applyDelta, which the queued pieces go through
|
||||
assert.match(store, /m\.role === 'assistant' \? tidyAnswer\(m\.content\) : m\.content\)/);
|
||||
assert.match(await readFile(new URL('../src/lib/deliverables.ts', import.meta.url), 'utf8'), /tidyAnswer\(last\.text \+ text\)/);
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/** A streamed token changes one message. The others must not render again, or a long chat does work (and makes garbage) for every
|
||||
* token in proportion to its whole history: measured 0.7 s of main-thread time for a 10-turn reply on a new chat and 1.9 s after ten
|
||||
* replies, peak heap 95 MB instead of 50 MB (scripts/chat-growth.mjs). Source guards: the DOM is not available in this suite. */
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const chat = readFileSync(resolve(import.meta.dirname, '../src/screens/Chat.tsx'), 'utf8');
|
||||
|
||||
test('the message row is a memoised component', () => {
|
||||
assert.match(chat, /import \{[^}]*\bmemo\b[^}]*\} from 'react'/);
|
||||
assert.match(chat, /const Message = memo\(MessageView\)/);
|
||||
});
|
||||
|
||||
test('the list passes handlers whose identity never changes', () => {
|
||||
assert.match(chat, /const messageHandlers = useMemo\(\(\) => \(\{[\s\S]*?\}\), \[\]\)/);
|
||||
for (const name of ['onEditPrompt', 'onPreview', 'onOpenTimeSkip', 'onOpenLink'])
|
||||
assert.match(chat, new RegExp(`${name}=\{messageHandlers\.${name}\}`), name);
|
||||
assert.doesNotMatch(chat, /onEditPrompt=\{editPrompt\}/);
|
||||
});
|
||||
|
||||
const store = readFileSync(resolve(import.meta.dirname, '../src/lib/store.ts'), 'utf8');
|
||||
|
||||
test('streamed text and reasoning are applied in groups, and everything else applies what is waiting first', () => {
|
||||
assert.match(store, /const queueText = \(kind: 'reasoning' \| 'delta', text: string\)/);
|
||||
assert.match(store, /waitingTimer \?\?= setTimeout\(flushWaiting, 90\)/);
|
||||
// the order of frames the person sees is the order written: a frame of any other kind flushes first
|
||||
assert.match(store, /if \(type === 'delta' \|\| type === 'reasoning'\) \{ queueText\(type, String\(frame\.text \?\? ''\)\); return; \}\s*flushWaiting\(\);/);
|
||||
// a failure or the end of the turn never leaves text behind
|
||||
assert.match(store, /\} catch \(err\) \{\s*flushWaiting\(\);/);
|
||||
assert.match(store, /\} finally \{\s*flushWaiting\(\);/);
|
||||
});
|
||||
@@ -71,11 +71,13 @@ test('the plan says what the picked folder means and keeps the person\'s spellin
|
||||
test('a whole drive is planned as <drive>\\Strata, shown as <drive>\\Strata\\Models', async t => {
|
||||
const f = fixture(t);
|
||||
const root = f.root.slice(0, f.root.indexOf(process.platform === 'win32' ? '\\' : '/') + 1) || '/';
|
||||
const before = existsSync(join(root, 'Strata'));
|
||||
const plan = await f.native.planModelsFolder(root);
|
||||
assert.equal(existsSync(join(root, 'Strata')), before, 'planning must not create or remove anything');
|
||||
assert.equal(plan.wholeDrive, true);
|
||||
assert.equal(plan.folder, join(root, 'Strata'));
|
||||
assert.equal(plan.shown, join(root, 'Strata', 'Models'));
|
||||
rmSync(join(root, 'Strata'), { recursive: true, force: true });
|
||||
// Planning must not touch the disk. This test never creates <drive>\Strata, so it never deletes it either (#4689).
|
||||
});
|
||||
|
||||
test('the card is blocked while a model runs or a job runs: a running model must be stopped on purpose', async t => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { parseCpuTopology, parseNvidiaReadings, parsePcieThroughput, gpuMemoryTotals } from '../server/device-probes.mjs';
|
||||
import { parseCpuTopology, parseNvidiaReadings, parsePcieThroughput, gpuMemoryTotals, pickGpuTemperature, isComputeAdapter } from '../server/device-probes.mjs';
|
||||
import { cpuUtilization, diskRate, parseLinuxDiskCounters, parseMacDiskCounters } from '../server/system-telemetry.mjs';
|
||||
import { parseEngineMetrics } from '../server/supervisor.mjs';
|
||||
import { originalDashboard } from '../server/monitor-dashboard.mjs';
|
||||
import { originalDashboard, mergeOwnReading } from '../server/monitor-dashboard.mjs';
|
||||
import { gpuMetric, gpuCapacity, gpuReported, shortGpuName, defaultGpuSet, gpuId } from '../src/lib/monitor-hardware.ts';
|
||||
import { referencePaths } from '../src/lib/monitor-chart.ts';
|
||||
|
||||
@@ -117,3 +117,50 @@ test('an RTX 5070 beside the unsupported Radeon iGPU: All GPUs is the RTX alone,
|
||||
assert.deepEqual(defaultGpuSet([igpu]).map(g=>g.id),[igpu.id]);
|
||||
assert.equal(defaultGpuSet([{...igpu,vramGb:96,unified:true},rtx]).length,2);
|
||||
});
|
||||
|
||||
// ---- 0.2.8: Monitor readings for AMD cards (#3727) ----
|
||||
const amd=(id,index)=>({id,index,unified:false,vramGb:16,name:'AMD Radeon RX 9070 XT',vendor:'amd',backend:'vulkan'});
|
||||
const ownReading=(id,extra={})=>({id,source:'sensor',sampledAt:'t0',temperatureC:null,utilizationPct:37,memoryUsedGb:5.5,powerW:null,powerLimitW:null,pcieGen:null,pcieGenMax:null,pcieWidth:null,pcieRxMbs:null,...extra});
|
||||
const shell=(own)=>({context:{maxTokens:32768},hardware:{gpus:own,gpuLoad:null},history:[],totals:{},telemetry:{}});
|
||||
const rawNoGpu={engine:{max_context:32768},live:{state:'generating'},hardware:{cpu:12},history:{}};
|
||||
|
||||
test('Windows AMD, one card, engine running: the engine has no GPU reader, the host load and memory stay (#3727)',()=>{
|
||||
const mapped=originalDashboard(rawNoGpu,{gpus:[amd('dx:1',0)]},shell([ownReading('dx:1')]));
|
||||
assert.equal(mapped.hardware.gpus.length,1);
|
||||
assert.equal(mapped.hardware.gpus[0].utilizationPct,37);
|
||||
assert.equal(mapped.hardware.gpus[0].memoryUsedGb,5.5);
|
||||
assert.equal(mapped.hardware.gpus[0].source,'sensor');
|
||||
assert.equal(mapped.hardware.gpus[0].temperatureC,null,'a value nobody read stays unavailable');
|
||||
});
|
||||
test('engine numbers win, the host fills only the gaps (fan and clock come from the host)',()=>{
|
||||
const raw={...rawNoGpu,hardware:{gpu_util:80,gpu_mem_used:4*1024**3,gpu_temp:61,gpu_power:200}};
|
||||
const mapped=originalDashboard(raw,{gpus:[amd('a',0)]},shell([ownReading('a',{utilizationPct:10,temperatureC:50,fanPct:35,clockMhz:2400,powerLimitW:300})]));
|
||||
const g=mapped.hardware.gpus[0];
|
||||
assert.equal(g.utilizationPct,80); assert.equal(g.temperatureC,61); assert.equal(g.powerW,200);
|
||||
assert.equal(g.powerLimitW,300); assert.equal(g.fanPct,35); assert.equal(g.clockMhz,2400); assert.equal(g.source,'engine');
|
||||
});
|
||||
test('a card the host has no reading for keeps the engine reading unchanged',()=>{
|
||||
const e={id:'x',source:'engine',temperatureC:null,utilizationPct:5};
|
||||
assert.equal(mergeOwnReading(e,undefined),e);
|
||||
});
|
||||
test('the headline aggregates fall back to the host when the engine reports none',()=>{
|
||||
const fb=shell([ownReading('a')]); fb.hardware.gpuTempC=55; fb.hardware.vramUsedGb=5.5;
|
||||
const mapped=originalDashboard(rawNoGpu,{gpus:[amd('a',0)]},fb);
|
||||
assert.equal(mapped.hardware.gpuTempC,55); assert.equal(mapped.hardware.vramUsedGb,5.5);
|
||||
});
|
||||
test('amdgpu temperature: edge first (what nvtop shows), junction when there is no edge, 0 is no reading',()=>{
|
||||
assert.equal(pickGpuTemperature([{label:'edge',milli:36000},{label:'junction',milli:38000},{label:'mem',milli:56000}]),36);
|
||||
assert.equal(pickGpuTemperature([{label:'mem',milli:56000},{label:'junction',milli:48000}]),48);
|
||||
assert.equal(pickGpuTemperature([{label:'',milli:41000}]),41);
|
||||
assert.equal(pickGpuTemperature([{label:'edge',milli:0},{label:'junction',milli:44000}]),44);
|
||||
assert.equal(pickGpuTemperature([{label:'edge',milli:0}]),null);
|
||||
assert.equal(pickGpuTemperature([{label:'mem',milli:56000}]),null,'a memory sensor alone is not the GPU temperature');
|
||||
assert.equal(pickGpuTemperature([]),null);
|
||||
});
|
||||
test('a remote-console chip with no memory is not a GPU; an unnamed card with memory and an Intel iGPU are',()=>{
|
||||
assert.equal(isComputeAdapter('unknown',null),false);
|
||||
assert.equal(isComputeAdapter('unknown',0),false);
|
||||
assert.equal(isComputeAdapter('unknown',8*1024**3),true);
|
||||
assert.equal(isComputeAdapter('intel',null),true);
|
||||
assert.equal(isComputeAdapter('amd',34208743424),true);
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* The options the app hands to the DeepSeek engine (strata-dsv41), which exits with "unknown option X" on a name it does not take:
|
||||
* - #4358 #4359 #4505 #4507: a model with pictures off had its `--vision off` pair cut in half (`--eos 1 off --dspark auto`);
|
||||
* - #4504: the Settings field "GPU memory for other apps" wrote --vram-reserve-mib, the resource preference wrote --pool-workers.
|
||||
* Nothing here starts an engine: Control is a fake that records the run config the host saved for the start (a scratch profile,
|
||||
* never the real one).
|
||||
*/
|
||||
import './helpers/isolate-profile.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createNativeControl } from '../server/native-control.mjs';
|
||||
import { DS_VALUE_FLAGS, DS_SWITCH_FLAGS, DS_RENAMED, dsEngineArgs, dsUserArgs, dsArgsNote, withoutVisionFlag } from '../server/native-ds-args.mjs';
|
||||
|
||||
const DS_ARGS = ['--pack', '/p/ds', '--max-context', '32768', '--eos', '1', '--vision', 'off', '--dspark', 'auto', '--dspark-conf', '0.65', '--prefill-chunk', '1024'];
|
||||
|
||||
function fixture(config = {}, id = 'deepseek-v4.1-flash-ist-das-2.25bpw') {
|
||||
const root = mkdtempSync(join(tmpdir(), 'strata-ds-args-'));
|
||||
const install = join(root, 'install'), data = join(root, 'data');
|
||||
mkdirSync(install, { recursive: true });
|
||||
process.env.STRATA_INSTALL_DIR = install;
|
||||
const path = join(install, `strata-${id}.json`);
|
||||
writeFileSync(path, JSON.stringify({
|
||||
exe: 'C:\\Users\\x\\AppData\\Local\\Strata\\engine-dsv41\\0.1.52-windows-x86_64-cuda-cuda13\\strata-dsv41.exe', arch: 'deepseek41',
|
||||
tokenizer: join(install, 'tok'), args: DS_ARGS, model_name: 'DeepSeek V4.1', port: 8080, ...config }));
|
||||
const saved = [];
|
||||
const control = {
|
||||
control: { dir: join(root, 'control') }, version: '0.2.7',
|
||||
async installedModels() { return { install_dir: install, models: [{ id, name: 'DeepSeek V4.1', source_config: path, component_status: 'present', context_tokens: 32768 }] }; },
|
||||
async engine() { return { usable: true, backend: 'cuda', components_dir: 'E:\\c', engine: { version: '0.1.52', platform: 'windows-x86_64', backend: 'cuda', exe: 'E:\\e\\strata.exe', root: 'E:\\e' } }; },
|
||||
async saveProfile(name, file) { saved.push(JSON.parse(readFileSync(file, 'utf8'))); return {}; },
|
||||
async start() { return { ok: true, state: 'ready' }; },
|
||||
async stop() { return { ok: true }; },
|
||||
};
|
||||
const native = createNativeControl({ control, hardwareProbe: () => [{ index: 0, vram_gb: 24 }], directory: data, spawnProcess: () => { throw new Error('no process'); }, fetcher: async () => Response.json({ status: 'ok' }), requests: () => [] });
|
||||
return { native, saved, id, path, cleanup: () => rmSync(root, { recursive: true, force: true }) };
|
||||
}
|
||||
const finish = async (native, id) => { for (let i = 0; i < 500; i++) { const job = native.state().jobs.find(j => j.id === id); if (job.state !== 'running') return job; await new Promise(r => setTimeout(r, 10)); } throw new Error('job did not finish'); };
|
||||
/** Every word of the argument list that is not an option name or the value right after one: a stray word means a flag lost its name. */
|
||||
function strayWords(args, valued) {
|
||||
const stray = [];
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i].startsWith('-')) { if (valued.has(args[i]) && i + 1 < args.length && !args[i + 1].startsWith('--')) i++; continue; }
|
||||
stray.push(args[i]);
|
||||
}
|
||||
return stray;
|
||||
}
|
||||
|
||||
test('the DeepSeek option list is a subset of what ds4_main.cpp parses (it cannot drift ahead of the engine)', () => {
|
||||
const source = fileURLToPath(new URL('../../../src/dsv41/ds4_main.cpp', import.meta.url));
|
||||
if (!existsSync(source)) return; // a package without the engine source
|
||||
const parsed = new Set([...readFileSync(source, 'utf8').matchAll(/a == "(-{1,2}[a-z][a-z0-9-]*)"/g)].map(m => m[1]));
|
||||
assert.ok(parsed.has('--eos') && parsed.has('--reserve-mib'), 'the pattern found the engine option parser');
|
||||
for (const flag of [...DS_VALUE_FLAGS, ...DS_SWITCH_FLAGS, ...Object.values(DS_RENAMED)]) assert.ok(parsed.has(flag), `${flag} is not an option of strata-dsv41`);
|
||||
for (const flag of Object.keys(DS_RENAMED)) assert.ok(!parsed.has(flag), `${flag} is the Qwen engine's name, strata-dsv41 rejects it`);
|
||||
assert.ok(!parsed.has('--pool-workers'), '--pool-workers is rejected by strata-dsv41');
|
||||
});
|
||||
|
||||
test('dsEngineArgs renames --vram-reserve-mib, leaves out --pool-workers with its value and keeps the --vision pair', () => {
|
||||
const r = dsEngineArgs([...DS_ARGS, '--vram-reserve-mib', '512', '--pool-workers', '20', '--resident-budget-gib', '40']);
|
||||
assert.deepEqual(r.args, [...DS_ARGS, '--reserve-mib', '512']);
|
||||
assert.deepEqual(r.renamed, [{ from: '--vram-reserve-mib', to: '--reserve-mib' }]);
|
||||
assert.deepEqual(r.dropped.map(d => [d.flag, d.value]), [['--pool-workers', '20'], ['--resident-budget-gib', '40']]);
|
||||
assert.match(dsArgsNote(r), /--vram-reserve-mib is passed as --reserve-mib/);
|
||||
assert.match(dsArgsNote(r), /--pool-workers 20, --resident-budget-gib 40/);
|
||||
assert.equal(dsArgsNote(dsEngineArgs(DS_ARGS)), '', 'nothing changed, nothing said');
|
||||
});
|
||||
|
||||
test('dsEngineArgs: a reserve under both names keeps the engine\'s own, a switch at the end and values that look like numbers survive', () => {
|
||||
assert.deepEqual(dsEngineArgs(['--reserve-mib', '300', '--vram-reserve-mib', '512']).args, ['--reserve-mib', '300']);
|
||||
assert.deepEqual(dsEngineArgs(['--pack', '/p', '--no-expert-cache']).args, ['--pack', '/p', '--no-expert-cache']);
|
||||
assert.deepEqual(dsEngineArgs(['--pool-workers']).args, [], 'a flag with no value');
|
||||
assert.deepEqual(dsEngineArgs(['--spec', '4', '--tp', '2', '--kv', 'int8', '--experts', 'auto']).args, ['--tp', '2', '--experts', 'auto']);
|
||||
assert.deepEqual(dsEngineArgs(undefined).args, []);
|
||||
});
|
||||
|
||||
test('dsUserArgs drops what the engine rejects and keeps the options it takes and the app\'s reserve', () => {
|
||||
assert.deepEqual(dsUserArgs(['--pool-workers', '20', '--vram-reserve-mib', '512', '--cpu-threads', '8']), ['--vram-reserve-mib', '512', '--cpu-threads', '8']);
|
||||
assert.deepEqual(dsUserArgs(['--pool-workers', '20']), []);
|
||||
});
|
||||
|
||||
test('withoutVisionFlag takes the Qwen engine\'s stand-alone --vision out, and a picture word an old file left after it', () => {
|
||||
assert.deepEqual(withoutVisionFlag(['--max-context', '4096', '--vision', '--kv', 'int8']), ['--max-context', '4096', '--kv', 'int8']);
|
||||
assert.deepEqual(withoutVisionFlag(['--a', '1', '--vision']), ['--a', '1']);
|
||||
assert.deepEqual(withoutVisionFlag(['--vision', 'off', '--eos', '1']), ['--eos', '1']);
|
||||
assert.deepEqual(withoutVisionFlag(['--vision', 'gpu', '--x']), ['--x']);
|
||||
});
|
||||
|
||||
test('#4358 #4359 #4505 #4507: a DeepSeek load with pictures off leaves the --vision pair whole and no stray word', async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
// what the Models screen sends for a model it does not list pictures for: vision "no"
|
||||
const job = await finish(f.native, f.native.operation('load', { model: f.id, vision: 'no' }).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
const args = f.saved[0].args;
|
||||
assert.equal(args[args.indexOf('--vision') + 1], 'off', args.join(' '));
|
||||
assert.equal(args[args.indexOf('--eos') + 1], '1', 'the word after --eos is its value, not a left-over of --vision');
|
||||
assert.equal(args[args.indexOf('--eos') + 2], '--vision');
|
||||
assert.deepEqual(strayWords(args, new Set(['--pack', '--max-context', '--eos', '--vision', '--dspark', '--dspark-conf', '--prefill-chunk'])), []);
|
||||
assert.deepEqual(args, DS_ARGS, 'nothing was added or removed');
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('a DeepSeek model with pictures on keeps `--vision on` too', async () => {
|
||||
const f = fixture({ args: DS_ARGS.map(a => (a === 'off' ? 'on' : a)) });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, f.native.operation('load', { model: f.id, vision: 'no' }).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
const args = f.saved[0].args;
|
||||
assert.equal(args[args.indexOf('--vision') + 1], 'on');
|
||||
assert.equal(args[args.indexOf('--eos') + 1], '1');
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('#4504: the reserve field and the resource preference reach strata-dsv41 as --reserve-mib, with no --pool-workers', async () => {
|
||||
const f = fixture({ vram_reserve_mib: 512, ui_resource_profile: 'work', args: [...DS_ARGS, '--pool-workers', '20', '--vram-reserve-mib', '512'], args_written: [...DS_ARGS, '--pool-workers', '20', '--vram-reserve-mib', '512'], user_args: ['--pool-workers', '20'] });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, f.native.operation('load', { model: f.id, vision: 'no', resourceProfile: 'work' }).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
const saved = f.saved[0], args = saved.args;
|
||||
assert.ok(!args.includes('--pool-workers') && !args.includes('20'), args.join(' '));
|
||||
assert.ok(!args.includes('--vram-reserve-mib'), args.join(' '));
|
||||
assert.equal(args[args.indexOf('--reserve-mib') + 1], '512', args.join(' '));
|
||||
assert.equal(args.filter(a => a === '--reserve-mib').length, 1);
|
||||
assert.ok(!(saved.user_args ?? []).includes('--pool-workers'), 'the person\'s saved options lose it too');
|
||||
for (const flag of args.filter(a => a.startsWith('--'))) assert.ok(DS_VALUE_FLAGS.has(flag) || DS_SWITCH_FLAGS.has(flag), `${flag} is an option of strata-dsv41`);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('the resource preference no longer writes Qwen-engine options into a DeepSeek model\'s file when it is tuned', async () => {
|
||||
const f = fixture({ args: DS_ARGS });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const before = JSON.parse(readFileSync(f.path, 'utf8'));
|
||||
const job = await finish(f.native, f.native.operation('tune', { model: f.id, profile: 'work', calibrate: false }).id);
|
||||
void job; // the load and benchmark after it need a real engine; the file is what matters
|
||||
const after = JSON.parse(readFileSync(f.path, 'utf8'));
|
||||
assert.ok(!(after.args ?? []).includes('--pool-workers'), (after.args ?? []).join(' '));
|
||||
assert.ok(!(after.args ?? []).includes('--vram-reserve-mib'), (after.args ?? []).join(' '));
|
||||
assert.deepEqual(after.args.slice(0, before.args.length), before.args);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('a Qwen model with pictures off still loses its stand-alone --vision (nothing else changed)', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'strata-ds-args-q-'));
|
||||
const install = join(root, 'install');
|
||||
mkdirSync(install, { recursive: true });
|
||||
process.env.STRATA_INSTALL_DIR = install;
|
||||
const path = join(install, 'strata-q2_0.json');
|
||||
writeFileSync(path, JSON.stringify({ exe: 'component:strata-engine', tokenizer: join(install, 'tok'), args: ['--pack', '/p', '--max-context', '4096', '--vision', '--kv', 'int8'], model_name: 'Qwen', port: 8080 }));
|
||||
const saved = [];
|
||||
const control = {
|
||||
control: { dir: join(root, 'control') }, version: '0.2.7',
|
||||
async installedModels() { return { install_dir: install, models: [{ id: 'q2_0', name: 'Qwen', source_config: path, component_status: 'present', context_tokens: 4096 }] }; },
|
||||
async engine() { return { usable: true, backend: 'cuda', components_dir: 'E:\\c', engine: { version: '0.2.0', platform: 'windows-x86_64', backend: 'cuda', exe: 'E:\\e\\strata.exe', root: 'E:\\e' } }; },
|
||||
async saveProfile(name, file) { saved.push(JSON.parse(readFileSync(file, 'utf8'))); return {}; },
|
||||
async start() { return { ok: true, state: 'ready' }; },
|
||||
async stop() { return { ok: true }; },
|
||||
};
|
||||
const native = createNativeControl({ control, hardwareProbe: () => [{ index: 0, vram_gb: 12 }], directory: join(root, 'data'), spawnProcess: () => { throw new Error('no process'); }, fetcher: async () => Response.json({ status: 'ok' }), requests: () => [] });
|
||||
try {
|
||||
await native.refresh(true);
|
||||
const job = await finish(native, native.operation('load', { model: 'q2_0', vision: 'no' }).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
assert.deepEqual(saved[0].args, ['--pack', '/p', '--max-context', '4096', '--kv', 'int8']);
|
||||
} finally { await native.close(); rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/** The app must not keep the one card a DeepSeek config of setup 0.2.8 and earlier names after setup's own pick (strata_control/gpu_pick.py). */
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { setupPickedOneCard } from '../server/native-control.mjs';
|
||||
|
||||
const amd = (index, extra = {}) => ({ index, vendor: 'amd', vram_gb: 32, usable: true, ...extra });
|
||||
const four = { gpus: [0, 1, 2, 3].map(i => amd(i)) };
|
||||
const old = { arch: 'deepseek41', backend: 'hip', gpu: 0, gpus_asked: true };
|
||||
const linux = process.platform !== 'win32';
|
||||
|
||||
test('setup\'s own one-card pick is not a choice (Linux, several cards)', { skip: !linux }, () => {
|
||||
assert.equal(setupPickedOneCard(old, four), true);
|
||||
});
|
||||
test('another card, a marker, roles, a list or another model are the person\'s', () => {
|
||||
assert.equal(setupPickedOneCard({ ...old, gpu: 2 }, four), false);
|
||||
assert.equal(setupPickedOneCard({ ...old, gpus_by: 'person' }, four), false);
|
||||
assert.equal(setupPickedOneCard({ ...old, gpu_roles: { 0: 'main' } }, four), false);
|
||||
assert.equal(setupPickedOneCard({ ...old, gpu: [0, 1] }, four), false);
|
||||
assert.equal(setupPickedOneCard({ ...old, arch: 'qwen4exp' }, four), false);
|
||||
assert.equal(setupPickedOneCard({ ...old, backend: 'cuda' }, four), false);
|
||||
});
|
||||
test('one usable card, or an APU beside one discrete card, leaves nothing to widen', () => {
|
||||
assert.equal(setupPickedOneCard(old, { gpus: [amd(0)] }), false);
|
||||
assert.equal(setupPickedOneCard({ ...old, gpu: 1 }, { gpus: [amd(0, { uma: true, vram_gb: 96 }), amd(1)] }), false);
|
||||
});
|
||||
@@ -27,3 +27,22 @@ test('native gateway preserves all three protocols and routes exact IDs, usage,
|
||||
result=await call('/v1/responses',{model:'native-a',input:'slow',stream:true});const read=result.body.getReader();await read.read();await read.cancel();for(let n=0;n<30&&!cancelled;n++)await new Promise(r=>setTimeout(r,20));assert.equal(cancelled,true);
|
||||
}finally{await close(gateway);await close(engine);}
|
||||
});
|
||||
test('a client that hangs up right after data: [DONE] has its answer: the request is done, not "Request cancelled." (#4690)',{timeout:30000},async()=>{
|
||||
const net=await import('node:net');const records=[];let closeDelay=0;
|
||||
const engine=http.createServer(async(req,res)=>{for await(const _ of req);res.writeHead(200,{'content-type':'text/event-stream',connection:'close'});
|
||||
res.write('data: {"choices":[{"delta":{"content":"Hi"}}]}\n\n');res.write('data: {"choices":[],"usage":{"prompt_tokens":12,"completion_tokens":3}}\n\n');res.write('data: [DONE]\n\n');
|
||||
await new Promise(r=>setTimeout(r,closeDelay));res.end();});
|
||||
const enginePort=await listen(engine);
|
||||
const serving={open:value=>{const record={id:records.length,state:'queued',tokensIn:null,tokensOut:null,...value};records.push(record);return record;},mark:(id,value)=>Object.assign(records[id],value),finish:(id,{tokensOut,error})=>Object.assign(records[id],{state:error?'error':'done',tokensOut,error})};
|
||||
const control={resolveInstance:()=>({id:'x',apiModelId:'x',port:enginePort,key:'k',gpus:[0]})};
|
||||
const gateway=http.createServer(async(req,res)=>{let text='';for await(const chunk of req)text+=chunk;await nativeGateway({req,res,path:req.url,body:JSON.parse(text),control,serving});});
|
||||
const gatewayPort=await listen(gateway);
|
||||
const call=()=>new Promise(resolve=>{const body=JSON.stringify({model:'x',stream:true,messages:[]});
|
||||
const socket=net.connect(gatewayPort,'127.0.0.1',()=>socket.write(`POST /v1/chat/completions HTTP/1.1\r\nHost: x\r\nConnection: close\r\nContent-Type: application/json\r\nContent-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`));
|
||||
let text='';socket.on('data',data=>{text+=data;if(text.includes('data: [DONE]'))socket.destroy();});socket.on('close',()=>resolve(text));});
|
||||
try{
|
||||
for(const delay of [0,5,30])for(let run=0;run<5;run++){closeDelay=delay;const before=records.length;assert.match(await call(),/\[DONE\]/);
|
||||
for(let n=0;n<100&&!['done','error'].includes(records[before]?.state);n++)await new Promise(r=>setTimeout(r,20));
|
||||
const last=records[before];assert.equal(last.state,'done',`delay ${delay} ms: ${last.error}`);assert.equal(last.tokensIn,12);assert.equal(last.tokensOut,3);}
|
||||
}finally{await close(gateway);await close(engine);}
|
||||
});
|
||||
@@ -88,10 +88,26 @@ test('runCli passes the arguments untouched (no shell) and reads JSON', async ()
|
||||
|
||||
// ---------------------------------------------------------------------------------- native-control in packaged mode
|
||||
|
||||
function packagedFixture({ engineUsable = true, updater = null, repair = false, ids = ['iq2_xs', 'iq3_s'] } = {}) {
|
||||
function packagedFixture({ engineUsable = true, updater = null, repair = false, ids = ['iq2_xs', 'iq3_s'], controlVersion = '0.1.45', extraSizes = [], cliStartWait = 'old' } = {}) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'strata-packaged-test-'));
|
||||
const install = join(root, 'install'), data = join(root, 'data');
|
||||
mkdirSync(install, { recursive: true });
|
||||
/* The Control this app found, as it is on disk. The app reads the start-wait ceiling out of these files instead of trusting
|
||||
the version, because a start wait is checked three times before the engine is reached and a released Control has those
|
||||
three ceilings disagreeing:
|
||||
'old' no runtime ceiling at all, both gates 600 (Control before #3949)
|
||||
'split' the runtime takes 1800, the CLI and the record still stop at 600 (every Control 0.1.50 to 0.1.52)
|
||||
'cli-only' the CLI follows the runtime, the record still stops at 600 (`direct runtime intent has an invalid timeout`)
|
||||
'runtime' all three hand the wait to the runtime's own ceiling */
|
||||
const controlSource = join(root, 'control', 'strata_control');
|
||||
mkdirSync(controlSource, { recursive: true });
|
||||
writeFileSync(join(controlSource, 'runtime.py'), cliStartWait === 'old' ? '' : 'MAX_START_WAIT = 1800\n');
|
||||
writeFileSync(join(controlSource, 'management.py'), ['cli-only', 'runtime'].includes(cliStartWait)
|
||||
? ' wait = _seconds(wait, 0, runtime.MAX_START_WAIT, "start wait")\n'
|
||||
: ' wait = _seconds(wait, 0, 600, "start wait")\n');
|
||||
writeFileSync(join(controlSource, 'journal.py'), cliStartWait === 'runtime'
|
||||
? ' ranges = {"wait_s": (0, start_wait_max) if kind == "runtime.start" else\n'
|
||||
: ' ranges = {"wait_s": (0, 600) if kind == "runtime.start" else\n');
|
||||
const earlierInstallEnv = process.env.STRATA_INSTALL_DIR;
|
||||
process.env.STRATA_INSTALL_DIR = install; // the app's own install folder (the host always tells Control which one; it never falls back to an older CLI folder)
|
||||
const configs = ids.map(id => {
|
||||
@@ -105,7 +121,7 @@ function packagedFixture({ engineUsable = true, updater = null, repair = false,
|
||||
const rpc = [];
|
||||
let startFailure = null, startFailureLeft = Infinity;
|
||||
const control = {
|
||||
control: { dir: join(root, 'control'), python: join(root, 'control', 'python', 'python.exe') }, version: '0.1.45',
|
||||
control: { dir: join(root, 'control'), python: join(root, 'control', 'python', 'python.exe') }, version: controlVersion,
|
||||
async installedModels(options) { calls.push(['installedModels', options?.installDir ?? null]); return { install_dir: install, models: configs.map(c => ({ id: c.id, name: `Qwen ${c.id}`, source_config: c.path, component_status: 'present', context_tokens: 32768, backend: 'cuda' })) }; },
|
||||
async engine() {
|
||||
calls.push(['engine']);
|
||||
@@ -126,7 +142,7 @@ function packagedFixture({ engineUsable = true, updater = null, repair = false,
|
||||
// answers like it: the catalog, the hardware with the GPUs, and an install plan or job.
|
||||
const catalog = { families: [{ family: 'qwen', title: 'Qwen3.8-Flash-Next', about: 'x', images: true, experimental: false, sizes: [
|
||||
{ id: 'q2_0', model: 'Q2_0', download_gb: 66.4, ram_needed_gb: 48, about: 'fast', on_this_pc: 'fits', installed: false, images: true, experimental: false },
|
||||
{ id: 'iq3_xxs', model: 'IQ3_XXS', download_gb: 75.8, ram_needed_gb: 60, about: 'balanced', on_this_pc: 'fits', installed: false, images: true, experimental: false }] }],
|
||||
{ id: 'iq3_xxs', model: 'IQ3_XXS', download_gb: 75.8, ram_needed_gb: 60, about: 'balanced', on_this_pc: 'fits', installed: false, images: true, experimental: false }, ...extraSizes] }],
|
||||
contexts: [8192, 32768], recommendation: { family: 'qwen', model: 'IQ3_XXS', context: 32768 }, context_note: '' };
|
||||
const hardware = { ram_gb: 63, cpu: { name: 'cpu', cores: 12 }, gpus: [{ index: 0, name: 'NVIDIA GeForce RTX 5070', vram_gb: 11.9, usable: true }] };
|
||||
let installJob = null;
|
||||
@@ -606,3 +622,43 @@ test("onboarding 'use your own GGUF' (#3507, #3424): the models folder the perso
|
||||
assert.ok(sent.data_dir === undefined || sent.data_dir === join(f.root, 'Strata-data'), `data_dir was ${sent.data_dir}`);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
/* The start wait follows the model's size, as `strata launch` does since #3949, and never asks an older Control for more than it takes.
|
||||
Reported on the app side by #3459 and #4191: a big model on a slow disk was stopped at 300 s while the engine went on reading. */
|
||||
test('packaged: a big model is waited for by its size, inside the ceiling of the Control this app has (#3459 #4191)', async () => {
|
||||
const run = async (cliStartWait, downloadGb) => {
|
||||
const f = packagedFixture({ ids: ['big'], cliStartWait, extraSizes: [{ id: 'big', model: 'BIG', download_gb: downloadGb, ram_needed_gb: 60, about: 'x', on_this_pc: 'fits', installed: false, images: false, experimental: false }] });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, f.native.operation('load', { model: 'big' }).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
const start = f.calls.find(c => c[0] === 'start');
|
||||
const saved = f.saved[0];
|
||||
await f.native.close();
|
||||
return { wait: start[2], ready: saved.engine_ready_s };
|
||||
} finally { f.cleanup(); }
|
||||
};
|
||||
assert.equal((await run('old', 180)).wait, 600, 'a Control with no long-wait ceiling at all is asked for 600 s, as today');
|
||||
assert.equal((await run('split', 180)).wait, 600, 'a Control whose runtime takes 1800 s but whose CLI still stops at 600 is asked for 600 s: the CLI would refuse the start outright (#3949)');
|
||||
assert.equal((await run('cli-only', 180)).wait, 600, 'a Control whose CLI takes it but whose operation record still stops at 600 is asked for 600 s too: the record is written before the start runs');
|
||||
const long = await run('runtime', 180);
|
||||
assert.equal(long.wait, 1720, '600 s + 8 s per GB above 40 GB, as in strata launch, once the CLI really takes it');
|
||||
assert.equal(long.ready, 1780, 'the server readies after the start gave up, never before it');
|
||||
const small = await run('runtime', 20);
|
||||
assert.equal(small.wait, 300, 'a model under 40 GB keeps the 300 s it always had');
|
||||
assert.equal(small.ready, undefined, 'nothing is added to a small model’s run config');
|
||||
const huge = await run('runtime', 400);
|
||||
assert.equal(huge.wait, 1800, 'and never more than this Control accepts');
|
||||
});
|
||||
|
||||
test('packaged: a run config that names a longer engine wait is honoured, and never shortened by the app', async () => {
|
||||
const f = packagedFixture({ ids: ['deepseek-v4.1-flash-ist-das-2.00bpw'], cliStartWait: 'runtime' });
|
||||
try {
|
||||
writeFileSync(join(f.install, 'strata-deepseek-v4.1-flash-ist-das-2.00bpw.json'), JSON.stringify({ exe: join(f.install, 'engine-dsv41', 'strata-dsv41'), arch: 'deepseek41', tokenizer: join(f.install, 'tok'), args: ['--pack', join(f.install, 'pack')], model_name: 'deepseek-v4.1-flash', port: 8080, engine_ready_s: 3600 }));
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, f.native.operation('load', { model: 'deepseek-v4.1-flash-ist-das-2.00bpw' }).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
assert.equal(f.calls.find(c => c[0] === 'start')[2], 1800, 'the wait of the newest Control, which is what the ceiling allows');
|
||||
assert.equal(f.saved[0].engine_ready_s, 3600, 'setup’s own longer wait stays as it is');
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
@@ -57,5 +57,5 @@ test('Cargo.toml, tauri.conf.json and the desktop package agree on the app versi
|
||||
const cargo = /^version\s*=\s*"([^"]+)"/m.exec(await readFile(new URL('src-tauri/Cargo.toml', root), 'utf8'))[1];
|
||||
const pkg = JSON.parse(await readFile(new URL('package.json', root), 'utf8')).version;
|
||||
assert.equal(cargo, pkg);
|
||||
assert.equal(cargo, '0.2.7');
|
||||
assert.equal(cargo, '0.2.9');
|
||||
});
|
||||
@@ -47,3 +47,14 @@ test('the monitor uses the pace, the Monitor screen asks while it is open, and /
|
||||
const screen = await readFile(new URL('../src/screens/Monitor.tsx', import.meta.url), 'utf8');
|
||||
assert.match(screen, /api\.get\('\/api\/monitor'\)/);
|
||||
});
|
||||
|
||||
test('the drive-temperature reader starts only while the Monitor is watched and ends after it is gone (#4661)', async () => {
|
||||
const monitor = await readFile(new URL('../server/monitor.mjs', import.meta.url), 'utf8');
|
||||
assert.match(monitor, /STRATA_REAL_ENGINE==='1'&&Date\.now\(\)<watchedUntil\?readTemperatures\(\)/);
|
||||
assert.match(monitor, /stopTemperatures\(\)/);
|
||||
const { readTemperatures, stopTemperatures } = await import('../server/temperature-telemetry.mjs');
|
||||
assert.equal(typeof stopTemperatures, 'function');
|
||||
// nothing is started by importing or by stopping: on a PC that never opens the Monitor no PowerShell and no disk query exists
|
||||
stopTemperatures();
|
||||
assert.deepEqual(await readTemperatures({ platform: 'darwin' }), { cpuTempC: null, ssdTempC: null });
|
||||
});
|
||||
@@ -13,19 +13,19 @@ import { nativeFixture } from './helpers/native-fixture.mjs';
|
||||
const read = rel => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8').replace(/\r\n/g, '\n');
|
||||
const ENV = { // what each switch writes outside its default
|
||||
faster_long_prompts: { STRATA_KVA: '1' },
|
||||
faster_12gb_nvidia: { STRATA_ROUTE_PRIOR: '0' },
|
||||
faster_12gb_nvidia: { STRATA_ROUTE_PRIOR: '1.5' },
|
||||
faster_radeon_9000: { STRATA_PCIE_FRAC_DEFAULT: 'old' },
|
||||
deepseek_speedups: { DS4_DEFAULTS: '026', DS4_FAST_ATTN: '0', DS4_ATTN_SLAB: '0', DS4_COPY_PIN: '0', DS4_COPY_HUGETLB: '0', DS4_STORE_SPARE: '0', DS4_STORE_PUT: '0' },
|
||||
};
|
||||
const SIX = ['DS4_FAST_ATTN', 'DS4_ATTN_SLAB', 'DS4_COPY_PIN', 'DS4_COPY_HUGETLB', 'DS4_STORE_SPARE', 'DS4_STORE_PUT'];
|
||||
const DEFAULT = { faster_long_prompts: 'off', faster_12gb_nvidia: 'on', faster_radeon_9000: 'on', deepseek_speedups: 'on' };
|
||||
const DEFAULT = { faster_long_prompts: 'off', faster_12gb_nvidia: 'off', faster_radeon_9000: 'on', deepseek_speedups: 'on' };
|
||||
const RTX3060 = { index: 0, name: 'NVIDIA GeForce RTX 3060', vendor: 'nvidia', arch: '86', vram_gb: 12 };
|
||||
const R9700 = { index: 0, name: 'AMD Radeon AI PRO R9700', vendor: 'amd', arch: 'gfx1201', vram_gb: 32 };
|
||||
const RX7900 = { index: 0, name: 'AMD Radeon RX 7900 XTX', vendor: 'amd', arch: 'gfx1100', vram_gb: 24 };
|
||||
const RTX = { index: 0, name: 'NVIDIA GeForce RTX 4090', vendor: 'nvidia', arch: '89', vram_gb: 24 };
|
||||
const STRIX = { index: 0, name: 'AMD Radeon 8060S', vendor: 'amd', arch: 'gfx1151', vram_gb: 96, uma: true };
|
||||
|
||||
test('there are exactly four switches in the app (the Terminal has one more); each writes the engine names of the contract and has its default (KVA off, the others on)', () => {
|
||||
test('there are exactly four switches in the app (the Terminal has one more); each writes the engine names of the contract and has its default (KVA and the 12 GB routing off, the other two on)', () => {
|
||||
assert.deepEqual(SPEED_SWITCHES.map(s => s.id), Object.keys(ENV));
|
||||
for (const sw of SPEED_SWITCHES) {
|
||||
assert.deepEqual(sw.env, ENV[sw.id], sw.id);
|
||||
@@ -38,7 +38,7 @@ test('the Terminal\'s Qwen "0.2.6 behaviour" switch is not in the app', () => {
|
||||
assert.ok(!Object.values(SPEED_SWITCHES.flatMap(s => Object.keys(s.env))).includes('STRATA_DEFAULTS'));
|
||||
});
|
||||
|
||||
test('Faster answers on 12 GB NVIDIA cards: only for Qwen on a discrete CUDA card of 13 GiB or less; Off writes STRATA_ROUTE_PRIOR=0', () => {
|
||||
test('Faster answers on 12 GB NVIDIA cards: only for Qwen on a discrete CUDA card of 13 GiB or less; off by default since 0.2.8 (#4402), On writes STRATA_ROUTE_PRIOR=1.5', () => {
|
||||
const ids = context => speedSwitchRows({}, context).map(r => r.id);
|
||||
assert.deepEqual(ids({ backend: 'cuda', gpus: [RTX3060] }), ['faster_long_prompts', 'faster_12gb_nvidia']);
|
||||
assert.deepEqual(ids({ backend: 'cuda', gpus: [{ ...RTX3060, vram_gb: 13 }] }), ['faster_long_prompts', 'faster_12gb_nvidia'], '13 GiB counts');
|
||||
@@ -55,12 +55,24 @@ test('Faster answers on 12 GB NVIDIA cards: only for Qwen on a discrete CUDA car
|
||||
assert.deepEqual(ids({ backend: 'sycl', gpus: [RTX3060] }), [], 'not a CUDA build');
|
||||
assert.deepEqual(ids({ backend: 'cuda', gpus: [RTX3060], arch: 'qwen35moe' }), [], 'Qwen3.6-35B-A3B: the routing needs 512 experts');
|
||||
assert.deepEqual(ids({ deepseek: true, backend: 'cuda', gpus: [RTX3060] }), ['deepseek_speedups']);
|
||||
const off = setSpeedSwitch({ PATH: '/bin' }, 'faster_12gb_nvidia', false);
|
||||
assert.deepEqual(off, { PATH: '/bin', STRATA_ROUTE_PRIOR: '0' });
|
||||
assert.equal(speedSwitchIsOn(off, 'faster_12gb_nvidia'), false);
|
||||
assert.equal(speedSwitchIsOn({}, 'faster_12gb_nvidia'), true);
|
||||
assert.deepEqual(setSpeedSwitch(off, 'faster_12gb_nvidia', true), { PATH: '/bin' });
|
||||
assert.equal(speedSwitchIsOn({ STRATA_ROUTE_PRIOR: '2.0' }, 'faster_12gb_nvidia'), true, 'any other value is the user\'s own and reads as the default');
|
||||
// a default launch carries nothing: the engine then runs as 0.2.6 did (STRATA_ROUTE_PRIOR unset = 0)
|
||||
assert.equal(speedSwitchIsOn({}, 'faster_12gb_nvidia'), false);
|
||||
assert.equal(speedSwitchRows({}, { backend: 'cuda', gpus: [RTX3060] }).find(r => r.id === 'faster_12gb_nvidia').on, false);
|
||||
assert.deepEqual(setSpeedSwitch({ PATH: '/bin' }, 'faster_12gb_nvidia', false), { PATH: '/bin' }, 'Off is the default: it writes nothing');
|
||||
// the explicit opt-in the row writes
|
||||
const on = setSpeedSwitch({ PATH: '/bin' }, 'faster_12gb_nvidia', true);
|
||||
assert.deepEqual(on, { PATH: '/bin', STRATA_ROUTE_PRIOR: '1.5' });
|
||||
assert.equal(speedSwitchIsOn(on, 'faster_12gb_nvidia'), true);
|
||||
assert.deepEqual(setSpeedSwitch(on, 'faster_12gb_nvidia', false), { PATH: '/bin' }, 'back to Off removes the entry');
|
||||
// the 0.2.7 "Off" wrote STRATA_ROUTE_PRIOR=0: that file is in the default state now, and On replaces it with 1.5
|
||||
assert.equal(speedSwitchIsOn({ STRATA_ROUTE_PRIOR: '0' }, 'faster_12gb_nvidia'), false);
|
||||
assert.deepEqual(setSpeedSwitch({ STRATA_ROUTE_PRIOR: '0' }, 'faster_12gb_nvidia', true), { STRATA_ROUTE_PRIOR: '1.5' });
|
||||
assert.deepEqual(setSpeedSwitch({ STRATA_ROUTE_PRIOR: '0' }, 'faster_12gb_nvidia', false), {}, 'and Off clears it');
|
||||
// the words say both sides
|
||||
const hint = SPEED_SWITCHES.find(s => s.id === 'faster_12gb_nvidia').hint;
|
||||
assert.match(hint, /faster on 12 GB cards/);
|
||||
assert.match(hint, /Off by default/);
|
||||
assert.match(hint, /long agent sessions it can repeat itself/);
|
||||
});
|
||||
|
||||
test('DeepSeek speed-ups Off writes DS4_DEFAULTS=026 and the six; On removes all seven; the six alone (an earlier test build) still read as Off', () => {
|
||||
|
||||
@@ -45,3 +45,44 @@ test('counter lines are read, and the load and VRAM in use reach the card by its
|
||||
assert.equal(reader.read('luid_0x00000000_0x00099999'), null);
|
||||
assert.equal(procs.length, 1, 'one process for every read');
|
||||
});
|
||||
|
||||
// ---- 0.2.8: the card's temperature from the display driver (D3DKMT), #3727 ----
|
||||
import { d3dkmtTemperatureC, GPU_TEMP_CSHARP } from '../server/windows-gpu.mjs';
|
||||
|
||||
test('temperature comes in tenths of a degree; no answer, zero and nonsense are not a reading', () => {
|
||||
assert.equal(d3dkmtTemperatureC(300), 30);
|
||||
assert.equal(d3dkmtTemperatureC(437), 44);
|
||||
assert.equal(d3dkmtTemperatureC(-1), null);
|
||||
assert.equal(d3dkmtTemperatureC(0), null);
|
||||
assert.equal(d3dkmtTemperatureC(4294967295), null);
|
||||
assert.equal(d3dkmtTemperatureC(undefined), null);
|
||||
assert.equal(d3dkmtTemperatureC('abc'), null);
|
||||
});
|
||||
|
||||
test('a counter line carries the temperature; the older three-field line and a failed query still parse', () => {
|
||||
assert.equal(parseGpuCounterLine('G luid_0x00000000_0x00016102 37 4294967296 430').temperatureC, 43);
|
||||
assert.equal(parseGpuCounterLine('G luid_0x00000000_0x00016102 37 4294967296 -1').temperatureC, null);
|
||||
const old = parseGpuCounterLine('G luid_0x00000000_0x00016102 37 4294967296');
|
||||
assert.equal(old.temperatureC, null); assert.equal(old.utilizationPct, 37);
|
||||
});
|
||||
|
||||
test('the temperature reaches the reader by LUID next to load and memory', () => {
|
||||
const procs = [];
|
||||
const reader = createWindowsGpuReader({ spawnFn: () => { const p = new EventEmitter(); p.stdout = new EventEmitter(); p.kill = () => {}; procs.push(p); return p; }, now: () => 5 });
|
||||
reader.read('luid_0x00000000_0x00016102');
|
||||
procs[0].stdout.emit('data', 'G luid_0x00000000_0x00016102 90 17000000000 465\n');
|
||||
const r = reader.read('luid_0x00000000_0x00016102');
|
||||
assert.equal(r.temperatureC, 47); assert.equal(r.utilizationPct, 90);
|
||||
});
|
||||
|
||||
test('the PowerShell script that asks D3DKMT is single-quote safe and uses adapter performance data type 62 with the temperature at byte 56', () => {
|
||||
assert.ok(!GPU_TEMP_CSHARP.includes("'"), 'it sits inside a single-quoted PowerShell string');
|
||||
assert.match(GPU_TEMP_CSHARP, /i\.T=62/);
|
||||
assert.match(GPU_TEMP_CSHARP, /ReadInt32\(buf,56\)/);
|
||||
assert.match(GPU_TEMP_CSHARP, /AllocHGlobal\(64\)/);
|
||||
let script = '';
|
||||
createWindowsGpuReader({ spawnFn: (_cmd, args) => { script = args.at(-1); const p = new EventEmitter(); p.stdout = new EventEmitter(); p.kill = () => {}; return p; } }).read('x');
|
||||
assert.ok(script.includes('Add-Type -TypeDefinition'), 'compiled once, before the loop');
|
||||
assert.ok(script.indexOf('Add-Type') < script.indexOf('while($true)'));
|
||||
assert.match(script, /try\{Add-Type[^]*\}catch\{\}/, 'a failed compile must not stop load and memory');
|
||||
});
|
||||
@@ -10,8 +10,8 @@ holds the technical context behind the short [README](../README.md). Use [AI_SET
|
||||
- Strata runs supported language models locally, with a desktop app, a terminal app, and an inference engine.
|
||||
- The engine uses the GPU, system RAM and, for applicable models and memory modes, SSD storage.
|
||||
See [how it works](HOW_IT_WORKS.md) and [engine details](DETAILS.md).
|
||||
- There is one app installer per system (`Strata-0.2.7-windows-x64-setup.exe`, `Strata-0.2.7-linux-x86_64.AppImage`,
|
||||
`Strata-0.2.7-macos-arm64.dmg`). The Windows and Linux installers carry no engine: the app downloads the engine for the
|
||||
- There is one app installer per system (`Strata-0.2.9-windows-x64-setup.exe`, `Strata-0.2.9-linux-x86_64.AppImage`,
|
||||
`Strata-0.2.9-macos-arm64.dmg`). The Windows and Linux installers carry no engine: the app downloads the engine for the
|
||||
graphics card on first start, or installs a file the user already has. The Mac dmg keeps its Metal engine inside.
|
||||
The Terminal's `strata launch` downloads the appropriate engine and model.
|
||||
See [installation](INSTALL.md) and [Terminal launch](TERMINAL_LAUNCH.md).
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@ Use this guide when a user asks an AI agent to install Strata (version 0.2.5). T
|
||||
one; do not start from a source checkout.
|
||||
|
||||
- **The Terminal and `strata launch`** (below). This is the route for an agent that has a shell.
|
||||
- **The desktop app.** One installer per system: `Strata-0.2.7-windows-x64-setup.exe`,
|
||||
`Strata-0.2.7-linux-x86_64.AppImage`, `Strata-0.2.7-macos-arm64.dmg`, from the
|
||||
- **The desktop app.** One installer per system: `Strata-0.2.9-windows-x64-setup.exe`,
|
||||
`Strata-0.2.9-linux-x86_64.AppImage`, `Strata-0.2.9-macos-arm64.dmg`, from the
|
||||
[latest release](https://github.com/Niko1221/Strata/releases/latest). The Windows and Linux installers carry no
|
||||
engine: on first start the app shows the engine for the user's graphics card and downloads it, or installs a file the
|
||||
user already has ("I already have the file"). Details: [INSTALL.md](INSTALL.md).
|
||||
|
||||
+2
-2
@@ -49,7 +49,7 @@ carries it but it is untested. Details: [MODELS.md](MODELS.md#deepseek-v41-flash
|
||||
## Ready-made packages
|
||||
|
||||
The Terminal one-liner and `strata update` pick the package whose GPU code covers the card. The app installer
|
||||
(`Strata-0.2.7-windows-x64-setup.exe`, `Strata-0.2.7-linux-x86_64.AppImage`) has no engine inside: on its first start the app
|
||||
(`Strata-0.2.9-windows-x64-setup.exe`, `Strata-0.2.9-linux-x86_64.AppImage`) has no engine inside: on its first start the app
|
||||
downloads the matching package for the card. You can also press **Update engine** in Settings (or run `strata update`).
|
||||
|
||||
| Package (variant) | OS | GPU code inside | ROCm |
|
||||
@@ -131,7 +131,7 @@ reused. The Monitor shows the card's load, VRAM, temperature and power from Linu
|
||||
|
||||
## Windows
|
||||
|
||||
Install the [app](INSTALL.md) (`Strata-0.2.7-windows-x64-setup.exe` from the
|
||||
Install the [app](INSTALL.md) (`Strata-0.2.9-windows-x64-setup.exe` from the
|
||||
[release page](https://github.com/Niko1221/Strata/releases/latest); it downloads the engine for the card), or
|
||||
install the [Terminal](INSTALL.md#the-terminal) and run `strata launch`. You need Windows 10 or 11 (64-bit), a supported card
|
||||
and a current AMD driver. No ROCm or HIP SDK, no compiler, no admin rights. `START-HERE.bat --backend hip` is a
|
||||
|
||||
+62
-3
@@ -87,13 +87,13 @@ With a layer split, the engine options go into the config's `args`:
|
||||
|
||||
| Option | What it does |
|
||||
| --- | --- |
|
||||
| `"parallel": N` / `--batch N` / `--slots N` (2..8 normally; with a layer split up to 8 per group, see below) | up to N conversations have batch slots; more requests wait for a free slot. Each slot gets its own state (a session carved like the stage's own: GDN recurrence, QSA K/V and indexer, PLE history) on every GPU of the split. With grouped MTP, more than 8 slots can rotate through eight-row windows if memory permits. |
|
||||
| `"parallel": N` / `--batch N` / `--slots N` (2..8 normally; with a layer split up to 8 per group, 16 in a HIP build, see below) | up to N conversations have batch slots; more requests wait for a free slot. Each slot gets its own state (a session carved like the stage's own: GDN recurrence, QSA K/V and indexer, PLE history) on every GPU of the split. With grouped MTP, more than 8 slots can rotate through eight-row windows if memory permits. |
|
||||
| `--batch-groups G` | with a layer split (default: auto, below): the N slots in G groups that flow through the GPUs as a pipeline (GPU k runs one group while GPU k+1 runs another). G must divide N. 1 = all slots in one window, GPU after GPU. |
|
||||
| `--batch-groups auto` | the default on a layer split since 0.1.41 (give no `--batch-groups`): the engine pipelines one group per GPU stage (the most that divide the slots; 8 slots on 4 GPUs = 4 groups of 2) and says so (`INFO batch_groups=G`). `--batch-groups 1` turns it off (all slots in one window, GPU after GPU). Measured, 8 clients, total tok/s against one group: 4 x R9700 166 against 86, 2 GPUs 109 against 78, 3 GPUs 110 against 78. With `--adapt-async 1` and no `--batch-groups` the default is one group (the adaptive tier between batch windows does not run in pipelined groups) and the log says so; with `--batch-mtp` and no `--batch-groups` the pipeline wins and `--batch-mtp` is left off (`--batch-groups 1` or `STRATA_BATCH_MTP_SERIAL=1` keeps it); `--batch-groups G` or `--batch-groups auto` given with `--batch-mtp` pipelines the groups and turns `--batch-mtp` off, and given with `--adapt-async 1` pipelines the groups and turns `--adapt-async` off (the pipelined path does not adapt). | PROPOSED for 0.2.1 (owner decides; branch f-021-split): on NVIDIA builds with exactly two stages the default is one group instead (2x RTX A4000, IQ3_XXS, aggregate tok/s against the pipelined default: +16% at 2 slots, +12% at 4, +15% at 8, 5 of 5 pairs each; `--batch-groups auto` or a number asks for the pipeline, `STRATA_BATCH_GROUPS_NV_SERIAL=0` restores the old default).
|
||||
| `--trim-stage-weights` | with an **explicit** `--layer-split` (e.g. `12,24,36`, not `auto`): every GPU loads only the dense weights of its own layers instead of the whole model's (the same as `STRATA_STAGE_TRIM=1`, PR #639). The VRAM this frees goes to the expert cache. Useful without `--batch` too. |
|
||||
|
||||
The engine never refuses a count it cannot run: it says so in its log and runs what it can - at most 8 slots without a
|
||||
layer split (a window holds 8 rows; with a layer split and pipelined groups, 8 per group), as many as fit in VRAM, or none (one request at a time) when not two fit. The server reads
|
||||
layer split (a window holds 8 rows; with a layer split and pipelined groups, 8 per group, 16 in a HIP build: see "Windows of 16 rows" below), as many as fit in VRAM, or none (one request at a time) when not two fit. The server reads
|
||||
the count the engine reports (`INFO batch_slots=N`), and `GET /v1/status` says it (`concurrency.serving`).
|
||||
|
||||
## More than 8 slots on a layer split, and the opt-in admission and decode switches
|
||||
@@ -103,7 +103,7 @@ the count the engine reports (`INFO batch_slots=N`), and `GET /v1/status` says i
|
||||
**More than 8 slots.** With a layer split a pipelined window holds at most 8 rows, but each group of the pipeline is its
|
||||
own window, so `--batch 16` on 4 GPUs runs 4 groups of 4 slots (`--batch-groups auto`), `--batch 32` runs 4 groups of 8.
|
||||
Until 0.1.44.1 the engine cut such a request down to 8 slots and said so; it now runs what is asked for, as many slots
|
||||
as fit in VRAM and at most 8 per group (`INFO batch_slots=N`). `--batch-mtp` keeps its limit (rows per window are
|
||||
as fit in VRAM and at most 8 per group, 16 in a HIP build (`INFO batch_slots=N`; "Windows of 16 rows" below). `--batch-mtp` keeps its limit (rows per window are
|
||||
doubled by the proposals). Nothing changes for 8 slots or fewer. Every slot of a 16-slot pipelined batch decodes
|
||||
exactly the tokens it decodes alone (`tools/batch_test.py --n 16`, which now takes up to 32 prompts).
|
||||
|
||||
@@ -536,6 +536,65 @@ run without it; inside one run every slot still equals its own solo decode.
|
||||
Not run: AMD (HIP), the draft layer on the first stage, helper expert caches, more than two stages, SYCL (the SYCL
|
||||
tree has its own copy of the batch code and does not have this).
|
||||
|
||||
## Windows of 16 rows, and the four-card AMD profile
|
||||
|
||||
A verify or batch window held at most 8 rows. On a layer split every group of the pipeline is its own window, so 32 slots ran as
|
||||
4 groups of 8 and nothing larger could run. HIP builds now take windows of 16 rows: `--batch 64` on four cards is 4 groups of
|
||||
16, and `--batch 16 --batch-groups 1` is one 16-row window over all stages. The limit is a build option
|
||||
(`-DSTRATA_WINDOW_MAX_T=auto|8|16`, default `auto`):
|
||||
|
||||
| build | window | why |
|
||||
|---|---|---|
|
||||
| HIP (AMD) | 16 | the code objects load when a kernel is first used: the bigger binary (44.9 to 53.7 MB) costs +7 MiB per card after the first request and about 30 to 45 ms once per server start |
|
||||
| CUDA | 8 | CUDA loads every kernel when the engine starts: 16 adds 14 MB of kernels and +16 MiB of VRAM (RTX PRO 5000) and nothing is gained below 9 rows. On a 16 GB card those MiB shrink the auto-sized expert cache (2 x A4000: 5292 slots instead of 5301), and a cached expert rounds differently from a CPU one, so the texts differ from the 8-row build by that alone (main run with a 17 MiB larger `--vram-reserve-mib` gives the 16-row build's texts, 10 of 10). `-DSTRATA_WINDOW_MAX_T=16` builds it |
|
||||
| SYCL, Metal | 8 | not touched (they build the shared sources with the limit left at 8) |
|
||||
|
||||
A window of up to 8 rows runs exactly the kernels of 0.2.7; the code for 9 to 16 rows is only reached by a wider window. Checked
|
||||
instruction by instruction: HIP gfx1201, window 8 against the previous main, 3096 of 3097 kernels identical (the other is a
|
||||
`STRATA_DBG_GDN` debug variant) and 5 renamed; CUDA sm_86 SASS, 3393 of 3397 identical or renamed, the other four differ in the
|
||||
order of the same instructions. The CPU expert kernels keep 8 tokens per job (a group of 9 to 16 rows is two jobs over the same
|
||||
expert), and the hyper-connection read is made in slices of 8 rows.
|
||||
|
||||
Exactness, `tools/batch_test.py` (greedy tokens of every slot against the same prompt alone): 4 x R9700, 64 slots in 4 groups of 16
|
||||
(average 13.7 rows a window), 64 of 64 identical, with and without the three switches below; one R9700, `--batch 16`, 16 of 16;
|
||||
4 x R9700, `--batch 16 --batch-groups 1` (one 16-row window), 16 of 16; 32 slots in 4 groups of 8 with the profile below, 32 of 32.
|
||||
(Note: `--batch 16` on four stages is 4 groups of 4 slots, windows of 4 rows; it does not test 16-row windows.) Identity
|
||||
(`rules-144` flags, 10 prompts and a 4K and a 20K prompt, residual dumps compared): p620 (IQ3_S and IQ3_XXS), r9700, l620 and boston (CUDA, window 8 and 16), z440 and p1004 (CUDA, window 8) identical to main
|
||||
(z440 with window 16: see the CUDA row above).
|
||||
|
||||
### Four Radeon AI PRO R9700 cards: the serving profile
|
||||
|
||||
The four cards hold the whole of Qwen3.8-Flash-Next IQ3_S, so what limits them is how many requests decode together and how the
|
||||
four stages are fed. Measured through `serve/server.py` with the launcher's own configuration (400-token answers, short
|
||||
prompts, total tokens per second of the second round; 7.9K-token prompts for the long row):
|
||||
|
||||
| | 1 request | 8 at once | 32 at once | 4 long prompts, out / in tok/s | follow-up TTFT |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| before: `parallel` 4, split auto | 66.6 | 156.6 | 155.4 | 24.3 / 1892 | 0.78 s |
|
||||
| profile (below) | 67.9 | 261.2 | 369.6 | 35.0 / 2706 | 0.77 s |
|
||||
|
||||
The profile, filled in by `setup.py` and by the launcher only for four cards of that kind (R9700, at least 28 GB each, no
|
||||
`--peer-device`) running Qwen3.8 IQ3_S:
|
||||
|
||||
- `parallel` 32 and `--batch-groups 4`: 32 slots, one group of 8 per card;
|
||||
- layer split `13,26,39`: the output head sits on the last card and costs about four layers, so it takes 9 and the others 13;
|
||||
- `--prefill 2048` instead of `auto`;
|
||||
- `STRATA_QSA_SLOTS=1`, `STRATA_BATCH_SELF_COMMIT=1`, `STRATA_SLOT_COPY_DIRECT=1` (switches from the table above).
|
||||
|
||||
What each part gives at 32 at once (tok/s, against 398.7 for the profile with the prompt cache off, which is the owner's profile B): without the three switches 354.3,
|
||||
split `auto` 374.6, 8 slots instead of 32 277. `--prefill auto` gives the same throughput but a longer first token for a 4.7K prompt
|
||||
(2.06 against 1.52 s). 64 slots give 418.9 at 64 clients against 398.4 for 32 slots (+5%, measured at an 8K context only) and were left out; `GPU_MAX_HW_QUEUES=4` changes nothing. The prompt cache stays on: turning it off was +8% at 32 clients
|
||||
with new prompts, but a follow-up turn of a conversation then waits 1.5 s for its first token instead of 0.8 s, and agent loops
|
||||
are mostly follow-up turns.
|
||||
|
||||
A setting of the person always wins: the profile fills only what is not set (the config, `--parallel`, `--layer-split`,
|
||||
`--batch`, `--prefill` other than `auto`, an environment variable). `STRATA_FOUR_CARD_PROFILE=0`, or `"four_card_profile": false`
|
||||
in the run config, turns it off. 25 minutes of 32 clients (1088 requests of 600 tokens) through the launcher's configuration ran
|
||||
without an error at a steady 435 tok/s and with no change in VRAM.
|
||||
|
||||
Not done: the hand-off of a batch window between stages (`§7f` of the parallelization report, one flag a group) is not part of
|
||||
this; it can emit a wrong token from time to time and is listed for later.
|
||||
|
||||
## Exactness
|
||||
|
||||
A batch row's arithmetic is the single-token window's, so with greedy decoding **every conversation of a batch
|
||||
|
||||
@@ -39,6 +39,15 @@ perplexity +7.3% (code +3.2%, agent work +2.2%). IQ1_M: perplexity +21% (KL 0.21
|
||||
| IQ4_NL: `DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf` | 111 GB | the 4-bit Engram lookup table the model reads from RAM or SSD. Not a model to pick: all three sets use this one file, and it is already in their download sizes |
|
||||
| DSpark drafter (Q8_0) | 8 GB | the small draft model (Unsloth) |
|
||||
|
||||
**Two layouts of the same sets.** Setup downloads the four-shard files (`...-0000N-of-00004.gguf`, the Hugging Face revision `cb02422c`). On 27 October 2026
|
||||
ISTA-DASLab re-split the three sets: five shards each (`...-0000N-of-00006.gguf`; shard 1 is 5.8 MB of metadata and has no tensors) and the Engram file as the
|
||||
sixth shard of the split. Strata takes either from files you already have (`--gguf-dir`, `--model-path`, "Use a local GGUF" in the app, `strata models import`),
|
||||
checks their size and SHA-256, and packs them. The same goes for the four-shard files whose first shard ISTA-DASLab re-published on 26 October with a larger
|
||||
chat template. What differs in the six-shard files: the two Engram tables are one tensor (the same bytes), the Engram gate is stored already folded (q times k),
|
||||
the attention output matrix is one 2-D tensor instead of a 3-D one, and there is a routing bias for picture tokens. The packs are the same size and the engine
|
||||
reads both. The Engram has the same name and size in both layouts but another hash; either one works with either layout, and setup downloads the pinned one
|
||||
if you have none. The download default is still the four-shard files.
|
||||
|
||||
**On a Ryzen AI Max "Strix Halo" (Windows)** the graphics chip and the CPU share one pool of memory, so the engine counts it once: the
|
||||
128 GB box (a 64 GB BIOS carve-out plus the 64 GB Windows sees) keeps about 109 GB of the 2.00 set's experts in that pool (the dense
|
||||
weights and 12 GB for Windows and the desktop aside) and reads the other 23 GB from the SSD. Measured there (2.00 bpw, default Faster
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# DeepSeek-V4.1-Flash on a Mac
|
||||
|
||||
DeepSeek-V4.1-Flash runs on Apple Silicon Macs on its own Metal engine, `strata-dsv41`. It is a separate download from the
|
||||
Qwen engine: the app and `strata launch` fetch it the first time you pick DeepSeek. This page lists what it needs, what it did
|
||||
on the one Mac it was measured on, and what it cannot do yet.
|
||||
|
||||
> **Short version:** a Mac with 64 GB of memory or more, macOS 15 or newer, and 260 to 280 GB of free disk on a fast drive.
|
||||
> Text only, no pictures. The model does not fit in memory, so part of it is read from the SSD for every word. On an M5 Max with
|
||||
> 128 GB, DeepSeek 2.00 answers a short question at 15 to 18 tokens a second and reads a 3,000-token prompt in about 11 seconds.
|
||||
|
||||
## What it needs
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Mac | Apple Silicon (M1 or newer), macOS 15 or newer. Measured on an M5 Max with 128 GB. |
|
||||
| Memory | 64 GB or more. A Mac with less still starts, but most of the model is read from the SSD for every word, so it is much slower. Setup says so and asks before it goes on. |
|
||||
| Disk | 2.00 set: 257 GB. 2.25 set: 274 GB (the 111 GB n-gram table is shared). Use the internal SSD or an NVMe drive, not a USB or network drive. |
|
||||
| Pictures | Off. DeepSeek's picture reader was not run on the Metal engine, so a Mac reads text only. Qwen on the Mac keeps its pictures. |
|
||||
|
||||
## Install it
|
||||
|
||||
In the app: Models, DeepSeek-V4.1-Flash, pick 2.00 bpw (the default) or 2.25 bpw, Download and prepare.
|
||||
|
||||
In the Terminal:
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan # what it would do on this Mac
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw
|
||||
```
|
||||
|
||||
Setup downloads the set from Hugging Face (pinned revision, size and SHA-256 checked, resumable), builds the pack (a few MB),
|
||||
downloads the DeepSeek engine package for macOS from the signed update feed, and runs a short test question before it starts.
|
||||
Files you already have: `--model-path DIR`.
|
||||
|
||||
## What it did (M5 Max, 128 GB, 0.2.7.1 build)
|
||||
|
||||
Speeds are from the packaged engine with the shipped defaults (two Engram reader threads), one fresh process per run, two runs each. They were
|
||||
measured while another person's Qwen server (a 60 GB model, loaded and idle) was running on the same Mac, so the file cache that
|
||||
serves the streamed experts was shared with it. A Mac with nothing else running may be faster; that was not measured.
|
||||
|
||||
| | 2.00 bpw | 2.25 bpw |
|
||||
|---|---|---|
|
||||
| Short answer, code prompt | 15.2 to 16.0 tokens/s | 14.7 to 15.3 |
|
||||
| Short answer, agent prompt | 15.3 to 16.2 | 13.1 to 13.6 |
|
||||
| After a 3,000-token prompt | 14.5 to 15.1 | 11.7 to 11.8 |
|
||||
| After a 14,000-token prompt | 13.8 to 15.1 | 10.5 to 11.1 |
|
||||
| 3,000-token prompt | 236 to 241 tokens/s | 293 to 294 |
|
||||
| 14,000-token prompt | 479 to 512 | 553 to 586 |
|
||||
| 32,000-token prompt | 609 | 720 |
|
||||
| Read from the SSD per word | 48 MB | 111 MB |
|
||||
| Quality against the original model (KL, 4,995 positions) | 0.170 | 0.111 |
|
||||
|
||||
The 2.25 set is the better of the two (about 8% above the original model's perplexity on the Mac's scoring run, against about 17%). The 2.00 set is
|
||||
faster and smaller. The quality numbers come from the engine's scoring run with a fixed expert placement
|
||||
(`--profile-readonly`); a run that learns the placement as it goes moves the last digits.
|
||||
|
||||
How the memory is used: the engine keeps about 68 GB of the experts in memory on a 128 GB Mac. That share follows from the
|
||||
Mac's memory (the RAM less 40% and the engine's own footprint), not from what happens to be free at the start. The rest of the memory goes to
|
||||
macOS and its file cache, and a Mac with less memory keeps fewer experts. The remaining experts are read from the SSD in place.
|
||||
A decode token is limited by those SSD reads and by a CPU lane that computes the experts the GPU does not hold, not by the GPU.
|
||||
If another program already holds a lot of memory (a browser, another model's server), the engine keeps fewer experts so that
|
||||
macOS does not run out. That costs some speed and keeps the Mac responsive.
|
||||
|
||||
The needle test (a code hidden in a long text, asked for at the end) finds it at 14,600, 17,000, 19,500 and 26,200 tokens.
|
||||
|
||||
## Speed setting
|
||||
|
||||
DeepSeek's Speed choice (Faster = 5 experts a word, Full quality = 6) works as on a PC. On a Mac the engine's own default is
|
||||
Full quality. That is what was measured and scored.
|
||||
|
||||
## Limits
|
||||
|
||||
- Text only. A picture sent to a DeepSeek model on a Mac is refused.
|
||||
- The drafter (DSpark) is off. It gave the same answers but no speed-up on the Mac.
|
||||
- Context longer than about 32,000 tokens was not measured on a Mac.
|
||||
- Smaller Macs: nothing under 128 GB was run with DeepSeek. The plan screen shows how much of the model your Mac keeps in memory
|
||||
and warns when that is under a quarter of it. That estimate follows the engine's own rule and is not a measurement.
|
||||
- The 1.75 bpw set shares the 2.00 set's kernel but was not run on a Mac, so it is not offered there.
|
||||
- On the Mac, DeepSeek reads its Engram rows with two threads, because more threads gave slightly different words from run to
|
||||
run (in 3 to 9 of 16 runs; the cause is being studied). This costs about 3 to 12% of prompt speed (3,000 tokens about 12%,
|
||||
14,000 tokens 7 to 12%, 32,000 tokens about 3%); answer speed is the same. `DS4_ENGRAM_THREADS=N` sets another number.
|
||||
- Two runs of the same prompt can differ in the last digits of a score, because the engine moves experts between memory and the
|
||||
SSD as it learns what the prompt needs. The answers stay equally good.
|
||||
|
||||
## Building and packaging the engine (developers)
|
||||
|
||||
```sh
|
||||
bash packaging/mac/build_dsv41_engine.sh # configure, build, stage, package and verify the macOS package
|
||||
```
|
||||
|
||||
It needs the Xcode Command Line Tools, cmake and ninja (a pip venv is enough), Apple's metal-cpp and the llama.cpp tree that
|
||||
setup pins (`METAL_CPP_DIR`, `STRATA_GGML_DIR`). The result is `pkg/engine/<version>-macos-arm64-metal/` with `strata-dsv41`,
|
||||
the Metal sources it compiles at start-up and `engine-component.json`. The package is unsigned; the feed key signs it
|
||||
(`packaging/UPDATES.md`). The Qwen Metal engine is not rebuilt by this: the DeepSeek runtime rules sit behind
|
||||
`STRATA_DS4_RUNTIME` and are compiled only into `strata_ds4`.
|
||||
|
||||
Diagnostics, all off by default: `DS4_CPU_LOG=1` (per-token phase times), `DS4_PROF=1` (per-kernel GPU time),
|
||||
`DS4_GPU_TOTAL=1`, `DS4_ENC_TIMES=1`, `DS4_TLOG=1`, `DS4_TRACE_DISP=1`.
|
||||
+23
-4
@@ -472,7 +472,7 @@ applies at the next start. Network, key, MCP and program keys are not editable t
|
||||
| `repeat_stop_tokens` | `256` | A reply that repeats one token this many times in a row ends as `"length"`. `0` turns it off. |
|
||||
| `effort_position` | `"start"` | `"end"` puts a `low` or `medium` effort in a short system turn right before the answer, so a request that only changes the effort reuses the conversation cache. How well the model follows a level in that place is not measured. |
|
||||
| `anthropic_thinking` | as the template | `"on_request"` renders a `/v1/messages` request that asks for no thinking without thinking, as Anthropic does. Claude Code's short helper calls need it. |
|
||||
| `tool_call_recovery` | off | Also reads a declared tool's call written as `<parameter=NAME>`, as JSON inside `<tool_call>`, as a `<function=NAME>` block without `<tool_call>`, or as a second call inside one `<tool_call>`. On 1,462 agent turns 98.3% read as intended with it and 92.9% without. |
|
||||
| `tool_call_recovery` | on (since 0.2.8; off in 0.2.7 and before) | Also reads a declared tool's call written as `<parameter=NAME>`, as JSON inside `<tool_call>`, as a `<function=NAME>` block without `<tool_call>`, or as a second call inside one `<tool_call>`. On 1,462 agent turns 98.3% read as intended with it and 92.9% without; the 49 specimens of the test corpus read the same with it on or off. Only a declared tool's call is ever recovered; anything else stays text. `"tool_call_recovery": false` turns it off. |
|
||||
| `env_to_user` | off | Moves opencode's `<env>` block (working directory, date) to the first user message so the system turn is the same for every session and the disk prefix cache restores it. |
|
||||
| `return_progress` | off | Per request `"return_progress": true` adds llama.cpp's `prompt_progress` (`total`, `cache`, `processed`, `time_ms`) to the stream, one update per `--prefill` chunk. A prompt shorter than one chunk sends nothing. |
|
||||
| `prevent_sleep` | off | Windows only: keeps the PC awake while a request runs or waits (`SetThreadExecutionState`). On Linux and macOS one startup line says it does nothing. |
|
||||
@@ -501,6 +501,16 @@ applies at the next start. Network, key, MCP and program keys are not editable t
|
||||
guessed: `bash` is not `shell`, and a reordered name (`server1_label_values` for `label_values_mcp_server1`) is not the declared one.
|
||||
A call whose envelope drifted by a token (`<=NAME>`,
|
||||
`<function:NAME>` or `<NAME>` instead of `<function=NAME>`) is read as the call when NAME is a declared tool.
|
||||
- **What the parser repairs, and what it leaves.** A string argument that quotes a whole tool call, or repeats its own
|
||||
`</parameter><parameter=NAME>`, stays exact (#4078). A call whose `</function>` was left out is read as the call (#4427), in the
|
||||
answer and inside the thinking. An argument the tool declares as an array or an object, or declares nothing for, is read as
|
||||
written when it has a slip in it: a trailing or doubled comma, single quotes or `True`/`False`/`None`, a bare string between two
|
||||
members of an object (`"status": "pending", "in_progress", "pending": false`), brackets left open at the end. The server window
|
||||
says `N tool call argument(s) repaired` and `/metrics/prometheus` counts them in `strata:tool_args_repaired_total`. Text that
|
||||
is not an array or an object, a string cut short and a string-typed parameter are never touched. After the thinking has ended at
|
||||
a closing think tag, a second one at the start of a line in the answer is dropped (the model quoted the tag while thinking, #4077):
|
||||
the call that follows is kept. The reasoning between the two went out as the answer already, because a stream cannot take text
|
||||
back.
|
||||
- **A call that came back as text because its name was not declared (#3796).** The text stays exactly as the model wrote it and
|
||||
`finish_reason` stays `stop` (or `end_turn` on `/v1/messages`). Next to it the reply carries a list, only when there is something in it:
|
||||
`"strata_rejected_tool_calls": [{"name": "bash", "reason": "undeclared"}]` (at the top level of a Chat Completions reply, in
|
||||
@@ -698,6 +708,15 @@ the engine (v2 `memory.max` and `memory.high`, v1 `memory.limit_in_bytes`, less
|
||||
`--memory-limit-mib` cap. The startup line `strata generate: memory guard: limit X GiB (source: flag|cgroup|meminfo) ...` names
|
||||
the source. It covers parking and session files only, not the startup sizing of the arena, the resident budget or the Windows commit check.
|
||||
|
||||
**When the arena does not fit in the RAM that is free (#4051).** The engine checks the RAM before it reads the experts into the
|
||||
arena. A memory limit below the arena (a container) stops the start with the numbers. Less free RAM than the arena (an engine
|
||||
that was still exiting, another program) is waited for up to 20 s, and the log says so (`STRATA_ARENA_RAM_WAIT_S` changes the
|
||||
seconds, `0` loads at once); if it is still short the load goes on with a warning that says how much, never a refusal. Below half
|
||||
of the arena the warning adds that the whole PC can stall in memory reclaim, and that a systemd `MemoryHigh=` below the RAM keeps
|
||||
one program from taking all of it. The server, before it starts an engine, ends one that an earlier run left behind and now waits up
|
||||
to 120 s for it to be gone (it used to wait 10 s): an engine with tens of GB of pinned memory needs longer to give it back, and the
|
||||
next one started beside it ran the PC out of RAM (#4051).
|
||||
|
||||
Snapshots hold running state, checkpoints, used K/V pages and draft-layer K/V. They add host RAM, not VRAM. Oldest parked entries
|
||||
are evicted first. A new conversation that starts at a checkpoint inside a parked one (sub-agents that share a system prompt)
|
||||
restores that prefix and leaves the parked conversation in place (the log says "borrowed"). Oversized snapshots or host
|
||||
@@ -1019,7 +1038,7 @@ them on your own card.
|
||||
| `STRATA_PREFILL_EQUAL=1` | CUDA | A prompt segment is read in chunks of equal size, not full chunks and a short last one. Changes rounding. | |
|
||||
| `STRATA_OWNED_PRICE=exact` | CUDA | The cache sizing prices the prompt path's own buffers by their real allocation. | |
|
||||
| `STRATA_IQ_MT_MIN=1`, `STRATA_IQ3S_MT1=1` | CPU | See [Draft layer](#draft-layer) (reproducible output; the second gives IQ3_S the multi-token kernel for one token on Alder Lake and later without AVX-512). | |
|
||||
| `STRATA_ROUTE_PRIOR`, `STRATA_ROUTE_RESIDENT`, `STRATA_ROUTE_RESIDENT_MTP` | CUDA | Routing changes that add speed and also add KL. | Not recommended, except that 0.2.7 turns `STRATA_ROUTE_PRIOR=1.5` on by itself on a discrete NVIDIA card of 13 GiB or less (the owner approved the quality trade for 12 GB cards; the first 0.2.7 candidates used 2.0, see the 12 GB default in the release notes for why it is 1.5); `STRATA_ROUTE_PRIOR=0` turns it off and an explicit value wins. It is never a default on a Radeon, an integrated GPU or a bigger card. The 1.5 default was checked on an RTX 3060 12 GB (IQ3_XXS) with 80 tasks: no copy or code errors seen; agent loops may finish slightly less often, within the run-to-run spread (out of 10 agent tasks 4.5 passed on average over 4 runs at 1.5, against 5.6 over 7 runs with the prior off, where the off runs alone ranged from 4 to 6). Speed against off: story +15%, code +37%. Off: the Expert-settings row "Faster answers on 12 GB NVIDIA cards" or `STRATA_ROUTE_PRIOR=0`. |
|
||||
| `STRATA_ROUTE_PRIOR`, `STRATA_ROUTE_RESIDENT`, `STRATA_ROUTE_RESIDENT_MTP` | CUDA | Routing changes that add speed and also add KL. | Off by default. `STRATA_ROUTE_PRIOR=1.5` makes the router prefer experts that are already on the card; it is the one worth trying, on a discrete NVIDIA card of 13 GiB or less (an RTX 3060 12 GB, IQ3_XXS: story +15%, code +37% decode against off). It is an opt-in because it can repeat itself and give weaker results in long agent sessions (#4402; our own check on that card: out of 10 agent tasks 4.5 passed on average over 4 runs at 1.5, against 5.6 over 7 runs with the prior off, where the off runs alone ranged from 4 to 6; a stronger setting, 2.0, showed copy and code defects). Recommended for a 12 GB card used mostly for chat and short answers, not for long agent runs. 0.2.7 turned 1.5 on by itself on those cards and 0.2.8 took that back (owner decision of 2026-10-29): with nothing set, the engine routes exactly as 0.2.6 did (the same as `STRATA_ROUTE_PRIOR=0` and as `STRATA_DEFAULTS=026`). Turn it on: the Expert-settings row "Faster answers on 12 GB NVIDIA cards" (it writes `STRATA_ROUTE_PRIOR=1.5`; shown only for a discrete NVIDIA card of 13 GiB or less), `strata launch --set STRATA_ROUTE_PRIOR=1.5`, or the variable itself. Any value you set wins on every card; it is never on by itself. |
|
||||
| `STRATA_HOST_CORE=last\|sibling` (or `--host-core`) | Windows | `last` puts the host thread on the last physical core, away from the core that gets the GPU's interrupts. `sibling` keeps it on the first core's other hardware thread, so the workers keep every core. The default `first` leaves a hybrid CPU as it is. | One layer-split box went from a ±6% swing to steady and 8% faster. |
|
||||
| `--pool-affinity measure` | Hybrid CPUs, native packs | Times the `all` and `p-cores` layouts at start (about a second) and takes the faster. The measurement is taken once; where the E-cores' speed changes while the engine runs, `--pool-affinity p-cores` is the steady choice. | |
|
||||
| `STRATA_KEEP_EMPTY_TURNS=1`, `STRATA_TOPK_STREAM=0` | | See [TROUBLESHOOTING.md](TROUBLESHOOTING.md). | |
|
||||
@@ -1222,7 +1241,7 @@ On 12.0 cards (sm_120) these also apply:
|
||||
prompt time and start-up time, which is why smaller PCs keep the SSD reads.
|
||||
|
||||
A prompt chunk above 8192 (`--prefill auto:16384` or `auto:32768`) is not bit-exact on CUDA, so the default stays 8192 on
|
||||
NVIDIA. It is exact on gfx12, where 16384 is the default. `STRATA_ROUTE_PRIOR` raises KL, so below 0.2.7 it was opt-in; 0.2.7 sets 1.5 by itself only on a discrete NVIDIA card of 13 GiB or less. An 80-task check of 1.5 on an RTX 3060 saw no copy or code errors; agent loops may finish slightly less often, within the run-to-run spread; story +15%, code +37%.
|
||||
NVIDIA. It is exact on gfx12, where 16384 is the default. `STRATA_ROUTE_PRIOR` raises KL and is an opt-in again in 0.2.8 (0.2.7 set 1.5 by itself on a discrete NVIDIA card of 13 GiB or less, and the owner took that back on 2026-10-29 because of repetition loops and weaker results in long agent sessions, #4402). `STRATA_ROUTE_PRIOR=1.5` on an RTX 3060 12 GB: story +15%, code +37%, 80 tasks with no copy or code errors, but 4.5 of 10 agent tasks against 5.6 with it off.
|
||||
|
||||
---
|
||||
|
||||
@@ -1446,7 +1465,7 @@ with `--batch 2` at 262,144 context saw 538 MiB at the free line become 395 MiB
|
||||
| not stated | RTX 5070 Ti Laptop, Swift 1.5 IQ3_XXS, a new opencode session in a second folder | `env_to_user`: first request 14,073 tokens with 13,693 from the cache, 1.1 s, against 14,753 read, 11.9 s |
|
||||
| 0.1.40.2 | Tesla P100 | A question after a long document re-reads up to 16K tokens (55 s at 262K) without `strata_prefix` |
|
||||
| not stated | Codex CLI 0.160.0, Q2_0, RTX 5070 | First prompt 9,443 tokens in 10 s; later tool-loop turns reused about 96% from the cache and read the new part in 1-2 s. Compaction after an 85,000-token conversation reused 84,895 of 84,997 tokens with `codex_compaction_cache` and 41 of 80,683 without |
|
||||
| not stated | Signalnine/q27's drift corpus, 1,462 agent turns (Qwen3.8 under Claude Code) | `tool_call_recovery`: 98.3% read as intended, 92.9% without |
|
||||
| not stated | Signalnine/q27's drift corpus, 1,462 agent turns (Qwen3.8 under Claude Code) | `tool_call_recovery`: 98.3% read as intended, 92.9% without (on by default from 0.2.8) |
|
||||
| not stated | RTX 3080, RVN IQ3_S config, 8-turn chat replay on a 16K-token system prompt | NaN guard on and off: byte-identical answers in 3 pairs; cost 4.8 us of GPU time a window (7.8 us at most), 0.009% of decode time |
|
||||
|
||||
### Context beyond 262K (contributors' runs, RTX 5080, IQ3_S, native path, PR #84)
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
# Strata on the NVIDIA DGX Spark (Linux, ARM64)
|
||||
|
||||
The DGX Spark is a small desktop box with a GB10 chip: a Blackwell GPU (compute capability 12.1, 48 SMs) and 20 Arm CPU cores that share
|
||||
128 GB of memory. Strata 0.2.7.1 runs Qwen3.8-Flash-Next and DeepSeek-V4.1-Flash on it. This page says what works, how to install it, what
|
||||
the Spark does with its memory, and how the speeds were measured. It was tested on one Spark (Ubuntu 24.04, driver 580.173, CUDA 13.0).
|
||||
Nothing here is known to work on another ARM machine.
|
||||
|
||||
## What works, and what does not
|
||||
|
||||
- **Works:** Qwen3.8-Flash-Next IQ3_S and the NVFP4 GPTQ pack (64K context, every expert in the GPU's cache), DeepSeek-V4.1-Flash 2.00 bpw
|
||||
(a short chat), the Terminal (`strata launch`, `strata update`, `strata serve`) and the engine packages of the signed feed.
|
||||
- **Not built for ARM:** the desktop app (no AppImage; use the Terminal), the HIP, SYCL and CPU-only engines, and the Q2_0 and Gyro packs,
|
||||
whose CPU kernels are x86-only (the engine refuses them at start). Picture reading is compiled in and was not tried on the Spark.
|
||||
- **Not tested:** contexts above 128K, more than eight chat slots (four and eight were measured: "Several chats at once" below), a long soak. `strata doctor --live` lists the GB10 with no memory
|
||||
figure, because `nvidia-smi` reports none for it.
|
||||
|
||||
## Install with the Terminal
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
`install.sh` unpacks the Terminal into `~/.local/share/strata` (no administrator rights) and checks the release's signed feed. `strata launch`
|
||||
fetches the `linux-aarch64` engine package, shows what each model needs, downloads the one you choose and starts it. Pick Qwen3.8-Flash-Next
|
||||
IQ3_S (84 GB download) or the NVFP4 pack, or DeepSeek-V4.1-Flash 2.00 bpw (257 GB). The engine package needs glibc 2.39 or newer
|
||||
(Ubuntu 24.04, DGX OS 7) and NVIDIA driver 580.65 or newer.
|
||||
|
||||
## Install from a source checkout
|
||||
|
||||
```sh
|
||||
./setup.sh --setup --family qwen --model IQ3_S --context 65536 --vision no --yes --no-start --data-dir ~/Strata-data
|
||||
./setup.sh --setup --family nvfp4 --model NVFP4 --context 65536 --vision no --yes --no-start --data-dir ~/Strata-data
|
||||
```
|
||||
|
||||
`--data-dir` must be a folder you can write (the default, next to the checkout, may be under `/home`, which a normal user cannot create).
|
||||
Setup finds the GB10, calls it unified memory, takes the engine from the feed (or compiles it with `spark/build_engine.sh` when the release
|
||||
has none) and writes the run config with the settings measured on this part:
|
||||
|
||||
| Setting | Why |
|
||||
|---|---|
|
||||
| `--mmap-experts` | the experts are read from the model files in place, so the GPU's cache holds all 24,576 of them instead of a second copy in RAM |
|
||||
| `--prefill auto:32768` | a 32,768-token prompt chunk; the automatic 8,192 measured 2 to 6% slower here |
|
||||
| `--mtp-q4 all` | the draft layer as 4-bit copies: +3.5% decode, the answers do not change because the target checks every draft |
|
||||
| `--mtp-hnorm stream` | one norm per hyper-connection stream in the draft layer: draft acceptance 0.60 to 0.63, +4.3% decode |
|
||||
|
||||
Setup prints `--draft-vocab en` as a tip and does not set it: it measured +3.1% decode on English prompts, and it would cut the Chinese,
|
||||
Japanese and Korean drafts.
|
||||
|
||||
Start a model with its run script (`./run-iq3_s.sh`) or `./setup.sh`.
|
||||
|
||||
## Build the engine yourself
|
||||
|
||||
You need the CUDA 13.0 toolkit (`/usr/local/cuda`; DGX OS has it), cmake, ninja and Python 3.10 or newer.
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv-build
|
||||
.venv-build/bin/pip install -r requirements.txt -r requirements-build.txt
|
||||
PATH="$PWD/.venv-build/bin:/usr/local/cuda/bin:$PATH" PYTHON="$PWD/.venv-build/bin/python" JOBS=16 sh spark/build_engine.sh
|
||||
cp -r out-spark/engine ./engine
|
||||
```
|
||||
|
||||
About 3 minutes for the Qwen engine and 6 with the DeepSeek engine and the picture reader (the first run also fetches llama.cpp at the
|
||||
pinned commit). `out-spark/engine/` holds `strata`, `strata-dsv41`, `strata-vision`, `libstrata_kq.so`, the CUDA libraries and a `BUILD.json`.
|
||||
`BUILD_VISION=0` and `BUILD_DSV41=0` leave a part out. `packaging/build_cuda_linux_aarch64.sh` runs this build and makes the engine packages
|
||||
for the feed ([packaging/README.md](../packaging/README.md)). A model on a Q2_0 pack cannot run: its CPU kernels need AVX-512.
|
||||
|
||||
## What the Spark does with its memory
|
||||
|
||||
The GPU's memory and the CPU's are the same 121.6 GB, so a model's experts are held once. With `--mmap-experts` there is one copy: the model
|
||||
file's pages, which the GPU cache uses.
|
||||
|
||||
| | IQ3_S | NVFP4 GPTQ |
|
||||
|---|---|---|
|
||||
| Expert bytes | 50.3 GB | 68.0 GB |
|
||||
| Expert cache | 24,576 of 24,576 slots, 46.84 GiB | 24,576 of 24,576 slots, 63.29 GiB |
|
||||
| n-gram table | 28.8 GB, locked in RAM (`--ple-io ram`) | 51.2 GB, read from the SSD |
|
||||
| Context | 64K, 8-bit KV, 32,768 cells streamed | same |
|
||||
| Free memory at start | about 1 GiB | about 1.2 GiB |
|
||||
|
||||
The NVFP4 table does not fit in RAM beside the page cache the 68 GB expert file needs. Locking it measured 137 tok/s on a 4K prompt and 26 tok/s
|
||||
decode, against 1,252 and 39 without. Leave it on the SSD (setup does).
|
||||
|
||||
DeepSeek-V4.1-Flash 2.00 bpw places 12,163 of its 15,744 experts in the GPU's share and runs the rest on the CPU lane from the model's files.
|
||||
The engine sizes that from the memory Linux can still give back (`MemAvailable` less 6 GiB, `DS4_UMA_HEADROOM_GIB`), because the model files' page
|
||||
cache is most of the memory `cudaMemGetInfo` calls used.
|
||||
|
||||
## Speeds
|
||||
|
||||
Measured on the release-candidate engine package of 0.2.7.1 with `tools/dgx_bench.py`: fresh 4K, 16K and 32K prompts at temperature 0, 256-token
|
||||
answers, 64K context, the model's own run config, four samples a cell in two blocks (on, off, off, on), medians. "Table off" is
|
||||
`STRATA_SM121_PERF=0`. One Spark, 27 October 2026; the figures move a few percent between sessions.
|
||||
|
||||
| Model | Table | Prompt tok/s (4K / 16K / 32K) | Decode tok/s (4K / 16K / 32K) |
|
||||
|---|---|---|---|
|
||||
| Qwen3.8-Flash-Next IQ3_S | on (default) | 1,296 / 1,680 / 1,735 | 45.5 / 46.5 / 46.5 |
|
||||
| Qwen3.8-Flash-Next IQ3_S | off | 1,203 / 1,416 / 1,276 | 48.5 / 45.3 / 45.2 |
|
||||
| Qwen3.8-Flash-Next NVFP4 | on (default) | 1,408 / 1,670 / 1,698 | 42.8 / 40.5 / 40.1 |
|
||||
| Qwen3.8-Flash-Next NVFP4 | off | 1,156 / 1,350 / 1,353 | 41.0 / 41.2 / 38.4 |
|
||||
|
||||
The table makes prompts faster: IQ3_S 1.20x over the three sizes (90% interval 1.12 to 1.31; 1.08x at 4K, 1.19x at 16K, 1.36x at 32K), NVFP4
|
||||
1.24x (1.14 to 1.36). It does not change decode speed in this check: 0.996x (0.97 to 1.02) on IQ3_S, with the 4K cell 0.94x, and 1.02x (1.00 to
|
||||
1.05) on NVFP4. The first run of the port (26 October) had measured +5% each for the two decode switches; this check did not see it.
|
||||
|
||||
DeepSeek-V4.1-Flash 2.00 bpw, 8K context, the default Faster mode (5 of 6 experts per word), 11,187 of 15,744 experts in the GPU's share: a
|
||||
short chat answers at 13 to 18 tok/s (the first answer after a start is the slowest). Full quality (`--experts 6`) places more of them and was
|
||||
measured at 17 to 23 tok/s on a 1.7K-token chat in the first run.
|
||||
|
||||
Several chats at once (`"parallel"` is 4 on a Spark for `strata launch`; the setup script leaves it off): IQ3_S, 2K-token prompts, 128-token cap,
|
||||
the aggregate tokens per second over all clients: 8 clients 35.6, 16 clients 41.7, 32 clients 47.3 at 4 slots; 32 clients 54.2 at 8 slots. With
|
||||
a prompt of its own and a hidden code for each client, every answer named its own code and nobody else's (8 clients at 4 slots, 16 clients at 8
|
||||
slots), and none failed or hung.
|
||||
|
||||
## The four speed defaults of this part
|
||||
|
||||
On a GB10 the engine switches on four settings that were measured here, and prints them at every start. `STRATA_SM121_PERF=0` (or
|
||||
`STRATA_DEFAULTS=026`, which gives back everything 0.2.7.1 changed) turns the whole set off; a switch you set yourself is kept.
|
||||
|
||||
| Switch | What it does | Measured in the first run (26 October) |
|
||||
|---|---|---|
|
||||
| `STRATA_PF_FUSED=1` | the fused IQ prompt kernels | IQ3_S prompt +10 to 27% |
|
||||
| `STRATA_PREFILL_NVFP4=w4a4` | NVFP4 prompt matrix product with FP4 activations | NVFP4 prompt +16% |
|
||||
| `STRATA_HC_UPMIX=1` | the hyper-connection read's up projection and mix in one kernel | decode +5.4% |
|
||||
| `STRATA_SPEC_COST=1` | the draft window follows what a draft costs | decode +4.7% |
|
||||
|
||||
**What the first two switches do to the answers.** The fused prompt path and the FP4 activations round differently from the paths they replace,
|
||||
and the other two do not touch the text (the fused hyper-connection read gives bit-identical logits; the cost-aware window only chooses
|
||||
which drafts to try, and the target checks every draft). 24 short greedy prompts gave 24 identical answers on IQ3_S and on NVFP4. On
|
||||
24 long prompts (1.5K to 24K tokens, the first-token logits compared with the switches off, `STRATA_DUMP_FIRST_LOGITS`) the distribution
|
||||
does move: IQ3_S mean KL 0.019 (largest 0.082), NVFP4 0.032 (largest 0.126); the most likely token changed on 2 of 24 prompts for each, in
|
||||
IQ3_S's case a near tie both times (0.20 against 0.20, 0.15 against 0.14); and a 24-token greedy answer stayed the same on 1 of 6 prompts. Two runs
|
||||
of the same setting gave identical logits, so the difference is the switches, not noise. All of the IQ3_S difference comes from the fused
|
||||
prompt path, and all of the NVFP4 difference from `w4a4`. On the 61-task quality suite (long-document retrieval, extraction of up to 72 exact rows, summaries, code, JSON, agent loops with tools), run through the server with the switches on and off: IQ3_S passed 56 tasks on and 51 off, NVFP4 56 on and 52 off. Retrieval found all 63 facts in every run, and no agent loop made an invalid tool call. The extraction facts were equal on IQ3_S (621 of 642 both ways); on NVFP4 one 40-row register lost a row with the switches on (616 against 621 of 642). One summary in each model gained or lost a flagged name or figure. Answers differ in wording on 21 of 61 tasks (IQ3_S) and 25 of 61 (NVFP4), and the wins and losses go both ways, so read the suite as "no loss we could measure", not as an improvement. The same setting run twice gave identical answers, so every difference comes from the switches. If you want the answers of 0.2.7, set `STRATA_SM121_PERF=0`: you keep the other
|
||||
Spark settings and lose the prompt speed-up.
|
||||
|
||||
With `"parallel": 4` (the default) the set adds up: 32 clients at 2,000-token prompts read 41.3 tok/s with all four on and 35.7 with all off. At
|
||||
`"parallel": 8` the two decode switches cost about 3.5% each; set `STRATA_HC_UPMIX=0 STRATA_SPEC_COST=0` there.
|
||||
|
||||
## What this does not cover
|
||||
|
||||
Decode on the 48-SM GB10 is limited by the GPU's work, about 930 kernel launches per token. No configuration switch measured here moved it more
|
||||
than a few percent. A DFlash2 drafter is slower here than the MTP drafter (37 against 45 tok/s) and is not used. Teacher-forced quality over
|
||||
long text and task suites was run only for the prompt path (above).
|
||||
@@ -0,0 +1,88 @@
|
||||
# GDN prompt-path contract (Flash-Next, CUDA reference)
|
||||
|
||||
Audited on main `870aedd9e` (v0.2.8), `src/prefill/kernels.cu` and `src/prefill/prefill.cpp`. Everything below is read from
|
||||
that source; the harness (`gdn_harness`, `tests/gdn/gdn_fixture.hpp`) checks it against an FP64 oracle that starts from the
|
||||
raw inputs, so a wrong reading shows up as a failed parity case.
|
||||
|
||||
## Shapes
|
||||
|
||||
`S = 128` (head size), `HK = 16` key heads, `HV = 48` value heads (3 per key head, `VPK = 3`), `C = 2*HK*S + HV*S = 10240`
|
||||
channels per token. Value head `vh` uses key head `qh = vh % HK` (NOT `vh / 3`; FLA groups the other way, an adapter is needed
|
||||
for any comparison with FLA). One layer's prompt call has `T` tokens (the engine's prefill chunk, not the whole prompt); the
|
||||
state and the convolution history carry from call to call.
|
||||
|
||||
## One layer, in the order the engine runs it (`prefill.cpp`, `pt.mark` stages)
|
||||
|
||||
| Stage (STRATA_PREFILL_TIMING name) | Work | In the harness |
|
||||
|---|---|---|
|
||||
| `gdn` (kPfGdn) | attn_qkv, attn_gate (z), ssm_alpha, ssm_beta projections (quantized GEMMs) | no (not a GDN kernel) |
|
||||
| `gdn conv+gates` (kPfGdnConv) | `gdn_gates`, `gdn_conv` | yes, timed separately |
|
||||
| `gdn recurrence` (kPfGdnRec) | `gdn_recurrence`: scan + output norm | yes |
|
||||
| `gdn out proj` (kPfGdnOut) | ssm_out projection from `y16` (quantized GEMM) | no |
|
||||
|
||||
Note the stage names: the profile line `gdn` is the INPUT projections, `gdn out proj` is the output projection. The task's
|
||||
"GDN stage = kPfGdn + Conv + Rec + Out" therefore contains two dense GEMM stages the recurrence work cannot speed up.
|
||||
|
||||
## Tensors (all FP32 unless noted, row-major, `t` = token in the call)
|
||||
|
||||
| Tensor | Layout | Meaning |
|
||||
|---|---|---|
|
||||
| `qkv` | `[T][C]` | projection output, channels `q (HK*S) | k (HK*S) | v (HV*S)` |
|
||||
| `ab` | `[T][2*HV]` | alpha at `[t][h]`, beta input at `[t][HV+h]` |
|
||||
| `dt`, `ssm_a` | `[HV]` | per-head bias and (negative) decay base |
|
||||
| `conv_w` | `[C][4]` | depthwise causal 4-tap filter, tap 3 is the current token |
|
||||
| `history` | `[C][3]` | the three previous raw `qkv` inputs of every channel (a state object of its own) |
|
||||
| `state` | `[S][HV][S]` (row i, head, column) | recurrent state, in place; `history` follows it in the engine's layer state |
|
||||
| `gate`, `beta` | `[T][HV]` | outputs of `gdn_gates` |
|
||||
| `h` | `[T][C]` | outputs of `gdn_conv` (SiLU, then q and k L2-normalized per head) |
|
||||
| `z` | `[T][HV*S]` | output gate input |
|
||||
| `y` | `[T][HV*S]` FP32 | scratch the recurrence writes the raw output to |
|
||||
| `y16` | FP16, row stride `ld16` | normalized output the out projection reads |
|
||||
|
||||
`ld16 = HV*S = 6144`, or `6144 + 64` when STRATA_PF_PAD is on and `T >= max(pf_switch_min_t, 64)`. A kernel must write exactly
|
||||
the 6144 columns and leave the pad columns untouched (the harness fills them with a sentinel and fails if they change).
|
||||
|
||||
## Math (per value head, `g_t = exp(gate_t)`)
|
||||
|
||||
- `gate_t = softplus(alpha_t + dt) * ssm_a` (softplus is the identity above 20); `beta_t = sigmoid(beta_input_t)`
|
||||
- `h_c(t) = silu(sum_k w[c][k] * x_c(t-3+k))` with `x_c(t<0)` from `history`; q and k heads are then scaled by `1/sqrt(sum^2 + 1e-6)`
|
||||
- `kv = S^T k`, `delta = (v - g*kv) * beta`, `S <- g*S + k delta^T`, `o = S^T q / sqrt(128)` (uses the UPDATED state)
|
||||
- `y16 = fp16( o * rsqrt(mean(o^2) + 1e-6) * gamma[col] * sigmoid(z) )`
|
||||
- after the call: `history <- last three raw inputs` (older history where `T < 3`)
|
||||
|
||||
## State mutation rules
|
||||
|
||||
- A call with `T <= 0` does nothing.
|
||||
- The call advances `state` and `history` in place. A path that cannot run must decline BEFORE touching the state (the helper
|
||||
returns an error and "nothing was launched"); once a kernel has started, there is no replay. A forced path that is not
|
||||
eligible launches nothing and reports kGdnNone with the reason (`gdn_probe.hpp`); the harness never times it.
|
||||
- Calls are sequential per layer state. The chunked path's scratch is one static buffer per device (single prompt stream per
|
||||
device); concurrent prompt streams on one device are not supported by it.
|
||||
|
||||
## Dispatch on CUDA (production, no probe)
|
||||
|
||||
1. `STRATA_GDN_TC=1` and `T >= 128`: `gdn_rec_tc` (tensor-core scan; sm_80+, >= 48 SMs, ~122 KB smem per block). Not mentioned in the
|
||||
2026-10-26 research pack: it landed with PR #3779. FP16 operands, FP32 accumulation: mixed-precision tier.
|
||||
2. `STRATA_GDN_CHUNKED=1` (`=2` skips the SM-count test) and `T >= 128`: `gdn_rec_chunked` (FP32, C = 32, scan grid `HK x S/16 = 128`
|
||||
blocks of 384 threads, 93.7 KB smem; sm_80+).
|
||||
3. else `gdn_rec_kh_kernel` when `gdn_keyhead_ok()` (sm_80+, all 64 blocks resident), otherwise `gdn_rec_cols_pipe_kernel`
|
||||
(`STRATA_GDN_PIPELINE=0`: `gdn_rec_cols_kernel`).
|
||||
4. `gdn_out_norm_kernel` after any of them (it is the part that writes `y16`).
|
||||
`STRATA_GDN_REC_WARP` and `STRATA_GDN_CHUNK` exist only in `STRATA_EXPERIMENTAL_SM60` builds and act only on a cc 7.0 device at run
|
||||
time (`gdn_chunk_available()`): they do nothing on sm_60, sm_86 or sm_120.
|
||||
|
||||
## Numerical tiers (gdn-research 06)
|
||||
|
||||
1. exact: output and state bitwise equal to the incumbent (`kh` and `pipe` are the same bits by construction).
|
||||
2. reordered FP32 (chunked): same recurrence, other summation order. Verdict in the harness: FP64-oracle error not above
|
||||
`max(floor, 3 x the incumbent's own error)` with floors 2e-3 (normalized max of the FP16 output) and 1e-4 (state); finite; the
|
||||
conv history exact.
|
||||
3. mixed precision (tc): same checks, plus the stress fixtures (`--stress`).
|
||||
The harness prints `normalized_max = max|d| / max|ref|` and relative L2 against the oracle and against the incumbent over ALL
|
||||
heads, tokens and state elements.
|
||||
|
||||
## Test hook
|
||||
|
||||
`include/strata/prefill/gdn_probe.hpp`: a thread-local `GdnProbe` installed by a test selects/forces a path, a smaller chunked scratch
|
||||
segment (`gsb`), or the SM-count bypass, and reads back `selected`, `wanted` and the fallback `reason`. With no probe installed the
|
||||
dispatch is unchanged. The engine never installs one.
|
||||
+4
-3
@@ -24,6 +24,7 @@ install does not need a source checkout or a startup `.bat` file.
|
||||
| Linux x86-64 with Intel Arc | [Intel Arc guide](INTEL_ARC.md); install the driver/compute runtime and check model preparation requirements |
|
||||
| Apple Silicon Mac, macOS 15 or newer | [Mac guide](MAC.md). Intel Macs are not supported by the Mac package. |
|
||||
| AMD Ryzen AI Max / Strix Halo | [Strix Halo guide](STRIX_HALO.md) |
|
||||
| NVIDIA DGX Spark (Linux ARM64) | [DGX Spark guide](DGX_SPARK_LINUX.md). Use the Terminal; there is no desktop app for ARM64 Linux. |
|
||||
|
||||
Memory and disk needs depend on the **model, quantization, context and enabled features**. Check
|
||||
[which model fits](MODELS.md#pick-by-memory); download size is not the same as RAM use. Unified memory counts once.
|
||||
@@ -37,10 +38,10 @@ hardware drivers. Models and updates need an internet connection to download.
|
||||
|
||||
1. Open the [latest release](https://github.com/Niko1221/Strata/releases/latest).
|
||||
2. Download the one installer for your system. There is one per system, not one per graphics card:
|
||||
- **Windows:** run `Strata-0.2.7-windows-x64-setup.exe`. It installs for your user and needs no administrator rights.
|
||||
- **Linux:** make `Strata-0.2.7-linux-x86_64.AppImage` executable, then open it. If FUSE is unavailable, run the
|
||||
- **Windows:** run `Strata-0.2.9-windows-x64-setup.exe`. It installs for your user and needs no administrator rights.
|
||||
- **Linux:** make `Strata-0.2.9-linux-x86_64.AppImage` executable, then open it. If FUSE is unavailable, run the
|
||||
downloaded file with `--appimage-extract-and-run`. [Linux help](TROUBLESHOOTING.md).
|
||||
- **Mac:** open `Strata-0.2.7-macos-arm64.dmg`, drag Strata into Applications, then open it. [Mac help](MAC.md).
|
||||
- **Mac:** open `Strata-0.2.9-macos-arm64.dmg`, drag Strata into Applications, then open it. [Mac help](MAC.md).
|
||||
3. On Windows and Linux the installer has no engine inside. On first start the app shows the engine for your
|
||||
graphics card with its size and one **Download** button, with progress and resume. If you already downloaded the
|
||||
engine file, choose **I already have the file** and it installs without internet. The Mac file keeps its small
|
||||
|
||||
@@ -38,6 +38,11 @@ engine package. Otherwise [build the image yourself](DOCKER.md#build-it-yourself
|
||||
- **Linux**, for example Ubuntu 24.04, with Intel's GPU driver: the `xe` or `i915` kernel driver plus the compute runtime and
|
||||
Level Zero. On Ubuntu: `intel-opencl-icd libze1 libze-intel-gpu1`, or follow Intel's
|
||||
[client GPU guide](https://dgpu-docs.intel.com/driver/client/overview.html).
|
||||
- **The CPU and the model folder.** The CPU does not need AVX-512. A Ryzen 3000-5000 or an Intel Core 12th-14th generation runs the
|
||||
experts that do not fit on the card with AVX2 (measured on an A750 with a Ryzen 5 5600X, [INTEL.md](INTEL.md)). This applies to
|
||||
model folders that contain `native_experts.txt` (IQ3_XXS, IQ3_S, NVFP4, Unsloth). An older Q2_0 folder has no such file, and its
|
||||
CPU kernel needs AVX-512. With that folder the engine stops with "this CPU cannot run the expert kernel", and `strata doctor`
|
||||
warns about it before the start. Building the model folder again with `strata setup` (or `tools/iq_pack.py`) fixes it.
|
||||
- For a source build without Docker: **Intel oneAPI** with the DPC++ compiler (`icpx`, 2025.3 or newer) and **oneMKL**
|
||||
(about 5 GB), `cmake` 3.24+, `ninja`, `git`, Python 3. The build fetches ggml unless you point `STRATA_GGML_DIR` at a
|
||||
llama.cpp checkout.
|
||||
@@ -121,6 +126,13 @@ Things that matter on an Arc (details in [INTEL.md](INTEL.md)):
|
||||
- `--layer-split auto` on two equal cards takes the split that leaves the fullest card the most VRAM to spare, within 0.2% of
|
||||
the best predicted time (`STRATA_SPLIT_HEADROOM_TOL`). `STRATA_SPLIT_HEADROOM=0` turns that off. A full card under `xe` moves
|
||||
buffers to system memory instead of failing. The startup line names both splits when the choice moves.
|
||||
- **Two Arc B-series cards decode faster by default.** With exactly two B-series cards (B50, B60, B65, B70, B570, B580) in a layer
|
||||
split, the first card starts the next window while the second still checks this one (`--pipeline-windows 2`), and setup writes
|
||||
`--spec 2` for the pair. On 2x Arc Pro B65 this decodes 17% to 23% faster than the old serial `--spec 4` setup, 5 pairs out of 5:
|
||||
chat 71-74 tok/s against 60-63, JSON 80-83 against 67-68, code 72-78 against 59-65, plain text 67-68 against 56-57. The answers are
|
||||
those of a serial `--spec 2` run (100 of 100 agent requests); a serial `--spec 4` run differs in a rare near-tie (3 of 100).
|
||||
One card, an A-series card, a mixed pair and three or more cards run as before. `--pipeline-windows 0` turns it off, and a
|
||||
`--spec` of your own, put in the config's `user_args`, is kept.
|
||||
- **Arc Pro B60:** PCI id `8086:e211` (`lspci -nn`), `sycl-ls` prints `Intel(R) Arc(TM) Pro B60 Graphics 20.1.0`, so
|
||||
`-DSTRATA_SYCL_AOT=bmg-g21` is the right target. `setup_intel.py` knows both B60 ids, `e211` and `e221`. On a `--layer-split`
|
||||
the startup line "N experts are neither in VRAM nor mirrored" counts the other card's layers; read the following
|
||||
@@ -149,6 +161,7 @@ Things that matter on an Arc (details in [INTEL.md](INTEL.md)):
|
||||
| Community | One Arc Pro B60 24 GB | Coder IQ1_M with 4,042 of 12,288 experts mirrored in RAM: 11.9 tok/s decode | same |
|
||||
| Maintainers | Arc Pro B70 32 GB (xe), 0.1.40.2 and later | Flash-Next IQ3_S with 11.5k of 24.6k experts resident: 10-prompt gate passes, prompt 980-1,000 tok/s on 4K tokens, decode 30 (prose) to 41 (code) tok/s | [INTEL.md](INTEL.md#arc-pro-b70-with-a-model-that-does-not-fit-flash-next-iq3_s) |
|
||||
| Maintainers | Arc A750 8 GB (i915) | Flash-Next IQ3_XXS about 10-19 tok/s decode | [INTEL.md](INTEL.md#arc-a750-and-the-other-alchemist-cards) |
|
||||
| Maintainers | 2x Arc Pro B65 32 GB (xe), 0.2.8 | Flash-Next IQ3_S, every expert in VRAM, the JIT package build: decode 71-83 tok/s (chat, JSON, code), prompts 1,261 / 1,511 / 1,628 tok/s at 4K / 8K / 16K | the bullet above |
|
||||
|
||||
Two cards on `xe` (2x Arc Pro B70, IQ3_S, 0.1.44): `--layer-split auto` picked a split at about 11 tok/s where
|
||||
`--layer-split 30` ran about 54. The auto choice now prefers the split with the most VRAM headroom (above).
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@
|
||||
This page is for people with an Apple Silicon Mac (M1 or newer). Strata runs there on its own Metal engine: the NVIDIA kernels
|
||||
are replaced by Metal ones (`metal/`), and the host code and the server are the PC's. Intel Macs cannot run it.
|
||||
|
||||
DeepSeek V4.1-Flash does not run on a Mac yet. It runs in the Terminal on NVIDIA RTX 30 and 50 cards and on the Radeon AI PRO
|
||||
R9700 ([MODELS.md](MODELS.md#deepseek-v41-flash)).
|
||||
DeepSeek V4.1-Flash runs on a Mac with 64 GB of memory or more, as text only: [DEEPSEEK_MAC.md](DEEPSEEK_MAC.md). It has its
|
||||
own engine, separate from the Qwen one described on this page.
|
||||
|
||||
> **On this page:** [What you need](#what-you-need) · [Install](#install) · [Defaults](#defaults-and-options) ·
|
||||
> [Gyro](#gyro-s-and-gyro-m-agentionais-trellis-experts) · [Measured](#measured-m5-max-128-gb) · [Limits](#limits-this-version) ·
|
||||
|
||||
+3
-2
@@ -71,11 +71,12 @@ you have the disk. Perplexity is measured on a fixed set of 4,995 test texts; lo
|
||||
| NVIDIA RTX 30, 40 and 50 series, 12 GB or more | Windows, Linux |
|
||||
| AMD Radeon AI PRO R9700 | Linux |
|
||||
| AMD Ryzen AI Max (Strix Halo, gfx1151, 128 GB unified; 64 GB or more shared with Windows) | Windows |
|
||||
| Apple Silicon Mac, 64 GB or more, macOS 15 or newer (text only, [DEEPSEEK_MAC.md](DEEPSEEK_MAC.md)) | macOS |
|
||||
|
||||
Tested on an RTX 3060 (Linux), an RTX 5070 (Windows), four R9700 cards (Linux) and a Ryzen AI Max+ 395 with 128 GB unified (Windows:
|
||||
14 to 16 tokens a second on code with the default Faster mode, 10 to 11 on chat, 78 tokens a second on a 4K prompt and 109 on
|
||||
8K). The RTX 30 kernels were also checked on an RTX A4000. Windows on RTX 30 and the RX 9070 are built but were not run. Mac
|
||||
and Strix Halo on Linux do not run it yet; on Strix Halo Windows the graphics memory is the RAM, so most of the experts are
|
||||
8K). The RTX 30 kernels were also checked on an RTX A4000. Windows on RTX 30 and the RX 9070 are built but were not run. On a
|
||||
Mac it was run on an M5 Max with 128 GB ([DEEPSEEK_MAC.md](DEEPSEEK_MAC.md)). Strix Halo on Linux does not run it yet; on Strix Halo Windows the graphics memory is the RAM, so most of the experts are
|
||||
read from the SSD and an NVMe drive matters.
|
||||
|
||||
RTX 40 cards are covered by the engine packages since 0.2.7 (sm_89 code); we have not run one ourselves, see [DEEPSEEK41.md](DEEPSEEK41.md#the-engine-package) for a user's measurement.
|
||||
|
||||
@@ -181,6 +181,9 @@ tokens. The tokens are the serial loop's, up to CPU rounding.
|
||||
- **Default:** on for AMD RDNA4 (gfx1200 / gfx1201) with `--serve`, the draft layer, and a split of two or more stages when the
|
||||
drafter can sit on the first card, otherwise three or more. The log says "is on by default for a layer split of N stages".
|
||||
`--pipeline-windows 0` or `STRATA_PIPELINE_AUTO=0` turns it off. NVIDIA builds keep it opt-in (`--pipeline-windows 2`).
|
||||
The Intel Arc engine turns it on for exactly two Arc B-series (Battlemage) cards, with `--serve` and the draft layer (`--spec 2`
|
||||
or more): on 2x Arc Pro B65 decode is 17% to 23% faster, and setup writes `--spec 2` for those cards ([INTEL_ARC.md](INTEL_ARC.md)).
|
||||
It runs exactly two cards, so one card, Arc A-series cards and three or more cards stay serial.
|
||||
- **Draft layer placement:** with the pipeline on, the draft layer loads on the first card, whose window has finished when the
|
||||
chain starts. `STRATA_MTP_STAGE0=0` keeps the old place.
|
||||
- **Off, with one log line saying why:** `--batch` slots, `--peer-device`, the helper caches (`--expert-cache-device1..3`, which
|
||||
|
||||
+5
-3
@@ -87,9 +87,11 @@ The choice is kept in the model's config (`"cuda": 12`). Starts and `UPDATE.bat`
|
||||
Setting the model up again chooses again (add `--cuda 12` to keep a forced choice). A Pascal or Volta card added to a model at a
|
||||
start (`--gpus`) moves that model to the CUDA 12 engine.
|
||||
|
||||
`--cuda 12` is also the way to run Strata with an NVIDIA driver older than 580. The CUDA 12 engine needs driver 528 or newer on
|
||||
Windows and 525 on Linux (`CUDA12_MIN_DRIVER` in `setup.py`); setup's "driver too old" stop says so. The Docker `cuda12` image
|
||||
asks for 575 ([DOCKER.md](DOCKER.md)). Drivers that old were not tested here.
|
||||
`--cuda 12` is also the way to run Strata with an NVIDIA driver older than 580. The oldest driver the CUDA 12 engine runs on is the
|
||||
`min_driver` of its package (575.51 or newer at the moment, as in the Docker `cuda12` image, [DOCKER.md](DOCKER.md)). Setup reads it from
|
||||
the installed package or the signed update feed and says it in the "driver too old" stop; `strata update` enforces the same number. NVIDIA's
|
||||
own table lets CUDA 12 programs start on older drivers (525 on Linux, 527 on Windows), but the packages are built with CUDA 12.9 and were
|
||||
not tested on such drivers, so Strata does not promise that range.
|
||||
|
||||
### Mixed cards
|
||||
|
||||
|
||||
+10
-2
@@ -23,8 +23,8 @@ Use the [current release](https://github.com/Niko1221/Strata/releases/latest). Y
|
||||
|
||||
| System | What to install | Engine variant |
|
||||
| --- | --- | --- |
|
||||
| Linux | `Strata-0.2.7-linux-x86_64.AppImage` (it downloads the engine on first start), or `strata launch` | `linux-x86_64-hip-halo` |
|
||||
| Windows | `Strata-0.2.7-windows-x64-setup.exe` (it downloads the engine on first start), or `strata launch` | `windows-x86_64-hip-rdna3` |
|
||||
| Linux | `Strata-0.2.9-linux-x86_64.AppImage` (it downloads the engine on first start), or `strata launch` | `linux-x86_64-hip-halo` |
|
||||
| Windows | `Strata-0.2.9-windows-x64-setup.exe` (it downloads the engine on first start), or `strata launch` | `windows-x86_64-hip-rdna3` |
|
||||
|
||||
Packaged engines carry the matching hipBLASLt tuning table for gfx1151 and select it on their own. Setup recognizes `gfx1151` and
|
||||
PCI ID `1002:1586`; other integrated Radeon chips have separate support limits. Unified memory is counted once. Setup does not
|
||||
@@ -214,6 +214,14 @@ export STRATA_PREFILL_STREAM_MIN=128 # prompt reads of 128-1,023 tokens on the
|
||||
integrated Radeon: `STRATA_IGPU_PROMPT_SET=1` ([AMD_HIP.md](AMD_HIP.md), "Integrated GPUs"), measured on a Radeon 780M only, where
|
||||
it is the default. `STRATA_PF_FUSED`, `STRATA_PF_SWITCH_MIN_T` and `STRATA_PREFILL_STREAM_MIN` stay opt-in as well.
|
||||
|
||||
**On Windows the Strix Halo is the exception** (engine 0.1.52 and later): `gfx1151` takes the matrix-core set by itself -
|
||||
`STRATA_HIP_WMMA`, `STRATA_PF_GEMM`, `STRATA_PA_FAST`, `STRATA_HC_UPMIX` and `STRATA_PF_FUSED`, without `STRATA_SELECT_WMMA`
|
||||
(x1.003 there) - and the prompt keeps its own buffers instead of lending expert-cache slots. Measured on Qwen3.8-Flash-Next
|
||||
IQ3_S, prompts of 4K to 64K: x1.59 to x1.72 over 0.2.7. The whole block is off with `STRATA_GFX1151_DEFAULTS=0`; the set with
|
||||
`STRATA_IGPU_PROMPT_SET=0`, the buffers with `STRATA_PREFILL_BORROW=1`
|
||||
([STRIX_HALO_WINDOWS.md](STRIX_HALO_WINDOWS.md#4-what-is-on-by-default)). Linux `gfx1151` keeps the opt-in above until it is
|
||||
measured there.
|
||||
|
||||
| Switch | What it does |
|
||||
| --- | --- |
|
||||
| `STRATA_PREFILL_STREAM_MIN=N` | `STRATA_PF_FUSED=1` reaches only chunks of N tokens or more (1,024 by default, the floor measured on a discrete card). With every expert in unified memory nothing streams and MMQ is the slow part, so a smaller N (128) speeds up an agent's turn (a tool result of about 1,000 tokens on a cached conversation: 3.1 to 6.0 s at 1,024 against 0.9 to 2.5 s at 128). |
|
||||
|
||||
@@ -85,6 +85,23 @@ environment and the package's `rocm/bin` folder on the library path.
|
||||
about 9% faster, near-tie tokens can differ); `STRATA_HC_Q8=0` keeps the old read.
|
||||
- The hipBLASLt table for `gfx1151` ships in the package and is chosen without a setting (`prefill gemm: hipBLASLt tuning enabled (32 rows,
|
||||
gfx1151, version 100500)` in the log). A hand-built engine has to be pointed at it: [AMD_HIP.md](AMD_HIP.md).
|
||||
- **The matrix-core prompt set is on by default on `gfx1151` on Windows** (engine 0.1.52 and the build after it): `STRATA_HIP_WMMA`,
|
||||
`STRATA_PF_GEMM`, `STRATA_PA_FAST`, `STRATA_HC_UPMIX` and `STRATA_PF_FUSED`. It is the set that has been on the Radeon 780M since
|
||||
021-minipc, and the start line lists it (`integrated GPU (gfx1151): the matrix-core prompt set is on ...`). Prompts on Qwen IQ3_S:
|
||||
4K 688.8 -> 1,192.8 tok/s, 8K 787.0 -> 1,283.5, 12K 750.4 -> 1,256.5, 32K 775.4 -> 1,245.1, 64K 748.5 -> 1,188.9 (x1.59 to x1.72;
|
||||
interleaved pairs, one engine at a time). The answers differ in the last bits: the first-token KL against the 0.2.7 default is
|
||||
0.0014 mean over four prompts of 4K to 24K, top-1 the same everywhere, and the measured noise floor is 0.00000 - against the
|
||||
accepted `HC_Q8` bar of 0.0228 mean. `STRATA_IGPU_PROMPT_SET=0` turns it off, `STRATA_SELECT_WMMA=1` adds the block scorer back
|
||||
(it measured x1.003), a single switch can be turned off on its own (`STRATA_HC_UPMIX=0`).
|
||||
- **The prompt keeps its own buffers instead of lending expert-cache slots** (`--no-prefill-borrow`, engine 0.1.52 and after, on
|
||||
`gfx1151` Windows only). A lent slot's expert has to be read back from the host arena during every prompt chunk, and on this PC
|
||||
that copy runs at about 0.5 GB/s (the arena is not registered with the driver in UMA mode): 823 slots took 2.75 s of a 3.3 s 4K
|
||||
prompt inside the copy thread. With its own buffers every expert stays in the cache (`experts streamed 0 ... resident 24576` in the
|
||||
log) and prompts are 8.5% (4K) to 8.9% (8K) faster. `STRATA_PREFILL_BORROW=1` brings lending back. `--prefill auto` keeps a
|
||||
16,384-token chunk on this PC instead of falling back to 2,048: the prompt reads every expert once per chunk, so a bigger chunk reads
|
||||
them fewer times (32K 1,250.6-1,260.0 -> 1,286.3-1,307.9 tok/s, 64K 1,188.4-1,191.0 -> 1,234.3-1,238.2; at 4K and 8K the chunk is
|
||||
the prompt itself and nothing changes). `STRATA_GFX1151_CHUNK=0` keeps 6,144, and `--prefill auto:32768` is another ~2% at 64K for
|
||||
about 12.8 GiB of prompt buffers.
|
||||
- `--mmap-experts` is not needed. The first Windows guide found that without it the pinned expert arena used up the graphics memory
|
||||
budget and decode fell to 28 tok/s on engine 0.1.41; the engine on this page sizes the cache from the shared pool and reaches every slot.
|
||||
- DeepSeek runs in "Faster" mode here: 5 of 6 experts per word. The app and `strata launch` say so; the quality change is small.
|
||||
@@ -134,3 +151,5 @@ python tools/strata_bench.py --url http://127.0.0.1:8080 --runs 3 --long
|
||||
|
||||
`--long` adds the 32K prompt. For DeepSeek add `--context 64k` to `strata launch` first; the default 8K context skips the 16K prompt and
|
||||
the agent session.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ strata launch --status
|
||||
**I cannot find `START-HERE.bat` or `SETUP.bat`.**
|
||||
Open the installed Strata app, or run `strata launch`. Those wrappers are in the source repository and are not the
|
||||
startup path for the app. For a fresh desktop install, download the one installer for your system
|
||||
(`Strata-0.2.7-windows-x64-setup.exe`, `Strata-0.2.7-linux-x86_64.AppImage` or `Strata-0.2.7-macos-arm64.dmg`), not a
|
||||
(`Strata-0.2.9-windows-x64-setup.exe`, `Strata-0.2.9-linux-x86_64.AppImage` or `Strata-0.2.9-macos-arm64.dmg`), not a
|
||||
`strata-engine-…` archive.
|
||||
|
||||
**`strata` is not found.**
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// include/strata/core/auto_cache.hpp - the slot count of the automatic expert cache (--expert-cache auto), as a rule with a test.
|
||||
//
|
||||
// The auto cache is sized from the VRAM free after the weights, the session and the drafter, minus --vram-reserve-mib (the reserve is what
|
||||
// the operator wants LEFT once everything is loaded), the draft head that binds later (priced), the prompt path's own buffers when it does
|
||||
// not borrow cache slots (priced) and the second verifier of --pipeline-windows (priced). What was never priced is what the load allocates
|
||||
// AFTER the cache whatever the settings: the CUDA libraries the prompt path and the verifier warm up (cuBLAS handles and workspaces, the
|
||||
// lazily loaded modules) and the verifier's small buffers. Measured with STRATA_TRACE=1 on the same build, config and flags
|
||||
// (--vram-reserve-mib 700, 256K context, MTP, 4 drafts):
|
||||
// RTX A4000 16 GB (z440, IQ3_XXS): 883 MiB free after the cache -> 719 after the head and the prompt path (-164) -> 461 after the
|
||||
// verifier and the drafter's binding (-258 = 178 MiB draft head, priced, + 80) = 244 MiB unpriced
|
||||
// RTX A6000 48 GB (boston, IQ3_S): 919 -> 755 (-164) -> 459 (-296 = 213 MiB draft head, priced, + 83) = 247 MiB unpriced
|
||||
// --prefill 512 / 2048 / auto on the A4000: the same 164 + 80. v0.2.6 ends at 461 MiB on the same card: not new, but every 16 GB card with
|
||||
// the default 700 MiB ends under the 512 MiB line the engine itself warns about (#2065, #4474), and the warning cannot be cured by the
|
||||
// operator raising the reserve by the advice's figure without losing the cache it was meant to keep.
|
||||
// So the reserve is now honest: the cache leaves `reserve + kLateAllocMib`, and the card ends with about the reserve.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace strata::core {
|
||||
|
||||
/// MiB the load allocates after the cache that no setting prices (see above; measured 244-247, a little margin).
|
||||
constexpr int64_t kLateAllocMib = 256;
|
||||
|
||||
struct AutoCacheSlots {
|
||||
int64_t slots = 0; ///< the cache's slot count (of the largest blob), >= 0
|
||||
bool late_priced = false; ///< kLateAllocMib was left out of the cache
|
||||
};
|
||||
|
||||
/// `free_b`: VRAM free now; `reserve_b`: the reserve and everything else that is priced (bytes); `blob`: the largest slot; `min_slots`: the least a working
|
||||
/// cache needs (#496). `late_mib` 0 turns the late allocation off (STRATA_DEFAULTS=026). The late allocation is only left out when the cache still has
|
||||
/// `min_slots` after it: a small card keeps the sizing it had (its reserve is lowered further elsewhere, #496), never a smaller cache than it can work with.
|
||||
inline AutoCacheSlots auto_cache_slots(int64_t free_b, int64_t reserve_b, int64_t blob, int64_t min_slots, int64_t late_mib = kLateAllocMib) {
|
||||
AutoCacheSlots r;
|
||||
if (blob <= 0) return r;
|
||||
const int64_t plain = (free_b - reserve_b) / blob;
|
||||
const int64_t late = late_mib > 0 ? (free_b - reserve_b - (late_mib << 20)) / blob : plain;
|
||||
if (late_mib > 0 && late >= min_slots && late > 0) {
|
||||
r.slots = late;
|
||||
r.late_priced = true;
|
||||
} else {
|
||||
r.slots = plain > 0 ? plain : 0;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace strata::core
|
||||
@@ -465,6 +465,33 @@ public:
|
||||
superseded_ += dropped;
|
||||
return dropped;
|
||||
}
|
||||
// A batch slot's chain holds one checkpoint (the one its admission copied in), while the main session's parked copy of the same
|
||||
// conversation keeps the whole chain - the root checkpoint at the end of an agent's system prompt and tools among them, the one its
|
||||
// next conversations borrow (borrows()). Parking the slot (#2526) would drop that copy as superseded (#342: the slot's chain holds its
|
||||
// deepest checkpoint) and the root with it: every sibling then reads the shared prefix again (#4420, 0.2.6 -> 0.2.7: 323 ms -> 4130 ms
|
||||
// for a 5 K-token prefix). True when drop_superseded(ids, images, checkpoints, cvec) would drop an entry that has a checkpoint the
|
||||
// given chain does not hold. Nothing is changed.
|
||||
bool supersedes_richer(const std::vector<int32_t>& ids, const std::vector<ConversationImageKey>& images,
|
||||
const std::vector<ConversationCheckpoint>& checkpoints, bool cvec) const {
|
||||
auto held = [&](const ConversationCheckpoint& c) {
|
||||
if (c.ids == ids && c.imgs == images) return true;
|
||||
for (const auto& k : checkpoints)
|
||||
if (k.ids == c.ids && k.imgs == c.imgs) return true;
|
||||
return false;
|
||||
};
|
||||
for (size_t i = 0; i < entries_.size(); ++i) {
|
||||
const auto& e = entries_[i];
|
||||
const ConversationCheckpoint* deepest = nullptr;
|
||||
for (const auto& c : e.checkpoints)
|
||||
if (!deepest || c.ids.size() > deepest->ids.size()) deepest = &c;
|
||||
if (e.cvec != cvec || !deepest || deepest->ids.empty() || !held(*deepest) ||
|
||||
(guarded_ != 0 && serials_[i] == guarded_))
|
||||
continue;
|
||||
for (const auto& c : e.checkpoints)
|
||||
if (!held(c)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
size_t superseded() const { return superseded_; }
|
||||
/// --conversation-cache-park-gain N: the longest parked entry this conversation's outgoing state supersedes, in
|
||||
/// tokens, 0 when it has none. It is the entry drop_superseded() would remove, so the caller can tell whether a
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// include/strata/core/defaults_026.hpp - STRATA_DEFAULTS=026: the master switch of the Qwen engine's 0.2.7 defaults.
|
||||
//
|
||||
// Every default the Qwen engine (`strata`) changed since v0.2.6 goes back to what v0.2.6 did when STRATA_DEFAULTS=026 is in the
|
||||
// environment (the app's "off" for its speed switches sends it): the residency-biased routing (STRATA_ROUTE_PRIOR), the wide
|
||||
// environment (the app's "off" for its speed switches sends it): the wide
|
||||
// hyper-connection up read (STRATA_GR_UP_WIDE), the stream-split hyper-connection read (STRATA_GR_V3), the int8 prompt attention
|
||||
// (STRATA_PROMPT_ATTN_IMMA), the four gfx12 decode switches of the arch table, the PLE residency policy (STRATA_PLE_AUTO_RAM), and the
|
||||
// gfx12 decode miss split of 0.15 (back to 0.55). KVA is off by default in 0.2.7 and stays whatever STRATA_KVA says.
|
||||
// The residency-biased routing (STRATA_ROUTE_PRIOR) was in this set in 0.2.7; it is off by default again (owner, 2026-10-29), so the master switch
|
||||
// has nothing to take back there, and route_prior_default() answers 0 with the switch on or off.
|
||||
// An explicit per-feature variable (STRATA_ROUTE_PRIOR=2, STRATA_GR_V3=1, STRATA_PLE_AUTO_RAM=1, ...) or flag (--pcie-frac, --ple-io)
|
||||
// still wins over the master switch. Each rule is a pure function that takes the answer of qwen_defaults_026() as its `v026`
|
||||
// argument, so a test can check both settings without touching the environment (src/core/arch_defaults_test.cpp,
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#include <cstddef>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
@@ -94,6 +95,16 @@ bool host_available_memory(HostMemory& m, const std::string& meminfo = "/proc/me
|
||||
const std::string& self_cgroup = "/proc/self/cgroup",
|
||||
const std::string& cgroup_root = "/sys/fs/cgroup");
|
||||
|
||||
/// #4051: wait for the RAM an arena needs to come back. An engine that was still exiting (or a program that was ending)
|
||||
/// holds memory a few seconds longer than its process; loading into that shortfall sent a 60 GiB host into reclaim for
|
||||
/// good. Calls `sample(available)` (false: the RAM cannot be read; the wait ends) every `step_s` seconds, through
|
||||
/// `sleep(step_s)`, until `available >= need` or `max_wait_s` seconds have gone. `waited` gets the seconds waited,
|
||||
/// `available` the last reading. True when the RAM is there. `progress(waited, available)` is called about every
|
||||
/// `note_s` seconds while waiting. `max_wait_s <= 0` is no wait at all.
|
||||
bool wait_for_ram(uint64_t need, double max_wait_s, double step_s, double note_s,
|
||||
const std::function<bool(uint64_t&)>& sample, const std::function<void(double)>& sleep,
|
||||
const std::function<void(double, uint64_t)>& progress, double& waited, uint64_t& available);
|
||||
|
||||
/// Bound a resident budget by RAM and commit capacity after headroom; leave 256 MiB more when clamping.
|
||||
/// Pass UINT64_MAX for commit when the platform does not report it.
|
||||
uint64_t clamp_resident_budget(uint64_t requested, uint64_t physical, uint64_t commit, uint64_t headroom);
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
//
|
||||
// STRATA_SM86_PERF=0 turns the sm_86 table off (cards of compute capability 8.6: RTX 3050-3090, RTX A2000-A6000)
|
||||
// STRATA_SM60_PERF=0 turns the sm_60 table off (Pascal GP100: Tesla P100)
|
||||
// STRATA_SM121_PERF=0 turns the sm_121 table off (GB10: NVIDIA DGX Spark; new in 0.2.7.1, also off under STRATA_DEFAULTS=026). Its
|
||||
// switches are measured speed settings, not bit-exact ones: the fused prompt kernels and the FP4-activation NVFP4
|
||||
// prompt round differently from the paths they replace (24 of 24 greedy answers identical; no KL run yet).
|
||||
//
|
||||
// A switch the user set (to anything, including 0) is never overridden. Applied once at start, before any engine code reads its
|
||||
// switches; the table is used only when every visible CUDA device has the same compute capability.
|
||||
@@ -22,6 +25,8 @@ namespace strata::core {
|
||||
/// The environment settings that are the defaults of a card with compute capability major.minor (raw, as the driver reports it);
|
||||
/// empty for every other card and when its off switch is 0.
|
||||
std::vector<std::pair<std::string, std::string>> nv_default_env(int major, int minor);
|
||||
/// The same with the answer of STRATA_DEFAULTS=026 given (the sm_121 table is empty then), so a test can check both without the environment.
|
||||
std::vector<std::pair<std::string, std::string>> nv_default_env(int major, int minor, bool defaults026);
|
||||
|
||||
/// Sets `nv_default_env(major, minor)` in this process's environment where the user has not set the variable, prints one line to
|
||||
/// stderr when it set anything, and returns the names it set.
|
||||
|
||||
@@ -90,6 +90,9 @@ struct PinnedArena {
|
||||
std::vector<uint64_t> slice_starts;
|
||||
void* mapping_base = nullptr; ///< actual mapping start; differs from base when a shared-file header exists
|
||||
uint64_t mapping_bytes = 0; ///< bytes to release from mapping_base
|
||||
/// shared-file arena only: the load state word in the file's header (0: nobody has loaded it, a pid: that engine is
|
||||
/// loading it, ~0: it is complete). Shared by every engine that maps the file; null for a private arena.
|
||||
std::atomic<uint64_t>* shared_state = nullptr;
|
||||
~PinnedArena();
|
||||
PinnedArena(const PinnedArena&) = delete;
|
||||
PinnedArena& operator=(const PinnedArena&) = delete;
|
||||
|
||||
@@ -34,4 +34,15 @@ inline bool ple_auto_ram_allowed(const char* env_value, bool v026) {
|
||||
return !v026;
|
||||
}
|
||||
|
||||
/// #4518: the table was locked at start on the policy's word (table + arena + headroom fit), but what the rest of the engine takes after
|
||||
/// that (the KV pools, the pinned staging, the GPUs' host buffers) is not known then. On a 128 GB two-GPU Windows PC it left less than the
|
||||
/// 5 GiB the automatic conversation cache needs, and the cache turned itself off. The cache is worth more than the lock (parking a 100 K
|
||||
/// conversation is 1.1 s against 30 s to read it again, the lock was measured at 2-3% of prompt speed), so once the cache is to be sized:
|
||||
/// does giving the lock back make the automatic cache bigger? `available` is the RAM the cache sees now (the table locked), the table's
|
||||
/// pages count as available again once unlocked (clean file pages). Unknown telemetry: no.
|
||||
inline bool ple_lock_blocks_cache(std::optional<uint64_t> available, uint64_t table_bytes, int64_t min_free_mib) {
|
||||
if (!available || table_bytes == 0 || *available > UINT64_MAX - table_bytes) return false;
|
||||
return auto_conversation_cache_mib(*available + table_bytes, min_free_mib) > auto_conversation_cache_mib(available, min_free_mib);
|
||||
}
|
||||
|
||||
} // namespace strata::core
|
||||
@@ -49,16 +49,22 @@ inline bool sm120_default(int major, int minor) { return is_sm120(major, minor)
|
||||
// `total_bytes`: the card's memory (cudaDeviceProp::totalGlobalMem); `integrated`: a GPU that shares the PC's memory.
|
||||
constexpr double kSmallCardGiB = 13.0; // "a 12 GB card": 12 GB cards report 11.6 - 12.0 GiB, 16 GB ones 15.6 - 16.0
|
||||
|
||||
/// The residency-bias bonus a discrete CUDA card of 13 GiB or less gets when the user set nothing: THE one place the value lives. 1.5 since the
|
||||
/// owner decision of 2026-10-26 (it was 2.0 in the first 0.2.7 candidates): on the RTX 3060 12 GB with IQ3_XXS the copy and code check found real
|
||||
/// defects at 2.0 and none in 19 tasks at 1.5 or 1.0, and 1.5 keeps the speed (story +15%, code +37% against off). A longer 61-task check of
|
||||
/// 0 / 1.5 / 2.0 is still running; if it shows defects at 1.5 this falls back to 1.0.
|
||||
/// The residency-bias bonus the user can ask for on a discrete CUDA card of 13 GiB or less (STRATA_ROUTE_PRIOR=1.5; the app's Expert-settings row
|
||||
/// "Faster answers on 12 GB NVIDIA cards" writes it): THE one place the value lives. 1.5 since the owner decision of 2026-10-26 (2.0 had real defects in
|
||||
/// the copy and code check on the RTX 3060 12 GB with IQ3_XXS; none in 19 tasks at 1.5 or 1.0). Speed against off: story +15%, code +37%.
|
||||
/// It was the default on those cards in 0.2.7 only. Owner decision of 2026-10-29: OFF by default again (#4402: repetition loops and weaker results in long
|
||||
/// agent sessions; our own 0.2.7 agent check passed 4.5 of 10 tasks with it on against 5.6 with it off). It stays an opt-in.
|
||||
constexpr float kRoutePriorSmallCard = 1.5f;
|
||||
|
||||
/// STRATA_ROUTE_PRIOR when the user set nothing: kRoutePriorSmallCard (residency-biased routing, changes which experts run; owner-approved for 12 GB
|
||||
/// cards) on a discrete CUDA card of 13 GiB or less, 0 everywhere else. Measured on an RTX 3060 12 GB only.
|
||||
inline float route_prior_default(bool hip, bool integrated, double total_bytes, bool v026 = false) {
|
||||
return !v026 && !hip && !integrated && total_bytes > 0.0 && total_bytes / 1073741824.0 <= kSmallCardGiB ? kRoutePriorSmallCard : 0.0f;
|
||||
/// STRATA_ROUTE_PRIOR when the user set nothing: 0 on every card (residency-biased routing changes which experts run, so it is only on when asked for:
|
||||
/// STRATA_ROUTE_PRIOR=1.5, or the Expert-settings row). The same answer as v0.2.6 and as 0.2.7 with STRATA_ROUTE_PRIOR=0 or STRATA_DEFAULTS=026.
|
||||
/// The arguments are kept so a caller and the table test (arch_defaults_test) still ask the same question per card; none of them changes the answer.
|
||||
inline float route_prior_default(bool /*hip*/, bool /*integrated*/, double /*total_bytes*/, bool /*v026*/ = false) { return 0.0f; }
|
||||
|
||||
/// The lambda the engine uses given the text of STRATA_ROUTE_PRIOR (nullptr = unset): any set value is the user's own and wins on every card
|
||||
/// (atof; "0" and text that reads as 0 are off; "1.5" is what the Expert-settings row writes), unset is route_prior_default (0).
|
||||
inline float route_prior_resolve(const char* env, bool hip, bool integrated, double total_bytes, bool v026 = false) {
|
||||
return env != nullptr ? (float) std::atof(env) : route_prior_default(hip, integrated, total_bytes, v026);
|
||||
}
|
||||
|
||||
/// STRATA_GR_V3 (the stream-split hyper-connection read, another summation order) when the user set nothing: ON for a discrete compute capability
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "strata/core/layer.hpp"
|
||||
#include "strata/core/session.hpp"
|
||||
#include "strata/kernels/sampler.hpp"
|
||||
#include "strata/kernels/window_limits.hpp"
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
@@ -404,13 +405,13 @@ private:
|
||||
std::vector<SessionState*> slots_;
|
||||
bool batch_rec_ = false; ///< record_window is capturing a batch window
|
||||
int row_base_ = 0; ///< ... its hand-off rows start here (a pipeline group's own rows)
|
||||
int brow_[8] = {}; ///< ... and row t is slot brow_[t]
|
||||
int brow_[strata::kernels::kWindowMaxT] = {}; ///< ... and row t is slot brow_[t]
|
||||
bool last_batch_ = false; ///< the last run was a batch window (set_plan_slot: one group)
|
||||
std::map<std::vector<int>, cudaGraphExec_t> exec_bm_, commit_bm_; ///< full row layout avoids slot-ID collisions
|
||||
std::map<std::vector<int>, uint64_t> bm_used_; ///< last use of each captured layout (LRU)
|
||||
uint64_t bm_tick_ = 0;
|
||||
size_t batch_graph_limit_ = 0; ///< 0: keep every captured batch graph (0.1.39); N: LRU-evict beyond N layouts
|
||||
int last_rows_[8] = {}; ///< the slots of the last batch window's rows
|
||||
int last_rows_[strata::kernels::kWindowMaxT] = {}; ///< the slots of the last batch window's rows
|
||||
std::vector<int> bkey(const int* rows, int S, int hbase) const { // #871: the doorbell variant has its own graphs
|
||||
std::vector<int> k = batch_key(rows, S, hbase);
|
||||
k.push_back(ar_off_ ? 1 : 0);
|
||||
@@ -439,14 +440,14 @@ private:
|
||||
std::chrono::steady_clock::duration(b_last_.load(std::memory_order_acquire)));
|
||||
}
|
||||
std::chrono::steady_clock::time_point ext_checked_{}; ///< batch_poll's last check with external serving
|
||||
int32_t b_out_[8] = {};
|
||||
int32_t b_out_[strata::kernels::kWindowMaxT] = {};
|
||||
std::vector<strata::kernels::SamplerParams> slot_sp_;
|
||||
bool sample_rows(int S, std::string& err); ///< the sampled slots' rows of the last batch window
|
||||
int32_t* h_commitb_ = nullptr; int32_t* m_commitb_ = nullptr; // per slot [1, 0, pos, -1 ..], stride 2 + max_t
|
||||
int32_t* commitb_ = nullptr;
|
||||
float* tail_snap_b_ = nullptr; ///< per (slot, QSA layer) indexer tail snapshot
|
||||
void* arena_b_ = nullptr;
|
||||
int64_t last_pos_b_[8] = {};
|
||||
int64_t last_pos_b_[strata::kernels::kWindowMaxT] = {};
|
||||
bool capture_batch(const int* rows, int S, int hbase, std::string& err);
|
||||
// STRATA_PIPED_MTP (init_deferred_commit): the saved rows, laid out as the window scratch (global layer index) with
|
||||
// dc_rows_ rows per layer instead of max_t_; the slots' accepted prefixes (mapped and device, 2 + max_t_ per slot)
|
||||
@@ -534,7 +535,7 @@ private:
|
||||
unsigned* qcnt_ = nullptr; ///< S26 STRATA_QFUSE: the HC read's q8_1 group counters (n_embd / 32)
|
||||
unsigned long long* slot_off_d_ = nullptr; ///< E-6: the slot offsets on the device
|
||||
int64_t lb_ = 0, le_ = -1; ///< set_stage: the layers this verifier runs (-1: to the last)
|
||||
int handoff_rows_ = 0; ///< set_handoff_rows (0: kVerifyMaxT)
|
||||
int handoff_rows_ = 0; ///< set_handoff_rows (0: kVerifyNarrowT)
|
||||
const float* hand_in_ = nullptr;
|
||||
float* hand_out_ = nullptr;
|
||||
uint32_t* hflag_in_h_ = nullptr; ///< set_handoff_flags: the incoming hand-off's ready word (host / device)
|
||||
@@ -573,7 +574,7 @@ private:
|
||||
std::vector<unsigned long long> prof_h_;
|
||||
double prof_sum_[2][kProfPer] = {}; // [GDN / QSA layers][stage]
|
||||
int64_t prof_windows_ = 0;
|
||||
double prof_t_sum_[9][2][kProfPer] = {}; // the same per window size T (1..8), for profile_by_t_report
|
||||
double prof_t_sum_[9][2][kProfPer] = {}; // the same per window size T (1..8: wider windows are not profiled), for profile_by_t_report
|
||||
int64_t prof_t_win_[9] = {};
|
||||
bool rs_on_ = false; // STRATA_ROUTE_STATS
|
||||
unsigned long long* rs_dev_ = nullptr; // [9 T][n_layers][8 rows]: experts of row r that no earlier row of the window routed to
|
||||
@@ -589,7 +590,7 @@ private:
|
||||
float* ple_val_ = nullptr;
|
||||
int last_t_ = 0;
|
||||
int64_t last_pos0_ = 0;
|
||||
int32_t last_tokens_[8] = {};
|
||||
int32_t last_tokens_[strata::kernels::kWindowMaxT] = {};
|
||||
int64_t n_vocab_ = 0;
|
||||
cudaStream_t cs_ = nullptr;
|
||||
cudaStream_t sh_cs_ = nullptr;
|
||||
@@ -602,9 +603,10 @@ private:
|
||||
cudaEvent_t df_join_[2] = {};
|
||||
// [sel_long_][T]: 1 is the long-context QSA select (STRATA_SELECT_LONG=1: multi-CTA top-k, shuffle-free decode
|
||||
// scores; see run()); without it only [0] is ever captured
|
||||
cudaGraphExec_t exec_[2][9] = {};
|
||||
cudaGraphExec_t exec_nr_[2][9] = {}; // #871: the doorbell variant of a stage that is all-resident otherwise
|
||||
bool launched_[2][9] = {}, launched_nr_[2][9] = {}; ///< this graph has been launched once (early_ple_flag: a cold first launch raises the PLE flag first)
|
||||
// (per window size T = 1..kWindowMaxT: a solo window is at most 8 rows, but a merge window of a wide batch is not)
|
||||
cudaGraphExec_t exec_[2][strata::kernels::kWindowMaxT + 1] = {};
|
||||
cudaGraphExec_t exec_nr_[2][strata::kernels::kWindowMaxT + 1] = {}; // #871: the doorbell variant of a stage that is all-resident otherwise
|
||||
bool launched_[2][strata::kernels::kWindowMaxT + 1] = {}, launched_nr_[2][strata::kernels::kWindowMaxT + 1] = {}; ///< this graph has been launched once (early_ple_flag: a cold first launch raises the PLE flag first)
|
||||
int sel_long_ = 0;
|
||||
// fork 34a35db (STRATA_SIDE_MIXER=1): the GDN gates and the QSA indexer on a branch of the window's graph beside the
|
||||
// mixer's projections, joined before the recurrence / the attention
|
||||
@@ -672,7 +674,7 @@ private:
|
||||
void set_plan_slot(int grp);
|
||||
bool split_ = false; // opt-in (--spec-split): exact but slower, see the overlap study
|
||||
std::function<bool(cudaStream_t, std::string&)> pre_launch_; // set_pre_launch
|
||||
int groups_[9] = {};
|
||||
int groups_[strata::kernels::kWindowMaxT + 1] = {};
|
||||
float* h_ymiss_ = nullptr; float* m_ymiss_ = nullptr; // T * k * n_embd
|
||||
|
||||
// device
|
||||
|
||||
@@ -158,6 +158,11 @@ struct SetArgs {
|
||||
// their shared-memory tile anyway, so the same values come out; this only saves the F32 read of the same rows by
|
||||
// every column block of an expert (18-36 reads of 1.8-3.7 MB on a prompt) - MarLin's "small operand cheap".
|
||||
const uint16_t* x16 = nullptr;
|
||||
int skip_ge = 0; // (ds4's low-bit GGUF experts) > 0: experts of at least this many rows are the caller's (FP16 GEMMs)
|
||||
#if defined(STRATA_METAL) // (only on Metal: keeps the CUDA / HIP struct as it was)
|
||||
const int* hbounds = nullptr; // the same bounds on the host: [n_exp + 1], absolute rows
|
||||
int total_rows = 0; // the set's rows in all
|
||||
#endif
|
||||
};
|
||||
/// h[local row] = FP16 clamped swiglu of gate and up over x[row_idx[local row]] (F32 rows, ldx floats apart).
|
||||
void set_gate_up(const SetArgs& a, const float* x, int ldx, const int* row_idx, uint16_t* h16, int ldh, cudaStream_t s);
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
// must be different buffers (every block reads the former while one block writes the latter).
|
||||
#pragma once
|
||||
|
||||
#include "strata/kernels/window_limits.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace strata::kernels {
|
||||
@@ -66,6 +68,9 @@ void fused_gr_quantize_q8_0(const uint16_t* bf16, uint8_t* out, int64_t n_blocks
|
||||
/// weight pointers and eps must be the same for every t); `xn_scratch` is n_tok * hc * n_embd floats. Every
|
||||
/// token's outputs are bitwise `fused_gr_read(a[t])`.
|
||||
constexpr int kFusedGrMaxT = 8;
|
||||
/// A window of more than kFusedGrMaxT tokens is read in slices of at most kFusedGrMaxT (every token's outputs are
|
||||
/// strictly its own, so the slices are bitwise the one read); `a` and `xn_scratch` hold up to kFusedGrWideT tokens.
|
||||
constexpr int kFusedGrWideT = kWindowMaxT;
|
||||
/// Returns true when it also wrote the q8_1 images (every token's q8_mixed set and this read supports it).
|
||||
bool fused_gr_read_multi(const FusedGrArgs* a, int n_tok, float* xn_scratch, void* stream,
|
||||
unsigned long long* stamp_buf = nullptr, int stamp_i0 = 0);
|
||||
|
||||
@@ -205,6 +205,10 @@ public:
|
||||
bool is_open() const;
|
||||
/// True when `PleIoOptions::lock` was asked for and the OS lock succeeded (false: pages only pre-touched).
|
||||
bool locked() const;
|
||||
/// Gives a resident lock back (Mmap mode): the pages stay mapped and cached, but the OS may reclaim them again, so the RAM counts as
|
||||
/// available. False when the table was not locked. Used by the residency policy when the table's lock would leave the
|
||||
/// conversation cache no room (#4518).
|
||||
bool unlock();
|
||||
uint64_t rows() const;
|
||||
/// Bytes of the whole table (rows x row bytes) while open; 0 when closed. The residency policy in
|
||||
/// generate.cpp sizes the table with this before it decides to keep it in RAM.
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
// can keep one captured graph per layer with the maximum grid and a device-side count.
|
||||
#pragma once
|
||||
|
||||
#include "strata/kernels/window_limits.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace strata::kernels {
|
||||
@@ -100,6 +102,29 @@ void moe_hit_add(float* parts, const float* hit_out, const int32_t* dst, const i
|
||||
void moe_grouped_s2(const unsigned long long* grp_ptr, const int32_t* grp_start, const int32_t* n_groups,
|
||||
const int32_t* ent_dst, const int32_t* ent_tok, int64_t cap_groups, int64_t cap_entries,
|
||||
const uint8_t* x_q8_0, const float* x_scales, void* scratch, float* out, void* stream);
|
||||
#if STRATA_VERIFY_MAX_T > 8
|
||||
/// The same for a window of 9..16 rows (CUDA / HIP engines built with a wide window, window_limits.hpp): `max_group` is the
|
||||
/// most entries one group can hold = the window's rows (a token routes to an expert once). Up to 8 it is
|
||||
/// `moe_grouped_s2` (one pass of each kernel); 9..16 adds a second pass over the entries from the 9th on.
|
||||
void moe_grouped_s2_wide(const unsigned long long* grp_ptr, const int32_t* grp_start, const int32_t* n_groups,
|
||||
const int32_t* ent_dst, const int32_t* ent_tok, int64_t cap_groups, int64_t cap_entries,
|
||||
const uint8_t* x_q8_0, const float* x_scales, void* scratch, float* out, void* stream,
|
||||
int max_group);
|
||||
#endif
|
||||
/// What a verify window calls: `max_group` is the window's rows. A build with narrow windows (the default, and every SYCL / Metal
|
||||
/// build, whose kernel libraries have only `moe_grouped_s2`) runs `moe_grouped_s2`: its windows hold at most 8 rows.
|
||||
inline void moe_grouped_s2_window(const unsigned long long* grp_ptr, const int32_t* grp_start, const int32_t* n_groups,
|
||||
const int32_t* ent_dst, const int32_t* ent_tok, int64_t cap_groups, int64_t cap_entries,
|
||||
const uint8_t* x_q8_0, const float* x_scales, void* scratch, float* out, void* stream,
|
||||
int max_group) {
|
||||
#if STRATA_VERIFY_MAX_T > 8
|
||||
moe_grouped_s2_wide(grp_ptr, grp_start, n_groups, ent_dst, ent_tok, cap_groups, cap_entries, x_q8_0, x_scales, scratch, out,
|
||||
stream, max_group);
|
||||
#else
|
||||
(void) max_group;
|
||||
moe_grouped_s2(grp_ptr, grp_start, n_groups, ent_dst, ent_tok, cap_groups, cap_entries, x_q8_0, x_scales, scratch, out, stream);
|
||||
#endif
|
||||
}
|
||||
/// Plan v0.3 P6: the groups for `moe_grouped_s2` when every expert id is resident at base + id * blob (the MTP
|
||||
/// layer's 512 experts): counts[0] groups, counts[1] entries; entry dst = routing index, tok = index / k_per_tok.
|
||||
void moe_group_resident(const int32_t* ids, int n, int k_per_tok, const uint8_t* base, int64_t blob,
|
||||
|
||||
@@ -14,11 +14,15 @@
|
||||
// Everything that varies per window (token ids, `n_keep`) is read from DEVICE memory so the kernels can be captured.
|
||||
#pragma once
|
||||
|
||||
#include "strata/kernels/window_limits.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace strata::kernels {
|
||||
|
||||
inline constexpr int kVerifyMaxT = 8;
|
||||
/// The most rows one verify / batch window holds (8; 16 in the CUDA / HIP builds, window_limits.hpp). A window of up to
|
||||
/// kVerifyNarrowT rows runs the kernels it always ran.
|
||||
inline constexpr int kVerifyMaxT = kWindowMaxT;
|
||||
|
||||
/// For token t of T: conv over [history(3) | qkv_0 .. qkv_t] -> SiLU -> L2 norm of the q/k heads -> h[t].
|
||||
/// `history` is NOT written, but with `commit` (one token): it then keeps the token, as gdn_conv_commit. Bitwise
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// include/strata/kernels/window_limits.hpp - how many rows one verify / batch window can hold.
|
||||
//
|
||||
// Until 0.2.7 a window held at most 8 rows. The CUDA and HIP engines now hold up to 16 (four pipelined groups of 16
|
||||
// slots = 64 slots on a four-card split: docs/BATCHING.md), so every compile-time maximum that follows the window
|
||||
// (the verifier's per-row arrays, the expert pool's per-token tables, the resident plan's one-thread-per-entry block)
|
||||
// is derived from ONE number. It is a macro because the SYCL and Metal builds compile the same shared headers and
|
||||
// must not change: they get the default, 8. The CUDA / HIP CMake sets STRATA_VERIFY_MAX_T from the cache variable
|
||||
// STRATA_WINDOW_MAX_T (8 or 16; -DSTRATA_WINDOW_MAX_T=8 builds an engine whose windows are the 0.2.7 ones).
|
||||
//
|
||||
// The rule every user of the number follows: a window of up to kVerifyNarrowT rows runs EXACTLY the code it ran at 8
|
||||
// (the same kernel instantiations, the same buffer sizes); the wider code is reached only by a window of 9..16 rows.
|
||||
#pragma once
|
||||
|
||||
#ifndef STRATA_VERIFY_MAX_T
|
||||
#define STRATA_VERIFY_MAX_T 8
|
||||
#endif
|
||||
|
||||
namespace strata::kernels {
|
||||
|
||||
/// The window every 0.2.7 kernel was built for: windows of up to this many rows keep their instantiations.
|
||||
inline constexpr int kVerifyNarrowT = 8;
|
||||
/// The most rows one window can hold in this build (8 or 16).
|
||||
inline constexpr int kWindowMaxT = STRATA_VERIFY_MAX_T;
|
||||
|
||||
static_assert(kWindowMaxT == 8 || kWindowMaxT == 16, "STRATA_VERIFY_MAX_T is 8 or 16 (the wide kernels are built for 16)");
|
||||
|
||||
} // namespace strata::kernels
|
||||
@@ -0,0 +1,61 @@
|
||||
// include/strata/prefill/gdn_probe.hpp - the GDN recurrence dispatch, made observable and forceable by tests.
|
||||
//
|
||||
// strata::prefill::gdn_recurrence picks its kernel from environment switches that are read once per process. A
|
||||
// test or benchmark that links the real strata_prefill library installs a GdnProbe on its thread to
|
||||
// * read back which path ran for the last call (`selected`) and why a path that was asked for did not run
|
||||
// (`reason`), instead of guessing from timings, and
|
||||
// * force one path for the call (`force`): a forced path that is not eligible launches NOTHING and reports
|
||||
// kGdnNone plus the reason, so a harness can never time a silent fallback as if it were the candidate.
|
||||
// Without a probe (the engine) the dispatch is byte-for-byte what it was: one thread-local pointer read.
|
||||
// Header only; CUDA builds fill the probe (src/prefill/kernels.cu), other backends may adopt the same struct.
|
||||
#pragma once
|
||||
|
||||
namespace strata::prefill {
|
||||
|
||||
enum GdnPath : int {
|
||||
kGdnNone = -1, // out: nothing was launched
|
||||
kGdnAuto = 0, // in: the production choice (the environment switches decide)
|
||||
kGdnKeyHead = 1, // gdn_rec_kh_kernel: the three value heads of a key head in one thread (sm_80+, 64 blocks resident)
|
||||
kGdnPipe = 2, // gdn_rec_cols_pipe_kernel: the software-pipelined column kernel
|
||||
kGdnCols = 3, // gdn_rec_cols_kernel (STRATA_GDN_PIPELINE=0)
|
||||
kGdnChunked = 4, // STRATA_GDN_CHUNKED: FP32 chunked prep + scan (CUDA, C = 32)
|
||||
kGdnTc = 5, // STRATA_GDN_TC=1: chunked, scan on the tensor cores (CUDA sm_80+, 125 KB of shared memory a block)
|
||||
kGdnTcSplit = 6, // the same scan with v / T / P single-buffered: 98 KB, fits the 99-KB cards (sm_86, sm_89, sm_120)
|
||||
kGdnTcS2 = 7, // experiment: the split scan with 2 passes a product (lo.hi + hi.hi), not for production
|
||||
kGdnTcS1 = 8, // experiment: the split scan with plain f16 operands (1 pass), not for production
|
||||
kGdnPathCount = 9
|
||||
};
|
||||
|
||||
struct GdnProbe {
|
||||
// in
|
||||
int force = kGdnAuto; // kGdnAuto: dispatch as production does; otherwise only this path may run
|
||||
bool any_sms = false; // chunked: skip the 128-SM test, as STRATA_GDN_CHUNKED=2 does
|
||||
int tc_tiles = 0; // tcs: value tiles a head (1, 2; 0 = STRATA_GDN_TC_TILES, else automatic: 2 on cards with >= 96 SMs)
|
||||
int gsb = 0; // chunked: tokens per scratch super-block (a multiple of 32, at most 2048; 0 = 2048)
|
||||
// out (set by every gdn_recurrence call made on the thread that installed the probe)
|
||||
int selected = kGdnNone; // the path that launched
|
||||
int wanted = kGdnAuto; // the path that was asked for: `force`, or the first the environment enabled
|
||||
const char* reason = ""; // when selected != wanted: why the wanted path did not run
|
||||
long calls = 0;
|
||||
};
|
||||
|
||||
inline thread_local GdnProbe* g_gdn_probe = nullptr;
|
||||
inline void gdn_probe_install(GdnProbe* p) { g_gdn_probe = p; }
|
||||
inline GdnProbe* gdn_probe_get() { return g_gdn_probe; }
|
||||
|
||||
inline const char* gdn_path_name(int p) {
|
||||
switch (p) {
|
||||
case kGdnKeyHead: return "keyhead";
|
||||
case kGdnPipe: return "pipe";
|
||||
case kGdnCols: return "cols";
|
||||
case kGdnChunked: return "chunked";
|
||||
case kGdnTc: return "tc";
|
||||
case kGdnTcSplit: return "tcs";
|
||||
case kGdnTcS2: return "tcs2";
|
||||
case kGdnTcS1: return "tcs1";
|
||||
case kGdnAuto: return "auto";
|
||||
default: return "none";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace strata::prefill
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace strata::prefill {
|
||||
|
||||
@@ -92,6 +93,13 @@ private:
|
||||
void* side_ready_[2] = {};
|
||||
void* side_free_[2] = {};
|
||||
int side_cur_ = 0;
|
||||
// STRATA_PF_W16_CACHE_MIB=N (CUDA, opt-in): native() keeps the FP16 dequantization of up to N MiB of weights, so a
|
||||
// repeat product skips the dequant kernel; the FP16 values and the product are the uncached form's, bit for bit.
|
||||
// A weight is keyed by its device pointer and shape (the engine's dense weights never move).
|
||||
struct W16Entry { const void* p; int type; int64_t n, k; uint16_t* d; };
|
||||
std::vector<W16Entry> w16_;
|
||||
int64_t w16_budget_ = -1; // bytes; -1: env not read yet, 0: off
|
||||
int64_t w16_used_ = 0;
|
||||
void* hipblaslt_state_ = nullptr;
|
||||
void* rocblas_state_ = nullptr; // HIP: the rocBLAS solution table of f16_inplace (STRATA_ROCBLAS_TUNING)
|
||||
bool f16_io_ = false;
|
||||
|
||||
@@ -75,6 +75,12 @@ void gdn_recurrence(float* state, const float* h, const float* gate, const float
|
||||
void gdn_recurrence_variant(int variant, float* state, const float* h, const float* gate, const float* beta,
|
||||
const float* z, const float* gamma, float eps, float* y, uint16_t* y16, int64_t T,
|
||||
void* stream, int64_t ld16 = 0);
|
||||
/// HIP gfx12 only, STRATA_GDN_CHUNKED=1 (STRATA_GDN_FUSE=0 off): gdn_conv + gdn_recurrence on the chunked matrix-core path with
|
||||
/// the conv output kept as FP16 in `hbuf` (the FP32 h buffer of gdn_conv, reused). Returns false, having launched nothing, when
|
||||
/// it does not apply (not gfx12, short prompt, unaligned stride, off): the caller then calls the two.
|
||||
bool gdn_conv_recurrence_fused(float* history, const float* qkv, const float* conv_w, float* hbuf, float* state, const float* gate,
|
||||
const float* beta, const float* z, const float* gamma, float eps, float l2eps, float* y,
|
||||
uint16_t* y16, int64_t T, void* stream, int64_t ld16 = 0);
|
||||
/// KVA (CUDA/HIP only): the same recurrence kernels as gdn_recurrence, bit for bit, without the output
|
||||
/// normalization: state, h, gate, beta and the scratch `y` only. The output projection and the gated residual
|
||||
/// write have no consumer when only the state advances, and their input (z, the gate projection) is not needed
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
// Requires the native weights (`--native`): every quantized projection must carry its GGUF blocks.
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "strata/core/expert_cache.hpp"
|
||||
#include "strata/core/expert_source.hpp"
|
||||
#include "strata/core/layer.hpp"
|
||||
@@ -265,6 +267,19 @@ public:
|
||||
/// Layer-major prefill (STRATA_PREFILL_LAYER_MAJOR=1): true while `on_chunk` reports a sub-chunk before the end of
|
||||
/// its super-chunk. The session state is already at the super-chunk's end then, so no checkpoint may be taken.
|
||||
/// On a layer split it is the last stage's (the one that reports the rows), asked through the first.
|
||||
/// One prompt of a multi-prompt read (run_multi): its tokens, the position it starts at and, per stage of a layer split
|
||||
/// (index 0 = the first stage), the session it reads into (nullptr / missing: that stage's own main session).
|
||||
struct Seg {
|
||||
const int64_t* tokens = nullptr;
|
||||
int64_t n = 0;
|
||||
int64_t pos0 = 0;
|
||||
std::vector<core::SessionState*> stage_ss;
|
||||
};
|
||||
/// Several prompts in ONE layer-major pass (the streamed experts of a layer are read once for all of them). Each
|
||||
/// prompt fills its own sessions; `on_chunk` / `on_stage_chunk` report the FIRST prompt only. Returns false with `err`
|
||||
/// when the layer-major walk does not apply (then nothing was read; the caller reads the prompts one by one).
|
||||
bool run_multi(const std::vector<Seg>& segs, std::string& err);
|
||||
|
||||
bool layer_major_mid() const {
|
||||
const Prefill* p = this;
|
||||
while (p->next_ != nullptr) p = p->next_;
|
||||
@@ -282,8 +297,11 @@ private:
|
||||
int64_t lm_prepare_chain(int64_t n, std::string& why);
|
||||
void lm_release_chain();
|
||||
bool run_layer_major(const int64_t* tokens, int64_t n, int64_t pos0, std::string& err);
|
||||
struct LmSub { int seg = 0; int64_t c0 = 0, T = 0, t0 = 0; }; ///< a sub-chunk: prompt, first token, tokens, row in the super-chunk
|
||||
bool lm_run(const std::vector<Seg>& segs, int depth, const std::vector<LmSub>* given_super, std::string& err);
|
||||
void lm_release();
|
||||
bool lm_mid_ = false;
|
||||
bool lm_force_ = false; ///< run_multi: the walk even for a short total
|
||||
bool lm_by_default_ = false; ///< the walk is on because nothing was set (a wider VRAM margin, the measured super-chunk size)
|
||||
// Stage-1 pipeline: intermediate stages return after handing their chunk to
|
||||
// the direct successor. The public run() drains the chain once at prompt end.
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// include/strata/program/par_counters.hpp - STRATA_PAR_COUNTERS=1: counters-only parallelism accounting (research, opt-in).
|
||||
//
|
||||
// Off (the default), every hook is one branch on a const bool and reads no clock. On, the batch loop (one host thread)
|
||||
// adds plain integers and, at the places that already wait (a drain, an admission), two clock reads. Nothing here
|
||||
// synchronizes a device. Printed to stderr as "strata par: ..." with the batch summary line (when the loop goes idle):
|
||||
// windows group windows launched, rows carried, rows used by live slots, padded rows, histogram of live slots per window
|
||||
// drains pipeline drains before a control line, by line kind (BGEN = an admission), groups in flight, ms waited
|
||||
// admissions prompts admitted into a slot: prompt tokens, ms in the prompt phase, ms of the drain before it
|
||||
// idle ticks with stage 0 free and no launchable group (no live slot / every group in flight)
|
||||
// Stage busy/idle with reason stays in STRATA_P4_TIMING (pipe_probe.hpp); this adds what it lacked.
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace strata::program {
|
||||
|
||||
/// Expert-dispatch totals (cumulative in the engine); a span's delta is printed at flush.
|
||||
struct ParEx { long long misses = 0, entries = 0, hits = 0, offload = 0, pcie = 0; double run_ms = 0, jobs_ms = 0, vwait_ms = 0, vpool_ms = 0; };
|
||||
|
||||
class ParCounters {
|
||||
public:
|
||||
using Ex = ParEx;
|
||||
using Clock = std::chrono::steady_clock;
|
||||
ParCounters() { const char* e = std::getenv("STRATA_PAR_COUNTERS"); on_ = e != nullptr && e[0] == '1' && e[1] == '\0'; }
|
||||
bool on() const { return on_; }
|
||||
|
||||
void window(int rows, int live) {
|
||||
if (!on_) return;
|
||||
++windows_; rows_ += rows; used_ += live;
|
||||
++hist_[live < 0 ? 0 : (live > kHist - 1 ? kHist - 1 : live)];
|
||||
}
|
||||
void drain(const std::string& line, int groups_in_flight, double ms) {
|
||||
if (!on_) return;
|
||||
const bool adm = line.rfind("BGEN", 0) == 0;
|
||||
if (adm) { ++adm_drains_; adm_drain_ms_ += ms; } else { ++other_drains_; other_drain_ms_ += ms; }
|
||||
if (groups_in_flight > 0) ++drains_busy_;
|
||||
groups_at_drain_ += groups_in_flight;
|
||||
}
|
||||
void admission(long long prompt_tokens, double prompt_ms) {
|
||||
if (!on_) return;
|
||||
++adm_; adm_tokens_ += prompt_tokens; adm_ms_ += prompt_ms;
|
||||
}
|
||||
void begin(const Ex& e, int layers) { if (on_) { ex0_ = e; layers_ = layers > 0 ? layers : 1; } }
|
||||
void adapt(double ms) { if (on_) { ++adapt_; adapt_ms_ += ms; } }
|
||||
void no_launch(bool any_live) { if (on_) { if (any_live) ++idle_all_inflight_; else ++idle_no_live_; } }
|
||||
|
||||
void flush(double wall_ms, const ParEx& e = ParEx{}) {
|
||||
if (!on_ || (windows_ == 0 && adm_ == 0 && adm_drains_ + other_drains_ == 0)) return;
|
||||
std::fprintf(stderr, "strata par: wall %.0f ms; windows %lld rows %lld used %lld padded %lld (%.1f%% used); live slots/window:",
|
||||
wall_ms, windows_, rows_, used_, rows_ - used_, rows_ ? 100.0 * used_ / rows_ : 0.0);
|
||||
for (int i = 0; i < kHist; ++i) if (hist_[i]) std::fprintf(stderr, " %d:%lld", i, hist_[i]);
|
||||
std::fprintf(stderr, "; drains: admission %lld (%.0f ms) other %lld (%.0f ms), %lld with groups in flight, avg %.2f groups; "
|
||||
"admissions %lld, %lld prompt tokens in %.0f ms; adapt rounds %lld %.0f ms; "
|
||||
"ticks without a launch: no live slot %lld, all groups in flight %lld\n",
|
||||
adm_drains_, adm_drain_ms_, other_drains_, other_drain_ms_, drains_busy_,
|
||||
(adm_drains_ + other_drains_) ? (double) groups_at_drain_ / (adm_drains_ + other_drains_) : 0.0, adm_, adm_tokens_, adm_ms_,
|
||||
adapt_, adapt_ms_, idle_no_live_, idle_all_inflight_);
|
||||
if (windows_ > 0 && (e.misses | e.entries | e.hits | e.offload) != 0) {
|
||||
const double wl = (double) windows_ * layers_;
|
||||
std::fprintf(stderr, "strata par experts: per window-layer: routed entries (CPU) %.2f, distinct CPU-computed experts %.2f, "
|
||||
"GPU cache hits %.2f, PCIe/offload entries %.2f, distinct PCIe experts %.2f; ms per window: CPU pool run %.2f, "
|
||||
"job build %.2f, verifier wait %.2f, verifier pool %.2f (of %.2f wall)\n",
|
||||
(e.entries - ex0_.entries) / wl, (e.misses - ex0_.misses) / wl, (e.hits - ex0_.hits) / wl,
|
||||
(e.offload - ex0_.offload) / wl, (e.pcie - ex0_.pcie) / wl, (e.run_ms - ex0_.run_ms) / windows_,
|
||||
(e.jobs_ms - ex0_.jobs_ms) / windows_, (e.vwait_ms - ex0_.vwait_ms) / windows_,
|
||||
(e.vpool_ms - ex0_.vpool_ms) / windows_, wall_ms / windows_);
|
||||
}
|
||||
std::fflush(stderr);
|
||||
reset();
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr int kHist = 18;
|
||||
void reset() {
|
||||
windows_ = rows_ = used_ = adm_drains_ = other_drains_ = drains_busy_ = groups_at_drain_ = adm_ = adm_tokens_ = adapt_ = 0;
|
||||
idle_no_live_ = idle_all_inflight_ = 0;
|
||||
adm_drain_ms_ = other_drain_ms_ = adm_ms_ = adapt_ms_ = 0;
|
||||
std::memset(hist_, 0, sizeof hist_);
|
||||
}
|
||||
bool on_ = false;
|
||||
Ex ex0_{};
|
||||
int layers_ = 1;
|
||||
long long windows_ = 0, rows_ = 0, used_ = 0, adm_drains_ = 0, other_drains_ = 0, drains_busy_ = 0, groups_at_drain_ = 0;
|
||||
long long adm_ = 0, adm_tokens_ = 0, adapt_ = 0, idle_no_live_ = 0, idle_all_inflight_ = 0, hist_[kHist] = {};
|
||||
double adm_drain_ms_ = 0, other_drain_ms_ = 0, adm_ms_ = 0, adapt_ms_ = 0;
|
||||
};
|
||||
|
||||
} // namespace strata::program
|
||||
@@ -0,0 +1,148 @@
|
||||
// include/strata/program/pipe_probe.hpp - STRATA_P4_TIMING / STRATA_P4_TRACE: where the wall time of a pipelined batch window goes.
|
||||
//
|
||||
// Opt-in measurement for the layer-split batch loop (generate.cpp, `pump`). Off, every hook is one predictable branch and
|
||||
// reads no clock. On:
|
||||
// STRATA_P4_TIMING=1 per stage: the window's time from launch to done, the cycle between launches, the gap between a
|
||||
// window done and the next launch, the stage's busy share, the host launch cost; the pump's cost
|
||||
// per tick, the verdict work per window, how many groups were in flight, the ticks a free stage had
|
||||
// a group waiting or was starved. Printed to stderr with the batch summary line.
|
||||
// STRATA_P4_TRACE=PATH one line per window event ("L stage time_us group" at a launch, "D stage time_us 0" when the stage
|
||||
// is done), flushed on every line so a killed run keeps its trace. "1" or an empty value writes
|
||||
// ./strata-p4-trace.csv.
|
||||
// Written for the four-card R9700 investigation (docs/BATCHING.md): it found that the stages run 4 of 4 concurrently in the
|
||||
// decode phase and that the serial admissions were the idle time.
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace strata::program {
|
||||
|
||||
class PipeProbe {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using Time = Clock::time_point;
|
||||
static constexpr int kMaxStages = 16;
|
||||
static constexpr int kMaxGroups = 64;
|
||||
|
||||
PipeProbe(int n_stages, size_t n_groups)
|
||||
: n_(std::min(n_stages, kMaxStages)), n_groups_(n_groups) {
|
||||
timing_ = std::getenv("STRATA_P4_TIMING") != nullptr;
|
||||
const char* tr = std::getenv("STRATA_P4_TRACE");
|
||||
if (tr != nullptr) {
|
||||
const char* path = (tr[0] == '\0' || std::strcmp(tr, "1") == 0) ? "strata-p4-trace.csv" : tr;
|
||||
trace_ = std::fopen(path, "w");
|
||||
if (trace_ != nullptr) std::setvbuf(trace_, nullptr, _IONBF, 0);
|
||||
else std::fprintf(stderr, "strata p4: cannot write the trace %s\n", path);
|
||||
}
|
||||
}
|
||||
~PipeProbe() { if (trace_ != nullptr) std::fclose(trace_); }
|
||||
PipeProbe(const PipeProbe&) = delete;
|
||||
PipeProbe& operator=(const PipeProbe&) = delete;
|
||||
|
||||
bool on() const { return timing_ || trace_ != nullptr; }
|
||||
bool timing() const { return timing_; }
|
||||
Time now() const { return on() ? Clock::now() : Time{}; }
|
||||
|
||||
/// The pump's cost: constructed at the top of `pump`, adds its life to the per-tick figure.
|
||||
struct PumpScope {
|
||||
PipeProbe* p = nullptr;
|
||||
Time t0{};
|
||||
explicit PumpScope(PipeProbe& probe) {
|
||||
++probe.ticks_;
|
||||
if (probe.on()) { p = &probe; t0 = Clock::now(); }
|
||||
}
|
||||
~PumpScope() { if (p != nullptr) p->pump_ms_ += ms(Clock::now() - t0); }
|
||||
};
|
||||
/// The verdict work of a window that reached the last stage.
|
||||
struct VerdictScope {
|
||||
PipeProbe* p = nullptr;
|
||||
Time t0{};
|
||||
explicit VerdictScope(PipeProbe& probe) {
|
||||
if (probe.timing_) { p = &probe; t0 = Clock::now(); ++probe.verdict_n_; }
|
||||
}
|
||||
~VerdictScope() { if (p != nullptr) p->verdict_ms_ += ms(Clock::now() - t0); }
|
||||
};
|
||||
|
||||
/// Per tick: stages with a window in flight, groups in flight.
|
||||
void busy(int stages, int groups) { busy_ += stages; inflight_ += groups; }
|
||||
/// A stage k >= 1 that is free: whether a group is waiting at it (else it is starved by the stage before).
|
||||
void free_stage(int k, bool group_waiting) {
|
||||
if (k < 0 || k >= n_) return;
|
||||
if (group_waiting) ++starve_idle_[k]; else ++starve_none_[k];
|
||||
}
|
||||
void span_start(Time t) { span_start_ = t; span_ms_ = 0; }
|
||||
void span_end(double wall_ms) { span_ms_ = wall_ms; }
|
||||
void polled(Time before) { if (on()) poll_ms_ += ms(Clock::now() - before); }
|
||||
/// Stage k reported its window done.
|
||||
void done(int k, int group) {
|
||||
if (!on() || k < 0 || k >= n_) return;
|
||||
const Time t = Clock::now();
|
||||
if (timing_ && launched_[k]) { win_ms_[k] += ms(t - at_[k]); ++win_n_[k]; }
|
||||
if (trace_ != nullptr) std::fprintf(trace_, "D %d %.3f 0\n", k, us(t));
|
||||
done_at_[k] = t;
|
||||
if (timing_ && group >= 0 && group < kMaxGroups && left_[group] != Time{}) { q_wait_ += ms(t - left_[group]); ++q_n_; }
|
||||
}
|
||||
/// Stage k launched `group`'s window; `before` is now() taken before the launch call.
|
||||
void launched(int k, int group, Time before) {
|
||||
if (!on() || k < 0 || k >= n_) return;
|
||||
const Time t = Clock::now();
|
||||
launch_ms_ += ms(t - before);
|
||||
at_[k] = t;
|
||||
launched_[k] = true;
|
||||
if (group >= 0 && group < kMaxGroups) left_[group] = t;
|
||||
if (trace_ != nullptr) std::fprintf(trace_, "L %d %.3f %d\n", k, us(t), group);
|
||||
if (timing_) {
|
||||
if (last_launch_[k] != Time{}) cycle_ms_[k] += ms(t - last_launch_[k]);
|
||||
if (done_at_[k] != Time{}) gap_ms_[k] += ms(t - done_at_[k]);
|
||||
last_launch_[k] = t;
|
||||
}
|
||||
}
|
||||
/// Print and reset (the batch summary line calls it when the loop goes idle).
|
||||
void flush() {
|
||||
if (!timing_) return;
|
||||
const double ticks = (double) std::max(1LL, ticks_);
|
||||
std::fprintf(stderr, "strata p4: pump %.3f ms/tick over %lld ticks; stage-busy %.2f of %d; group wait %.1f ms;\n",
|
||||
pump_ms_ / ticks, ticks_, busy_ / ticks, n_, q_n_ ? q_wait_ / (double) q_n_ : 0.0);
|
||||
for (int k = 0; k < n_; ++k) {
|
||||
const double n = win_n_[k] ? (double) win_n_[k] : 1.0;
|
||||
std::fprintf(stderr, "strata p4: s%d windows %lld win %.2f ms cycle %.2f ms gap %.2f ms busy %.0f%% launch %.2f ms\n",
|
||||
k, win_n_[k], win_ms_[k] / n, cycle_ms_[k] / n, gap_ms_[k] / n,
|
||||
100.0 * win_ms_[k] / std::max(1e-9, span_ms_), launch_ms_ / n);
|
||||
}
|
||||
std::fprintf(stderr, "strata p4: verdict work %.2f ms/window; inflight groups avg %.2f of %zu\n",
|
||||
verdict_ms_ / std::max(1.0, (double) verdict_n_), inflight_ / ticks, n_groups_);
|
||||
for (int k = 1; k < n_; ++k)
|
||||
std::fprintf(stderr, "strata p4: s%d free ticks: with a group waiting %lld, starved %lld\n", k, starve_idle_[k],
|
||||
starve_none_[k]);
|
||||
pump_ms_ = poll_ms_ = launch_ms_ = verdict_ms_ = busy_ = inflight_ = q_wait_ = 0;
|
||||
ticks_ = verdict_n_ = q_n_ = 0;
|
||||
for (int k = 0; k < kMaxStages; ++k) {
|
||||
win_ms_[k] = cycle_ms_[k] = gap_ms_[k] = 0;
|
||||
win_n_[k] = starve_idle_[k] = starve_none_[k] = 0;
|
||||
}
|
||||
span_ms_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
static double ms(Clock::duration d) { return std::chrono::duration<double, std::milli>(d).count(); }
|
||||
static double us(Time t) { return std::chrono::duration<double, std::micro>(t.time_since_epoch()).count(); }
|
||||
|
||||
int n_ = 0;
|
||||
size_t n_groups_ = 0;
|
||||
bool timing_ = false;
|
||||
std::FILE* trace_ = nullptr;
|
||||
double pump_ms_ = 0, poll_ms_ = 0, launch_ms_ = 0, verdict_ms_ = 0, busy_ = 0, inflight_ = 0, q_wait_ = 0, span_ms_ = 0;
|
||||
long long ticks_ = 0, verdict_n_ = 0, q_n_ = 0;
|
||||
double win_ms_[kMaxStages] = {}, cycle_ms_[kMaxStages] = {}, gap_ms_[kMaxStages] = {};
|
||||
long long win_n_[kMaxStages] = {}, starve_idle_[kMaxStages] = {}, starve_none_[kMaxStages] = {};
|
||||
Time at_[kMaxStages] = {}, last_launch_[kMaxStages] = {}, done_at_[kMaxStages] = {};
|
||||
bool launched_[kMaxStages] = {};
|
||||
Time left_[kMaxGroups] = {};
|
||||
Time span_start_{};
|
||||
};
|
||||
|
||||
} // namespace strata::program
|
||||
+66
-3
@@ -39,7 +39,8 @@ target_compile_definitions(mtl_rt PRIVATE
|
||||
target_link_libraries(mtl_rt PUBLIC "-framework Metal" "-framework Foundation")
|
||||
|
||||
file(GLOB STRATA_METAL_TESTS CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tests/*.cpp)
|
||||
list(REMOVE_ITEM STRATA_METAL_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/runtime_hello.cpp)
|
||||
list(REMOVE_ITEM STRATA_METAL_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/runtime_hello.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/dsu_ref_vs_ggml.cpp) # (needs ggml: below)
|
||||
foreach(src ${STRATA_METAL_TESTS})
|
||||
get_filename_component(t ${src} NAME_WE)
|
||||
add_executable(${t} ${src})
|
||||
@@ -86,6 +87,10 @@ set(GGML_BACKEND_DL OFF CACHE BOOL "" FORCE)
|
||||
set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(${STRATA_GGML_DIR}/ggml ${CMAKE_BINARY_DIR}/ggml EXCLUDE_FROM_ALL)
|
||||
# ref_quant's IQ1_S / IQ1_M / Q6_K dequantizers against ggml's own (the Unsloth sets' kernels are checked against ref_quant: dsu_parity)
|
||||
add_executable(dsu_ref_vs_ggml tests/dsu_ref_vs_ggml.cpp)
|
||||
target_include_directories(dsu_ref_vs_ggml PRIVATE ${STRATA_GGML_DIR}/ggml/include)
|
||||
target_link_libraries(dsu_ref_vs_ggml PRIVATE mtl_rt ggml-base ggml-cpu)
|
||||
|
||||
set(S ${STRATA_ROOT}/src)
|
||||
set(STRATA_METAL_HOST_SOURCES
|
||||
@@ -99,7 +104,8 @@ set(STRATA_METAL_HOST_SOURCES
|
||||
${S}/spec/suffix_drafter.cpp ${S}/spec/controller.cpp ${S}/spec/draft_policy.cpp ${S}/spec/draft_source.cpp
|
||||
${S}/kernels/cpu/pool.cpp ${S}/kernels/cpu/expert_layout.cpp ${S}/kernels/cpu/native_expert.cpp
|
||||
${S}/kernels/cpu/hadamard.cpp ${S}/kernels/cpu/tq_cpu.cpp) # Gyro: the Hadamard rotation, the scalar trellis rows
|
||||
set_source_files_properties(${S}/core/device.cu ${S}/core/pinned.cu PROPERTIES LANGUAGE CXX)
|
||||
set_source_files_properties(${S}/core/device.cu ${S}/core/pinned.cu ${DS4}/ds4_vision.cu ${DS4}/vendor/exl3.cu ${DS4}/vendor/moe_win.cu ${DS4}/vendor/sampler.cu
|
||||
PROPERTIES LANGUAGE CXX)
|
||||
|
||||
# The kernel library: Metal implementations (src/engine/*.cpp) and, for every entry point not ported yet, a stub
|
||||
# that stops the engine with the entry point's name (src/engine/stubs.cpp, generated by tools/gen_stubs.py).
|
||||
@@ -108,7 +114,7 @@ file(GLOB STRATA_METAL_ENGINE_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_D
|
||||
add_library(strata_metal_host STATIC ${STRATA_METAL_HOST_SOURCES})
|
||||
target_include_directories(strata_metal_host PUBLIC ${STRATA_ROOT}/include ${S}/core ${STRATA_ROOT}/third_party/ggml
|
||||
${STRATA_GGML_DIR}/ggml/include ${STRATA_GGML_DIR}/ggml/src ${STRATA_GGML_DIR}/ggml/src/ggml-cpu)
|
||||
target_compile_definitions(strata_metal_host PUBLIC STRATA_NATIVE_EXPERTS=1 STRATA_VERSION="0.1.52-metal")
|
||||
target_compile_definitions(strata_metal_host PUBLIC STRATA_NATIVE_EXPERTS=1 STRATA_VERSION="0.1.55-metal")
|
||||
target_compile_options(strata_metal_host PRIVATE -Wno-unused-parameter -Wno-deprecated-declarations)
|
||||
target_link_libraries(strata_metal_host PUBLIC strata_gpu_metal ggml-cpu ggml-base)
|
||||
|
||||
@@ -122,3 +128,60 @@ target_link_libraries(strata_metal_kernels PUBLIC strata_metal_host mtl_rt)
|
||||
|
||||
add_executable(strata ${S}/program/generate.cpp)
|
||||
target_link_libraries(strata PRIVATE strata_metal_kernels strata_metal_host strata_metal_kernels)
|
||||
|
||||
# --- the DeepSeek engine on this Metal runtime (M5MAX-DEEPSEEK-PLAN.md, steps 3+). The .cu sources are compiled as C++
|
||||
# through include/metal_compat, which gives them CUDA's names over the Metal runtime; the kernel entry points they call are
|
||||
# the Metal implementations in src/engine/* or the stubs from tools/gen_stubs.py that stop the engine with the name.
|
||||
set(DS4 ${STRATA_ROOT}/src/dsv41)
|
||||
# The DeepSeek engine's sources. Every .cu compiles as C++ through include/metal_compat; the EXL3 legacy path is left out
|
||||
# (the 2.25bpw text model never calls it) and the vision tower is included because ds4_model.cu references it.
|
||||
set(STRATA_DS4_SOURCES
|
||||
${DS4}/ds4_main.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/engine/ds4_finish.cpp ${DS4}/ds4_model.cu ${DS4}/ds4_gguf.cu ${DS4}/ds4_iq.cu ${DS4}/ds4_kernels.cu ${DS4}/ds4_tq.cu
|
||||
${DS4}/ds4_vision.cu
|
||||
${DS4}/vendor/exl3.cu ${DS4}/vendor/exl3_cpu.cpp ${DS4}/vendor/moe_win.cu ${DS4}/vendor/sampler.cu
|
||||
${DS4}/ds4_gg_cpu.cpp ${DS4}/ds4_gguf_cpu.cpp ${DS4}/ds4_iq_cpu.cpp ${DS4}/ds4_tq_cpu.cpp ${DS4}/ds4_packinfo.cpp)
|
||||
set_source_files_properties(
|
||||
${DS4}/ds4_model.cu ${DS4}/ds4_gguf.cu ${DS4}/ds4_iq.cu ${DS4}/ds4_kernels.cu ${DS4}/ds4_tq.cu ${DS4}/ds4_vision.cu
|
||||
${DS4}/vendor/exl3.cu ${DS4}/vendor/moe_win.cu ${DS4}/vendor/sampler.cu
|
||||
PROPERTIES LANGUAGE CXX)
|
||||
list(APPEND STRATA_DS4_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/engine/exl3_legacy_stub.cpp) # v1.1.8 EXL3: absent on Metal
|
||||
# The CPU lane for the experts the GPU does not hold (ggml-cpu's NEON row dots): without it every non-resident expert contributes
|
||||
# zero and the residual stream stays the embedding (measured: layers 0-3 changed nothing, then random tokens).
|
||||
set_source_files_properties(${DS4}/ds4_gg_cpu.cpp PROPERTIES COMPILE_DEFINITIONS "STRATA_DSV41_TQ_CPU=1")
|
||||
# The DeepSeek engine's queue, residency and barrier rules (deferred command buffers, the second queue, the residency set shared with
|
||||
# strata::gpu, ...) live in mtl_rt.mm and gpu/runtime.cpp behind STRATA_DS4_RUNTIME. strata_ds4 compiles its own copy of these two
|
||||
# files with it; the Qwen engine (strata) and every other target keep the code of 0.2.7 (the macro is off there, so the files read
|
||||
# exactly as they did). The copies are linked as objects, so they win over the same symbols in the libraries.
|
||||
add_library(ds4_rt_mm OBJECT src/mtl_rt.mm)
|
||||
target_include_directories(ds4_rt_mm PRIVATE src ${STRATA_ROOT}/third_party/ggml ${STRATA_ROOT}/include)
|
||||
target_compile_options(ds4_rt_mm PRIVATE $<$<COMPILE_LANGUAGE:OBJCXX>:-fobjc-arc>)
|
||||
target_compile_definitions(ds4_rt_mm PRIVATE STRATA_DS4_RUNTIME=1 STRATA_GGML_COMMON_DEFAULT="${GGML_COMMON}"
|
||||
STRATA_METAL_KERNELS_DEFAULT="${CMAKE_CURRENT_SOURCE_DIR}/kernels")
|
||||
add_library(ds4_rt_gpu OBJECT src/gpu/runtime.cpp)
|
||||
target_include_directories(ds4_rt_gpu BEFORE PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/metal_compat
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include ${STRATA_ROOT}/include)
|
||||
target_include_directories(ds4_rt_gpu SYSTEM PRIVATE ${METAL_CPP_DIR})
|
||||
target_compile_definitions(ds4_rt_gpu PRIVATE STRATA_DS4_RUNTIME=1 STRATA_GPU_METAL=1)
|
||||
add_executable(strata_ds4 ${STRATA_DS4_SOURCES} $<TARGET_OBJECTS:ds4_rt_mm> $<TARGET_OBJECTS:ds4_rt_gpu>)
|
||||
target_include_directories(strata_ds4 PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/metal_compat ${STRATA_ROOT}/include ${DS4} ${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${STRATA_ROOT}/third_party/ggml ${STRATA_GGML_DIR}/ggml/include ${STRATA_GGML_DIR}/ggml/src ${STRATA_GGML_DIR}/ggml/src/ggml-cpu)
|
||||
target_compile_definitions(strata_ds4 PRIVATE STRATA_METAL=1) # the real host CPU lane (ds4_gg_cpu.cpp)
|
||||
target_compile_options(strata_ds4 PRIVATE -Wno-unused-parameter -Wno-deprecated-declarations -Wno-implicit-function-declaration)
|
||||
target_link_libraries(strata_ds4 PRIVATE strata_metal_kernels strata_metal_host)
|
||||
# The commit the binary was built from, readable with --version (the root CMakeLists does the same for the CUDA and HIP builds):
|
||||
# -DSTRATA_BUILD_ID=..., else BUILD_ID beside the sources (a git archive has no .git), else git, else "unknown".
|
||||
if(NOT STRATA_BUILD_ID AND EXISTS ${STRATA_ROOT}/BUILD_ID)
|
||||
file(STRINGS ${STRATA_ROOT}/BUILD_ID STRATA_BUILD_ID LIMIT_COUNT 1)
|
||||
endif()
|
||||
if(NOT STRATA_BUILD_ID)
|
||||
execute_process(COMMAND git -C ${STRATA_ROOT} rev-parse --short=9 HEAD OUTPUT_VARIABLE STRATA_BUILD_ID OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
|
||||
endif()
|
||||
if(NOT STRATA_BUILD_ID)
|
||||
set(STRATA_BUILD_ID unknown)
|
||||
endif()
|
||||
set_source_files_properties(${DS4}/ds4_main.cpp PROPERTIES COMPILE_DEFINITIONS "DS4_BUILD_ID=\"${STRATA_BUILD_ID}\"")
|
||||
# -DSTRATA_DS4_VERSION=0.1.55-metal: the version this DeepSeek engine reports, when its package is released apart from the Qwen engine's
|
||||
if(STRATA_DS4_VERSION)
|
||||
set_source_files_properties(${DS4}/ds4_main.cpp PROPERTIES COMPILE_OPTIONS "-USTRATA_VERSION;-DSTRATA_VERSION=\"${STRATA_DS4_VERSION}\"")
|
||||
endif()
|
||||
@@ -0,0 +1,72 @@
|
||||
// metal/include/metal_compat/cublas_v2.h - the cuBLAS surface the dsv41 sources use, over the Metal device runtime.
|
||||
//
|
||||
// The engine makes 9 GEMM calls (7 cublasGemmEx, 2 cublasSgemm) for the dense projections, the LM head and the vision
|
||||
// tower's matmuls. On Metal the dense path is kernels/dense_mm.metal; this header gives the calls the CUDA names so the
|
||||
// shared sources compile unchanged, and routes them to strata::gpu::dense_gemm(), which the metal host runtime provides.
|
||||
//
|
||||
// Compile-audit finding (M5 Max, 2026-10-23): the dsv41 core needs exactly cublasCreate/Destroy/SetStream/SetWorkspace,
|
||||
// cublasSgemm and cublasGemmEx, plus cublasStatus_t/cublasHandle_t/cublasOperation_t/cublasComputeType_t/cudaDataType_t.
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "cuda_runtime.h"
|
||||
#include "ds4_metal_host.h" // the kernels that have a Metal dispatch
|
||||
|
||||
typedef void* cublasHandle_t;
|
||||
|
||||
enum cublasStatus_t { CUBLAS_STATUS_SUCCESS = 0, CUBLAS_STATUS_NOT_INITIALIZED = 1, CUBLAS_STATUS_ALLOC_FAILED = 3,
|
||||
CUBLAS_STATUS_INVALID_VALUE = 7, CUBLAS_STATUS_ARCH_MISMATCH = 8, CUBLAS_STATUS_NOT_SUPPORTED = 14 };
|
||||
enum cublasOperation_t { CUBLAS_OP_N = 0, CUBLAS_OP_T = 1, CUBLAS_OP_C = 2 };
|
||||
enum cublasComputeType_t { CUBLAS_COMPUTE_16F = 64, CUBLAS_COMPUTE_32F = 68, CUBLAS_COMPUTE_32F_FAST_16F = 74 };
|
||||
enum cublasGemmAlgo_t { CUBLAS_GEMM_DEFAULT = -1, CUBLAS_GEMM_DEFAULT_TENSOR_OP = 99 };
|
||||
enum cudaDataType_t { CUDA_R_32F = 0, CUDA_R_16F = 2, CUDA_R_16BF = 14, CUDA_R_8F_E4M3 = 28 };
|
||||
using cudaDataType = cudaDataType_t; // the engine names it both ways
|
||||
|
||||
static inline cublasStatus_t cublasCreate(cublasHandle_t* h) {
|
||||
*h = (void*) 1; // the Metal runtime is a singleton: the handle carries no state
|
||||
return CUBLAS_STATUS_SUCCESS;
|
||||
}
|
||||
static inline cublasStatus_t cublasDestroy(cublasHandle_t) { return CUBLAS_STATUS_SUCCESS; }
|
||||
static inline cublasStatus_t cublasSetStream(cublasHandle_t, cudaStream_t) { return CUBLAS_STATUS_SUCCESS; }
|
||||
static inline cublasStatus_t cublasSetWorkspace(cublasHandle_t, void*, size_t) { return CUBLAS_STATUS_SUCCESS; }
|
||||
|
||||
// C = alpha * op(A) * op(B) + beta * C, all row-major floats.
|
||||
// NOT WIRED YET: the metal host runtime has no dense GEMM entry point. This aborts with a message instead of returning a
|
||||
// wrong result, so the shared sources compile and a run fails where the work is, not somewhere confusing. The real
|
||||
// implementation drives kernels/dense_mm.metal (the kernel exists and passes its parity test).
|
||||
[[noreturn]] static inline void strata_metal_dense_gemm_missing(const char* what) {
|
||||
std::fprintf(stderr, "strata metal: %s is not wired to kernels/dense_mm.metal yet\n", what);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
static inline cublasStatus_t cublasSgemm(cublasHandle_t, cublasOperation_t opA, cublasOperation_t opB, int m, int n, int k,
|
||||
const float* alpha, const float* A, int lda, const float* B, int ldb,
|
||||
const float* beta, float* C, int ldc) {
|
||||
// the mHC mixes (ds4_model.cu:2737): C = A^T B, F32, alpha 1, beta 0
|
||||
if (opA == CUBLAS_OP_T && opB == CUBLAS_OP_N && alpha && beta && *alpha == 1.0f && *beta == 0.0f &&
|
||||
strata::dsv41::metal_host::sgemm_tn(A, B, C, m, n, k, lda, ldb, ldc)) {
|
||||
return CUBLAS_STATUS_SUCCESS;
|
||||
}
|
||||
strata_metal_dense_gemm_missing("cublasSgemm");
|
||||
}
|
||||
|
||||
// the mixed-precision form: any argument list, because the engine passes cuBLAS's full 19-argument spelling; the body
|
||||
// names kernels/dense_mm.metal, which is where the real implementation goes.
|
||||
// the chunk path's call (F16 weights [m][k] x F16 rows [n][k] -> F32 rows [n][m]): the MPP tile GEMM
|
||||
static inline cublasStatus_t cublasGemmEx(cublasHandle_t, cublasOperation_t ta, cublasOperation_t tb, int m, int n, int k,
|
||||
const void* alpha, const void* A, cudaDataType_t at, int lda, const void* B,
|
||||
cudaDataType_t bt, int ldb, const void* beta, void* C, cudaDataType_t ct, int ldc,
|
||||
cublasComputeType_t, cublasGemmAlgo_t) {
|
||||
if (ta == CUBLAS_OP_T && tb == CUBLAS_OP_N && at == CUDA_R_16F && bt == CUDA_R_16F && ct == CUDA_R_32F && lda == k &&
|
||||
strata::dsv41::metal_host::gemm_f16_nt((const uint16_t*) A, (const uint16_t*) B, ldb, (float*) C, ldc, n, m, k, *(const float*) alpha,
|
||||
*(const float*) beta))
|
||||
return CUBLAS_STATUS_SUCCESS;
|
||||
strata_metal_dense_gemm_missing("cublasGemmEx (this operand combination)");
|
||||
}
|
||||
|
||||
template <typename... A>
|
||||
static inline cublasStatus_t cublasGemmEx_unsupported(A...) {
|
||||
strata_metal_dense_gemm_missing("cublasGemmEx");
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// metal/include/metal_compat/cuda_bf16.h - the handful of CUDA bf16/fp8 names the dsv41 sources use, on Metal.
|
||||
//
|
||||
// Only what the engine touches (found by compiling src/dsv41/*.cu against this directory, 2026-10-23): the storage typedef
|
||||
// and four conversions. On a Mac the bf16 storage is a uint16_t and the conversion is the standard round-to-nearest-even
|
||||
// truncation CUDA's __float2bfloat16_rn does; fp8 is E4M3 with the same rounding.
|
||||
#pragma once
|
||||
|
||||
#include "cuda_fp16.h" // __half_raw, which the fp8 conversions return
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
typedef uint16_t __nv_bfloat16;
|
||||
typedef uint16_t __nv_bfloat162;
|
||||
typedef uint8_t __nv_fp8_storage_t;
|
||||
|
||||
static inline uint16_t strata_f32_to_bf16_rn(float f) {
|
||||
uint32_t u;
|
||||
std::memcpy(&u, &f, sizeof u);
|
||||
if ((u & 0x7fffffffu) > 0x7f800000u) return (uint16_t) ((u >> 16) | 0x0040u); // NaN keeps a payload
|
||||
uint32_t r = (u + 0x7fffu + ((u >> 16) & 1u)) >> 16;
|
||||
return (uint16_t) r;
|
||||
}
|
||||
|
||||
static inline __nv_bfloat16 __float2bfloat16_rn(float f) { return strata_f32_to_bf16_rn(f); }
|
||||
static inline uint16_t __bfloat16_as_ushort(__nv_bfloat16 v) { return v; }
|
||||
|
||||
// E4M3 (no infinities, max 448), round to nearest even - the inverse of the half conversion the engine also uses
|
||||
static inline __nv_fp8_storage_t __nv_cvt_float_to_fp8(float f, int, int) {
|
||||
if (f != f) return 0x7f;
|
||||
float a = f < 0 ? -f : f;
|
||||
int sign = f < 0 ? 0x80 : 0;
|
||||
if (a == 0.0f) return (__nv_fp8_storage_t) sign;
|
||||
if (a >= 448.0f) return (__nv_fp8_storage_t) (sign | 0x7e);
|
||||
int e = 0;
|
||||
while (a >= 2.0f) { a *= 0.5f; ++e; }
|
||||
while (a < 1.0f) { a *= 2.0f; --e; }
|
||||
// a in [1,2): 3 mantissa bits, bias 7
|
||||
float scaled = (a - 1.0f) * 8.0f;
|
||||
int m = (int) (scaled + 0.5f);
|
||||
int biased = e + 7;
|
||||
if (m == 8) { m = 0; ++biased; }
|
||||
if (biased <= 0) return (__nv_fp8_storage_t) sign; // subnormal -> zero here
|
||||
if (biased >= 15) return (__nv_fp8_storage_t) (sign | 0x7e); // clamp to 448
|
||||
return (__nv_fp8_storage_t) (sign | (biased << 3) | m);
|
||||
}
|
||||
static inline uint16_t __nv_cvt_fp8_to_halfraw(__nv_fp8_storage_t v, int) {
|
||||
// half (IEEE binary16) from E4M3: the engine only reads these back for the vision tower's tables
|
||||
const int sign = (v & 0x80) ? 0x8000 : 0;
|
||||
const int e = (v >> 3) & 0xf, m = v & 0x7;
|
||||
if (e == 0 && m == 0) return (uint16_t) sign;
|
||||
int he, hm;
|
||||
if (e == 0) { he = 0; hm = m << 7; }
|
||||
else { he = e - 7 + 15; hm = m << 7; }
|
||||
return (uint16_t) (sign | (he << 10) | hm);
|
||||
}
|
||||
|
||||
// the conversion modes __nv_cvt_float_to_fp8 / __nv_cvt_fp8_to_halfraw take (CUDA spells them as an enum)
|
||||
enum __nv_fp8_interpretation_t { __NV_E4M3 = 0, __NV_E5M2 = 1 };
|
||||
enum __nv_saturation_t { __NV_NOSAT = 0, __NV_SATFINITE = 1 };
|
||||
@@ -0,0 +1,165 @@
|
||||
#ifndef __grid_constant__
|
||||
#define __grid_constant__
|
||||
#endif
|
||||
#ifndef __device_builtin__
|
||||
#define __device_builtin__
|
||||
#endif
|
||||
|
||||
// cuda_device_builtins.h - the CUDA device built-ins, declared for the HOST compile only.
|
||||
//
|
||||
// The DeepSeek kernels are compiled twice in this port: as C++ here, to enumerate exactly which API the device code uses,
|
||||
// and as MSL (kernels/*.metal) where the real work happens. These declarations let the first pass run; nothing here is a
|
||||
// working kernel. Metal spells the same things thread_position_in_grid, threadgroup_barrier, simd_shuffle and so on.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
struct dim3 {
|
||||
unsigned x, y, z;
|
||||
dim3(unsigned _x = 1, unsigned _y = 1, unsigned _z = 1) : x(_x), y(_y), z(_z) {}
|
||||
};
|
||||
struct strata_dim3 { unsigned x, y, z; };
|
||||
static strata_dim3 threadIdx{0, 0, 0}, blockIdx{0, 0, 0}, blockDim{1, 1, 1}, gridDim{1, 1, 1};
|
||||
|
||||
static inline void __syncthreads() {}
|
||||
static inline void __syncwarp(unsigned = 0xffffffffu) {}
|
||||
static inline unsigned __ballot_sync(unsigned, int) { return 0; }
|
||||
static inline int __popc(unsigned v) { return __builtin_popcount(v); }
|
||||
static inline int __ffs(unsigned v) { return __builtin_ffs((int) v); }
|
||||
|
||||
// the bit and integer intrinsics the block decoders use. Each is the exact CUDA semantics, computed on the host here; the
|
||||
// MSL kernels implement the same operations with Metal's own select/rotate/permute built-ins.
|
||||
static inline unsigned __byte_perm(unsigned x, unsigned y, unsigned s) {
|
||||
unsigned out = 0;
|
||||
const unsigned long long src = ((unsigned long long) y << 32) | x;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const unsigned sel = (s >> (4 * i)) & 0x7u;
|
||||
out |= (unsigned) ((src >> (8 * sel)) & 0xffu) << (8 * i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
static inline int __mul24(int a, int b) { return a * b; }
|
||||
static inline unsigned __umul24(unsigned a, unsigned b) { return a * b; }
|
||||
static inline unsigned __brev(unsigned v) { return __builtin_bitreverse32(v); }
|
||||
static inline int __clz(int v) { return v ? __builtin_clz((unsigned) v) : 32; }
|
||||
static inline int __clzll(long long v) { return v ? __builtin_clzll((unsigned long long) v) : 64; }
|
||||
static inline float __fdividef(float a, float b) { return a / b; }
|
||||
static inline float __saturatef(float a) { return a < 0.0f ? 0.0f : (a > 1.0f ? 1.0f : a); }
|
||||
static inline float __int_as_float(int v) { float f; __builtin_memcpy(&f, &v, 4); return f; }
|
||||
static inline int __float_as_int(float f) { int v; __builtin_memcpy(&v, &f, 4); return v; }
|
||||
static inline unsigned __float_as_uint(float f) { unsigned v; __builtin_memcpy(&v, &f, 4); return v; }
|
||||
static inline float __uint_as_float(unsigned v) { float f; __builtin_memcpy(&f, &v, 4); return f; }
|
||||
static inline void __threadfence() {}
|
||||
static inline void __threadfence_block() {}
|
||||
static inline float atomicAdd(float* p, float v) { const float o = *p; *p = o + v; return o; }
|
||||
static inline int atomicAdd(int* p, int v) { const int o = *p; *p = o + v; return o; }
|
||||
static inline unsigned atomicAdd(unsigned* p, unsigned v) { const unsigned o = *p; *p = o + v; return o; }
|
||||
static inline int atomicMax(int* p, int v) { const int o = *p; if (v > o) *p = v; return o; }
|
||||
static inline int atomicCAS(int* p, int c, int v) { const int o = *p; if (o == c) *p = v; return o; }
|
||||
|
||||
// the SIMD video instructions (packed 8-bit lanes) the i-quant block decoders use: on CUDA these are one instruction, on
|
||||
// Apple Silicon NEON has them directly (vsubq_u8, vpaddlq / vdotq_s32) and the MSL kernels use those. Here they are the
|
||||
// exact per-lane semantics, so the host compile reproduces the same numbers.
|
||||
static inline unsigned __vsub4(unsigned a, unsigned b) {
|
||||
unsigned out = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const int d = (int) ((a >> (8 * i)) & 0xffu) - (int) ((b >> (8 * i)) & 0xffu);
|
||||
out |= (unsigned) ((d < 0 ? 0 : d) & 0xff) << (8 * i); // unsigned saturated
|
||||
}
|
||||
return out;
|
||||
}
|
||||
static inline unsigned __vsubss4(unsigned a, unsigned b) {
|
||||
unsigned out = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
int d = (int) ((a >> (8 * i)) & 0xffu) - (int) ((b >> (8 * i)) & 0xffu);
|
||||
if (d < -128) d = -128;
|
||||
if (d > 127) d = 127;
|
||||
out |= (unsigned) (d & 0xff) << (8 * i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
static inline unsigned __vadd4(unsigned a, unsigned b) {
|
||||
unsigned out = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const int d = (int) ((a >> (8 * i)) & 0xffu) + (int) ((b >> (8 * i)) & 0xffu);
|
||||
out |= (unsigned) ((d > 255 ? 255 : d) & 0xff) << (8 * i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
static inline unsigned __vaddss4(unsigned a, unsigned b) {
|
||||
unsigned out = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
int d = (int) ((a >> (8 * i)) & 0xffu) + (int) ((b >> (8 * i)) & 0xffu);
|
||||
if (d > 127) d = 127;
|
||||
out |= (unsigned) (d & 0xff) << (8 * i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
static inline unsigned __vcmpeq4(unsigned a, unsigned b) {
|
||||
unsigned out = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const unsigned ea = (a >> (8 * i)) & 0xffu, eb = (b >> (8 * i)) & 0xffu;
|
||||
out |= (ea == eb ? 0xffu : 0u) << (8 * i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
static inline unsigned __vcmpgtu4(unsigned a, unsigned b) {
|
||||
unsigned out = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const unsigned ea = (a >> (8 * i)) & 0xffu, eb = (b >> (8 * i)) & 0xffu;
|
||||
out |= (ea > eb ? 0xffu : 0u) << (8 * i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
static inline int __dp4a(unsigned a, unsigned b, int c) {
|
||||
for (int i = 0; i < 4; ++i) c += (int) (signed char) ((a >> (8 * i)) & 0xffu) * (int) (signed char) ((b >> (8 * i)) & 0xffu);
|
||||
return c;
|
||||
}
|
||||
|
||||
// the remaining scalar math helpers CUDA provides and the decoders use (round-to-nearest, plus min/max for integers - CUDA
|
||||
// brings those in from its own headers, the C++ standard library does not for every type)
|
||||
static inline float __fsub_rn(float a, float b) { return a - b; }
|
||||
static inline float __fdiv_rn(float a, float b) { return a / b; }
|
||||
static inline float __frcp_rn_(float a) { return 1.0f / a; }
|
||||
template <typename T> static inline T strata_min(T a, T b) { return a < b ? a : b; }
|
||||
template <typename T> static inline T strata_max(T a, T b) { return a > b ? a : b; }
|
||||
|
||||
// the last names the kernel sources use (a full-list compile found twelve families, 2026-10-23): the grid type above, a
|
||||
// system fence, the funnel shift, the hardware transcendentals and the read-only/volatile loads.
|
||||
static inline void __threadfence_system() {}
|
||||
static inline unsigned __funnelshift_r(unsigned lo, unsigned hi, unsigned shift) {
|
||||
const unsigned long long v = ((unsigned long long) hi << 32) | lo;
|
||||
return (unsigned) ((v >> (shift & 31)) & 0xffffffffu);
|
||||
}
|
||||
static inline unsigned __funnelshift_l(unsigned lo, unsigned hi, unsigned shift) {
|
||||
const unsigned long long v = ((unsigned long long) hi << 32) | lo;
|
||||
return (unsigned) (((v << (shift & 31)) >> 32) & 0xffffffffu);
|
||||
}
|
||||
static inline float rsqrtf(float a) { return 1.0f / __builtin_sqrtf(a); }
|
||||
static inline float __expf(float a) { return __builtin_expf(a); }
|
||||
static inline float __logf(float a) { return __builtin_logf(a); }
|
||||
static inline float __sinf(float a) { return __builtin_sinf(a); }
|
||||
static inline float __cosf(float a) { return __builtin_cosf(a); }
|
||||
static inline void sincosf(float a, float* s, float* c) { *s = __builtin_sinf(a); *c = __builtin_cosf(a); }
|
||||
static inline int __float2int_rn(float a) { return (int) __builtin_roundf(a); }
|
||||
static inline int __float2int_rz(float a) { return (int) a; }
|
||||
static inline int __float2int_rd(float a) { return (int) __builtin_floorf(a); }
|
||||
static inline int __float2int_ru(float a) { return (int) __builtin_ceilf(a); }
|
||||
template <typename T> static inline T __ldg(const T* p) { return *p; }
|
||||
template <typename T> static inline T __ldcv(const T* p) { return *p; }
|
||||
|
||||
// shuffles: one template each, so int/unsigned/float/short all bind (CUDA has overloads; a template is the same thing here)
|
||||
template <typename T> static inline T __shfl_xor_sync(unsigned, T v, int) { return v; }
|
||||
template <typename T> static inline T __shfl_down_sync(unsigned, T v, int) { return v; }
|
||||
template <typename T> static inline T __shfl_up_sync(unsigned, T v, int) { return v; }
|
||||
template <typename T> static inline T __shfl_sync(unsigned, T v, int) { return v; }
|
||||
|
||||
// the integer multiply-high and the bitwise atomics the sampler uses
|
||||
static inline unsigned __umulhi(unsigned a, unsigned b) { return (unsigned) (((unsigned long long) a * b) >> 32); }
|
||||
static inline int __mulhi(int a, int b) { return (int) (((long long) a * b) >> 32); }
|
||||
static inline unsigned atomicOr(unsigned* p, unsigned v) { const unsigned o = *p; *p = o | v; return o; }
|
||||
static inline unsigned atomicAnd(unsigned* p, unsigned v) { const unsigned o = *p; *p = o & v; return o; }
|
||||
static inline unsigned atomicXor(unsigned* p, unsigned v) { const unsigned o = *p; *p = o ^ v; return o; }
|
||||
static inline unsigned atomicExch(unsigned* p, unsigned v) { const unsigned o = *p; *p = v; return o; }
|
||||
static inline unsigned atomicInc(unsigned* p, unsigned v) { const unsigned o = *p; *p = (o >= v ? 0 : o + 1); return o; }
|
||||
static inline unsigned atomicDec(unsigned* p, unsigned v) { const unsigned o = *p; *p = (o == 0 || o > v ? v : o - 1); return o; }
|
||||
@@ -0,0 +1,99 @@
|
||||
// metal/include/metal_compat/cuda_fp16.h - the half-precision surface the dsv41 sources use, on a Mac.
|
||||
//
|
||||
// The engine touches exactly these (counted by compiling src/dsv41/*.cu against this directory, M5 Max 2026-10-23):
|
||||
// __half 16, __half2 11, __half_raw 3, __half_as_ushort 8,
|
||||
// __float2half_rn 17, __half2float 18, __half22float2 6,
|
||||
// the round-to-nearest float helpers (__fmul_rn, __fadd_rn, __fmaf_rn, __frcp_rn, __fsqrt_rn),
|
||||
// and the plain half/half2 typedefs ggml-common.h uses.
|
||||
// On Apple Silicon half is IEEE binary16 too, so the storage is a uint16_t and the conversions are the standard
|
||||
// round-to-nearest-even ones - the same bits CUDA produces.
|
||||
#pragma once
|
||||
|
||||
#include "vector_types.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
struct __half_raw {
|
||||
uint16_t x;
|
||||
__half_raw() = default;
|
||||
constexpr __half_raw(uint16_t bits) : x(bits) {}
|
||||
};
|
||||
|
||||
struct __half {
|
||||
uint16_t x;
|
||||
__half() = default;
|
||||
__half(const __half_raw& r) : x(r.x) {}
|
||||
operator __half_raw() const { return __half_raw{x}; }
|
||||
};
|
||||
|
||||
struct __half2 {
|
||||
uint16_t x, y;
|
||||
};
|
||||
|
||||
typedef __half half;
|
||||
typedef __half2 half2;
|
||||
|
||||
static inline uint16_t strata_f32_to_f16_bits(float f) {
|
||||
uint32_t u;
|
||||
std::memcpy(&u, &f, sizeof u);
|
||||
const uint32_t sign = (u >> 16) & 0x8000u;
|
||||
const uint32_t mag = u & 0x7fffffffu;
|
||||
if (mag >= 0x7f800000u) return (uint16_t) (sign | (mag > 0x7f800000u ? 0x7e00u : 0x7c00u));
|
||||
if (mag >= 0x477ff000u) return (uint16_t) (sign | 0x7c00u);
|
||||
const uint32_t r = (mag + 0x0fffu + ((mag >> 13) & 1u)) >> 13;
|
||||
if (r >= 0x7c00u) return (uint16_t) (sign | 0x7c00u);
|
||||
return (uint16_t) (sign | r);
|
||||
}
|
||||
|
||||
static inline float strata_f16_bits_to_f32(uint16_t h) {
|
||||
const uint32_t sign = (uint32_t) (h & 0x8000u) << 16;
|
||||
const uint32_t e = (h >> 10) & 0x1fu, m = h & 0x3ffu;
|
||||
uint32_t bits;
|
||||
if (e == 0) {
|
||||
if (m == 0) bits = sign;
|
||||
else {
|
||||
const float v = (float) m * (1.0f / 16777216.0f);
|
||||
return sign ? -v : v;
|
||||
}
|
||||
} else if (e == 31) {
|
||||
bits = sign | 0x7f800000u | (m << 13);
|
||||
} else {
|
||||
bits = sign | ((e - 15 + 127) << 23) | (m << 13);
|
||||
}
|
||||
float out;
|
||||
std::memcpy(&out, &bits, sizeof out);
|
||||
return out;
|
||||
}
|
||||
|
||||
static inline __half __float2half_rn(float f) { __half h; h.x = strata_f32_to_f16_bits(f); return h; }
|
||||
static inline __half __float2half(float f) { return __float2half_rn(f); }
|
||||
static inline float __half2float(__half h) { return strata_f16_bits_to_f32(h.x); }
|
||||
static inline uint16_t __half_as_ushort(__half h) { return h.x; }
|
||||
static inline __half __ushort_as_half(uint16_t u) { __half h; h.x = u; return h; }
|
||||
static inline float2 __half22float2(__half2 h) { return make_float2(strata_f16_bits_to_f32(h.x), strata_f16_bits_to_f32(h.y)); }
|
||||
static inline __half2 __floats2half2_rn(float a, float b) {
|
||||
__half2 h;
|
||||
h.x = strata_f32_to_f16_bits(a);
|
||||
h.y = strata_f32_to_f16_bits(b);
|
||||
return h;
|
||||
}
|
||||
|
||||
// round-to-nearest float helpers: the FP ops here are IEEE and un-fused, which is what these mean
|
||||
static inline float __fmul_rn(float a, float b) { return a * b; }
|
||||
static inline float __fadd_rn(float a, float b) { return a + b; }
|
||||
static inline float __fmaf_rn(float a, float b, float c) { return a * b + c; }
|
||||
static inline float __frcp_rn(float a) { return 1.0f / a; }
|
||||
static inline float __fsqrt_rn(float a) { return __builtin_sqrtf(a); }
|
||||
|
||||
// the half2 pack/unpack and arithmetic helpers the trellis and vision paths use
|
||||
static inline __half2 __halves2half2(__half lo, __half hi) { __half2 h; h.x = lo.x; h.y = hi.x; return h; }
|
||||
static inline __half2 __half2half2(__half v) { __half2 h; h.x = v.x; h.y = v.x; return h; }
|
||||
static inline __half __low2half(__half2 h) { __half o; o.x = h.x; return o; }
|
||||
static inline __half __high2half(__half2 h) { __half o; o.x = h.y; return o; }
|
||||
static inline float __low2float(__half2 h) { return strata_f16_bits_to_f32(h.x); }
|
||||
static inline float __high2float(__half2 h) { return strata_f16_bits_to_f32(h.y); }
|
||||
static inline __half2 __hadd2(__half2 a, __half2 b) { return __floats2half2_rn(__half2float(__low2half(a)) + __half2float(__low2half(b)), __half2float(__high2half(a)) + __half2float(__high2half(b))); }
|
||||
static inline __half2 __hmul2(__half2 a, __half2 b) { return __floats2half2_rn(__half2float(__low2half(a)) * __half2float(__low2half(b)), __half2float(__high2half(a)) * __half2float(__high2half(b))); }
|
||||
static inline __half2 __hfma2(__half2 a, __half2 b, __half2 c) { return __floats2half2_rn(__half2float(__low2half(a)) * __half2float(__low2half(b)) + __half2float(__low2half(c)), __half2float(__high2half(a)) * __half2float(__high2half(b)) + __half2float(__high2half(c))); }
|
||||
static inline __half2 __hneg2(__half2 a) { __half2 h; h.x = a.x ^ 0x8000u; h.y = a.y ^ 0x8000u; return h; }
|
||||
@@ -0,0 +1,4 @@
|
||||
// metal/include/metal_compat/cuda_fp8.h - the fp8 names the engine includes; the definitions live beside the bf16 ones.
|
||||
#pragma once
|
||||
|
||||
#include "cuda_bf16.h"
|
||||
@@ -0,0 +1,31 @@
|
||||
// cuda_launch.h - the <<<grid, block, shared, stream>>> launch syntax, as a call.
|
||||
//
|
||||
// A C++ compiler cannot parse CUDA's launch syntax, and the Metal port replaces every launch with a dispatch anyway (the grid
|
||||
// becomes a threadgroup count, the block a threadgroup size). strata_ds4_launch() is that call: the kernel first, then the
|
||||
// launch shape, then the kernel's own arguments - every parameter generic, because the shapes vary (dim3, plain ints, sizes,
|
||||
// and the stream type), and the point of this header is to make the sources parse, not to type-check the port.
|
||||
//
|
||||
// Until a kernel has a Metal implementation the call stops the run naming the kernel, so the engine reports the first one it
|
||||
// needs and the port proceeds one kernel at a time.
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
template <typename K, typename... A>
|
||||
static inline void strata_ds4_launch(K kernel, A&&...) {
|
||||
static_assert(sizeof(K) > 0, "kernel");
|
||||
std::fprintf(stderr, "strata metal: a dsv41 kernel launch has no Metal dispatch yet\n");
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// the call sites sit inside namespaces whose first argument would win argument-dependent lookup: force the global one
|
||||
// the call sites carry their own location, so a run names the exact launch that needs a dispatch
|
||||
#define strata_ds4_launch(...) ::strata_ds4_launch_at(__FILE__, __LINE__, __VA_ARGS__)
|
||||
|
||||
// the macro adds the call site's location, so a run names the exact launch that still needs a Metal dispatch
|
||||
template <typename K, typename... A>
|
||||
static inline void strata_ds4_launch_at(const char* file, int line, K, A&&...) {
|
||||
std::fprintf(stderr, "strata metal: kernel launch with no Metal dispatch at %s:%d\n", file, line);
|
||||
std::abort();
|
||||
}
|
||||
@@ -1,3 +1,32 @@
|
||||
// The CUDA function-space qualifiers and the few launch/kernel spellings the shared sources use. On Metal they say
|
||||
#include "cuda_launch.h"
|
||||
// nothing: a "kernel" here is an MSL function compiled at run time and dispatched by the host runtime, so the qualifiers
|
||||
// are empty and __forceinline__ is plain inline. Part of the metal_compat layer (M5 Max, 2026-10-23).
|
||||
#ifndef __host__
|
||||
#define __host__
|
||||
#endif
|
||||
#ifndef __device__
|
||||
#define __device__
|
||||
#endif
|
||||
#ifndef __global__
|
||||
#define __global__
|
||||
#endif
|
||||
#ifndef __shared__
|
||||
#define __shared__
|
||||
#endif
|
||||
#ifndef __constant__
|
||||
#define __constant__
|
||||
#endif
|
||||
#ifndef __forceinline__
|
||||
#define __forceinline__ inline
|
||||
#endif
|
||||
#ifndef __launch_bounds__
|
||||
#define __launch_bounds__(...)
|
||||
#endif
|
||||
#ifndef __restrict__
|
||||
#define __restrict__
|
||||
#endif
|
||||
|
||||
// metal/include/metal_compat/cuda_runtime.h - the CUDA runtime names the host files use, on Metal.
|
||||
//
|
||||
// The HIP build's approach (include/strata/hip_compat/cuda_runtime.h maps every cudaX the host code calls to hipX),
|
||||
@@ -8,6 +37,27 @@
|
||||
// uses another one fails to compile on Metal, which is the point.
|
||||
#pragma once
|
||||
|
||||
// CUDA's alignment qualifier
|
||||
#ifndef __align__
|
||||
#define __align__(n) alignas(n)
|
||||
#endif
|
||||
|
||||
#include "cuda_device_builtins.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
using std::fmaxf;
|
||||
using std::fminf;
|
||||
using std::sqrtf;
|
||||
using std::fabsf;
|
||||
using std::powf;
|
||||
using std::expf;
|
||||
using std::logf;
|
||||
|
||||
// CUDA's headers put min/max in scope for integers; the sources rely on it
|
||||
template <typename T> static inline T min(T a, T b) { return strata_min(a, b); }
|
||||
template <typename T> static inline T max(T a, T b) { return strata_max(a, b); }
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
@@ -45,6 +95,12 @@ inline constexpr unsigned cudaEventDefault = strata::gpu::kEventDefault;
|
||||
inline constexpr unsigned cudaEventDisableTiming = strata::gpu::kEventDisableTiming;
|
||||
inline constexpr unsigned cudaEventBlockingSync = strata::gpu::kEventBlockingSync;
|
||||
inline constexpr unsigned cudaStreamNonBlocking = strata::gpu::kStreamNonBlocking;
|
||||
enum cudaStreamCaptureStatus { cudaStreamCaptureStatusNone = 0, cudaStreamCaptureStatusActive = 1, cudaStreamCaptureStatusInvalidated = 2 };
|
||||
static inline strata::gpu::Error cudaStreamIsCapturing(strata::gpu::Stream, cudaStreamCaptureStatus* st) {
|
||||
if (st) *st = cudaStreamCaptureStatusNone;
|
||||
return strata::gpu::kSuccess;
|
||||
}
|
||||
|
||||
enum cudaStreamCaptureMode { cudaStreamCaptureModeGlobal = 0, cudaStreamCaptureModeThreadLocal = 1, cudaStreamCaptureModeRelaxed = 2 };
|
||||
// (the compute capability is asked by the 0.1.45 Turing defaults in verify.cpp: no answer here, which reads as "not a Turing card")
|
||||
enum cudaDeviceAttr {
|
||||
@@ -79,6 +135,11 @@ inline cudaError_t cudaDeviceEnablePeerAccess(int p, unsigned f) { return strata
|
||||
inline cudaError_t cudaDeviceDisablePeerAccess(int) { return cudaSuccess; } // a Mac has one GPU: nothing to disable
|
||||
|
||||
// ---- memory and copies
|
||||
// the deferred command buffer mtl_rt batches the port's dispatches into: a copy (or any host read of a device result)
|
||||
// has to commit it first, or it reads memory the GPU has not written yet. Defined in ds4_finish.cpp.
|
||||
extern "C" void ds4_metal_flush();
|
||||
extern "C" void ds4_metal_kick();
|
||||
|
||||
template <class T> inline cudaError_t cudaMalloc(T** p, size_t n) { return strata::gpu::malloc_device(p, n); }
|
||||
inline cudaError_t cudaFree(void* p) { return strata::gpu::free_device(p); }
|
||||
template <class T> inline cudaError_t cudaHostAlloc(T** p, size_t n, unsigned f) { return strata::gpu::host_alloc(p, n, f); }
|
||||
@@ -141,3 +202,17 @@ inline cudaError_t cudaGraphLaunch(cudaGraphExec_t x, cudaStream_t s) { return s
|
||||
inline cudaError_t cudaGraphUpload(cudaGraphExec_t x, cudaStream_t s) { return strata::gpu::graph_upload(x, s); }
|
||||
inline cudaError_t cudaGraphDestroy(cudaGraph_t g) { return strata::gpu::graph_destroy(g); }
|
||||
inline cudaError_t cudaGraphExecDestroy(cudaGraphExec_t x) { return strata::gpu::graph_exec_destroy(x); }
|
||||
|
||||
// cudaMemcpy2D(dst, dpitch, src, spitch, width, height, kind): a strided copy, used by the vision tower's patch rows and
|
||||
// the Engram tables' column slices. The Metal runtime has no pitched copy yet, so this reports failure rather than
|
||||
// copying nothing silently.
|
||||
inline strata::gpu::Error cudaMemcpy2D(void*, size_t, const void*, size_t, size_t, size_t, strata::gpu::CopyKind) {
|
||||
return strata::gpu::kErrorUnsupported; // cudaMemcpy2D_unimplemented_marker
|
||||
}
|
||||
|
||||
// occupancy query: the Metal runtime decides its own threadgroup limits, so answer "one block" and let the caller size grids
|
||||
template <typename K, typename... A>
|
||||
static inline strata::gpu::Error cudaOccupancyMaxActiveBlocksPerMultiprocessor(int* n, K&&, int, A&&...) {
|
||||
if (n) *n = 1;
|
||||
return strata::gpu::kSuccess;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// metal/include/metal_compat/ds4_metal_host.h - the dsv41 kernels that have a Metal dispatch.
|
||||
//
|
||||
// GENERATED from metal/src/engine/ds4_host.cpp and ds4_finish.cpp, which are the source of truth: do not hand-edit the
|
||||
// declaration block, regenerate it (see RIGS/m1002.md).
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace strata::gpu {
|
||||
struct StreamImpl;
|
||||
using Stream = StreamImpl*;
|
||||
} // namespace strata::gpu
|
||||
|
||||
namespace strata::dsv41::metal_host {
|
||||
|
||||
// ds4_kernels.cu:1894 finish_kernel<false> - the engine's Finish, as ds4_kernels.cuh declares it
|
||||
struct FinishIn {
|
||||
const float* w;
|
||||
float eps;
|
||||
int rd, pos, inverse, vpp, pos_step;
|
||||
const float* freqs;
|
||||
int quant;
|
||||
const uint16_t* dst;
|
||||
long long dst_stride;
|
||||
uint8_t* pdst;
|
||||
long long pdst_stride;
|
||||
};
|
||||
|
||||
bool embed(const uint16_t* rows, int T, float* h, int dim, int hc, void*);
|
||||
bool engram_dequant_tok(const uint8_t* src, size_t per, int cols, int T, int hd, float* out, int fmt);
|
||||
bool ext_to_ring(const void* ext, void* ring, long long p0, int n, int T, int d, size_t row_bytes, void* backup);
|
||||
bool finish(float* x, int n_vec, int len, long long stride, const FinishIn& f);
|
||||
bool gemv_f32(const float* W, const float* x, int n, int k, int T, float* y, int ldy);
|
||||
bool hc_mix(const float* h, const float* fn, int hcdim, int n_mix, int T, float eps, float* mixes);
|
||||
bool hc_mix_scale(const float* h, int hcdim, int n_mix, int T, float eps, float* mixes);
|
||||
bool hc_post(const float* y, float* h, const float* post, const float* comb, int dim, int hc, int T);
|
||||
bool hc_post_v(const float* y, const float* res, const float* post, const float* comb, int dim, int hc, int T, float* out);
|
||||
bool hc_pre_norm(const float* h, const float* mix, const float* w, int dim, int hc, int T, float eps, float* x);
|
||||
bool hc_split(const float* mixes, const float* scale, const float* base, int hc, int iters, float hc_eps, int T, float* pre, float* post, float* comb);
|
||||
bool hc_mix_split(const float* h, const float* fn, int hcdim, int n_mix, int T, float eps, float* mixes, const float* scale, const float* base, int hc, int iters, float hc_eps, float* pre, float* post, float* comb);
|
||||
bool init_pre(float* pre, int T, void*);
|
||||
bool iq_mv(const void* jobs, int nj);
|
||||
bool moe_combine(void* acc, const void* y, const int* pos, const float* w_, int T, int k, int r0, int r1, int dim);
|
||||
bool moe_sort(const int* idx, int T, int k, const int* vs, int n_vs, int* bounds, int* row_tok, int* pos);
|
||||
bool moe_win_group_cpu(const void* plan_ptr, void* grp_ptr, void* grp_start, void* grp_ent, void* n_groups, int T, int k);
|
||||
bool mx_mv(const void* jobs, int nj, int max_n, size_t job_bytes, int fmt);
|
||||
bool q8_quant_rows(const float* x, size_t ldx, int rows, int k, int8_t* q, float* d);
|
||||
bool ring_to_ext(const void* ring, void* ext, long long p0, int n, int d, size_t row_bytes);
|
||||
bool route_publish(const float* logits, const float* bias, int n, int k, int T, float route_scale, const void* vram, int* idx, float* w, void* plan_ptr, const float* bias_vl, unsigned vl);
|
||||
bool sgemm_tn(const float* A, const float* B, float* C, int m, int nt, int k, int lda, int ldb, int ldc);
|
||||
bool rows_f16(const float* x, int ldx, const int* idx, int nrows, int ncols, uint16_t* out);
|
||||
uint64_t zc_map(const void* p, size_t bytes); // zero-copy weights: the GPU address of host memory, 0 if not mappable
|
||||
bool q8_quant_multi(const void* specs, int n); // {const float* x; uint64 ldx; uint32 rows, k; int8* q; float* d} each
|
||||
bool iq_set_gu(const uint8_t* wbase, size_t stride, const size_t* tr, const int* hbounds, int n_exp, int total_rows, int n_embd, int n_ff, float limit, const float* xf, int ldx, const int* idx, uint16_t* h, int ldh, int tg, int td);
|
||||
bool iq_set_dn(const uint8_t* wbase, size_t stride, const size_t* tr, const int* hbounds, int n_exp, int total_rows, int n_embd, int n_ff, float limit, const uint16_t* h16, int ldh, float* y, int ldy, int tg, int td);
|
||||
bool dequant_rows(int kind, const void* raw, size_t ld, int k, int n, uint16_t* out);
|
||||
bool gemm_f16_nt(const uint16_t* W16, const uint16_t* X16, int ldx, float* Y, int ldy, int rows, int N, int K, float alpha, float beta);
|
||||
bool swiglu(const float* g, const float* u, int n, float lim, float* h);
|
||||
bool unpack_sel(const void* ckv, const int* sel, int sel_stride, int gen, long long p0, int t0, int R, int kcap, const int* nsel, int cap, void* out, int T, int nsel_count, size_t row_bytes);
|
||||
|
||||
// generated from ds4_host.cpp and ds4_finish.cpp
|
||||
bool block_max(const float* score, long long stride, int T, const void* rows, int blk, float* bm, long long bm_stride, int nb_max);
|
||||
bool combine(const float* ed, const float* cpu, const float* sh, const void* c, int T, int dim, float* out);
|
||||
bool compress_win(const float* kv, const float* score, float* kv_state, float* score_state, int ratio, int p0, int T, int d, const float* w, float eps, float* latent);
|
||||
bool confidence(const float* x, const uint16_t* E, const int* tok, const float* w, int T, int dim, int R, float* out);
|
||||
bool embed(const uint16_t* rows, int T, float* h, int dim, int hc, void*);
|
||||
bool embed_addr(const uint16_t* rows, int T, float* h, int dim, int hc);
|
||||
bool engram_dequant(const uint8_t* rows, const uint8_t* scales, int n, int hd, float* out, int fmt);
|
||||
bool engram_dequant_tok(const uint8_t* src, size_t per, int cols, int T, int hd, float* out, int fmt);
|
||||
bool engram_gate(float* h, const float* kv, const float* w, int hc, int dim, int T, float eps);
|
||||
bool ext_to_ring(const void* ext, void* ring, long long p0, int n, int T, int d, size_t row_bytes, void* backup);
|
||||
bool finish(float* x, int n_vec, int len, long long stride, const FinishIn& f);
|
||||
bool gather_rows(const float* x, const void* g, int n, int cols, float* out);
|
||||
bool gemv_f16_rows(const uint16_t* W, const float* x, size_t ldx, int n, int k, int T, float alpha, float* y, size_t ldy);
|
||||
bool gemv_f32(const float* W, const float* x, int n, int k, int T, float* y, int ldy);
|
||||
bool hc_mean(const float* h, int T, int hc, int dim, float* out, long long out_stride);
|
||||
bool hc_mix(const float* h, const float* fn, int hcdim, int n_mix, int T, float eps, float* mixes);
|
||||
bool hc_mix_scale(const float* h, int hcdim, int n_mix, int T, float eps, float* mixes);
|
||||
bool hc_post(const float* y, float* h, const float* post, const float* comb, int dim, int hc, int T);
|
||||
bool hc_post_v(const float* y, const float* res, const float* post, const float* comb, int dim, int hc, int T, float* out);
|
||||
bool hc_pre_norm(const float* h, const float* mix, const float* w, int dim, int hc, int T, float eps, float* x);
|
||||
bool hc_split(const float* mixes, const float* scale, const float* base, int hc, int iters, float hc_eps, int T, float* pre, float* post, float* comb);
|
||||
bool hc_mix_split(const float* h, const float* fn, int hcdim, int n_mix, int T, float eps, float* mixes, const float* scale, const float* base, int hc, int iters, float hc_eps, float* pre, float* post, float* comb);
|
||||
bool index_score(const float* q, const float* wq, const void* kc, int heads, int hd, int T, int n, const int* cand, int cand_stride, int blk, const void* rows, float* out, long long out_stride, int kfmt);
|
||||
bool init_pre(float* pre, int T, void*);
|
||||
bool iota_rows(int* out, int T, int n, int stride);
|
||||
bool iq_dn(const unsigned long long* plan_ptr, const unsigned long long* grp_ptr, const int* grp_start, const int* grp_ent, const int* n_groups, const size_t* tr, int n_embd, int n_ff, const int8_t* hq, const float* hd, float* part, int type, int groups);
|
||||
bool iq_gu(const unsigned long long* plan_ptr, const unsigned long long* grp_ptr, const int* grp_start, const int* grp_ent, const int* n_groups, const size_t* tr, int n_embd, int n_ff, float limit, const int8_t* xq, const float* xd, float* h, int type, int groups);
|
||||
bool iq_mv(const void* jobs, int nj);
|
||||
bool markov_add(const float* logits, const uint16_t* E, const uint16_t* H, const int* tok, int V, int R);
|
||||
bool moe_combine(void* acc, const void* y, const int* pos, const float* w_, int T, int k, int r0, int r1, int dim);
|
||||
bool moe_sort(const int* idx, int T, int k, const int* vs, int n_vs, int* bounds, int* row_tok, int* pos);
|
||||
bool moe_win_group_cpu(const void* plan_ptr, void* grp_ptr, void* grp_start, void* grp_ent, void* n_groups, int T, int k);
|
||||
bool mx_combine(const void* plan_ptr, const void* plan_w, const void* part, const void* cpu_part, size_t cpu_stride, const int* cpu_flag, const void* sh_out, void* out, size_t out_stride, int T, int k, int n_embd);
|
||||
bool mx_dn(const unsigned long long* plan_ptr, const unsigned long long* grp_ptr, const int* grp_start, const int* grp_ent, const int* n_groups, const size_t* tr, int n_embd, int n_ff, const int8_t* hq, const float* hd, float* part, int groups);
|
||||
bool mx_gu(const unsigned long long* plan_ptr, const unsigned long long* grp_ptr, const int* grp_start, const int* grp_ent, const int* n_groups, const size_t* tr, int n_embd, int n_ff, float limit, const int8_t* xq, const float* xd, float* h, int groups);
|
||||
bool mx_mv(const void* jobs, int nj, int max_n, size_t job_bytes, int fmt);
|
||||
bool q8_quant_rows(const float* x, size_t ldx, int rows, int k, int8_t* q, float* d);
|
||||
bool ring_to_ext(const void* ring, void* ext, long long p0, int n, int d, size_t row_bytes);
|
||||
bool route(const float* logits, const float* bias, int n, int k, int T, float route_scale, int* idx, float* w, const float* bias_vl, const void* vl);
|
||||
bool route_publish(const float* logits, const float* bias, int n, int k, int T, float route_scale, const void* vram, int* idx, float* w, void* plan_ptr, const float* bias_vl, unsigned vl);
|
||||
bool sample_greedy(const float* logits, int n_tokens, int n_vocab, const int* history, int history_len, int penalty_last_n, float penalty_repeat, float penalty_freq, float penalty_present, const float* logit_bias, int* out);
|
||||
bool sgemm_tn(const float* A, const float* B, float* C, int m, int nt, int k, int lda, int ldb, int ldc);
|
||||
bool sparse_attn(const float* q, const uint16_t* win, const void* ckv, const int* sel, int sel_stride, const void* rows, const float* sink, int heads, int d, int T, float scale, float* out, int ckv_fmt);
|
||||
bool swiglu(const float* g, const float* u, int n, float lim, float* h);
|
||||
bool topk(const float* vals, long long stride, int T, const void* rows, const int* cand, int cand_stride, int blk, int* out, int out_stride);
|
||||
bool unpack_sel(const void* ckv, const int* sel, int sel_stride, int gen, long long p0, int t0, int R, int kcap, const int* nsel, int cap, void* out, int T, int nsel_count, size_t row_bytes);
|
||||
|
||||
} // namespace strata::dsv41::metal_host
|
||||
Loaded 100 of 469 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user