mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 14:21:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cbcacbcae | ||
|
|
12d835dd61 | ||
|
|
89ae08e67f | ||
|
|
c4a329fefe | ||
|
|
e8e11a915f | ||
|
|
023dd01531 | ||
|
|
660e495e2d | ||
|
|
46d0136607 | ||
|
|
b1efb70c01 | ||
|
|
0c86bbecb6 | ||
|
|
118937c1ee | ||
|
|
9d9c9ca2a4 | ||
|
|
868f2ef0c0 | ||
|
|
3281ac326d | ||
|
|
8b5dd7adef | ||
|
|
3fd0460ba2 | ||
|
|
2715a75248 | ||
|
|
104a8485e9 | ||
|
|
5400f39a1f | ||
|
|
088e8a8220 | ||
|
|
fe4de5de85 | ||
|
|
44ffa86c53 | ||
|
|
822251bef6 | ||
|
|
ae3b249fd2 | ||
|
|
1690a4a38b | ||
|
|
16642363bd | ||
|
|
3ed6276cae | ||
|
|
58f91c144c | ||
|
|
ae93de6261 | ||
|
|
44b0ed4aa6 | ||
|
|
4a7a9871fb | ||
|
|
f53eb1b96a | ||
|
|
d1ec32b4ff | ||
|
|
bae372b9aa | ||
|
|
e9fc48f3f7 | ||
|
|
98b7ea944d | ||
|
|
3ce3358232 | ||
|
|
3b2e14cc11 | ||
|
|
c8c7e216ba | ||
|
|
ed15a3d04b | ||
|
|
1c59b7087f | ||
|
|
c7aec446e3 | ||
|
|
e02815a5e5 | ||
|
|
4f131fcdcc | ||
|
|
d034539560 | ||
|
|
07780dae1c | ||
|
|
4552a7f164 | ||
|
|
3772ded7c6 | ||
|
|
076108957c | ||
|
|
a3642eb4be | ||
|
|
c8a73fbf98 | ||
|
|
edc592b1ef | ||
|
|
4a5713fbb4 | ||
|
|
352cad8f0d | ||
|
|
ca83311616 | ||
|
|
3e4aa1d618 | ||
|
|
ac131a3af2 | ||
|
|
654f4af665 | ||
|
|
d69424c965 | ||
|
|
9c2be9fdb3 | ||
|
|
2fb9b6440a | ||
|
|
956fdfcbfb | ||
|
|
b55c48b29e | ||
|
|
edc0cb6490 | ||
|
|
4a9b9041c2 | ||
|
|
aaf99d3074 | ||
|
|
d4b8b6ad5c | ||
|
|
b211ba173a | ||
|
|
a733617558 | ||
|
|
ac7db2e522 | ||
|
|
8c4a6e1de7 | ||
|
|
87659162ed | ||
|
|
9227973b36 | ||
|
|
fa9bd5fbfa | ||
|
|
e2b32892a6 | ||
|
|
1bb5c72484 | ||
|
|
5e53c516b3 | ||
|
|
f07f060ae7 | ||
|
|
2467bb6899 | ||
|
|
9c9f639f25 | ||
|
|
1048d2392b | ||
|
|
ffaf958eaa | ||
|
|
1263ccdc91 | ||
|
|
8b2160888b | ||
|
|
07e0c26f94 | ||
|
|
d0289b88ba | ||
|
|
796bb8fc9c | ||
|
|
71faad4f4d | ||
|
|
ace0cfc866 | ||
|
|
e41463a995 | ||
|
|
0c9f21e257 | ||
|
|
aa17fe8648 | ||
|
|
f81876ecef | ||
|
|
025bf3b2ef | ||
|
|
9e3dbfe74a | ||
|
|
91cc8149b0 | ||
|
|
b5de344b73 | ||
|
|
9c139846d5 | ||
|
|
f71a128126 | ||
|
|
f1635c2174 | ||
|
|
458bcbff6f | ||
|
|
fea9ff5a4d | ||
|
|
65fc896329 | ||
|
|
1e3a9a4700 | ||
|
|
01f21ea683 | ||
|
|
5d83b431ba | ||
|
|
99a3fe8df9 | ||
|
|
972cad1bc5 | ||
|
|
771d5b73a7 | ||
|
|
b72ea678be | ||
|
|
fa228b094d | ||
|
|
e3336a13ba | ||
|
|
d81a57d5f4 | ||
|
|
6b2572e45f | ||
|
|
fd98b23849 | ||
|
|
12d6af092b | ||
|
|
2748e1471c | ||
|
|
534d07f5da | ||
|
|
d74e110f0e | ||
|
|
7984688e3c | ||
|
|
ce37456ccd | ||
|
|
43881f14a3 | ||
|
|
0f1390bd11 | ||
|
|
7fd67ddd6d | ||
|
|
a2ef9db5cd | ||
|
|
2e0f28e7d7 | ||
|
|
254415eac4 | ||
|
|
0d5b12b82e | ||
|
|
4f41b51c6b | ||
|
|
3982b2250a | ||
|
|
74d566bd1c | ||
|
|
bb19becc24 | ||
|
|
f60ae8d0a4 | ||
|
|
441a8f792a | ||
|
|
ebe1db1a0b | ||
|
|
066094c81f | ||
|
|
2d929a28b3 | ||
|
|
760c775434 | ||
|
|
cb33c0bac5 | ||
|
|
2de94bca5e | ||
|
|
fb57b6cd81 | ||
|
|
bea064293f | ||
|
|
7b80403c7b | ||
|
|
2ab6304867 | ||
|
|
8fb2dd34cb | ||
|
|
035da7f8c1 | ||
|
|
4f4c923cb2 | ||
|
|
f0b083efed | ||
|
|
62aaea4e14 | ||
|
|
621f84e62d | ||
|
|
5f6b7c2766 | ||
|
|
645cbb66e4 | ||
|
|
d215016f40 | ||
|
|
a751fbb128 | ||
|
|
92dc6f1de8 | ||
|
|
0fff115dfd | ||
|
|
3f8f333e92 | ||
|
|
d90028c755 | ||
|
|
3216d27118 | ||
|
|
028cb234a4 | ||
|
|
2bfac510d7 | ||
|
|
bfa532e97c | ||
|
|
8ba583fec2 | ||
|
|
d91f2960f7 | ||
|
|
82db014cc1 | ||
|
|
e57c907221 | ||
|
|
1e6cec80d1 | ||
|
|
5288cd2902 | ||
|
|
b437b8ea13 | ||
|
|
388db6b106 | ||
|
|
4cee400777 | ||
|
|
c8c79b17ea | ||
|
|
8cfb3fd7cc | ||
|
|
5ab99da482 | ||
|
|
abe8919c06 | ||
|
|
affb999c63 | ||
|
|
8e368af984 | ||
|
|
82579827e9 | ||
|
|
ee412cd776 | ||
|
|
378dc39348 | ||
|
|
0eacbaba81 | ||
|
|
c347326242 | ||
|
|
add41aef65 | ||
|
|
600c428821 | ||
|
|
5f317a3afc | ||
|
|
f6af1ddef9 | ||
|
|
d50dc91177 | ||
|
|
d6527e395a | ||
|
|
3709c15f1d | ||
|
|
d6b3f57811 | ||
|
|
64be7561c5 | ||
|
|
2190b48a82 | ||
|
|
0d95958f51 | ||
|
|
3794826b07 | ||
|
|
bcc39a1e42 | ||
|
|
773657cd1f | ||
|
|
a42a0c0b00 | ||
|
|
38c9a4dce2 | ||
|
|
b3c63f7e0b | ||
|
|
da170f8ae7 | ||
|
|
a8497cf157 | ||
|
|
612a4d450c | ||
|
|
0ad6e8ac54 | ||
|
|
e7c2edb857 | ||
|
|
d398d7c33f | ||
|
|
9cd3d00b6d | ||
|
|
ffbc39f4fb | ||
|
|
8d0bb44620 | ||
|
|
0b30051cb1 | ||
|
|
264e81265b | ||
|
|
58dd26b9f2 | ||
|
|
f96392dce8 | ||
|
|
bd50ff7a0a | ||
|
|
a5ada869ae | ||
|
|
525017f443 | ||
|
|
b70c9cbbfe | ||
|
|
7cd96e3eef | ||
|
|
a5682fea16 | ||
|
|
9661ecb882 | ||
|
|
cb87a62e82 | ||
|
|
16bdc7073d | ||
|
|
626a89bc1e | ||
|
|
e79b8fc143 | ||
|
|
74945954fe | ||
|
|
ce62f5058f | ||
|
|
e51fa6c362 | ||
|
|
6bd7e930da | ||
|
|
9708b549a4 | ||
|
|
8a0755a1da | ||
|
|
1bb3279d74 | ||
|
|
cbe3fddc59 | ||
|
|
1fb37cf810 | ||
|
|
bc6b6f87a9 | ||
|
|
258532d667 | ||
|
|
604056878e | ||
|
|
cfe09ab5f5 | ||
|
|
6f7f680529 | ||
|
|
82bc5de632 | ||
|
|
53ab5f7161 | ||
|
|
b4405a0eaa | ||
|
|
43c8cfa9e2 | ||
|
|
fe9c10cad7 | ||
|
|
5c4105c073 | ||
|
|
1ec615f585 | ||
|
|
528c1fb185 | ||
|
|
1d78e57bd0 | ||
|
|
f6601bf83f | ||
|
|
203ec5cdf7 | ||
|
|
e58d7fc849 | ||
|
|
a4c30dc5c6 | ||
|
|
2f510316ec | ||
|
|
9668e32400 | ||
|
|
ebfebd23e2 | ||
|
|
04e1e11afb | ||
|
|
9d9338dd1b | ||
|
|
d385870f77 | ||
|
|
609ce5a645 | ||
|
|
48994ee7a4 | ||
|
|
7440806987 | ||
|
|
a1641e9f77 | ||
|
|
91b0f53659 | ||
|
|
11212e08f8 | ||
|
|
f5410ae2be | ||
|
|
4f5e6c841d | ||
|
|
ad33b80748 | ||
|
|
bd34d0d87e | ||
|
|
0918cba0a5 | ||
|
|
56501a38db | ||
|
|
65a48f0d57 | ||
|
|
39cd7fbef4 | ||
|
|
bcd3603e3c | ||
|
|
18d853ab4c | ||
|
|
5eecc5dee6 | ||
|
|
2bb11b52a6 | ||
|
|
6448f0f7ad | ||
|
|
002f64da50 | ||
|
|
8fef4f1ff2 | ||
|
|
20f471be73 | ||
|
|
0c4d1fef09 | ||
|
|
e0446f394c | ||
|
|
2cc21100b4 | ||
|
|
1c219eb984 | ||
|
|
de19c046f0 | ||
|
|
04b8d346a1 | ||
|
|
bedb808741 | ||
|
|
b08cf3e160 | ||
|
|
b4ad373266 | ||
|
|
038430ff20 | ||
|
|
a184ac914e | ||
|
|
b6b7a87d84 | ||
|
|
94e4b623c0 | ||
|
|
5a8b292417 | ||
|
|
d0f2900027 | ||
|
|
3afe0e5092 | ||
|
|
cab62c933f | ||
|
|
005f7c7185 | ||
|
|
75c9fc6baa | ||
|
|
2b115071ed | ||
|
|
bd8371adcd | ||
|
|
2a1c54e7da | ||
|
|
3984194646 | ||
|
|
5ff6ae7530 | ||
|
|
1330ed56d4 | ||
|
|
de231f52d2 | ||
|
|
f420ee2dd1 | ||
|
|
5ffcce4382 | ||
|
|
335f742228 | ||
|
|
7990455358 | ||
|
|
7c5a71adf7 | ||
|
|
6234f26783 | ||
|
|
b8dbdfe12a | ||
|
|
f4dc113ee1 | ||
|
|
0736f05d8a | ||
|
|
8ca4e160c2 | ||
|
|
4175cbcb5b | ||
|
|
dc0f2f7916 | ||
|
|
ff12cbe2b9 | ||
|
|
e9a5f8d2b8 | ||
|
|
e3e30eb131 | ||
|
|
9d638ffed6 | ||
|
|
c3f2a5e63b | ||
|
|
5cc53cddbc | ||
|
|
0afba54b4c | ||
|
|
c330ec76cf | ||
|
|
b18278ce7e | ||
|
|
6b8b54963b | ||
|
|
8e8e641245 | ||
|
|
9ff290b493 | ||
|
|
51a60af92a | ||
|
|
bbb0650993 | ||
|
|
89351a799d | ||
|
|
00e56870b4 | ||
|
|
6a4b1265c6 | ||
|
|
ef18ede418 | ||
|
|
f13b869b6b | ||
|
|
3f32e4b66a | ||
|
|
174ce924de | ||
|
|
5e1926070c | ||
|
|
de9a78eed4 | ||
|
|
649fc7fcad | ||
|
|
456950beec | ||
|
|
0f476b4553 | ||
|
|
d072203bf9 | ||
|
|
200c35a8e9 | ||
|
|
a36be1db29 | ||
|
|
241dcab7ac | ||
|
|
30d2b5f07b | ||
|
|
b34cf81417 | ||
|
|
4dbc4c8b82 | ||
|
|
37769d3aa1 | ||
|
|
94db2c877d | ||
|
|
c198d8239a | ||
|
|
088937b978 | ||
|
|
63024cd848 | ||
|
|
586367cb00 | ||
|
|
b7760573ec | ||
|
|
733590721d | ||
|
|
dbbc0d600e | ||
|
|
73e71f40e0 | ||
|
|
5fc49bdcac | ||
|
|
8ac4b945a0 | ||
|
|
3ed9dafa1e | ||
|
|
46a4ef5cb2 | ||
|
|
92ca0c7fbd | ||
|
|
ed3bd791ff | ||
|
|
19ff212d1c | ||
|
|
fd5d665225 | ||
|
|
c3c034745b | ||
|
|
27db7cae1d | ||
|
|
fc38595c65 | ||
|
|
89bad9a4ec | ||
|
|
1b3eac7f8a | ||
|
|
0b48acea33 | ||
|
|
a20a5d480e | ||
|
|
4e413b344e | ||
|
|
5b735eccb2 | ||
|
|
8cd360707c | ||
|
|
0703ec90e0 | ||
|
|
4546bf7c03 | ||
|
|
0b1c763a75 | ||
|
|
13d0e2f4a3 | ||
|
|
a9fe9236bf | ||
|
|
a74c61f608 | ||
|
|
b591a45bd7 | ||
|
|
29181ca203 | ||
|
|
c9dbd8603c | ||
|
|
9eceb375ab | ||
|
|
1d789a14c0 | ||
|
|
f1281f7fd7 | ||
|
|
64b8d5dd31 | ||
|
|
16a6d77ed4 | ||
|
|
0d6e03fa0b | ||
|
|
935ac87a54 | ||
|
|
c16f31f850 | ||
|
|
19dbffde75 | ||
|
|
5c50fbbcf2 | ||
|
|
bb162e2edf | ||
|
|
2b726ba683 | ||
|
|
ad0df801ae | ||
|
|
021f544c60 | ||
|
|
2871ff4b68 | ||
|
|
90d3251790 | ||
|
|
9b6515b37c | ||
|
|
35c2bf9d9d | ||
|
|
5fc2afa9ce | ||
|
|
6149efd3a4 | ||
|
|
464ca57d7f | ||
|
|
d95805c61a | ||
|
|
e294e0109b | ||
|
|
502c7296c9 | ||
|
|
b6d60f01b8 | ||
|
|
d161e25ff6 | ||
|
|
b92b78b092 | ||
|
|
99fe8b2437 | ||
|
|
67f4dc72bf | ||
|
|
59a7e22fc2 | ||
|
|
af7918f02d | ||
|
|
953203be43 | ||
|
|
06125d2e20 | ||
|
|
db43f526d0 | ||
|
|
f2afdfc244 | ||
|
|
22abb92dff | ||
|
|
4c00d2e7a3 | ||
|
|
7aaf466d18 | ||
|
|
31b2472a3f | ||
|
|
db90f0b816 | ||
|
|
9461f5e3a3 | ||
|
|
48d10a4996 | ||
|
|
d92c9feb78 | ||
|
|
58729711d9 | ||
|
|
2f6c70f0d1 | ||
|
|
e3c3d6bade | ||
|
|
5cf0079bcc | ||
|
|
be7889ed84 | ||
|
|
c637ed5a6e | ||
|
|
8b30b1d587 | ||
|
|
348abafdd9 | ||
|
|
fe6c5260ec | ||
|
|
2c697fdace | ||
|
|
5e0c43bacc | ||
|
|
e35a061f1d | ||
|
|
2312c83bfd | ||
|
|
96f4065aae | ||
|
|
7e363dbebb | ||
|
|
55b9f8346f | ||
|
|
aa5754fa06 | ||
|
|
004b15cd4d | ||
|
|
41c89b1708 | ||
|
|
b3096357c1 | ||
|
|
4cca9043e0 | ||
|
|
835d30b186 | ||
|
|
5b8c84e7da | ||
|
|
a138f63067 | ||
|
|
9a42723283 | ||
|
|
92dde2f72e | ||
|
|
41dbcdff32 | ||
|
|
fd3214f52e | ||
|
|
c6a0d4033b | ||
|
|
601b7765b6 | ||
|
|
6edf8bde75 | ||
|
|
05c90db586 | ||
|
|
e26ea97e09 | ||
|
|
cbe442bfbb | ||
|
|
f2f4121a70 | ||
|
|
9ef7a73273 | ||
|
|
5da3769523 | ||
|
|
b5793b7c5d | ||
|
|
fddee3a47d | ||
|
|
c9fd9cd1d3 | ||
|
|
5b1acc00e4 | ||
|
|
fe95d59e25 | ||
|
|
90ff5da28e | ||
|
|
2837e43650 | ||
|
|
e533380d98 | ||
|
|
6f12431d53 | ||
|
|
c2c6ccf431 | ||
|
|
75d2c94d38 | ||
|
|
c53fe2331d | ||
|
|
e6d5e03006 | ||
|
|
5273a1cfb7 | ||
|
|
223c9c3a01 | ||
|
|
69b4b98f1b | ||
|
|
0012c9eb3f | ||
|
|
368ec61aac | ||
|
|
02013d01c8 | ||
|
|
90aed054ff | ||
|
|
cd83544431 | ||
|
|
e7c42b62b9 | ||
|
|
7becf121d6 | ||
|
|
21ccac1b01 | ||
|
|
29e379e0d2 | ||
|
|
1807ab8f2c | ||
|
|
6b5761d34a | ||
|
|
3467cc9d4e | ||
|
|
04e00c08c6 | ||
|
|
14e0c017bb | ||
|
|
d62ccc75ff | ||
|
|
aeb21b6b11 | ||
|
|
74df8331cf | ||
|
|
3b48ee56ce | ||
|
|
c697b15994 | ||
|
|
58e1f8d0c9 | ||
|
|
6396c46971 | ||
|
|
b88eff688b | ||
|
|
ac14e76bf5 | ||
|
|
828c3be921 | ||
|
|
d49f4b9d0f | ||
|
|
c550299fa8 | ||
|
|
0b6624317a | ||
|
|
1a234406dc | ||
|
|
2eab02c231 | ||
|
|
8b804379cc | ||
|
|
27ca717e75 | ||
|
|
46ecab1310 | ||
|
|
9af6ce12e3 | ||
|
|
cf0e9c1e6e | ||
|
|
a1c47bc90b | ||
|
|
39fdc3a56b | ||
|
|
39cc2087e0 | ||
|
|
4ce698f199 | ||
|
|
3aa60f0e37 | ||
|
|
f6e71c1c80 | ||
|
|
ca344a7c90 | ||
|
|
b3470a1722 |
No files matched your search
+10
@@ -37,17 +37,27 @@ td.log
|
||||
/strata-*.json
|
||||
/strata-*.log
|
||||
/build-strata.bat
|
||||
/build-strata-cuda12.bat
|
||||
/build-cuda12/
|
||||
/.strata-mcp/
|
||||
|
||||
# image encoder (setup.py --vision): build output and the generated build script
|
||||
build-vision*/
|
||||
build-vision.bat
|
||||
build-vision-cuda12.bat
|
||||
|
||||
# setup.py outputs (engine, downloads) and release builds
|
||||
/engine/
|
||||
/engine-cuda12/
|
||||
/build-release*/
|
||||
/dist/
|
||||
*.done
|
||||
/third_party/*.zip
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
.cursor/
|
||||
|
||||
/.ccache/
|
||||
/.rocm-win/
|
||||
+374
-18
@@ -8,7 +8,18 @@
|
||||
# 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.38 LANGUAGES CXX)
|
||||
project(strata VERSION 0.1.40 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)
|
||||
if(STRATA_ENABLE_SYCL)
|
||||
if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
message(FATAL_ERROR "STRATA_ENABLE_SYCL is a separate backend; leave STRATA_ENABLE_CUDA and STRATA_ENABLE_HIP off")
|
||||
endif()
|
||||
message(WARNING "STRATA_ENABLE_SYCL: the Intel Arc engine is experimental (community port, docs/INTEL_ARC.md)")
|
||||
add_subdirectory(sycl)
|
||||
return()
|
||||
endif()
|
||||
add_compile_definitions(STRATA_VERSION="${PROJECT_VERSION}")
|
||||
# issue #31: release builds keep a PDB beside the exe (not shipped in it), so a minidump from a stalled engine can
|
||||
# be read back to the exact source line
|
||||
@@ -21,6 +32,18 @@ endif()
|
||||
# instead of this machine's (Strata's own AVX-512 kernels are per-file and chosen at run time either way), and
|
||||
# the MSVC runtime linked in, so no Visual C++ redistributable is needed.
|
||||
option(STRATA_PORTABLE "build for other PCs: AVX2 baseline, static MSVC runtime" OFF)
|
||||
# EXPERIMENTAL, opt-in: an engine for CPUs without AVX2 (#394, #595, #623), compiled on that PC. ggml-cpu, which then
|
||||
# computes every CPU expert row, is built for this floor instead of AVX2 or the build host: "avx" (Sandy/Ivy Bridge:
|
||||
# AVX, no FMA/F16C/AVX2) or "none" (SSE4.2 + POPCNT: Nehalem/Westmere). Strata's own AVX2/AVX-512 kernels stay in the
|
||||
# binary behind their CPU checks; the router lookahead gets an AVX1 kernel. Empty (the default) changes nothing.
|
||||
set(STRATA_ISA_FLOOR "" CACHE STRING "experimental older-CPU build: empty (default, AVX2 floor), avx, or none (SSE4.2)")
|
||||
set_property(CACHE STRATA_ISA_FLOOR PROPERTY STRINGS "" avx none)
|
||||
if(NOT STRATA_ISA_FLOOR STREQUAL "" AND NOT STRATA_ISA_FLOOR STREQUAL "avx" AND NOT STRATA_ISA_FLOOR STREQUAL "none")
|
||||
message(FATAL_ERROR "STRATA_ISA_FLOOR must be empty, avx or none (got '${STRATA_ISA_FLOOR}')")
|
||||
endif()
|
||||
if(STRATA_ISA_FLOOR AND STRATA_PORTABLE)
|
||||
message(FATAL_ERROR "STRATA_ISA_FLOOR (an older-CPU build) and STRATA_PORTABLE (the AVX2 release build) exclude each other")
|
||||
endif()
|
||||
if(STRATA_PORTABLE AND MSVC)
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded")
|
||||
endif()
|
||||
@@ -33,17 +56,36 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
endif()
|
||||
|
||||
option(STRATA_ENABLE_CUDA "Build the CUDA targets (Phase 2+)" OFF)
|
||||
option(STRATA_ENABLE_HIP "Build the HIP targets for wave32 AMD GPUs (gfx1100, gfx1201; gfx1030 unvalidated)" OFF)
|
||||
# AMD gfx906 (MI50, wave64): the same .cu sources compiled as HIP through the gfx906 compat layer. The CUDA runtime and
|
||||
# cuBLAS names are mapped by the headers in include/strata/platform/hip_compat/, which go first on the include path,
|
||||
# and CUDA::cudart / CUDA::cublas become aliases of HIP's targets, so every CUDA target below stays as it is. This is
|
||||
# separate from STRATA_ENABLE_HIP (the wave32 RDNA backend): with it on, STRATA_ENABLE_CUDA is on and
|
||||
# STRATA_ENABLE_HIP stays off, so every "CUDA" branch of the tree is the one taken.
|
||||
option(STRATA_HIP_GFX906 "Build the CUDA targets for AMD gfx906 through HIP (wave64 compat layer)" OFF)
|
||||
if(STRATA_HIP_GFX906)
|
||||
if(STRATA_ENABLE_HIP)
|
||||
message(FATAL_ERROR "STRATA_HIP_GFX906 and STRATA_ENABLE_HIP are separate backends; enable only one")
|
||||
endif()
|
||||
set(STRATA_ENABLE_CUDA ON CACHE BOOL "" FORCE)
|
||||
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)
|
||||
# CUDA: MMQ prompt kernels for Q4_K / Q5_K / Q5_1 (Unsloth's UD-Q4_K_XL experts). Off by default: the engine loads
|
||||
# every kernel when it starts (CUDA_MODULE_LOADING=EAGER), so these took ~5 MB of VRAM - 1-3 expert slots - from every
|
||||
# model, and the UD-Q4_K_XL prompt is SSD-bound with or without them (docs/UNSLOTH_Q4.md)
|
||||
option(STRATA_MMQ_KQUANTS "Build the CUDA MMQ prompt kernels for Q4_K / Q5_K / Q5_1 experts" OFF)
|
||||
option(STRATA_MMQ_KQUANTS "Build the MMQ prompt kernels for Q4_K / Q5_K / Q5_1 / Q6_K: the CUDA experts of Unsloth's UD-Q4_K_XL, and on HIP the dense GGUF projections (STRATA_DENSE_MMQ)" OFF)
|
||||
# CUDA: Q6_K gate/up in the grouped expert kernels (Unsloth's UD-Q6_K_XL, docs/UNSLOTH_Q6.md). Off by default: every
|
||||
# grouped-kernel instance is loaded when the engine starts (CUDA_MODULE_LOADING=EAGER), so one more costs VRAM that
|
||||
# every other model would lose as expert slots (PR #860 review)
|
||||
option(STRATA_Q6K_EXPERTS "Build the Q6_K gate/up grouped expert kernels (UD-Q6_K_XL)" OFF)
|
||||
if(STRATA_Q6K_EXPERTS)
|
||||
add_compile_definitions(STRATA_Q6K_EXPERTS=1)
|
||||
endif()
|
||||
option(STRATA_ORCA_Q4KS_MMQ "Build the GGML Q5_0 MMQ instance for OrcaRouter Q4_K_S" OFF)
|
||||
if(STRATA_PREFILL_MMQ AND NOT STRATA_ENABLE_HIP)
|
||||
message(FATAL_ERROR "STRATA_PREFILL_MMQ requires STRATA_ENABLE_HIP=ON")
|
||||
endif()
|
||||
if(STRATA_ENABLE_CUDA AND STRATA_ENABLE_HIP)
|
||||
if(STRATA_ENABLE_CUDA AND STRATA_ENABLE_HIP AND NOT STRATA_HIP_GFX906)
|
||||
message(FATAL_ERROR "STRATA_ENABLE_CUDA and STRATA_ENABLE_HIP are separate backends; enable only one")
|
||||
endif()
|
||||
if(STRATA_ENABLE_HIP)
|
||||
@@ -63,12 +105,45 @@ if(STRATA_BUILD_CONVERSATION_TESTS)
|
||||
add_executable(conversation_cache_test src/core/conversation_cache_test.cpp)
|
||||
target_include_directories(conversation_cache_test PRIVATE include)
|
||||
add_test(NAME conversation_cache_test COMMAND conversation_cache_test)
|
||||
add_executable(conversation_split_failure_test src/core/conversation_split_failure_test.cpp)
|
||||
target_include_directories(conversation_split_failure_test PRIVATE include)
|
||||
add_test(NAME conversation_split_failure_test COMMAND conversation_split_failure_test)
|
||||
add_executable(conversation_memory_test src/core/conversation_memory_test.cpp src/core/conversation_memory.cpp)
|
||||
target_include_directories(conversation_memory_test PRIVATE include)
|
||||
add_test(NAME conversation_memory_test COMMAND conversation_memory_test)
|
||||
add_executable(conversation_file_test src/core/conversation_file_test.cpp src/core/conversation_file.cpp)
|
||||
target_include_directories(conversation_file_test PRIVATE include)
|
||||
add_test(NAME conversation_file_test COMMAND conversation_file_test)
|
||||
endif()
|
||||
|
||||
if(STRATA_ENABLE_CUDA)
|
||||
if(STRATA_HIP_GFX906)
|
||||
if(NOT DEFINED CMAKE_HIP_ARCHITECTURES)
|
||||
set(CMAKE_HIP_ARCHITECTURES gfx906 CACHE STRING "HIP architectures" FORCE)
|
||||
endif()
|
||||
enable_language(HIP)
|
||||
find_package(hip REQUIRED)
|
||||
find_package(hipblas REQUIRED)
|
||||
add_library(CUDA::cudart INTERFACE IMPORTED)
|
||||
target_link_libraries(CUDA::cudart INTERFACE hip::host)
|
||||
target_compile_definitions(CUDA::cudart INTERFACE STRATA_HIP_GFX906=1 __HIP_PLATFORM_AMD__=1)
|
||||
target_include_directories(CUDA::cudart BEFORE INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/platform/hip_compat)
|
||||
add_library(CUDA::cublas INTERFACE IMPORTED)
|
||||
target_link_libraries(CUDA::cublas INTERFACE roc::hipblas CUDA::cudart)
|
||||
set(CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/platform/hip_compat)
|
||||
include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/platform/hip_compat)
|
||||
add_compile_definitions(STRATA_HIP_GFX906=1)
|
||||
message(STATUS "Strata: HIP (gfx906 compat) enabled, arch ${CMAKE_HIP_ARCHITECTURES}")
|
||||
set(_strata_gpu_runtime_target CUDA::cudart)
|
||||
set(_strata_gpu_blas_target CUDA::cublas)
|
||||
set(_strata_gpu_include_directories ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
|
||||
elseif(STRATA_ENABLE_CUDA)
|
||||
# #585: CMake links CUDA code with the STATIC runtime by default, and the targets below also link CUDA::cudart (the
|
||||
# DLL's import library). MSVC with a CUDA 12.x toolkit - the experimental Pascal/Volta build - then fails with
|
||||
# LNK1169 (every cudart symbol defined twice); GNU ld lets it pass. That build links the shared runtime alone. The
|
||||
# ready-made engine (CUDA 13, no STRATA_EXPERIMENTAL_SM60) is unchanged; -DCMAKE_CUDA_RUNTIME_LIBRARY overrides.
|
||||
if(MSVC AND STRATA_EXPERIMENTAL_SM60 AND NOT DEFINED CMAKE_CUDA_RUNTIME_LIBRARY)
|
||||
set(CMAKE_CUDA_RUNTIME_LIBRARY Shared)
|
||||
endif()
|
||||
enable_language(CUDA)
|
||||
# `enable_language(CUDA)` gives the .cu compile rules but NOT the imported targets, so `CUDA::cudart` has to
|
||||
# be brought in explicitly for the host-side tests that call cudaMalloc themselves.
|
||||
@@ -107,13 +182,18 @@ if(STRATA_ENABLE_CUDA)
|
||||
"CUDA 12.8 crashed on long prompts on these cards (issues #220, #224). Use CUDA 13.0 or newer.")
|
||||
endif()
|
||||
endforeach()
|
||||
if(STRATA_EXPERIMENTAL_SM60)
|
||||
# every target sees it: the Pascal / Volta code paths (the Volta prompt attention, PR #600) are compiled into this
|
||||
# build only, so the ready-made engine (sm_75+) keeps exactly the code it had
|
||||
add_compile_definitions(STRATA_EXPERIMENTAL_SM60=1)
|
||||
endif()
|
||||
message(STATUS "Strata: CUDA enabled, arch ${CMAKE_CUDA_ARCHITECTURES}")
|
||||
set(_strata_gpu_runtime_target CUDA::cudart)
|
||||
set(_strata_gpu_blas_target CUDA::cublas)
|
||||
set(_strata_gpu_include_directories ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
|
||||
elseif(STRATA_ENABLE_HIP)
|
||||
set(_strata_gpu_runtime_target strata_hip_runtime)
|
||||
set(_strata_gpu_blas_target roc::hipblas)
|
||||
set(_strata_gpu_blas_target ${STRATA_HIP_BLAS_TARGETS})
|
||||
set(_strata_gpu_include_directories ${STRATA_HIP_COMPAT_INCLUDE_DIR})
|
||||
endif()
|
||||
|
||||
@@ -164,6 +244,12 @@ if(STRATA_BUILD_TESTS)
|
||||
add_executable(gguf_split_test tests/core/gguf_split_test.cpp)
|
||||
target_link_libraries(gguf_split_test PRIVATE strata_artifact)
|
||||
add_test(NAME gguf_split_test COMMAND gguf_split_test)
|
||||
|
||||
# the hipBLASLt tuning header's architecture gate: RDNA4 siblings share solution IDs, RDNA3 siblings share
|
||||
# theirs, cross-family pairs and version mismatches stay rejected (synthetic tables: no model, no GPU)
|
||||
add_executable(tuning_header_arch_family_test tests/prefill/tuning_header_arch_family_test.cpp)
|
||||
target_include_directories(tuning_header_arch_family_test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/prefill")
|
||||
add_test(NAME tuning_header_arch_family_test COMMAND tuning_header_arch_family_test)
|
||||
endif()
|
||||
|
||||
# ---- strata_plan ----------------------------------------------------------
|
||||
@@ -309,7 +395,7 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
src/kernels/cuda/native_flash_attn.cu
|
||||
src/kernels/cuda/native_qsa_indexer.cu
|
||||
src/kernels/cuda/native_qsa_score.cu
|
||||
PROPERTIES COMPILE_OPTIONS "--use_fast_math")
|
||||
PROPERTIES COMPILE_OPTIONS "$<IF:$<BOOL:${STRATA_HIP_GFX906}>,,--use_fast_math>")
|
||||
elseif(STRATA_ENABLE_HIP)
|
||||
# Keep Q8_K's multiply and magic-bias rounding as separate FP32 operations on HIP; contraction to an
|
||||
# FMA changes tie results. The embedding gather also relies on separately rounded operations.
|
||||
@@ -348,16 +434,30 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
# library is the honest shape of the dependency graph: core knows about devices and weights, kernels know
|
||||
# about arithmetic, and this one knows the ORDER the arithmetic happens in.
|
||||
add_library(strata_engine STATIC src/core/layer.cpp src/core/session.cpp src/core/expert_source.cpp src/core/remote_experts.cpp
|
||||
src/core/remote_expert_opt.cu
|
||||
src/core/expert_cache.cpp src/core/native_head.cpp src/core/native_dense.cpp src/core/verify.cpp src/core/mtp.cpp
|
||||
src/core/conversation_snapshot.cpp src/core/conversation_state.cpp src/core/conversation_memory.cpp src/core/peer_experts.cpp)
|
||||
src/core/conversation_snapshot.cpp src/core/conversation_state.cpp src/core/conversation_memory.cpp src/core/peer_experts.cpp
|
||||
src/core/arch_defaults.cpp src/core/vmm.cpp src/core/conversation_file.cpp)
|
||||
target_include_directories(strata_engine PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
target_link_libraries(strata_engine PUBLIC strata_core strata_kernels strata_kernels_cpu ${_strata_gpu_runtime_target})
|
||||
if(STRATA_BUILD_TESTS AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/core/exchange_storage_test.cpp")
|
||||
add_executable(exchange_storage_test tests/core/exchange_storage_test.cpp)
|
||||
target_include_directories(exchange_storage_test PRIVATE include)
|
||||
target_link_libraries(exchange_storage_test PRIVATE Threads::Threads)
|
||||
add_test(NAME exchange_storage_test COMMAND exchange_storage_test)
|
||||
set_tests_properties(exchange_storage_test PROPERTIES TIMEOUT 60)
|
||||
endif()
|
||||
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/core/file_expert_source_test.cpp")
|
||||
enable_testing()
|
||||
add_executable(file_expert_source_test tests/core/file_expert_source_test.cpp)
|
||||
target_link_libraries(file_expert_source_test PRIVATE strata_engine)
|
||||
add_test(NAME file_expert_source_test COMMAND file_expert_source_test)
|
||||
set_tests_properties(file_expert_source_test PROPERTIES TIMEOUT 60)
|
||||
add_executable(vmm_test tests/core/vmm_test.cpp)
|
||||
target_link_libraries(vmm_test PRIVATE strata_engine)
|
||||
add_test(NAME vmm_test COMMAND vmm_test)
|
||||
set_tests_properties(vmm_test PROPERTIES TIMEOUT 60)
|
||||
endif()
|
||||
# #477: the learned expert profile (--expert-profile-save): its order and make_profile.py's file format (CPU only)
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/core/expert_profile_save_test.cpp")
|
||||
@@ -384,6 +484,10 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(expert_cache_per_layer_test PRIVATE strata_engine)
|
||||
add_test(NAME expert_cache_per_layer_test COMMAND expert_cache_per_layer_test)
|
||||
set_tests_properties(expert_cache_per_layer_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||
add_executable(expert_cache_segmented_test tests/core/expert_cache_segmented_test.cpp)
|
||||
target_link_libraries(expert_cache_segmented_test PRIVATE strata_engine)
|
||||
add_test(NAME expert_cache_segmented_test COMMAND expert_cache_segmented_test)
|
||||
set_tests_properties(expert_cache_segmented_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||
endif()
|
||||
if(STRATA_BUILD_CONVERSATION_TESTS)
|
||||
add_executable(conversation_snapshot_test src/core/conversation_snapshot_test.cpp)
|
||||
@@ -410,6 +514,10 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
# against llama.cpp on the SAME ids and a tokenizer on one side of that comparison is a second variable.
|
||||
# plan v0.3 P5: batched prompt processing (tensor-core GEMMs through cuBLAS).
|
||||
add_library(strata_prefill STATIC src/prefill/gemm.cu src/prefill/kernels.cu src/prefill/prefill.cpp)
|
||||
if(STRATA_ENABLE_HIP)
|
||||
# #313: the RDNA3 WMMA GEMMs include hip_runtime.h: AMD builds only (a CUDA build never sees this file)
|
||||
target_sources(strata_prefill PRIVATE src/prefill/wmma_gemm.cu)
|
||||
endif()
|
||||
target_include_directories(strata_prefill PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${_strata_gpu_include_directories})
|
||||
target_link_libraries(strata_prefill PUBLIC strata_engine ${_strata_gpu_blas_target} ${_strata_gpu_runtime_target})
|
||||
if(STRATA_ENABLE_HIP AND STRATA_HIPBLASLT_AVAILABLE)
|
||||
@@ -427,6 +535,18 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
if(STRATA_ENABLE_HIP)
|
||||
enable_testing()
|
||||
set(_strata_hip_tests)
|
||||
add_executable(hip_prefill_gemm tests/hip/prefill_gemm.cpp)
|
||||
target_link_libraries(hip_prefill_gemm PRIVATE strata_prefill strata_hip_runtime)
|
||||
add_test(NAME hip_prefill_gemm COMMAND hip_prefill_gemm)
|
||||
list(APPEND _strata_hip_tests hip_prefill_gemm)
|
||||
add_executable(hip_gemm_f16_io_parity tests/hip/gemm_f16_io_parity.cpp)
|
||||
target_link_libraries(hip_gemm_f16_io_parity PRIVATE strata_prefill strata_hip_runtime)
|
||||
add_test(NAME hip_gemm_f16_io_parity COMMAND hip_gemm_f16_io_parity)
|
||||
list(APPEND _strata_hip_tests hip_gemm_f16_io_parity)
|
||||
add_executable(hip_q2_zero tests/hip/q2_zero.cpp)
|
||||
target_link_libraries(hip_q2_zero PRIVATE strata_kernels strata_hip_runtime)
|
||||
add_test(NAME hip_q2_zero COMMAND hip_q2_zero)
|
||||
list(APPEND _strata_hip_tests hip_q2_zero)
|
||||
add_executable(hip_expert_cache_staging tests/hip/expert_cache_staging.cpp)
|
||||
target_link_libraries(hip_expert_cache_staging PRIVATE strata_engine)
|
||||
add_test(NAME hip_expert_cache_staging COMMAND hip_expert_cache_staging)
|
||||
@@ -464,6 +584,23 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
add_test(NAME hip_prefill_native_batch COMMAND hip_prefill_native_batch)
|
||||
list(APPEND _strata_hip_tests hip_prefill_native_batch)
|
||||
endif()
|
||||
# Aurora: the prompt GDN recurrence's per-head kernel against the column-split + norm kernels, bitwise
|
||||
add_executable(hip_gdn_rec_head tests/hip/gdn_rec_head.cpp)
|
||||
target_link_libraries(hip_gdn_rec_head PRIVATE strata_prefill strata_hip_runtime)
|
||||
add_test(NAME hip_gdn_rec_head COMMAND hip_gdn_rec_head)
|
||||
list(APPEND _strata_hip_tests hip_gdn_rec_head)
|
||||
# #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")
|
||||
target_link_libraries(hip_prefill_wmma_gemm_parity PRIVATE strata_prefill strata_hip_runtime)
|
||||
add_test(NAME hip_prefill_wmma_gemm_parity COMMAND hip_prefill_wmma_gemm_parity)
|
||||
list(APPEND _strata_hip_tests hip_prefill_wmma_gemm_parity)
|
||||
# S (STRATA_HCD_EXACT): the exact HC down kernel against hipBLASLt solution 1176 / 1177 (needs STRATA_HIPBLASLT_TUNING; exit 77 else)
|
||||
add_executable(hip_prefill_hcd_exact_parity tests/hip/prefill_hcd_exact_parity.cpp)
|
||||
target_link_libraries(hip_prefill_hcd_exact_parity PRIVATE strata_prefill strata_hip_runtime)
|
||||
add_test(NAME hip_prefill_hcd_exact_parity COMMAND hip_prefill_hcd_exact_parity)
|
||||
set_tests_properties(hip_prefill_hcd_exact_parity PROPERTIES SKIP_RETURN_CODE 77)
|
||||
list(APPEND _strata_hip_tests hip_prefill_hcd_exact_parity)
|
||||
# S6: the AMD fast router against the portable kernel, bitwise
|
||||
add_executable(hip_router_fast tests/hip/router_fast.cpp)
|
||||
target_link_libraries(hip_router_fast PRIVATE strata_kernels strata_hip_runtime)
|
||||
@@ -560,6 +697,10 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_include_directories(mmvq_multi_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(mmvq_multi_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME mmvq_multi_parity COMMAND mmvq_multi_parity)
|
||||
if(STRATA_HIP_GFX906) # the CUDA layout, where the negative control has power (the wave64 one is exact at every T)
|
||||
add_test(NAME mmvq_multi_parity_cuda_layout COMMAND mmvq_multi_parity)
|
||||
set_tests_properties(mmvq_multi_parity_cuda_layout PROPERTIES ENVIRONMENT "STRATA_MMVQ_WAVE=0")
|
||||
endif()
|
||||
endif()
|
||||
# the IQ kernels that decode a weight part once for every column/entry, bitwise vs the per-column ones
|
||||
# (GPU, synthetic, no model; --bench adds old/new timings)
|
||||
@@ -568,11 +709,26 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(iq_multi_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME iq_multi_parity COMMAND iq_multi_parity)
|
||||
# the prompt path's DeltaNet recurrence: gdn_rec_kh_kernel against gdn_rec_cols_pipe_kernel, bitwise (GPU,
|
||||
# synthetic, no model; --bench adds timings)
|
||||
if(STRATA_ENABLE_CUDA)
|
||||
# synthetic, no model; --bench adds timings). NVIDIA only: cp.async and the CUDA occupancy API (not on gfx906)
|
||||
if(STRATA_ENABLE_CUDA AND NOT STRATA_HIP_GFX906)
|
||||
# Independent pinned expert uploads: byte parity and stream/event ordering.
|
||||
add_executable(dma_batch_parity tests/core/dma_batch_parity.cu)
|
||||
target_include_directories(dma_batch_parity PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
target_link_libraries(dma_batch_parity PRIVATE ${_strata_gpu_runtime_target})
|
||||
add_test(NAME dma_batch_parity COMMAND dma_batch_parity)
|
||||
set_tests_properties(dma_batch_parity PROPERTIES TIMEOUT 120)
|
||||
if(WIN32)
|
||||
add_test(NAME dma_batch_registered_parity COMMAND dma_batch_parity --registered)
|
||||
set_tests_properties(dma_batch_registered_parity PROPERTIES TIMEOUT 120)
|
||||
endif()
|
||||
add_executable(gdn_rec_parity src/prefill/gdn_rec_parity.cu)
|
||||
target_link_libraries(gdn_rec_parity PRIVATE ${_strata_gpu_runtime_target})
|
||||
add_test(NAME gdn_rec_parity COMMAND gdn_rec_parity)
|
||||
# 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)
|
||||
target_link_libraries(gemm_bf16_parity PRIVATE strata_prefill )
|
||||
add_test(NAME gemm_bf16_parity COMMAND gemm_bf16_parity)
|
||||
endif()
|
||||
# native_expert_grouped's group stride and fused SwiGLU + q8_1, bitwise vs the launches before them (GPU, synthetic,
|
||||
# no model; --bench adds v1/new timings of a window's 48 calls in a graph)
|
||||
@@ -584,7 +740,17 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_include_directories(shared_expert_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(shared_expert_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME shared_expert_parity COMMAND shared_expert_parity --selftest)
|
||||
# the split / batched verify-window kernels (from eddoursul's fork), bitwise vs the kernels they replace; CUDA only
|
||||
if(STRATA_ENABLE_CUDA)
|
||||
add_executable(verify_parity src/kernels/verify_parity.cpp)
|
||||
target_include_directories(verify_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(verify_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME verify_parity COMMAND verify_parity --selftest)
|
||||
endif()
|
||||
|
||||
add_executable(fused_gr_bench src/kernels/fused_gr_bench.cpp)
|
||||
target_include_directories(fused_gr_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(fused_gr_bench PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_executable(gr_parity src/kernels/gr_parity.cpp)
|
||||
target_include_directories(gr_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(gr_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
@@ -629,10 +795,42 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(quantize_act_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME quantize_act_parity COMMAND quantize_act_parity --selftest)
|
||||
|
||||
# the verify window's routing in one multi-column BF16 projection + one top-k, bitwise the per-token calls
|
||||
add_executable(route_window_parity src/kernels/route_window_parity.cpp)
|
||||
target_include_directories(route_window_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(route_window_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME route_window_parity COMMAND route_window_parity)
|
||||
|
||||
add_executable(router_top10_parity src/kernels/router_top10_parity.cpp)
|
||||
target_include_directories(router_top10_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(router_top10_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME router_top10_parity COMMAND router_top10_parity --selftest)
|
||||
# #783 PR-c: the multi-token router and the k = 10 float4 combine, bitwise against the single-token contracts
|
||||
add_executable(native_multi_parity src/kernels/native_multi_parity.cpp)
|
||||
target_include_directories(native_multi_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(native_multi_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME native_multi_parity COMMAND native_multi_parity)
|
||||
# #783 PR-d: the batched K/V append (INT8, Q4_0, the hybrid fold) against the per-cell appends, memcmp
|
||||
add_executable(kv_steps_parity src/kernels/kv_steps_parity.cpp)
|
||||
target_include_directories(kv_steps_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(kv_steps_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME kv_steps_parity COMMAND kv_steps_parity)
|
||||
# #783 PR-e: the device-built expert plan (parallel grouping + prefix scan) against a host replay of its contract
|
||||
add_executable(resident_plan_parity src/kernels/resident_plan_parity.cpp)
|
||||
target_include_directories(resident_plan_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(resident_plan_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME resident_plan_parity COMMAND resident_plan_parity)
|
||||
# #783 PR-g: the multi-token hyper-connection read/write (exact-T projections under them) against the single-token calls
|
||||
add_executable(gr_multi_parity src/kernels/gr_multi_parity.cpp)
|
||||
target_include_directories(gr_multi_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(gr_multi_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME gr_multi_parity COMMAND gr_multi_parity)
|
||||
if(STRATA_HIP_GFX906)
|
||||
# the S6 router (STRATA_HIP_ROUTER_FAST=1 on gfx906) against the portable kernel, bitwise, on wave64
|
||||
add_executable(hip_router_fast tests/hip/router_fast.cpp)
|
||||
target_include_directories(hip_router_fast PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(hip_router_fast PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
endif()
|
||||
|
||||
add_executable(s_gemv_parity src/kernels/s_gemv_parity.cpp)
|
||||
target_include_directories(s_gemv_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
@@ -659,6 +857,13 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(qsa_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME qsa_parity COMMAND qsa_parity --selftest)
|
||||
|
||||
# the QSA top-k the engine dispatches against the original kernel: identical ids on both sides of the register
|
||||
# kernel's reach, with tied and with equal scores (synthetic, no model)
|
||||
add_executable(qsa_topk_parity src/kernels/qsa_topk_parity.cpp)
|
||||
target_include_directories(qsa_topk_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(qsa_topk_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME qsa_topk_parity COMMAND qsa_topk_parity --selftest)
|
||||
|
||||
# ---- P2.S4: the PLE n-gram hash, the IQ4_NL table read, and the PLE block ----
|
||||
# `ngram.cpp` is host-only but lives in `strata_kernels` because `ple.cu` shares its geometry constants, and
|
||||
# splitting them across two libraries would only add a layer.
|
||||
@@ -785,9 +990,13 @@ endif()
|
||||
# worse opt-out. `cpu_require_expert_support()` therefore refuses at startup on a CPU without the
|
||||
# features, and the fallback exists for tests, exactly as P2.S3 specifies.
|
||||
# Plan v0.3 P6: speculation components that need no GPU.
|
||||
add_library(strata_spec STATIC src/spec/suffix_drafter.cpp src/spec/controller.cpp src/spec/draft_policy.cpp)
|
||||
add_library(strata_spec STATIC src/spec/suffix_drafter.cpp src/spec/controller.cpp src/spec/draft_policy.cpp
|
||||
src/spec/draft_source.cpp)
|
||||
target_include_directories(strata_spec PUBLIC include)
|
||||
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(suffix_drafter_test src/spec/suffix_drafter_test.cpp)
|
||||
target_link_libraries(suffix_drafter_test PRIVATE strata_spec)
|
||||
add_test(NAME suffix_drafter_test COMMAND suffix_drafter_test)
|
||||
@@ -799,6 +1008,9 @@ if(STRATA_BUILD_TESTS)
|
||||
add_test(NAME draft_policy_test COMMAND draft_policy_test)
|
||||
endif()
|
||||
if(STRATA_BUILD_TESTS OR STRATA_BUILD_CONVERSATION_TESTS)
|
||||
add_executable(message_boundary_test src/program/message_boundary_test.cpp)
|
||||
target_include_directories(message_boundary_test PRIVATE include)
|
||||
add_test(NAME message_boundary_test COMMAND message_boundary_test)
|
||||
# the serve loop's conversation-cache retention policy (header-only, no library to link)
|
||||
add_executable(conv_cache_test src/program/conv_cache_test.cpp)
|
||||
target_include_directories(conv_cache_test PRIVATE include)
|
||||
@@ -827,6 +1039,34 @@ else()
|
||||
PROPERTIES COMPILE_OPTIONS "-mavx512f;-mavx512bw;-mavx512vl;-mavx512dq;-mavx512vnni;-mavx512vbmi;-mf16c;-mfma")
|
||||
set_source_files_properties(src/kernels/cpu/q2_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
|
||||
endif()
|
||||
# AVX-VNNI (Alder Lake, Sapphire Rapids and later): the AVX-2 expert kernels' vpdpwssd / vpdpbusd copies (q2_avx2.cpp,
|
||||
# iq_avx2.cpp; *_rows.inl), taken at run time where cpu_avxvnni_ok(). GCC and Clang compile only those functions with
|
||||
# target("avxvnni") - never -mavxvnni on a file, which would let the compiler use it anywhere - and MSVC takes the
|
||||
# intrinsics under /arch:AVX2. A compiler without them (GCC < 11, Clang < 12) builds the files without the copies.
|
||||
include(CheckCXXSourceCompiles)
|
||||
if(MSVC)
|
||||
set(CMAKE_REQUIRED_FLAGS "/arch:AVX2")
|
||||
else()
|
||||
set(CMAKE_REQUIRED_FLAGS "-mavx2 -mfma -mf16c")
|
||||
endif()
|
||||
check_cxx_source_compiles([=[
|
||||
#include <immintrin.h>
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
__attribute__((target("avxvnni")))
|
||||
#endif
|
||||
static __m256i dots(__m256i a, __m256i b) { return _mm256_dpbusd_avx_epi32(_mm256_dpwssd_avx_epi32(a, a, b), a, b); }
|
||||
int main() {
|
||||
volatile int x = 1;
|
||||
return _mm256_extract_epi32(dots(_mm256_set1_epi32(x), _mm256_set1_epi32(x)), 0) == 0;
|
||||
}
|
||||
]=] STRATA_HAVE_AVXVNNI_INTRINSICS)
|
||||
unset(CMAKE_REQUIRED_FLAGS)
|
||||
if(STRATA_HAVE_AVXVNNI_INTRINSICS)
|
||||
set(_strata_avxvnni STRATA_AVXVNNI=1)
|
||||
else()
|
||||
set(_strata_avxvnni STRATA_AVXVNNI=0)
|
||||
endif()
|
||||
set_property(SOURCE src/kernels/cpu/q2_avx2.cpp APPEND PROPERTY COMPILE_DEFINITIONS ${_strata_avxvnni})
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(strata_kernels_cpu PUBLIC Threads::Threads)
|
||||
|
||||
@@ -857,6 +1097,22 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
foreach(_f GGML_AVX GGML_AVX2 GGML_FMA GGML_F16C GGML_BMI2)
|
||||
set(${_f} ON CACHE BOOL "" FORCE)
|
||||
endforeach()
|
||||
elseif(STRATA_ISA_FLOOR)
|
||||
# the experimental older-CPU build (STRATA_ISA_FLOOR above): one ggml-cpu for the floor, ggml's "sandybridge"
|
||||
# feature set (SSE4.2 + AVX) or SSE4.2 alone - not the build host's, which may be newer than the PC it runs on.
|
||||
# From the Strata_Dirigo fork (rwkeyes). ggml's defaults turn the rest on when GGML_NATIVE is off: all off here.
|
||||
set(GGML_NATIVE OFF CACHE BOOL "" FORCE)
|
||||
set(GGML_CPU_ALL_VARIANTS OFF CACHE BOOL "" FORCE)
|
||||
set(GGML_SSE42 ON CACHE BOOL "" FORCE)
|
||||
if(STRATA_ISA_FLOOR STREQUAL "avx")
|
||||
set(GGML_AVX ON CACHE BOOL "" FORCE)
|
||||
else()
|
||||
set(GGML_AVX OFF CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
foreach(_f GGML_AVX2 GGML_FMA GGML_F16C GGML_BMI2 GGML_AVX_VNNI GGML_AVX512 GGML_AVX512_VBMI GGML_AVX512_VNNI
|
||||
GGML_AVX512_BF16 GGML_AMX_TILE GGML_AMX_INT8 GGML_AMX_BF16)
|
||||
set(${_f} OFF CACHE BOOL "" FORCE)
|
||||
endforeach()
|
||||
else()
|
||||
set(GGML_NATIVE ON CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
@@ -877,20 +1133,51 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
# part of strata_kernels_cpu (the pool calls these and they call its CPU probe: one library, so GNU ld's
|
||||
# one-pass static linking has no cycle to resolve)
|
||||
target_sources(strata_kernels_cpu PRIVATE src/kernels/cpu/native_expert.cpp src/kernels/cpu/iq_avx512.cpp
|
||||
src/kernels/cpu/iq_avx2.cpp src/kernels/cpu/kq_avx2.cpp)
|
||||
src/kernels/cpu/iq_avx2.cpp src/kernels/cpu/kq_avx2.cpp
|
||||
src/kernels/cpu/kq_avx1.cpp)
|
||||
target_include_directories(strata_kernels_cpu PRIVATE third_party/ggml)
|
||||
if(MSVC)
|
||||
set_source_files_properties(src/kernels/cpu/iq_avx512.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX512")
|
||||
set_source_files_properties(src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
|
||||
set_source_files_properties(src/kernels/cpu/kq_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
|
||||
# the router lookahead's AVX1 dot (older CPUs; called behind cpu_avx1_ok): no FMA, no 256-bit integer ops
|
||||
set_source_files_properties(src/kernels/cpu/kq_avx1.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX")
|
||||
else()
|
||||
set_source_files_properties(src/kernels/cpu/iq_avx512.cpp PROPERTIES COMPILE_OPTIONS
|
||||
"-mavx512f;-mavx512bw;-mavx512vl;-mavx512dq;-mavx512vnni;-mavx512vbmi;-mf16c;-mfma")
|
||||
set_source_files_properties(src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
|
||||
set_source_files_properties(src/kernels/cpu/kq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
|
||||
set_source_files_properties(src/kernels/cpu/kq_avx1.cpp PROPERTIES COMPILE_OPTIONS "-mavx")
|
||||
endif()
|
||||
set_property(SOURCE src/kernels/cpu/iq_avx2.cpp APPEND PROPERTY COMPILE_DEFINITIONS ${_strata_avxvnni})
|
||||
if(STRATA_ISA_FLOOR STREQUAL "avx")
|
||||
target_compile_definitions(strata_kernels_cpu PRIVATE STRATA_ISA_FLOOR_AVX=1)
|
||||
elseif(STRATA_ISA_FLOOR STREQUAL "none")
|
||||
target_compile_definitions(strata_kernels_cpu PRIVATE STRATA_ISA_FLOOR_NONE=1)
|
||||
endif()
|
||||
target_link_libraries(strata_kernels_cpu PUBLIC ggml-cpu ggml-base)
|
||||
target_compile_definitions(strata_kernels_cpu PUBLIC STRATA_NATIVE_EXPERTS=1)
|
||||
if(STRATA_BUILD_TESTS)
|
||||
# the Q5_1 block dequantizer (a Q5_1 PLE table) against ggml's, synthetic blocks: no model, no GPU
|
||||
add_executable(dequant_q5_1_test tests/core/dequant_q5_1_test.cpp)
|
||||
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 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)
|
||||
if(STRATA_BUILD_TESTS)
|
||||
add_test(NAME router_dot_parity COMMAND router_dot_parity)
|
||||
add_test(NAME router_dot_parity_avx COMMAND ${CMAKE_COMMAND} -E env STRATA_FORCE_ISA=avx $<TARGET_FILE:router_dot_parity>)
|
||||
endif()
|
||||
# the AVX-2 expert kernels' variants (iq_avx2.cpp, q2_avx2.cpp) bit for bit against each other at 1..8 tokens and
|
||||
# against ggml-cpu's dot, on random blocks; --bench: ms per expert on one thread, the weights streamed from DRAM
|
||||
add_executable(iq_avx2_parity src/kernels/cpu/iq_avx2_parity.cpp)
|
||||
target_link_libraries(iq_avx2_parity PRIVATE strata_kernels_cpu)
|
||||
if(STRATA_BUILD_TESTS)
|
||||
add_test(NAME iq_avx2_parity COMMAND iq_avx2_parity)
|
||||
add_test(NAME iq_avx2_parity_iq3s_mt1 COMMAND ${CMAKE_COMMAND} -E env STRATA_IQ3S_MT1=1 $<TARGET_FILE:iq_avx2_parity>)
|
||||
endif()
|
||||
set(_strata_mmq_types q2_0 iq2_xxs iq2_xs iq2_s iq3_xxs iq3_s iq4_nl iq4_xs q8_0)
|
||||
# #296: OrcaRouter Q4_K_S's Q5_0 experts (Q4_K and Q5_1 come with STRATA_MMQ_KQUANTS on CUDA, S5's branch)
|
||||
if(STRATA_ORCA_Q4KS_MMQ)
|
||||
@@ -912,10 +1199,13 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
src/prefill/ggml_cuda_host.cu ${_strata_mmq}/quantize.cu)
|
||||
# q8_0: the drafter's matrices (E-9); STRATA_ORCA_Q4KS_MMQ adds q5_0 (#296, in _strata_mmq_types), and
|
||||
# STRATA_MMQ_KQUANTS q4_k q5_k q5_1, the experts of Unsloth's UD-Q4_K_XL (docs/UNSLOTH_Q4.md; the dispatch
|
||||
# follows eddoursul/Strata 7706c1b)
|
||||
# follows eddoursul/Strata 7706c1b); q6_k is HIP only
|
||||
set(_strata_mmq_cuda_types ${_strata_mmq_types})
|
||||
if(STRATA_MMQ_KQUANTS)
|
||||
list(APPEND _strata_mmq_cuda_types q4_k q5_k q5_1)
|
||||
if(STRATA_Q6K_EXPERTS) # UD-Q6_K_XL gate/up (docs/UNSLOTH_Q6.md)
|
||||
list(APPEND _strata_mmq_cuda_types q6_k)
|
||||
endif()
|
||||
endif()
|
||||
foreach(_t IN LISTS _strata_mmq_cuda_types)
|
||||
target_sources(strata_mmq PRIVATE ${_strata_mmq}/template-instances/mmq-instance-${_t}.cu)
|
||||
@@ -966,25 +1256,52 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
if(NOT EXISTS "${_strata_mmq}/common.cuh" OR NOT EXISTS "${_strata_mmq}/quantize.cu")
|
||||
message(FATAL_ERROR "The selected GGML source is missing the ggml-cuda MMQ sources: ${_strata_mmq}")
|
||||
endif()
|
||||
# Aurora: moe_fused.cu's and moe_fused_iq.cu's gfx11 WMMA kernels for the prompt experts (opt-in STRATA_PF_FUSED=1)
|
||||
set(_strata_mmq_sources
|
||||
src/prefill/moe_mmq.cu
|
||||
src/prefill/moe_fused.cu
|
||||
src/prefill/moe_fused_iq.cu
|
||||
src/prefill/ggml_cuda_host.cu
|
||||
"${_strata_mmq}/quantize.cu")
|
||||
if(STRATA_MMQ_KQUANTS)
|
||||
# the same K-quant instances as the CUDA branch: on HIP they serve the dense GGUF projections
|
||||
# of Gemm::native (STRATA_DENSE_MMQ=1), the mixed-quant packs' attention/PLE tensors
|
||||
list(APPEND _strata_mmq_types q4_k q5_k q5_1 q6_k)
|
||||
endif()
|
||||
foreach(_t IN LISTS _strata_mmq_types)
|
||||
list(APPEND _strata_mmq_sources "${_strata_mmq}/template-instances/mmq-instance-${_t}.cu")
|
||||
endforeach()
|
||||
set_source_files_properties(${_strata_mmq_sources} PROPERTIES LANGUAGE HIP)
|
||||
# moe_fused.cu is Strata's own CUDA-shaped source: it takes the HIP compat headers, ggml's MMQ sources do not
|
||||
set_source_files_properties(src/prefill/moe_fused.cu src/prefill/moe_fused_iq.cu
|
||||
PROPERTIES INCLUDE_DIRECTORIES "${STRATA_HIP_COMPAT_INCLUDE_DIR}")
|
||||
add_library(strata_mmq STATIC ${_strata_mmq_sources})
|
||||
target_include_directories(strata_mmq
|
||||
PRIVATE "${_strata_gg}/include" "${_strata_gg}/src" "${_strata_mmq}"
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
target_compile_definitions(strata_mmq PRIVATE GGML_USE_HIP=1)
|
||||
if(STRATA_MMQ_KQUANTS)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_KQUANTS=1)
|
||||
endif()
|
||||
if(STRATA_ORCA_Q4KS_MMQ)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_ORCA_Q4KS_MMQ=1)
|
||||
endif()
|
||||
target_link_libraries(strata_mmq PUBLIC ggml-base hip::host roc::hipblas)
|
||||
target_link_libraries(strata_prefill PUBLIC strata_mmq)
|
||||
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1)
|
||||
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1 STRATA_PREFILL_FUSED=1)
|
||||
# Aurora: the gfx11 fused experts against the FP64 reference and MMQ (tests/cuda's test; exit 77 off gfx11)
|
||||
add_executable(hip_prefill_fused_moe tests/cuda/prefill_fused_moe_test.cpp)
|
||||
target_include_directories(hip_prefill_fused_moe PRIVATE "${_strata_gg}/include")
|
||||
target_link_libraries(hip_prefill_fused_moe PRIVATE strata_mmq ggml-base strata_hip_runtime)
|
||||
add_test(NAME hip_prefill_fused_moe COMMAND hip_prefill_fused_moe)
|
||||
set_tests_properties(hip_prefill_fused_moe PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 300)
|
||||
list(APPEND _strata_hip_tests hip_prefill_fused_moe)
|
||||
add_executable(hip_prefill_fused_iq tests/cuda/prefill_fused_iq_test.cpp)
|
||||
target_include_directories(hip_prefill_fused_iq PRIVATE "${_strata_gg}/include")
|
||||
target_link_libraries(hip_prefill_fused_iq PRIVATE strata_mmq ggml-base strata_hip_runtime)
|
||||
add_test(NAME hip_prefill_fused_iq COMMAND hip_prefill_fused_iq)
|
||||
set_tests_properties(hip_prefill_fused_iq PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 600)
|
||||
list(APPEND _strata_hip_tests hip_prefill_fused_iq)
|
||||
|
||||
# Numerical diagnostic: synthetic layouts by default, optional first-expert IQ3_XXS/IQ4_NL check with a pack.
|
||||
add_executable(hip_prefill_mmq_parity tests/hip/prefill_mmq_parity.cpp)
|
||||
@@ -1003,6 +1320,9 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
add_executable(native_expert_parity src/kernels/native_expert_parity.cpp)
|
||||
target_include_directories(native_expert_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(native_expert_parity PRIVATE strata_kernels strata_kernels_cpu strata_artifact ${_strata_gpu_runtime_target})
|
||||
add_executable(native_expert_bench src/kernels/native_expert_bench.cpp)
|
||||
target_include_directories(native_expert_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(native_expert_bench PRIVATE strata_kernels strata_kernels_cpu strata_artifact ${_strata_gpu_runtime_target})
|
||||
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA)
|
||||
# Unsloth UD-Q4_K_XL's three expert pairs (and Q8_0 alone, its embedding) on synthetic ggml-quantized weights
|
||||
# at 2560/640: GPU and CPU experts against the float reference, GPU dequantizers against to_float.
|
||||
@@ -1011,7 +1331,17 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
# kernel for it before; q5_0/q5_0 (both roles Q5_0) is left out of this list since stacking the same
|
||||
# lower-precision format on both roles pushes the GPU path over this test's 3% threshold (quantization
|
||||
# noise, not a kernel bug -- see the PR).
|
||||
foreach(_pair q4_K/q5_1 q4_K/q8_0 q5_K/q8_0 q8_0/q8_0 q4_K/q5_0 q5_0/q8_0)
|
||||
# q4_0/q4_0 and q4_0/q4_1: a plain llama-quantize Q4_0 file (bartowski's Qwen3.8-Flash-Next-Q4_0: gate/up Q4_0,
|
||||
# down Q4_0 on 42 layers and Q4_1 on 6)
|
||||
# q6_K/q8_0: Unsloth UD-Q6_K_XL gate/up Q6_K with Q8_0 down (docs/UNSLOTH_Q6.md). Layer 2 is q8_0/q8_0, already listed.
|
||||
set(_strata_parity_pairs q4_K/q5_1 q4_K/q8_0 q5_K/q8_0 q8_0/q8_0 q4_K/q5_0 q5_0/q8_0 q4_0/q4_0 q4_0/q4_1)
|
||||
if(STRATA_Q6K_EXPERTS)
|
||||
list(APPEND _strata_parity_pairs q6_K/q8_0)
|
||||
else() # a pair without GPU kernels (Q6_K gate/up) is refused by native_expert_supported, not run
|
||||
add_test(NAME native_expert_parity_refuses_q6_K COMMAND native_expert_parity --synthetic q6_K/q8_0)
|
||||
set_tests_properties(native_expert_parity_refuses_q6_K PROPERTIES WILL_FAIL TRUE)
|
||||
endif()
|
||||
foreach(_pair ${_strata_parity_pairs})
|
||||
string(REPLACE "/" "_" _name "${_pair}")
|
||||
add_test(NAME native_expert_parity_${_name} COMMAND native_expert_parity --synthetic ${_pair})
|
||||
set_tests_properties(native_expert_parity_${_name} PROPERTIES TIMEOUT 120)
|
||||
@@ -1019,9 +1349,6 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
add_test(NAME native_expert_parity_q5_1_min COMMAND native_expert_parity --q5_1-min)
|
||||
# #290: the BF16 token embedding (--embd-gguf) gathered and dequantized bit for bit
|
||||
add_test(NAME native_expert_parity_bf16_embd COMMAND native_expert_parity --bf16-embd)
|
||||
# a pair without GPU kernels (Q6_K gate/up) is refused by native_expert_supported, not run
|
||||
add_test(NAME native_expert_parity_refuses_q6_K COMMAND native_expert_parity --synthetic q6_K/q8_0)
|
||||
set_tests_properties(native_expert_parity_refuses_q6_K PROPERTIES WILL_FAIL TRUE)
|
||||
endif()
|
||||
endif()
|
||||
# #296's real-weight checks (OrcaRouter Q4_K_S files; docs/ORCA_Q4_K_S.md) - test programs, not part of a release
|
||||
@@ -1043,6 +1370,15 @@ if(STRATA_ENABLE_CUDA AND STRATA_BUILD_TESTS AND EXISTS "${CMAKE_CURRENT_SOURCE_
|
||||
endif()
|
||||
|
||||
if(STRATA_BUILD_TESTS)
|
||||
if(WIN32)
|
||||
add_executable(pool_affinity_test src/kernels/cpu/pool_affinity_test.cpp)
|
||||
target_link_libraries(pool_affinity_test PRIVATE strata_kernels_cpu)
|
||||
add_test(NAME pool_affinity_test COMMAND pool_affinity_test)
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
add_executable(pool_affinity_test src/kernels/cpu/pool_affinity_linux_test.cpp)
|
||||
target_link_libraries(pool_affinity_test PRIVATE strata_kernels_cpu)
|
||||
add_test(NAME pool_affinity_test COMMAND pool_affinity_test)
|
||||
endif()
|
||||
# Plan v0.3 P6: one expert applied to several tokens, bitwise vs the single-token kernel (synthetic, CPU only).
|
||||
# plan v0.3 P5: the device dequantizers vs the artifact's CPU dequantizers on the real shards (not a ctest: needs
|
||||
# the 38 GB model; run it by hand or from tools/gpu_session.py)
|
||||
@@ -1055,6 +1391,14 @@ if(STRATA_BUILD_TESTS)
|
||||
target_link_libraries(expert_multi_test PRIVATE strata_kernels_cpu)
|
||||
add_test(NAME expert_multi_test COMMAND expert_multi_test)
|
||||
|
||||
# PR #706: the opt-in AVX-2 Q2_0 bit-plane kernel (STRATA_Q2_BITPLANE=1) against the legacy one, bitwise alone/window
|
||||
if(STRATA_BUILD_TESTS AND CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64|x86_64|x64|X86")
|
||||
add_executable(q2_bitplane_parity src/kernels/cpu/q2_bitplane_parity.cpp)
|
||||
target_link_libraries(q2_bitplane_parity PRIVATE strata_kernels_cpu)
|
||||
add_test(NAME q2_bitplane_parity COMMAND q2_bitplane_parity)
|
||||
set_tests_properties(q2_bitplane_parity PROPERTIES ENVIRONMENT "STRATA_Q2_BITPLANE=1")
|
||||
endif()
|
||||
|
||||
add_executable(expert_parity src/kernels/cpu/expert_parity.cpp)
|
||||
target_link_libraries(expert_parity PRIVATE strata_kernels_cpu)
|
||||
# The pack is a REQUIRED input, not an optional one: this test compares against a real expert blob, and a
|
||||
@@ -1085,3 +1429,15 @@ if(STRATA_BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ---- HIP gfx906: every CUDA source of the tree (and the ggml-cuda MMQ sources) is compiled as HIP ----
|
||||
if(STRATA_HIP_GFX906)
|
||||
file(GLOB_RECURSE _strata_cu CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cu ${CMAKE_CURRENT_SOURCE_DIR}/bench/micro/*.cu
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/vision/*.cu)
|
||||
if(_strata_mmq)
|
||||
file(GLOB _strata_mmq_cu ${_strata_mmq}/*.cu ${_strata_mmq}/template-instances/*.cu)
|
||||
list(APPEND _strata_cu ${_strata_mmq_cu})
|
||||
target_compile_definitions(strata_mmq PRIVATE GGML_USE_HIP)
|
||||
endif()
|
||||
set_source_files_properties(${_strata_cu} PROPERTIES LANGUAGE HIP)
|
||||
endif()
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
|
||||
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · **Deutsch** · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
|
||||
|
||||
<p align="center"><b>Ein KI-Modell mit 125 Milliarden Parametern auf deinem eigenen Gaming-PC</b><br>
|
||||
NVIDIA- oder AMD-Grafikkarte (ab 12 GB) · Windows oder Linux · kostenlos und Open Source</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Ein Voxel-Pagodengarten, den das Modell in Strata geschrieben hat, läuft im Browser"></a><br>
|
||||
<sub>Ein Voxel-Pagodengarten aus einem einzigen Prompt, auf einer RTX 5070 mit Strata (IQ3_S, 128K Kontext) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">ganzes Video (49 s)</a></sub></p>
|
||||
|
||||
Strata lässt **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** auf einem normalen PC laufen.
|
||||
Das ist ein großes, kluges KI-Modell, das sonst meist einen Server braucht. Es chattet, schreibt Code, versteht
|
||||
Bilder und arbeitet mit deinen Apps und Coding-Agents zusammen. Nichts verlässt deinen PC.
|
||||
|
||||
## Wie schnell ist es?
|
||||
|
||||
Wir haben es auf zwei ganz normalen Gaming-PCs gemessen. Ein Token ist etwa ¾ eines Wortes.
|
||||
|
||||
- **Schreibt Antworten:** wie schnell die Antwort in einem kurzen Chat erscheint. 60 Tokens pro Sekunde sind
|
||||
schneller, als du lesen kannst.
|
||||
- **Liest deinen Prompt:** wie schnell es aufnimmt, was du schickst (hier ein Dokument, Code oder Chatverlauf mit
|
||||
32K Tokens).
|
||||
|
||||
<table>
|
||||
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
|
||||
<tr><td>
|
||||
|
||||
| Größe | Schreibt Antworten | Liest deinen Prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 94 Tokens/s | 2,650 Tokens/s |
|
||||
| **IQ2_XS** | 79 Tokens/s | 2,090 Tokens/s |
|
||||
| **IQ3_XXS** | 62 Tokens/s | 1,750 Tokens/s |
|
||||
| **IQ3_S** | 53 Tokens/s | 1,620 Tokens/s |
|
||||
| **Coder** | 55 Tokens/s | 2,180 Tokens/s |
|
||||
|
||||
</td><td>
|
||||
|
||||
| Größe | Schreibt Antworten | Liest deinen Prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 60 Tokens/s | 1,160 Tokens/s |
|
||||
| **IQ2_XS** | 52 Tokens/s | 1,110 Tokens/s |
|
||||
| **Coder** | 44 Tokens/s | 1,420 Tokens/s |
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
NVIDIA: Q2_0 mit Engine 0.1.36, die anderen Zeilen mit 0.1.26 (4K Antworten, 32K Prompts). Die vollständigen
|
||||
Tabellen stehen in [DETAILS.md](docs/DETAILS.md#speed-measured). Eine Karte mit mehr VRAM ist schneller: Eine
|
||||
RTX 3090 (24 GB) sollte etwa 100-140 Tokens pro Sekunde schreiben. Lange Chats und andere Karten:
|
||||
[Tempo jedes Modells](docs/MODELS.md#how-fast-is-each-size), [Ergebnisse aus der Community](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
|
||||
<sub>Strata ist kostenlos. Wenn es auf deinem PC gut läuft, hält ein Kaffee die Arbeit daran am Laufen.</sub></p>
|
||||
|
||||
## Was du brauchst
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **Grafikkarte** | **NVIDIA** GeForce RTX 20, 30, 40 oder 50 Serie, oder **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 oder RX 6800 / 6900 Serie. Sie braucht **12 GB VRAM oder mehr**. |
|
||||
| **RAM** | 32 GB oder mehr. Dein RAM entscheidet, [welches Modell](#welches-modell-soll-ich-nehmen) passt. Mit 64 GB laufen alle Größen. |
|
||||
| **Festplatte** | Etwa 80 GB frei. Nimm wenn möglich eine SSD: Der erste Start geht dann viel schneller. |
|
||||
| **System** | Windows 10 / 11 oder Linux und ein aktueller Grafiktreiber von NVIDIA oder AMD. |
|
||||
|
||||
Alles andere richtet der Installer ein. Zwei oder drei Karten können sich das Modell teilen ([Multi-GPU](docs/MULTI_GPU.md)).
|
||||
|
||||
Experimentell, von Community-Mitgliedern auf ihren eigenen Rechnern geschrieben und getestet:
|
||||
|
||||
- **Ältere Grafikkarten** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Ältere GPUs](docs/OLDER_GPUS.md).
|
||||
- **Intel Arc**, unter Linux aus dem Quellcode gebaut: [Intel Arc](docs/INTEL_ARC.md).
|
||||
- **AMD Ryzen AI Max (Strix Halo)**, unter Linux aus dem Quellcode gebaut: [Strix Halo](docs/STRIX_HALO.md).
|
||||
- **Ältere Prozessoren ohne AVX2**: Sie funktionieren, aber langsam. [Ältere CPUs](docs/INSTALL.md#older-cpus-experimental).
|
||||
|
||||
Die vollständige Liste: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
|
||||
## Installieren
|
||||
|
||||
### Lass deine KI es einrichten
|
||||
|
||||
Nutzt du einen KI-Coding-Assistenten (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Füge dort diesen Text ein:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
Er prüft deine Grafikkarte, deinen RAM und deine Festplatte und wählt das passende Modell. Dann installiert und
|
||||
startet er es und sagt dir, wie du deine Apps verbindest. KI-Tools können Strata auch über seinen
|
||||
[MCP-Server](docs/MCP_SERVER.md) installieren, starten und stoppen.
|
||||
|
||||
### Oder mach es selbst
|
||||
|
||||
[Lade Strata herunter](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) und entpacke es (oder nutze
|
||||
`git clone`). **Windows:** Doppelklick auf **`START-HERE.bat`**. **Linux:** Führe im Strata-Ordner **`./setup.sh`** aus.
|
||||
|
||||
Die Schritte sind für NVIDIA und AMD gleich. Der Installer erkennt deine Karte und richtet die passende Engine
|
||||
dafür ein. Er stellt dir ein paar Fragen:
|
||||
|
||||
- welches Modell und welche Größe,
|
||||
- wie viel Kontext (wie viel Text sich das Modell merkt),
|
||||
- ob es Bilder lesen soll.
|
||||
|
||||
Drück jedes Mal Enter, um die empfohlene Antwort zu nehmen. Dann lädt er das Modell herunter (etwa 70 GB) und
|
||||
startet es. Bricht der Download ab, starte ihn einfach neu: Er macht dort weiter, wo er aufgehört hat. Dein Browser
|
||||
öffnet die Strata-App unter `http://127.0.0.1:8080`.
|
||||
|
||||
> **Während das Modell startet, kann dein PC 1-3 Minuten langsam sein oder nicht reagieren** (beim ersten Mal am
|
||||
> längsten). Strata lädt 35-55 GB in deinen RAM und reserviert einen Teil davon für die Grafikkarte. Das ist normal.
|
||||
> Warte und schließ das Fenster nicht. Im Fenster siehst du, was Strata gerade tut.
|
||||
|
||||
**Beim nächsten Mal** startest du wieder `START-HERE.bat` (oder `./setup.sh`). Es startet sofort und lädt nichts
|
||||
doppelt herunter. Schließ das Fenster, um das Modell zu stoppen. `UPDATE.bat` (`./update.sh`) aktualisiert Strata,
|
||||
ohne es zu starten. Updates, Docker, mehrere Karten, wo die Dateien landen und alle Optionen:
|
||||
[docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
## Welches Modell soll ich nehmen?
|
||||
|
||||
Der Installer empfiehlt eins passend zu deinem RAM. Dasselbe Modell gibt es in mehreren Größen, mehr oder weniger
|
||||
stark komprimiert. Kleinere Größen sind schneller. Größere sind etwas klüger.
|
||||
|
||||
| Dein RAM | Nimm | Warum |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Coder** | passt in 32 GB und ist für Code gemacht (mit einer 24-GB-Karte laufen auch Q2_0 und IQ2_XS) |
|
||||
| **48 GB** | **IQ2_XS** (oder Q2_0, das schnellste) | die größeren Größen passen nicht |
|
||||
| **64 GB** | **IQ2_XS** (empfohlen) oder IQ3_XXS / IQ3_S | alle Größen passen; IQ3_S ist das beste und das langsamste |
|
||||
| **96 GB oder mehr** | **IQ3_S** oder Unsloths UD-IQ4_XS (~4 Bit) | Platz für die größten Größen, auch wenn alles andere offen ist |
|
||||
|
||||
- **[Coder](docs/MODELS.md#coder):** eine Version für Code, bei der die Hälfte der Experten entfernt wurde. Sie
|
||||
erreicht 91 % des SWE-bench-Verified-Werts des vollen Modells (von ihren Autoren gemessen) und passt in 32 GB RAM.
|
||||
Außerhalb von Code ist sie schwächer, auch bei chinesischem und anderem CJK-Text (#438). Nimm dafür Q2_0, IQ2_XS
|
||||
oder IQ3_S, die alle Experten behalten.
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15):** ein Fine-Tune, das viel kürzer nachdenkt, bevor es antwortet. Du
|
||||
bekommst die Antwort früher, bei etwa gleicher Qualität.
|
||||
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** die ~4-Bit-Version von Unsloth, in der Qualität
|
||||
zwischen IQ3_S und UD-Q4_K_XL. 94 GB Download. Mit weniger als ~80 GB RAM liest Strata einen Teil davon während
|
||||
der Antwort von der SSD, dort ist es also langsamer (eine NVMe-SSD hilft).
|
||||
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimentell): am nächsten am vollen
|
||||
Modell. Aber Strata liest das meiste davon während der Antwort von der SSD, deshalb schreibt es auf einem PC mit
|
||||
64 GB nur 7-8.5 Tokens/s.
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** Das richtest du von Hand
|
||||
ein. Es steht nicht im Menü des Installers.
|
||||
|
||||
Größen, Downloads und was wohin passt: [docs/MODELS.md](docs/MODELS.md). Um später ein weiteres Modell
|
||||
hinzuzufügen, starte `SETUP.bat` (Linux: `./setup.sh --setup`).
|
||||
|
||||
## So benutzt du es
|
||||
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="Der Monitor-Tab der Strata-App neben einem Coding-Agent"><br>
|
||||
<sub>Der <b>Monitor</b> der Strata-App (links), während ein Coding-Agent den Pagodengarten aus dem Video schreibt (rechts)</sub></p>
|
||||
|
||||
- **Im Browser:** Öffne `http://127.0.0.1:8080`. Dort gibt es **Chat**, einen Live-**Monitor** für das Modell und
|
||||
deine GPU/CPU/RAM und **About** mit den Einstellungen und Adressen.
|
||||
- **Deine Apps und Coding-Agents:** Füge einen „OpenAI-kompatiblen“ Anbieter mit der Basis-URL
|
||||
**`http://127.0.0.1:8080/v1`** hinzu. Jeder API-Key und jeder Modellname funktioniert.
|
||||
- Apps, die die API von Anthropic nutzen: `http://127.0.0.1:8080/v1/messages` (Claude Code:
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
|
||||
- Codex CLI und andere Apps, die die OpenAI Responses API nutzen: `/v1/responses`
|
||||
([Einrichtung](docs/DETAILS.md#the-responses-api-and-codex-cli)).
|
||||
- **Nachdenken:** Wähle **aus, niedrig, mittel oder hoch** im Chat-Menü oder unter „Reasoning Effort“ in deiner
|
||||
App. Aus ist am schnellsten. Hoch ist am besten für schwierige Fragen.
|
||||
- **Bilder:** Antworte im Setup mit Ja auf „Images?“. Klick dann im Chat auf **Picture** oder hänge Bilder in deiner
|
||||
App an. AMD-Karten lesen Bilder unter Linux über den Prozessor; unter Windows geht das noch nicht.
|
||||
- **Vom Handy oder einem anderen PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Setz immer einen
|
||||
Key.
|
||||
- **Eine Anfrage nach der anderen:** Standardmäßig beantwortet Strata eine Anfrage, die anderen warten. Um mehrere
|
||||
gleichzeitig zu beantworten, setz `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Auf einer 12-GB-Karte wird
|
||||
dadurch jede Antwort langsamer.
|
||||
- **Lange Prompts:** Strata liest die erste Nachricht eines Chats komplett, etwa 1 Minute pro 30,000 Tokens.
|
||||
Folgenachrichten starten in Sekunden.
|
||||
|
||||
Mehr: [wo deine Chats gespeichert werden](docs/INSTALL.md#where-things-are-stored), [die API](docs/DETAILS.md#using-it).
|
||||
|
||||
## Etwas ist schiefgelaufen?
|
||||
|
||||
- **Mein PC ist beim ersten Start von Strata eingefroren.** Das ist normal, während das Modell lädt. Warte und
|
||||
schließ das Fenster nicht. Nach 10 Minuten immer noch eingefroren? Starte den PC neu, schließ andere Programme und
|
||||
versuch es noch einmal, oder nimm eine kleinere Größe.
|
||||
- **Es hat beim Herunterladen oder Installieren aufgehört.** Starte `START-HERE.bat` (oder `./setup.sh`) noch
|
||||
einmal. Es macht dort weiter, wo es aufgehört hat.
|
||||
- **Es ist sehr langsam und die Festplattenlampe blinkt ständig, oder es meldet „the engine stopped
|
||||
unexpectedly“.** Dein PC hat nicht genug freien RAM. Schließ andere Programme (Browser brauchen viel) oder nimm
|
||||
eine kleinere Größe (Q2_0 oder IQ2_XS).
|
||||
- **Es meldet, dass Port 8080 schon belegt ist.** Strata läuft bereits. Such nach seinem Fenster.
|
||||
|
||||
Weitere Probleme und ihre Lösungen: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Kommst du nicht weiter?
|
||||
Öffne ein [Issue](https://github.com/Niko1221/Strata/issues) und hänge `strata-<model>.log` aus dem Strata-Ordner
|
||||
an. Du hast ein Sicherheitsproblem gefunden? Melde es vertraulich: [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Wie funktioniert es?
|
||||
|
||||
Modelle wie dieses laufen normalerweise auf Servern mit Hunderten Gigabyte Grafikspeicher. Deine Grafikkarte hat
|
||||
12-24 GB. Strata bringt das Modell trotzdem unter, indem es **die Arbeit auf deinen ganzen PC verteilt**. Stell dir
|
||||
eine Küche vor: Was du ständig brauchst, bleibt auf der Arbeitsfläche, der Rest wartet in der Vorratskammer.
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Die 24,576 Experten des Modells: die meistgenutzten auf der Grafikkarte, alle im RAM, eine Nachschlagetabelle auf der SSD"></p>
|
||||
|
||||
- **Das Modell ist ein Team aus 24,576 kleinen Spezialisten („Experten“).** Jedes Wort braucht nur 10 davon.
|
||||
- **Deine Grafikkarte** hält die paar tausend Experten, die am häufigsten gebraucht werden. **Dein RAM** hält alle,
|
||||
und **dein Prozessor** rechnet gleichzeitig am Rest. **Deine SSD** hält eine große Nachschlagetabelle.
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Ein kleiner Helfer rät die nächsten Wörter; das große Modell prüft alle auf einmal und behält die richtigen"></p>
|
||||
|
||||
- **Raten, dann prüfen:** Ein kleiner Helfer rät die nächsten paar Wörter. Das große Modell prüft sie alle auf
|
||||
einmal. Du bekommst dieselbe Antwort, nur 1.6-1.8x schneller.
|
||||
- **Lange Texte werden in großen Stücken gelesen** (bis zu 8,192 Tokens auf einmal), mit über 1,000 Tokens pro
|
||||
Sekunde.
|
||||
|
||||
Die längere Erklärung: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Jeder Teil und seine Zahlen:
|
||||
[die Details](docs/DETAILS.md#how-it-works) und das [Paper](docs/paper/Strata-Paper.pdf).
|
||||
|
||||
## Danksagung und Lizenz
|
||||
|
||||
Das Modell ist [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) vom Qwen-Team. Komprimiert
|
||||
wurde es von [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
|
||||
und Unsloth. Strata nutzt Teile von [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Alle Danksagungen:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata ist Open Source unter der [MIT-Lizenz](LICENSE). Ein
|
||||
paar Teile und alle Modelle haben eigene Lizenzen ([welche](docs/HOW_IT_WORKS.md#license)).
|
||||
|
||||
## Strata unterstützen
|
||||
|
||||
Strata ist kostenlos und Open Source. Wenn es dir nützt, kannst du die Entwicklung unterstützen:
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
|
||||
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · **Español** · [Português](README.pt-BR.md)
|
||||
|
||||
<p align="center"><b>Ejecuta un modelo de IA de 125 mil millones de parámetros en tu propio PC gaming</b><br>
|
||||
Tarjeta gráfica NVIDIA o AMD (12 GB o más) · Windows o Linux · libre y de código abierto</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Un jardín con una pagoda de vóxeles que escribió el modelo de Strata, funcionando en el navegador"></a><br>
|
||||
<sub>Un jardín con una pagoda de vóxeles, hecho con un solo prompt en una RTX 5070 con Strata (IQ3_S, contexto de 128K) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vídeo completo (49 s)</a></sub></p>
|
||||
|
||||
Strata ejecuta **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** en un PC normal. Es un
|
||||
modelo de IA grande e inteligente que normalmente necesita un servidor. Conversa, escribe código, entiende imágenes
|
||||
y funciona con tus aplicaciones y agentes de programación. Nada sale de tu PC.
|
||||
|
||||
## ¿Qué velocidad tiene?
|
||||
|
||||
Lo medimos en dos PC gaming normales. Un token equivale más o menos a ¾ de una palabra.
|
||||
|
||||
- **Escribe respuestas:** lo rápido que aparece la respuesta en un chat corto. 60 tokens por segundo es más rápido de lo que puedes leer.
|
||||
- **Lee tu prompt:** lo rápido que procesa lo que le envías (aquí, un documento, código o historial de chat de 32K tokens).
|
||||
|
||||
<table>
|
||||
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
|
||||
<tr><td>
|
||||
|
||||
| Tamaño | Escribe respuestas | Lee tu prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
|
||||
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
|
||||
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
|
||||
| **Coder** | 55 tokens/s | 2,180 tokens/s |
|
||||
|
||||
</td><td>
|
||||
|
||||
| Tamaño | Escribe respuestas | Lee tu prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
|
||||
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
|
||||
| **Coder** | 44 tokens/s | 1,420 tokens/s |
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
NVIDIA: Q2_0 con el motor 0.1.36, las demás filas con 0.1.26 (respuestas de 4K, prompts de 32K). Las tablas
|
||||
completas están en [DETAILS.md](docs/DETAILS.md#speed-measured). Una tarjeta con más VRAM es más rápida: una
|
||||
RTX 3090 (24 GB) debería escribir unos 100-140 tokens por segundo. Chats largos y otras tarjetas:
|
||||
[velocidad de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados de la comunidad](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
|
||||
<sub>Strata es gratis. Si funciona bien en tu PC, un café ayuda a seguir trabajando en él.</sub></p>
|
||||
|
||||
## Qué necesitas
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **Tarjeta gráfica** | **NVIDIA** GeForce serie RTX 20, 30, 40 o 50, o **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 o serie RX 6800 / 6900. Necesita **12 GB de VRAM o más**. |
|
||||
| **RAM** | 32 GB o más. Tu RAM decide [qué modelo](#qué-modelo-elijo) cabe. Con 64 GB funcionan todos los tamaños. |
|
||||
| **Disco** | Unos 80 GB libres. Usa un SSD si puedes: el primer arranque es mucho más rápido. |
|
||||
| **Sistema** | Windows 10 / 11 o Linux, y un controlador gráfico actual de NVIDIA o AMD. |
|
||||
|
||||
El instalador prepara todo lo demás. Dos o tres tarjetas pueden repartirse el modelo ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
|
||||
Experimental, escrito y probado por miembros de la comunidad en sus propios equipos:
|
||||
|
||||
- **Tarjetas gráficas antiguas** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md).
|
||||
- **Intel Arc**, compilado desde el código fuente en Linux: [Intel Arc](docs/INTEL_ARC.md).
|
||||
- **AMD Ryzen AI Max (Strix Halo)**, compilado desde el código fuente en Linux: [Strix Halo](docs/STRIX_HALO.md).
|
||||
- **Procesadores antiguos sin AVX2**: funcionan, pero despacio. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
|
||||
|
||||
La lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
|
||||
## Instalación
|
||||
|
||||
### Deja que tu IA lo instale
|
||||
|
||||
¿Usas un asistente de programación con IA (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Pega esto en él:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
Revisa tu tarjeta gráfica, tu RAM y tu disco, y elige el modelo que cabe. Luego lo instala, lo arranca y te explica
|
||||
cómo conectar tus aplicaciones. Las herramientas de IA también pueden instalar, arrancar y detener Strata con su
|
||||
[servidor MCP](docs/MCP_SERVER.md).
|
||||
|
||||
### O hazlo tú mismo
|
||||
|
||||
[Descarga Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) y descomprímelo (o usa `git clone`).
|
||||
**Windows:** haz doble clic en **`START-HERE.bat`**. **Linux:** ejecuta **`./setup.sh`** en la carpeta de Strata.
|
||||
|
||||
Los pasos son los mismos para NVIDIA y AMD. El instalador detecta tu tarjeta y prepara el motor adecuado para ella.
|
||||
Te hace unas pocas preguntas:
|
||||
|
||||
- qué modelo y qué tamaño,
|
||||
- cuánto contexto (cuánto texto tiene presente el modelo),
|
||||
- si debe entender imágenes.
|
||||
|
||||
Pulsa Enter cada vez para usar la respuesta recomendada. Después descarga el modelo (unos 70 GB) y lo arranca. Si la
|
||||
descarga se detiene, vuelve a ejecutarlo: sigue donde lo dejó. Tu navegador abre la aplicación de Strata en
|
||||
`http://127.0.0.1:8080`.
|
||||
|
||||
> **Mientras el modelo arranca, tu PC puede ir lento o dejar de responder durante 1-3 minutos** (más la primera vez).
|
||||
> Strata carga 35-55 GB en tu RAM y reserva una parte para la tarjeta gráfica. Es normal. Espera y no cierres la
|
||||
> ventana. La ventana muestra lo que está haciendo Strata.
|
||||
|
||||
**La próxima vez**, vuelve a ejecutar `START-HERE.bat` (o `./setup.sh`). Arranca enseguida y no descarga nada dos
|
||||
veces. Cierra su ventana para detener el modelo. `UPDATE.bat` (`./update.sh`) actualiza Strata sin arrancarlo.
|
||||
Actualizaciones, Docker, varias tarjetas, dónde se guardan los archivos y todas las opciones: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
## ¿Qué modelo elijo?
|
||||
|
||||
El instalador te recomienda uno según tu RAM. El mismo modelo viene en varios tamaños, más o menos comprimidos. Los
|
||||
tamaños pequeños son más rápidos. Los grandes son algo más inteligentes.
|
||||
|
||||
| Tu RAM | Elige | Por qué |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Coder** | cabe en 32 GB y está hecho para programar (con una tarjeta de 24 GB también funcionan Q2_0 e IQ2_XS) |
|
||||
| **48 GB** | **IQ2_XS** (o Q2_0, el más rápido) | los tamaños más grandes no caben |
|
||||
| **64 GB** | **IQ2_XS** (recomendado), o IQ3_XXS / IQ3_S | caben todos los tamaños; IQ3_S es el mejor y el más lento |
|
||||
| **96 GB o más** | **IQ3_S**, o el UD-IQ4_XS de Unsloth (~4 bits) | espacio para los tamaños más grandes con todo lo demás abierto |
|
||||
|
||||
- **[Coder](docs/MODELS.md#coder):** una versión para programar a la que se le quitó la mitad de los expertos. Alcanza
|
||||
el 91% de la puntuación SWE-bench Verified del modelo completo (medido por sus autores) y cabe en 32 GB de RAM. Es
|
||||
más flojo fuera del código, también con chino y otros textos CJK (#438). Para eso, elige Q2_0, IQ2_XS o IQ3_S, que
|
||||
conservan todos los expertos.
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15):** un ajuste fino que piensa mucho menos tiempo antes de responder. Recibes
|
||||
la respuesta antes, con más o menos la misma calidad.
|
||||
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** la versión de ~4 bits de Unsloth, entre IQ3_S y
|
||||
UD-Q4_K_XL en calidad. Una descarga de 94 GB. Con menos de ~80 GB de RAM, Strata lee una parte desde el SSD
|
||||
mientras responde, así que ahí es más lento (un SSD NVMe ayuda).
|
||||
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental): el más parecido al modelo
|
||||
completo. Pero Strata lee la mayor parte desde el SSD mientras responde, así que solo escribe 7-8.5 tokens/s en un
|
||||
PC con 64 GB.
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** se instala a mano. No está en
|
||||
el menú del instalador.
|
||||
|
||||
Tamaños, descargas y qué cabe dónde: [docs/MODELS.md](docs/MODELS.md). Para añadir otro modelo más adelante, ejecuta
|
||||
`SETUP.bat` (Linux: `./setup.sh --setup`).
|
||||
|
||||
## Cómo usarlo
|
||||
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="La pestaña Monitor de la aplicación de Strata junto a un agente de programación"><br>
|
||||
<sub>El <b>Monitor</b> de la aplicación de Strata (izquierda) mientras un agente de programación escribe el jardín de la pagoda del vídeo (derecha)</sub></p>
|
||||
|
||||
- **En el navegador:** abre `http://127.0.0.1:8080`. Tiene **Chat**, un **Monitor** en vivo del modelo y de tu
|
||||
GPU/CPU/RAM, y **About** con los ajustes y las direcciones.
|
||||
- **Tus aplicaciones y agentes de programación:** añade un proveedor "compatible con OpenAI" con la URL base
|
||||
**`http://127.0.0.1:8080/v1`**. Sirve cualquier clave de API y cualquier nombre de modelo.
|
||||
- Aplicaciones que usan la API de Anthropic: `http://127.0.0.1:8080/v1/messages` (Claude Code:
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
|
||||
- Codex CLI y otras aplicaciones que usan la API Responses de OpenAI: `/v1/responses`
|
||||
([configuración](docs/DETAILS.md#the-responses-api-and-codex-cli)).
|
||||
- **Razonamiento:** elige **off, low, medium o high** en el menú del chat o en el "reasoning effort" de tu aplicación.
|
||||
Off es lo más rápido. High es lo mejor para preguntas difíciles.
|
||||
- **Imágenes:** responde que sí a "Images?" en la instalación. Luego haz clic en **Picture** en el chat, o adjunta
|
||||
imágenes en tu aplicación. Las tarjetas AMD leen imágenes en Linux a través del procesador; en Windows todavía no.
|
||||
- **Desde tu móvil u otro PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Pon siempre una clave.
|
||||
- **Una petición cada vez:** por defecto Strata responde una petición y las demás esperan. Para responder varias a la
|
||||
vez, pon `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). En una tarjeta de 12 GB, cada respuesta va más lenta.
|
||||
- **Prompts largos:** Strata lee entero el primer mensaje de un chat, más o menos 1 minuto por cada 30,000 tokens. Los
|
||||
mensajes siguientes empiezan en segundos.
|
||||
|
||||
Más: [dónde se guardan tus chats](docs/INSTALL.md#where-things-are-stored), [la API](docs/DETAILS.md#using-it).
|
||||
|
||||
## ¿Algo salió mal?
|
||||
|
||||
- **Mi PC se congeló la primera vez que arrancó Strata.** Es normal mientras carga el modelo. Espera y no cierres la
|
||||
ventana. ¿Sigue congelado después de 10 minutos? Reinicia el PC, cierra otros programas y vuelve a intentarlo, o
|
||||
elige un tamaño más pequeño.
|
||||
- **Se detuvo durante la descarga o la instalación.** Vuelve a ejecutar `START-HERE.bat` (o `./setup.sh`). Sigue
|
||||
donde se detuvo.
|
||||
- **Va muy lento y la luz del disco no deja de parpadear, o dice "the engine stopped unexpectedly".** Tu PC no tiene
|
||||
suficiente RAM libre. Cierra otros programas (los navegadores usan mucha), o elige un tamaño más pequeño (Q2_0 o
|
||||
IQ2_XS).
|
||||
- **Dice que el puerto 8080 ya está en uso.** Strata ya está en marcha. Busca su ventana.
|
||||
|
||||
Más problemas y sus soluciones: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). ¿Sigues atascado? Abre un
|
||||
[issue](https://github.com/Niko1221/Strata/issues) y adjunta `strata-<model>.log` de la carpeta de Strata. ¿Has
|
||||
encontrado un problema de seguridad? Infórmalo en privado: [SECURITY.md](SECURITY.md).
|
||||
|
||||
## ¿Cómo funciona?
|
||||
|
||||
Los modelos como este suelen funcionar en servidores con cientos de gigabytes de memoria gráfica. Tu tarjeta gráfica
|
||||
tiene 12-24 GB. Strata hace que el modelo quepa **repartiendo el trabajo por todo tu PC**. Piensa en una cocina: lo
|
||||
que usas todo el rato se queda en la encimera, y el resto espera en la despensa.
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Los 24,576 expertos del modelo: los más usados en la tarjeta gráfica, todos en la RAM y una tabla de consulta en el SSD"></p>
|
||||
|
||||
- **El modelo es un equipo de 24,576 pequeños especialistas ("expertos").** Cada palabra solo necesita 10 de ellos.
|
||||
- **Tu tarjeta gráfica** guarda los pocos miles de expertos que más se usan. **Tu RAM** los tiene todos, y **tu
|
||||
procesador** trabaja con el resto al mismo tiempo. **Tu SSD** guarda una gran tabla de consulta.
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Un pequeño ayudante adivina las siguientes palabras; el modelo grande las comprueba todas a la vez y se queda con las correctas"></p>
|
||||
|
||||
- **Adivinar y comprobar:** un pequeño ayudante adivina las siguientes palabras. El modelo grande las comprueba todas
|
||||
a la vez. Recibes la misma respuesta, 1.6-1.8x antes.
|
||||
- **Los textos largos se leen en trozos grandes** (hasta 8,192 tokens cada vez), a más de 1,000 tokens por segundo.
|
||||
|
||||
La explicación larga: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte y sus números:
|
||||
[los detalles](docs/DETAILS.md#how-it-works) y el [artículo](docs/paper/Strata-Paper.pdf).
|
||||
|
||||
## Créditos y licencia
|
||||
|
||||
El modelo es [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), del equipo de Qwen. Lo
|
||||
comprimieron [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
|
||||
y Unsloth. Strata usa partes de [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos los créditos:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata es de código abierto con la [licencia MIT](LICENSE).
|
||||
Algunas partes y cada modelo tienen sus propias licencias ([cuáles](docs/HOW_IT_WORKS.md#license)).
|
||||
|
||||
## Apoya a Strata
|
||||
|
||||
Strata es libre y de código abierto. Si te resulta útil, puedes apoyar su desarrollo:
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
|
||||
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · **Français** · [Español](README.es.md) · [Português](README.pt-BR.md)
|
||||
|
||||
<p align="center"><b>Faites tourner un modèle d'IA de 125 milliards de paramètres sur votre propre PC de jeu</b><br>
|
||||
Carte graphique NVIDIA ou AMD (12 Go ou plus) · Windows ou Linux · gratuit et open source</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Un jardin de pagode en voxels écrit par le modèle de Strata, qui tourne dans le navigateur"></a><br>
|
||||
<sub>Un jardin de pagode en voxels, généré en une seule requête sur une RTX 5070 avec Strata (IQ3_S, contexte de 128K) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vidéo complète (49 s)</a></sub></p>
|
||||
|
||||
Strata fait tourner **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** sur un PC normal. C'est
|
||||
un grand modèle d'IA, très capable, qui demande d'habitude un serveur. Il discute, écrit du code, lit des images et
|
||||
fonctionne avec vos applications et vos agents de code. Rien ne quitte votre PC.
|
||||
|
||||
## Quelle est sa vitesse ?
|
||||
|
||||
Nous l'avons mesurée sur deux PC de jeu ordinaires. Un token correspond à environ ¾ d'un mot.
|
||||
|
||||
- **Écrit les réponses :** la vitesse à laquelle la réponse s'affiche dans une courte discussion. 60 tokens par seconde, c'est plus rapide que votre lecture.
|
||||
- **Lit votre prompt :** la vitesse à laquelle il absorbe ce que vous envoyez (ici un document, du code ou un historique de discussion de 32K tokens).
|
||||
|
||||
<table>
|
||||
<tr><th>NVIDIA : RTX 5070 (12 Go), Ryzen 5 7600, 64 Go de RAM</th><th>AMD : RX 9070 XT (16 Go), Ryzen 9 3900X, 47 Go de RAM</th></tr>
|
||||
<tr><td>
|
||||
|
||||
| Taille | Écrit les réponses | Lit votre prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
|
||||
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
|
||||
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
|
||||
| **Coder** | 55 tokens/s | 2,180 tokens/s |
|
||||
|
||||
</td><td>
|
||||
|
||||
| Taille | Écrit les réponses | Lit votre prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
|
||||
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
|
||||
| **Coder** | 44 tokens/s | 1,420 tokens/s |
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
NVIDIA : Q2_0 avec le moteur 0.1.36, les autres lignes avec 0.1.26 (réponses de 4K, prompts de 32K). Les tableaux
|
||||
complets sont dans [DETAILS.md](docs/DETAILS.md#speed-measured). Une carte avec plus de VRAM va plus vite : une
|
||||
RTX 3090 (24 Go) devrait écrire environ 100-140 tokens par seconde. Longues discussions et autres cartes :
|
||||
[vitesse de chaque modèle](docs/MODELS.md#how-fast-is-each-size), [résultats de la communauté](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
|
||||
<sub>Strata est gratuit. S'il tourne bien sur votre PC, un café aide à poursuivre le travail.</sub></p>
|
||||
|
||||
## Ce qu'il vous faut
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **Carte graphique** | **NVIDIA** GeForce RTX série 20, 30, 40 ou 50, ou **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 ou série RX 6800 / 6900. Il faut **12 Go de VRAM ou plus**. |
|
||||
| **RAM** | 32 Go ou plus. Votre RAM décide [quel modèle](#quel-modèle-choisir-) tient. Avec 64 Go, toutes les tailles tournent. |
|
||||
| **Disque** | Environ 80 Go libres. Prenez un SSD si possible : le premier démarrage est bien plus rapide. |
|
||||
| **Système** | Windows 10 / 11 ou Linux, avec un pilote graphique NVIDIA ou AMD à jour. |
|
||||
|
||||
L'installateur s'occupe de tout le reste. Deux ou trois cartes peuvent se partager le modèle ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
|
||||
Expérimental, écrit et testé par des membres de la communauté sur leurs propres machines :
|
||||
|
||||
- **Cartes graphiques plus anciennes** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT) : [Older GPUs](docs/OLDER_GPUS.md).
|
||||
- **Intel Arc**, compilé depuis les sources sous Linux : [Intel Arc](docs/INTEL_ARC.md).
|
||||
- **AMD Ryzen AI Max (Strix Halo)**, compilé depuis les sources sous Linux : [Strix Halo](docs/STRIX_HALO.md).
|
||||
- **Processeurs plus anciens sans AVX2** : ça marche, mais lentement. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
|
||||
|
||||
La liste complète : [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
|
||||
## Installation
|
||||
|
||||
### Laissez votre IA l'installer
|
||||
|
||||
Vous utilisez un assistant de code IA (Claude Code, Cursor, Codex, GitHub Copilot, ...) ? Collez-lui ceci :
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
Il vérifie votre carte graphique, votre RAM et votre disque, et choisit le modèle qui convient. Ensuite il l'installe,
|
||||
le démarre et vous explique comment connecter vos applications. Les outils d'IA peuvent aussi installer, démarrer et
|
||||
arrêter Strata grâce à son [serveur MCP](docs/MCP_SERVER.md).
|
||||
|
||||
### Ou faites-le vous-même
|
||||
|
||||
[Téléchargez Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) et décompressez-le (ou faites un `git clone`).
|
||||
**Windows :** double-cliquez sur **`START-HERE.bat`**. **Linux :** lancez **`./setup.sh`** dans le dossier Strata.
|
||||
|
||||
Les étapes sont les mêmes pour NVIDIA et AMD. L'installateur détecte votre carte et installe le bon moteur pour elle.
|
||||
Il vous pose quelques questions :
|
||||
|
||||
- quel modèle et quelle taille,
|
||||
- combien de contexte (la quantité de texte que le modèle garde en tête),
|
||||
- s'il doit lire les images.
|
||||
|
||||
Appuyez sur Entrée à chaque fois pour garder la réponse recommandée. Ensuite il télécharge le modèle (environ 70 Go)
|
||||
et le démarre. Si le téléchargement s'arrête, relancez-le : il reprend là où il s'était arrêté. Votre navigateur
|
||||
ouvre l'application Strata à l'adresse `http://127.0.0.1:8080`.
|
||||
|
||||
> **Pendant le démarrage du modèle, votre PC peut ralentir ou ne plus répondre pendant 1-3 minutes** (plus longtemps la première fois).
|
||||
> Strata charge 35-55 Go dans votre RAM et en réserve une partie pour la carte graphique. C'est normal. Attendez, et
|
||||
> ne fermez pas la fenêtre. La fenêtre montre ce que fait Strata.
|
||||
|
||||
**La fois suivante**, relancez `START-HERE.bat` (ou `./setup.sh`). Il démarre tout de suite et ne télécharge rien
|
||||
deux fois. Fermez sa fenêtre pour arrêter le modèle. `UPDATE.bat` (`./update.sh`) met Strata à jour sans le démarrer.
|
||||
Mises à jour, Docker, plusieurs cartes, emplacement des fichiers et toutes les options : [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
## Quel modèle choisir ?
|
||||
|
||||
L'installateur en recommande un selon votre RAM. Le même modèle existe en plusieurs tailles, plus ou moins
|
||||
compressées. Les petites tailles sont plus rapides. Les grandes sont un peu plus intelligentes.
|
||||
|
||||
| Votre RAM | Prenez | Pourquoi |
|
||||
| --- | --- | --- |
|
||||
| **32 Go** | **Coder** | il tient dans 32 Go et il est fait pour le code (avec une carte de 24 Go, Q2_0 et IQ2_XS tournent aussi) |
|
||||
| **48 Go** | **IQ2_XS** (ou Q2_0, le plus rapide) | les plus grandes tailles ne tiennent pas |
|
||||
| **64 Go** | **IQ2_XS** (recommandé), ou IQ3_XXS / IQ3_S | toutes les tailles tiennent ; IQ3_S est le meilleur et le plus lent |
|
||||
| **96 Go ou plus** | **IQ3_S**, ou l'UD-IQ4_XS d'Unsloth (~4 bits) | de la place pour les plus grandes tailles, avec tout le reste ouvert |
|
||||
|
||||
- **[Coder](docs/MODELS.md#coder) :** une version pour le code, avec la moitié des experts en moins. Elle atteint 91 %
|
||||
du score SWE-bench Verified du modèle complet (mesuré par ses auteurs) et tient dans 32 Go de RAM. Elle est moins
|
||||
bonne en dehors du code, y compris pour le chinois et les autres textes CJK (#438). Pour ces cas, prenez Q2_0,
|
||||
IQ2_XS ou IQ3_S, qui gardent tous les experts.
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15) :** un fine-tune qui réfléchit bien moins longtemps avant de répondre. Vous
|
||||
avez la réponse plus tôt, avec à peu près la même qualité.
|
||||
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs)** : la version ~4 bits d'Unsloth, entre IQ3_S et
|
||||
UD-Q4_K_XL en qualité. Un téléchargement de 94 Go. Avec moins de ~80 Go de RAM, Strata en lit une partie depuis
|
||||
le SSD pendant qu'il répond, il y est donc plus lent (un SSD NVMe aide).
|
||||
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (expérimental) : le plus proche du modèle
|
||||
complet. Mais Strata en lit la plus grande partie depuis le SSD pendant qu'il répond, donc il n'écrit que
|
||||
7-8.5 tokens/s sur un PC avec 64 Go.
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs) :** vous l'installez à la main.
|
||||
Il n'est pas dans le menu de l'installateur.
|
||||
|
||||
Tailles, téléchargements et ce qui tient où : [docs/MODELS.md](docs/MODELS.md). Pour ajouter un autre modèle plus
|
||||
tard, lancez `SETUP.bat` (Linux : `./setup.sh --setup`).
|
||||
|
||||
## Utilisation
|
||||
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="L'onglet Monitor de l'application Strata à côté d'un agent de code"><br>
|
||||
<sub>Le <b>Monitor</b> de l'application Strata (à gauche) pendant qu'un agent de code écrit le jardin de pagode de la vidéo (à droite)</sub></p>
|
||||
|
||||
- **Dans le navigateur :** ouvrez `http://127.0.0.1:8080`. Vous y trouvez **Chat**, un **Monitor** en direct du
|
||||
modèle et de votre GPU/CPU/RAM, et **About** avec les réglages et les adresses.
|
||||
- **Vos applications et agents de code :** ajoutez un fournisseur « OpenAI-compatible » avec l'URL de base
|
||||
**`http://127.0.0.1:8080/v1`**. N'importe quelle clé API et n'importe quel nom de modèle fonctionnent.
|
||||
- Applications qui utilisent l'API d'Anthropic : `http://127.0.0.1:8080/v1/messages` (Claude Code :
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
|
||||
- Codex CLI et les autres applications qui utilisent l'API OpenAI Responses : `/v1/responses`
|
||||
([configuration](docs/DETAILS.md#the-responses-api-and-codex-cli)).
|
||||
- **Réflexion :** choisissez **off, low, medium ou high** dans le menu du chat ou dans le « reasoning effort » de
|
||||
votre application. Off est le plus rapide. High est le meilleur pour les questions difficiles.
|
||||
- **Images :** répondez oui à « Images? » pendant l'installation. Ensuite cliquez sur **Picture** dans le chat, ou
|
||||
joignez des images dans votre application. Les cartes AMD lisent les images sous Linux via le processeur ; sous
|
||||
Windows, pas encore.
|
||||
- **Depuis votre téléphone ou un autre PC :** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Définissez
|
||||
toujours une clé.
|
||||
- **Une requête à la fois :** par défaut, Strata répond à une requête et les autres attendent. Pour répondre à
|
||||
plusieurs en même temps, mettez `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Sur une carte de 12 Go, chaque
|
||||
réponse devient alors plus lente.
|
||||
- **Longs prompts :** Strata lit en entier le premier message d'une discussion, environ 1 minute pour 30,000 tokens.
|
||||
Les messages suivants démarrent en quelques secondes.
|
||||
|
||||
Plus d'infos : [où sont stockées vos discussions](docs/INSTALL.md#where-things-are-stored), [l'API](docs/DETAILS.md#using-it).
|
||||
|
||||
## Un problème ?
|
||||
|
||||
- **Mon PC s'est figé au premier démarrage de Strata.** C'est normal pendant le chargement du modèle. Attendez, et ne
|
||||
fermez pas la fenêtre. Toujours figé après 10 minutes ? Redémarrez le PC, fermez les autres programmes et
|
||||
réessayez, ou choisissez une taille plus petite.
|
||||
- **Il s'est arrêté pendant le téléchargement ou l'installation.** Relancez `START-HERE.bat` (ou `./setup.sh`). Il
|
||||
reprend là où il s'était arrêté.
|
||||
- **C'est très lent et le voyant du disque clignote sans arrêt, ou il affiche « the engine stopped unexpectedly ».**
|
||||
Votre PC n'a pas assez de RAM libre. Fermez les autres programmes (les navigateurs en utilisent beaucoup), ou
|
||||
choisissez une taille plus petite (Q2_0 ou IQ2_XS).
|
||||
- **Il dit que le port 8080 est déjà utilisé.** Strata tourne déjà. Cherchez sa fenêtre.
|
||||
|
||||
Autres problèmes et leurs solutions : [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Toujours bloqué ? Ouvrez une
|
||||
[issue](https://github.com/Niko1221/Strata/issues) et joignez `strata-<model>.log` depuis le dossier Strata. Vous
|
||||
avez trouvé un problème de sécurité ? Signalez-le en privé : [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Comment ça marche ?
|
||||
|
||||
Les modèles comme celui-ci tournent d'habitude sur des serveurs avec des centaines de gigaoctets de mémoire
|
||||
graphique. Votre carte graphique a 12-24 Go. Strata fait tenir le modèle en **répartissant le travail sur tout votre
|
||||
PC**. Pensez à une cuisine : ce que vous utilisez tout le temps reste sur le plan de travail, et le reste attend dans
|
||||
le placard.
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Les 24,576 experts du modèle : les plus sollicités sur la carte graphique, tous en RAM, une table de correspondance sur le SSD"></p>
|
||||
|
||||
- **Le modèle est une équipe de 24,576 petits spécialistes (les « experts »).** Chaque mot n'en demande que 10.
|
||||
- **Votre carte graphique** garde les quelques milliers d'experts les plus utilisés. **Votre RAM** les contient tous,
|
||||
et **votre processeur** travaille sur les autres en même temps. **Votre SSD** contient une grande table de
|
||||
correspondance.
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Un petit assistant devine les mots suivants ; le grand modèle les vérifie tous d'un coup et garde les bons"></p>
|
||||
|
||||
- **Deviner, puis vérifier :** un petit assistant devine les quelques mots suivants. Le grand modèle les vérifie
|
||||
tous d'un coup. Vous obtenez la même réponse, 1.6-1.8x plus tôt.
|
||||
- **Les longs textes sont lus par gros morceaux** (jusqu'à 8,192 tokens à la fois), à plus de 1,000 tokens par seconde.
|
||||
|
||||
L'explication plus longue : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Chaque partie et ses chiffres :
|
||||
[les détails](docs/DETAILS.md#how-it-works) et l'[article](docs/paper/Strata-Paper.pdf).
|
||||
|
||||
## Crédits et licence
|
||||
|
||||
Le modèle est [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), de l'équipe Qwen. Il a été
|
||||
compressé par [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
|
||||
et Unsloth. Strata utilise des parties de [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Tous les
|
||||
crédits : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata est open source sous [licence MIT](LICENSE).
|
||||
Quelques parties et chaque modèle ont leur propre licence ([lesquelles](docs/HOW_IT_WORKS.md#license)).
|
||||
|
||||
## Soutenir Strata
|
||||
|
||||
Strata est gratuit et open source. S'il vous est utile, vous pouvez soutenir son développement :
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
|
||||
[English](README.md) · [简体中文](README.zh-CN.md) · **日本語** · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
|
||||
|
||||
<p align="center"><b>1,250 億パラメータの AI モデルを、手元のゲーミング PC で動かす</b><br>
|
||||
NVIDIA または AMD のグラフィックカード(12 GB 以上) · Windows または Linux · 無料のオープンソース</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Strata のモデルが書いたボクセルの五重塔の庭。ブラウザで動いている様子"></a><br>
|
||||
<sub>ボクセルの五重塔の庭。1 回のプロンプトで作成し、RTX 5070 上の Strata(IQ3_S、128K コンテキスト)で実行 ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">動画全体(49 秒)</a></sub></p>
|
||||
|
||||
Strata は **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** を普通の PC で動かします。
|
||||
これは大きくて賢い AI モデルで、ふつうはサーバーが必要です。チャットをしたり、コードを書いたり、画像を読んだりできます。
|
||||
アプリやコーディングエージェントとも連携します。データが PC の外に出ることはありません。
|
||||
|
||||
## どのくらい速い?
|
||||
|
||||
ごく普通のゲーミング PC 2 台で測りました。1 トークンは英単語のおよそ ¾ です。
|
||||
|
||||
- **回答の書き出し:** 短いチャットで返答が表示される速さです。毎秒 60 トークンあれば、読むより速く表示されます。
|
||||
- **プロンプトの読み込み:** 送った内容を取り込む速さです(ここでは 32K トークンの文書、コード、またはチャット履歴)。
|
||||
|
||||
<table>
|
||||
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
|
||||
<tr><td>
|
||||
|
||||
| サイズ | 回答の書き出し | プロンプトの読み込み |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
|
||||
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
|
||||
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
|
||||
| **Coder** | 55 tokens/s | 2,180 tokens/s |
|
||||
|
||||
</td><td>
|
||||
|
||||
| サイズ | 回答の書き出し | プロンプトの読み込み |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
|
||||
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
|
||||
| **Coder** | 44 tokens/s | 1,420 tokens/s |
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
NVIDIA:Q2_0 はエンジン 0.1.36、ほかの行は 0.1.26 で測定(回答 4K、プロンプト 32K)。表の全体は
|
||||
[DETAILS.md](docs/DETAILS.md#speed-measured) にあります。VRAM が多いカードほど速くなります。RTX 3090(24 GB)なら
|
||||
毎秒およそ 100-140 トークンで書き出せるはずです。長いチャットやほかのカードについて:[モデルごとの速度](docs/MODELS.md#how-fast-is-each-size)、
|
||||
[コミュニティの測定結果](docs/COMMUNITY_BENCHMARKS.md)。
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
|
||||
<sub>Strata は無料です。あなたの PC でうまく動いたら、コーヒー 1 杯の支援が開発を続ける力になります。</sub></p>
|
||||
|
||||
## 必要なもの
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **グラフィックカード** | **NVIDIA** GeForce RTX 20、30、40、50 シリーズ、または **AMD** Radeon RX 7900 XT / XTX、RX 7800 XT / 7700 XT、RX 9060 XT、RX 9070 / 9070 XT、Radeon AI PRO R9700、RX 6800 / 6900 シリーズ。**VRAM 12 GB 以上**が必要です。 |
|
||||
| **RAM** | 32 GB 以上。RAM の量で[使えるモデル](#どのモデルを選べばいい)が決まります。64 GB あればすべてのサイズが動きます。 |
|
||||
| **ディスク** | 空き容量が約 80 GB。できれば SSD を使ってください。初回の起動がずっと速くなります。 |
|
||||
| **OS** | Windows 10 / 11 または Linux。NVIDIA または AMD の最新のグラフィックドライバー。 |
|
||||
|
||||
ほかに必要なものはすべてインストーラーが用意します。2 枚や 3 枚のカードでモデルを分担することもできます([マルチ GPU](docs/MULTI_GPU.md))。
|
||||
|
||||
試験的なサポート。コミュニティのメンバーが自分のマシンで書き、テストしたものです:
|
||||
|
||||
- **古いグラフィックカード**(Tesla P40 / V100、GTX 10、Radeon VII / MI50、RX 6700 XT、RX 5500 XT):[Older GPUs](docs/OLDER_GPUS.md)。
|
||||
- **Intel Arc**(Linux でソースからビルド):[Intel Arc](docs/INTEL_ARC.md)。
|
||||
- **AMD Ryzen AI Max(Strix Halo)**(Linux でソースからビルド):[Strix Halo](docs/STRIX_HALO.md)。
|
||||
- **AVX2 のない古いプロセッサー**:動きますが、遅いです。[Older CPUs](docs/INSTALL.md#older-cpus-experimental)。
|
||||
|
||||
全リスト:[docs/INSTALL.md](docs/INSTALL.md#what-you-need)。
|
||||
|
||||
## インストール
|
||||
|
||||
### AI にセットアップしてもらう
|
||||
|
||||
AI コーディングアシスタント(Claude Code、Cursor、Codex、GitHub Copilot など)を使っていますか? これを貼り付けてください:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
AI がグラフィックカード、RAM、ディスクを調べて、合うモデルを選びます。それからインストールして起動し、
|
||||
アプリのつなぎ方を教えてくれます。AI ツールは Strata の [MCP サーバー](docs/MCP_SERVER.md) を通じて、
|
||||
Strata のインストール、起動、停止もできます。
|
||||
|
||||
### 自分でやる
|
||||
|
||||
[Strata をダウンロード](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip)して展開します(または `git clone` します)。
|
||||
**Windows:** **`START-HERE.bat`** をダブルクリック。**Linux:** Strata フォルダーで **`./setup.sh`** を実行します。
|
||||
|
||||
手順は NVIDIA でも AMD でも同じです。インストーラーがカードを見つけて、それに合うエンジンを用意します。
|
||||
いくつか質問があります:
|
||||
|
||||
- どのモデルを、どのサイズで使うか
|
||||
- コンテキストをどのくらいにするか(モデルが覚えておけるテキストの量)
|
||||
- 画像を読ませるかどうか
|
||||
|
||||
毎回 Enter を押せば、おすすめの答えになります。そのあとモデル(約 70 GB)をダウンロードして起動します。
|
||||
ダウンロードが止まったら、もう一度実行してください。続きから再開します。ブラウザーで Strata アプリが
|
||||
`http://127.0.0.1:8080` に開きます。
|
||||
|
||||
> **モデルの起動中、1-3 分ほど PC が重くなったり、反応しなくなったりすることがあります**(初回がいちばん長いです)。
|
||||
> Strata は 35-55 GB を RAM に読み込み、その一部をグラフィックカード用に固定します。これは正常です。
|
||||
> ウィンドウを閉じずに待ってください。Strata が何をしているかはウィンドウに表示されます。
|
||||
|
||||
**次回からは** `START-HERE.bat`(または `./setup.sh`)をもう一度実行します。すぐに起動し、同じものを 2 回ダウンロードすることはありません。
|
||||
モデルを止めるときはウィンドウを閉じます。`UPDATE.bat`(`./update.sh`)を使うと、Strata を起動せずに更新できます。
|
||||
更新、Docker、複数のカード、ファイルの保存場所、すべてのオプションについて:[docs/INSTALL.md](docs/INSTALL.md)。
|
||||
|
||||
## どのモデルを選べばいい
|
||||
|
||||
インストーラーが RAM に合わせてひとつ勧めます。同じモデルにも、圧縮の強さが違う複数のサイズがあります。
|
||||
小さいサイズほど速く、大きいサイズほど少し賢くなります。
|
||||
|
||||
| RAM | 選ぶもの | 理由 |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Coder** | 32 GB に収まり、コード向けに作られている(24 GB のカードなら Q2_0 と IQ2_XS も動く) |
|
||||
| **48 GB** | **IQ2_XS**(または最速の Q2_0) | 大きいサイズは収まらない |
|
||||
| **64 GB** | **IQ2_XS**(おすすめ)、または IQ3_XXS / IQ3_S | すべてのサイズが収まる。IQ3_S がいちばん賢く、いちばん遅い |
|
||||
| **96 GB 以上** | **IQ3_S**、または Unsloth の UD-IQ4_XS(約 4-bit) | ほかのものを全部開いたままでも、最大のサイズが入る余裕がある |
|
||||
|
||||
- **[Coder](docs/MODELS.md#coder):** エキスパートを半分取り除いたコーディング向け版です。フルモデルの SWE-bench Verified スコアの
|
||||
91% に届き(作者による測定)、32 GB の RAM に収まります。コード以外では弱く、中国語などの CJK のテキストも苦手です(#438)。
|
||||
そうした用途には、すべてのエキスパートを残している Q2_0、IQ2_XS、IQ3_S を選んでください。
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15):** 答える前に考える時間がずっと短いファインチューン版です。
|
||||
ほぼ同じ品質のまま、答えが早く返ってきます。
|
||||
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth の約 4-bit 版で、品質は IQ3_S と UD-Q4_K_XL の間です。
|
||||
ダウンロードは 94 GB。RAM が約 80 GB 未満だと、答えている間に Strata がその一部を SSD から読むので遅くなります(NVMe SSD が有効です)。
|
||||
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)**(試験的):フルモデルにいちばん近いものです。
|
||||
ただし答えている間、Strata はその大部分を SSD から読むので、64 GB の PC では毎秒 7-8.5 トークンしか書き出せません。
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** 手動でセットアップします。
|
||||
インストーラーのメニューにはありません。
|
||||
|
||||
サイズ、ダウンロード、どれがどこに収まるかについて:[docs/MODELS.md](docs/MODELS.md)。あとで別のモデルを追加するには
|
||||
`SETUP.bat`(Linux:`./setup.sh --setup`)を実行します。
|
||||
|
||||
## 使い方
|
||||
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="Strata アプリの Monitor タブと、その横で動くコーディングエージェント"><br>
|
||||
<sub>Strata アプリの <b>Monitor</b>(左)。コーディングエージェントが動画の五重塔の庭を書いているところ(右)</sub></p>
|
||||
|
||||
- **ブラウザーで:** `http://127.0.0.1:8080` を開きます。**Chat**、モデルと GPU/CPU/RAM の様子をリアルタイムで見る **Monitor**、
|
||||
設定とアドレスが載った **About** があります。
|
||||
- **アプリやコーディングエージェントから:** 「OpenAI-compatible」プロバイダーを追加し、ベース URL に
|
||||
**`http://127.0.0.1:8080/v1`** を指定します。API キーとモデル名は何でもかまいません。
|
||||
- Anthropic の API を使うアプリ:`http://127.0.0.1:8080/v1/messages`(Claude Code:
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`)。
|
||||
- Codex CLI など OpenAI Responses API を使うアプリ:`/v1/responses`
|
||||
([設定方法](docs/DETAILS.md#the-responses-api-and-codex-cli))。
|
||||
- **思考(Thinking):** チャットのメニュー、またはアプリの「reasoning effort」で **off、low、medium、high** を選びます。
|
||||
off がいちばん速く、high は難しい質問に向いています。
|
||||
- **画像:** セットアップで「Images?」に yes と答えます。そのあとチャットで **Picture** をクリックするか、アプリで画像を添付します。
|
||||
AMD のカードは、Linux ではプロセッサーを使って画像を読みます。Windows ではまだ読めません。
|
||||
- **スマートフォンや別の PC から:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`。キーは必ず設定してください。
|
||||
- **リクエストは 1 つずつ:** 初期設定では Strata は 1 つのリクエストに答え、ほかは待ちます。複数に同時に答えるには
|
||||
`"parallel": 2` を設定します([BATCHING.md](docs/BATCHING.md))。12 GB のカードでは、それぞれの回答が遅くなります。
|
||||
- **長いプロンプト:** Strata はチャットの最初のメッセージをすべて読みます。30,000 トークンあたり約 1 分かかります。
|
||||
2 通目以降のメッセージは数秒で始まります。
|
||||
|
||||
詳しくは:[チャットの保存場所](docs/INSTALL.md#where-things-are-stored)、[API](docs/DETAILS.md#using-it)。
|
||||
|
||||
## うまくいかないとき
|
||||
|
||||
- **Strata の初回起動で PC が固まった。** モデルの読み込み中はよくあることです。ウィンドウを閉じずに待ってください。
|
||||
10 分たっても固まったまま? PC を再起動し、ほかのプログラムを閉じてもう一度試すか、小さいサイズを選んでください。
|
||||
- **ダウンロードやインストールの途中で止まった。** `START-HERE.bat`(または `./setup.sh`)をもう一度実行してください。
|
||||
止まったところから続きます。
|
||||
- **とても遅く、ディスクのランプが点滅し続ける。または「the engine stopped unexpectedly」と表示される。** PC の空き RAM が
|
||||
足りません。ほかのプログラムを閉じるか(ブラウザーは多く使います)、小さいサイズ(Q2_0 または IQ2_XS)を選んでください。
|
||||
- **ポート 8080 がすでに使われていると表示される。** Strata はもう動いています。そのウィンドウを探してください。
|
||||
|
||||
ほかの問題と解決方法:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。それでも解決しない場合は
|
||||
[issue](https://github.com/Niko1221/Strata/issues) を作成し、Strata フォルダーにある `strata-<model>.log` を添付してください。
|
||||
セキュリティの問題を見つけたら、非公開で報告してください:[SECURITY.md](SECURITY.md)。
|
||||
|
||||
## しくみ
|
||||
|
||||
このようなモデルは、ふつう数百ギガバイトのグラフィックメモリを持つサーバーで動きます。あなたのグラフィックカードは
|
||||
12-24 GB です。Strata は **PC 全体で作業を分担する** ことで、モデルを収めています。台所を思い浮かべてください。
|
||||
いつも使う道具は調理台に置き、残りは食料庫にしまっておきます。
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="モデルの 24,576 個のエキスパート:よく使うものはグラフィックカードに、すべては RAM に、ルックアップテーブルは SSD に"></p>
|
||||
|
||||
- **モデルは 24,576 個の小さな専門家(「エキスパート」)のチームです。** 1 語ごとに必要なのはそのうち 10 個だけです。
|
||||
- **グラフィックカード** は、よく使われる数千個のエキスパートを持ちます。**RAM** はすべてのエキスパートを持ち、
|
||||
**プロセッサー** が残りを同時に処理します。**SSD** は大きなルックアップテーブルを持ちます。
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="小さなヘルパーが次の単語を予想し、大きなモデルがまとめて確認して正しいものを残す"></p>
|
||||
|
||||
- **予想してから確認:** 小さなヘルパーが次の数語を予想します。大きなモデルがそれをまとめて確認します。
|
||||
答えは同じまま、1.6-1.8x 早く返ってきます。
|
||||
- **長いテキストは大きなまとまりで読みます**(一度に最大 8,192 トークン)。速さは毎秒 1,000 トークン以上です。
|
||||
|
||||
もっと詳しい説明:[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)。各部分とその数値:
|
||||
[詳細](docs/DETAILS.md#how-it-works)と[論文](docs/paper/Strata-Paper.pdf)。
|
||||
|
||||
## クレジットとライセンス
|
||||
|
||||
モデルは Qwen チームの [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) です。
|
||||
圧縮は [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)、UkisAI(Swift 1.5)、
|
||||
Unsloth によるものです。Strata は [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) の一部を使っています。すべてのクレジット:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits)。Strata は [MIT License](LICENSE) のオープンソースです。一部のパーツと
|
||||
すべてのモデルには、それぞれ独自のライセンスがあります([どれか](docs/HOW_IT_WORKS.md#license))。
|
||||
|
||||
## Strata を支援する
|
||||
|
||||
Strata は無料のオープンソースです。役に立ったら、開発を支援していただけます:
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
|
||||
@@ -1,5 +1,7 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
|
||||
**English** · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
|
||||
|
||||
<p align="center"><b>Run a 125-billion-parameter AI model on your own gaming PC</b><br>
|
||||
NVIDIA or AMD graphics card (12 GB or more) · Windows or Linux · free and open source</p>
|
||||
|
||||
@@ -7,15 +9,16 @@ NVIDIA or AMD graphics card (12 GB or more) · Windows or Linux · free and open
|
||||
<sub>A voxel pagoda garden, 1 shot prompt running on an RTX 5070 with Strata (IQ3_S, 128K context) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">full video (49 s)</a></sub></p>
|
||||
|
||||
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** - a large, smart AI model that
|
||||
normally needs a server - on a normal PC. It chats, writes code, reads pictures and works with your apps and coding
|
||||
agents, and nothing leaves your PC.
|
||||
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** on a normal PC. This is a
|
||||
large, smart AI model that usually needs a server. It chats, writes code, reads pictures and works with your apps
|
||||
and coding agents. Nothing leaves your PC.
|
||||
|
||||
## How fast is it?
|
||||
|
||||
Measured on two ordinary gaming PCs. "Writes answers" is how fast the reply appears in a short chat; "reads your
|
||||
prompt" is how fast it takes in what you send (a 32K-token document, code or chat history). A token is about ¾ of a
|
||||
word, so 60 tokens per second is faster than you can read.
|
||||
We measured it on two ordinary gaming PCs. A token is about ¾ of a word.
|
||||
|
||||
- **Writes answers:** how fast the reply appears in a short chat. 60 tokens per second is faster than you can read.
|
||||
- **Reads your prompt:** how fast it takes in what you send (here a 32K-token document, code or chat history).
|
||||
|
||||
<table>
|
||||
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
|
||||
@@ -40,8 +43,10 @@ word, so 60 tokens per second is faster than you can read.
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
A card with more VRAM is faster: an RTX 3090 (24 GB) should write roughly 100-140 tokens per second. Long chats,
|
||||
other cards: [speed of each model](docs/MODELS.md#how-fast-is-each-size), [community results](docs/COMMUNITY_BENCHMARKS.md).
|
||||
NVIDIA: Q2_0 with engine 0.1.36, the other rows with 0.1.26 (4K answers, 32K prompts). The full tables are in
|
||||
[DETAILS.md](docs/DETAILS.md#speed-measured). A card with more VRAM is faster: an RTX 3090 (24 GB) should write
|
||||
about 100-140 tokens per second. Long chats and other cards: [speed of each model](docs/MODELS.md#how-fast-is-each-size),
|
||||
[community results](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
|
||||
<sub>Strata is free. If it runs well on your PC, a coffee keeps the work on it going.</sub></p>
|
||||
@@ -50,26 +55,34 @@ other cards: [speed of each model](docs/MODELS.md#how-fast-is-each-size), [commu
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **Graphics card** | **NVIDIA** GeForce RTX 20, 30, 40 or 50 series, or **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 or RX 6800 / 6900 series - with **12 GB of VRAM or more** |
|
||||
| **RAM** | 32 GB or more - how much decides [which model](#which-model-should-i-pick) fits; 64 GB runs every size |
|
||||
| **Disk** | about 80 GB free, on an SSD if you can (the first start is much faster) |
|
||||
| **System** | Windows 10 / 11 or Linux, and a current graphics driver from NVIDIA or AMD |
|
||||
| **Graphics card** | **NVIDIA** GeForce RTX 20, 30, 40 or 50 series, or **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 or RX 6800 / 6900 series. It needs **12 GB of VRAM or more**. |
|
||||
| **RAM** | 32 GB or more. Your RAM decides [which model](#which-model-should-i-pick) fits. 64 GB runs every size. |
|
||||
| **Disk** | About 80 GB free. Use an SSD if you can: the first start is much faster. |
|
||||
| **System** | Windows 10 / 11 or Linux, and a current graphics driver from NVIDIA or AMD. |
|
||||
|
||||
The installer sets up everything else. Two or three cards can share the model ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
|
||||
Experimental, written and tested by community members on their own machines:
|
||||
|
||||
- **Older graphics cards** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md).
|
||||
- **Intel Arc**, built from source on Linux: [Intel Arc](docs/INTEL_ARC.md).
|
||||
- **AMD Ryzen AI Max (Strix Halo)**, built from source on Linux: [Strix Halo](docs/STRIX_HALO.md).
|
||||
- **Older processors without AVX2**: they work, but slowly. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
|
||||
|
||||
Everything else is installed for you. Two or three cards can share the model ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
The full list: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
|
||||
## Install
|
||||
|
||||
### Let your AI set it up
|
||||
|
||||
Use an AI coding assistant (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Paste this into it:
|
||||
Do you use an AI coding assistant (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Paste this into it:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
It checks your graphics card, RAM and disk, picks the model that fits, installs it, starts it and tells you how to
|
||||
connect your apps. AI tools can also install, start and stop Strata themselves through its
|
||||
It checks your graphics card, RAM and disk and picks the model that fits. Then it installs and starts it and tells
|
||||
you how to connect your apps. AI tools can also install, start and stop Strata through its
|
||||
[MCP server](docs/MCP_SERVER.md).
|
||||
|
||||
### Or do it yourself
|
||||
@@ -77,43 +90,51 @@ connect your apps. AI tools can also install, start and stop Strata themselves t
|
||||
[Download Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it (or `git clone` it).
|
||||
**Windows:** double-click **`START-HERE.bat`**. **Linux:** run **`./setup.sh`** in the Strata folder.
|
||||
|
||||
The same steps for NVIDIA and AMD: the installer finds your card and sets up the right engine for it. It asks which
|
||||
model, which size, how much context (how much text it keeps in mind) and whether it should read pictures - press
|
||||
Enter each time for the recommended answer. Then it downloads the model (~70 GB; you can stop and it continues where
|
||||
it left off) and starts it. Your browser opens the Strata app at `http://127.0.0.1:8080`.
|
||||
The steps are the same for NVIDIA and AMD. The installer finds your card and sets up the right engine for it. It
|
||||
asks you a few questions:
|
||||
|
||||
> **While the model starts, your PC can be slow or stop responding for 1-3 minutes** (longest the first time): Strata
|
||||
> loads 35-55 GB into your RAM and locks part of it for the graphics card. That's normal - wait, and don't close the
|
||||
> window. The window tells you what it is doing.
|
||||
- which model and which size,
|
||||
- how much context (how much text the model keeps in mind),
|
||||
- whether it should read pictures.
|
||||
|
||||
**Next time**, run `START-HERE.bat` (or `./setup.sh`) again: it starts right away, nothing is downloaded twice. Close
|
||||
Press Enter each time for the recommended answer. Then it downloads the model (about 70 GB) and starts it. If the
|
||||
download stops, run it again: it continues where it left off. Your browser opens the Strata app at
|
||||
`http://127.0.0.1:8080`.
|
||||
|
||||
> **While the model starts, your PC can be slow or stop responding for 1-3 minutes** (longest the first time).
|
||||
> Strata loads 35-55 GB into your RAM and locks part of it for the graphics card. This is normal. Wait, and don't
|
||||
> close the window. The window shows what Strata is doing.
|
||||
|
||||
**Next time**, run `START-HERE.bat` (or `./setup.sh`) again. It starts right away and downloads nothing twice. Close
|
||||
its window to stop the model. `UPDATE.bat` (`./update.sh`) updates Strata without starting it. Updating, Docker,
|
||||
several cards, where the files go and every option:
|
||||
[docs/INSTALL.md](docs/INSTALL.md).
|
||||
several cards, where the files go and every option: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
## Which model should I pick?
|
||||
|
||||
The installer recommends one for your RAM. The same model comes in sizes that are compressed more or less: smaller
|
||||
is faster, larger is a bit smarter.
|
||||
The installer recommends one for your RAM. The same model comes in several sizes, compressed more or less. Smaller
|
||||
sizes are faster. Larger sizes are a bit smarter.
|
||||
|
||||
| Your RAM | Take | Why |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Coder** | it fits 32 GB, and it is made for code (with a 24 GB card, Q2_0 and IQ2_XS run too) |
|
||||
| **48 GB** | **IQ2_XS** (or Q2_0, the fastest) | the larger sizes do not fit |
|
||||
| **64 GB** | **IQ2_XS** (recommended), or IQ3_XXS / IQ3_S | every size fits; IQ3_S is the best, and the slowest |
|
||||
| **96 GB or more** | **IQ3_S**, or Unsloth's 4-bit (experimental) | room for the largest sizes with everything else open |
|
||||
| **64 GB** | **IQ2_XS** (recommended), or IQ3_XXS / IQ3_S | every size fits; IQ3_S is the best and the slowest |
|
||||
| **96 GB or more** | **IQ3_S**, or Unsloth's UD-IQ4_XS (~4-bit) | room for the largest sizes with everything else open |
|
||||
|
||||
- **[Coder](docs/MODELS.md#coder)** - a coding version with half of the experts removed: 91% of the full model's
|
||||
SWE-bench Verified score (by its authors), fits 32 GB of RAM. Weaker outside code, including Chinese and other
|
||||
CJK text (#438): for those, take Q2_0, IQ2_XS or IQ3_S, which keep every expert.
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** - a fine-tune that thinks much shorter before it answers, so you get the
|
||||
answer sooner, at about the same quality.
|
||||
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental) - the closest to the full
|
||||
model, but most of it is read from the SSD while it answers: 7-8.5 tokens/s on a 64 GB PC.
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** - a manual setup, not in the
|
||||
installer's menu.
|
||||
- **[Coder](docs/MODELS.md#coder):** a coding version with half of the experts removed. It reaches 91% of the full
|
||||
model's SWE-bench Verified score (measured by its authors) and fits 32 GB of RAM. It is weaker outside code,
|
||||
including Chinese and other CJK text (#438). For those, take Q2_0, IQ2_XS or IQ3_S, which keep every expert.
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15):** a fine-tune that thinks for a much shorter time before it answers. You
|
||||
get the answer sooner, at about the same quality.
|
||||
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth's ~4-bit version, between IQ3_S and
|
||||
UD-Q4_K_XL in quality. A 94 GB download. With less than ~80 GB of RAM, Strata reads part of it from the SSD
|
||||
while it answers, so it is slower there (an NVMe SSD helps).
|
||||
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental): the closest to the full
|
||||
model. But Strata reads most of it from the SSD while it answers, so it writes only 7-8.5 tokens/s on a 64 GB PC.
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** you set it up by hand. It is
|
||||
not in the installer's menu.
|
||||
|
||||
Sizes, downloads and what fits where: [docs/MODELS.md](docs/MODELS.md). You can add another model later with
|
||||
Sizes, downloads and what fits where: [docs/MODELS.md](docs/MODELS.md). To add another model later, run
|
||||
`SETUP.bat` (Linux: `./setup.sh --setup`).
|
||||
|
||||
## Using it
|
||||
@@ -121,62 +142,68 @@ Sizes, downloads and what fits where: [docs/MODELS.md](docs/MODELS.md). You can
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="The Strata app's Monitor tab next to a coding agent"><br>
|
||||
<sub>The Strata app's <b>Monitor</b> (left) while a coding agent writes the pagoda garden from the video (right)</sub></p>
|
||||
|
||||
- **In the browser:** `http://127.0.0.1:8080` - **Chat**, a live **Monitor** of the model and your GPU/CPU/RAM, and
|
||||
**About** with the settings and addresses.
|
||||
- **Your apps and coding agents:** add an "OpenAI-compatible" provider with base URL **`http://127.0.0.1:8080/v1`**,
|
||||
any API key and any model name. Apps that use Anthropic's API: `http://127.0.0.1:8080/v1/messages` (Claude Code:
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
|
||||
- **Thinking:** choose **off, low, medium or high** in the chat menu or your app's "reasoning effort". Off is
|
||||
fastest; high is best for hard questions.
|
||||
- **Pictures:** say yes to "Images?" in setup, then click **Picture** in the chat, or attach them in your app
|
||||
(AMD cards: on Linux through the processor, not on Windows yet).
|
||||
- **From your phone or another PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>` - always with a key.
|
||||
- **Good to know:** it answers one request at a time. The first message of a chat is read in full (about 1 minute
|
||||
per 30,000 tokens); follow-ups start in seconds.
|
||||
- **In the browser:** open `http://127.0.0.1:8080`. It has **Chat**, a live **Monitor** of the model and your
|
||||
GPU/CPU/RAM, and **About** with the settings and addresses.
|
||||
- **Your apps and coding agents:** add an "OpenAI-compatible" provider with the base URL
|
||||
**`http://127.0.0.1:8080/v1`**. Any API key and any model name work.
|
||||
- Apps that use Anthropic's API: `http://127.0.0.1:8080/v1/messages` (Claude Code:
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
|
||||
- Codex CLI and other apps that use the OpenAI Responses API: `/v1/responses`
|
||||
([setup](docs/DETAILS.md#the-responses-api-and-codex-cli)).
|
||||
- **Thinking:** choose **off, low, medium or high** in the chat menu or in your app's "reasoning effort". Off is the
|
||||
fastest. High is best for hard questions.
|
||||
- **Pictures:** say yes to "Images?" in setup. Then click **Picture** in the chat, or attach pictures in your app.
|
||||
AMD cards read pictures on Linux through the processor; on Windows they can't yet.
|
||||
- **From your phone or another PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Always set a key.
|
||||
- **One request at a time:** by default Strata answers one request, and the others wait. To answer several at once,
|
||||
set `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). On a 12 GB card this makes each answer slower.
|
||||
- **Long prompts:** Strata reads the first message of a chat in full, about 1 minute per 30,000 tokens. Follow-up
|
||||
messages start in seconds.
|
||||
|
||||
More: [where your chats are stored](docs/INSTALL.md#where-things-are-stored), [the API](docs/DETAILS.md#using-it).
|
||||
|
||||
## Something went wrong?
|
||||
|
||||
- **My PC froze the first time Strata started.** Normal while it loads the model: wait, don't close the window.
|
||||
Still frozen after 10 minutes? Restart the PC, close other programs and try again, or pick a smaller size.
|
||||
- **It stopped while downloading or installing.** Run `START-HERE.bat` (or `./setup.sh`) again: it continues where
|
||||
- **My PC froze the first time Strata started.** This is normal while it loads the model. Wait, and don't close the
|
||||
window. Still frozen after 10 minutes? Restart the PC, close other programs and try again, or pick a smaller size.
|
||||
- **It stopped while downloading or installing.** Run `START-HERE.bat` (or `./setup.sh`) again. It continues where
|
||||
it stopped.
|
||||
- **It's very slow and the disk light keeps blinking, or "the engine stopped unexpectedly".** Not enough free RAM:
|
||||
close other programs (browsers use a lot), or pick a smaller size (Q2_0 or IQ2_XS).
|
||||
- **It says port 8080 is already in use.** Strata is already running - look for its window.
|
||||
- **It's very slow and the disk light keeps blinking, or it says "the engine stopped unexpectedly".** Your PC does
|
||||
not have enough free RAM. Close other programs (browsers use a lot), or pick a smaller size (Q2_0 or IQ2_XS).
|
||||
- **It says port 8080 is already in use.** Strata is already running. Look for its window.
|
||||
|
||||
More problems and their fixes: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Still stuck? Open an
|
||||
[issue](https://github.com/Niko1221/Strata/issues) and attach `strata-<model>.log` from the Strata folder.
|
||||
[issue](https://github.com/Niko1221/Strata/issues) and attach `strata-<model>.log` from the Strata folder. Found a
|
||||
security problem? Report it privately: [SECURITY.md](SECURITY.md).
|
||||
|
||||
## How does it work?
|
||||
|
||||
Models like this one normally run on servers with hundreds of gigabytes of graphics memory. Your graphics card has
|
||||
12-24 GB. Strata makes it fit by **sharing the work across your whole PC** - like a kitchen, where the things you use
|
||||
all the time stay on the counter and the rest waits in the pantry.
|
||||
Models like this one usually run on servers with hundreds of gigabytes of graphics memory. Your graphics card has
|
||||
12-24 GB. Strata makes the model fit by **sharing the work across your whole PC**. Think of a kitchen: the things
|
||||
you use all the time stay on the counter, and the rest waits in the pantry.
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
|
||||
|
||||
- **The model is a team of 24,576 small specialists ("experts"),** and each word needs only 10 of them.
|
||||
- **Your graphics card** keeps the few thousand experts that are asked most often; **your RAM** holds all of them,
|
||||
- **The model is a team of 24,576 small specialists ("experts").** Each word needs only 10 of them.
|
||||
- **Your graphics card** keeps the few thousand experts that are used most often. **Your RAM** holds all of them,
|
||||
and **your processor** works on the rest at the same time. **Your SSD** holds a big lookup table.
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
|
||||
|
||||
- **Guess, then check:** a small helper guesses the next few words and the big model checks them all at once, so
|
||||
you get the same answer, 1.6-1.8x sooner.
|
||||
- **Long texts are read in big pieces** (up to 8,192 tokens at a time): over 1,000 tokens per second.
|
||||
- **Guess, then check:** a small helper guesses the next few words. The big model checks them all at once. You get
|
||||
the same answer, 1.6-1.8x sooner.
|
||||
- **Long texts are read in big pieces** (up to 8,192 tokens at a time), at over 1,000 tokens per second.
|
||||
|
||||
The longer explanation: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Every part and its numbers: [the
|
||||
details](docs/DETAILS.md#how-it-works) and the [paper](docs/paper/Strata-Paper.pdf).
|
||||
The longer explanation: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Every part and its numbers:
|
||||
[the details](docs/DETAILS.md#how-it-works) and the [paper](docs/paper/Strata-Paper.pdf).
|
||||
|
||||
## Credits and license
|
||||
|
||||
The model is [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team, compressed by
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5) and Unsloth;
|
||||
Strata is built with parts of [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). All credits:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata is open source under the [MIT License](LICENSE); a few
|
||||
parts and every model carry their own licenses ([which ones](docs/HOW_IT_WORKS.md#license)).
|
||||
The model is [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team. It was
|
||||
compressed by [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
|
||||
and Unsloth. Strata uses parts of [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). All credits:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata is open source under the [MIT License](LICENSE). A few
|
||||
parts and every model have their own licenses ([which ones](docs/HOW_IT_WORKS.md#license)).
|
||||
|
||||
## Support Strata
|
||||
|
||||
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
|
||||
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · **Português**
|
||||
|
||||
<p align="center"><b>Rode um modelo de IA de 125 bilhões de parâmetros no seu próprio PC gamer</b><br>
|
||||
Placa de vídeo NVIDIA ou AMD (12 GB ou mais) · Windows ou Linux · gratuito e de código aberto</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Um jardim com pagode em voxels que o modelo do Strata escreveu, rodando no navegador"></a><br>
|
||||
<sub>Um jardim com pagode em voxels, feito com um único prompt numa RTX 5070 com o Strata (IQ3_S, contexto de 128K) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vídeo completo (49 s)</a></sub></p>
|
||||
|
||||
O Strata roda o **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** num PC comum. É um modelo
|
||||
de IA grande e inteligente que normalmente precisa de um servidor. Ele conversa, escreve código, entende imagens e
|
||||
funciona com seus apps e agentes de programação. Nada sai do seu PC.
|
||||
|
||||
## Qual é a velocidade?
|
||||
|
||||
Medimos em dois PCs gamer comuns. Um token equivale a cerca de ¾ de uma palavra.
|
||||
|
||||
- **Escreve respostas:** a velocidade com que a resposta aparece numa conversa curta. 60 tokens por segundo é mais rápido do que você consegue ler.
|
||||
- **Lê seu prompt:** a velocidade com que ele absorve o que você envia (aqui, um documento, código ou histórico de conversa de 32K tokens).
|
||||
|
||||
<table>
|
||||
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB de RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB de RAM</th></tr>
|
||||
<tr><td>
|
||||
|
||||
| Tamanho | Escreve respostas | Lê seu prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
|
||||
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
|
||||
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
|
||||
| **Coder** | 55 tokens/s | 2,180 tokens/s |
|
||||
|
||||
</td><td>
|
||||
|
||||
| Tamanho | Escreve respostas | Lê seu prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
|
||||
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
|
||||
| **Coder** | 44 tokens/s | 1,420 tokens/s |
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
NVIDIA: Q2_0 com o engine 0.1.36, as outras linhas com 0.1.26 (respostas de 4K, prompts de 32K). As tabelas
|
||||
completas estão em [DETAILS.md](docs/DETAILS.md#speed-measured). Uma placa com mais VRAM é mais rápida: uma
|
||||
RTX 3090 (24 GB) deve escrever cerca de 100-140 tokens por segundo. Conversas longas e outras placas:
|
||||
[velocidade de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados da comunidade](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
|
||||
<sub>O Strata é gratuito. Se ele roda bem no seu PC, um café ajuda a manter o trabalho nele.</sub></p>
|
||||
|
||||
## Do que você precisa
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **Placa de vídeo** | **NVIDIA** GeForce RTX séries 20, 30, 40 ou 50, ou **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 ou séries RX 6800 / 6900. Ela precisa de **12 GB de VRAM ou mais**. |
|
||||
| **RAM** | 32 GB ou mais. Sua RAM decide [qual modelo](#qual-modelo-devo-escolher) cabe. Com 64 GB, roda qualquer tamanho. |
|
||||
| **Disco** | Cerca de 80 GB livres. Use um SSD se puder: a primeira inicialização fica bem mais rápida. |
|
||||
| **Sistema** | Windows 10 / 11 ou Linux, e um driver de vídeo atualizado da NVIDIA ou da AMD. |
|
||||
|
||||
O instalador cuida de todo o resto. Duas ou três placas podem dividir o modelo ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
|
||||
Experimental, escrito e testado por membros da comunidade nas próprias máquinas:
|
||||
|
||||
- **Placas de vídeo mais antigas** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md).
|
||||
- **Intel Arc**, compilado a partir do código-fonte no Linux: [Intel Arc](docs/INTEL_ARC.md).
|
||||
- **AMD Ryzen AI Max (Strix Halo)**, compilado a partir do código-fonte no Linux: [Strix Halo](docs/STRIX_HALO.md).
|
||||
- **Processadores mais antigos sem AVX2**: funcionam, mas devagar. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
|
||||
|
||||
A lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
|
||||
## Instalação
|
||||
|
||||
### Deixe sua IA instalar
|
||||
|
||||
Você usa um assistente de programação com IA (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Cole isto nele:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
Ele verifica sua placa de vídeo, RAM e disco e escolhe o modelo que cabe. Depois instala, inicia e explica como
|
||||
conectar seus apps. Ferramentas de IA também podem instalar, iniciar e parar o Strata pelo
|
||||
[servidor MCP](docs/MCP_SERVER.md) dele.
|
||||
|
||||
### Ou faça você mesmo
|
||||
|
||||
[Baixe o Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) e descompacte (ou use `git clone`).
|
||||
**Windows:** clique duas vezes em **`START-HERE.bat`**. **Linux:** rode **`./setup.sh`** na pasta do Strata.
|
||||
|
||||
Os passos são os mesmos para NVIDIA e AMD. O instalador encontra sua placa e configura o engine certo para ela.
|
||||
Ele faz algumas perguntas:
|
||||
|
||||
- qual modelo e qual tamanho,
|
||||
- quanto contexto (quanto texto o modelo guarda na memória),
|
||||
- se ele deve entender imagens.
|
||||
|
||||
Aperte Enter em cada uma para usar a resposta recomendada. Depois ele baixa o modelo (cerca de 70 GB) e o inicia.
|
||||
Se o download parar, rode de novo: ele continua de onde parou. Seu navegador abre o app do Strata em
|
||||
`http://127.0.0.1:8080`.
|
||||
|
||||
> **Enquanto o modelo inicia, seu PC pode ficar lento ou parar de responder por 1-3 minutos** (mais na primeira vez).
|
||||
> O Strata carrega 35-55 GB na sua RAM e reserva parte dela para a placa de vídeo. Isso é normal. Espere e não
|
||||
> feche a janela. A janela mostra o que o Strata está fazendo.
|
||||
|
||||
**Da próxima vez**, rode `START-HERE.bat` (ou `./setup.sh`) de novo. Ele inicia na hora e não baixa nada duas
|
||||
vezes. Feche a janela para parar o modelo. `UPDATE.bat` (`./update.sh`) atualiza o Strata sem iniciá-lo.
|
||||
Atualização, Docker, várias placas, onde ficam os arquivos e todas as opções: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
## Qual modelo devo escolher?
|
||||
|
||||
O instalador recomenda um de acordo com sua RAM. O mesmo modelo vem em vários tamanhos, mais ou menos comprimidos.
|
||||
Os tamanhos menores são mais rápidos. Os maiores são um pouco mais inteligentes.
|
||||
|
||||
| Sua RAM | Escolha | Por quê |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Coder** | cabe em 32 GB e é feito para código (com uma placa de 24 GB, Q2_0 e IQ2_XS também rodam) |
|
||||
| **48 GB** | **IQ2_XS** (ou Q2_0, o mais rápido) | os tamanhos maiores não cabem |
|
||||
| **64 GB** | **IQ2_XS** (recomendado), ou IQ3_XXS / IQ3_S | todos os tamanhos cabem; IQ3_S é o melhor e o mais lento |
|
||||
| **96 GB ou mais** | **IQ3_S**, ou o UD-IQ4_XS da Unsloth (~4-bit) | espaço para os maiores tamanhos com todo o resto aberto |
|
||||
|
||||
- **[Coder](docs/MODELS.md#coder):** uma versão para programação com metade dos experts removida. Ela alcança 91%
|
||||
da pontuação do modelo completo no SWE-bench Verified (medido pelos autores) e cabe em 32 GB de RAM. É mais fraca
|
||||
fora do código, inclusive em chinês e outros textos CJK (#438). Para isso, escolha Q2_0, IQ2_XS ou IQ3_S, que
|
||||
mantêm todos os experts.
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15):** um fine-tune que pensa por bem menos tempo antes de responder. Você
|
||||
recebe a resposta mais cedo, com quase a mesma qualidade.
|
||||
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** a versão de ~4 bits da Unsloth, entre IQ3_S e
|
||||
UD-Q4_K_XL em qualidade. Um download de 94 GB. Com menos de ~80 GB de RAM, o Strata lê uma parte dele do SSD
|
||||
enquanto responde, então ali ele fica mais lento (um SSD NVMe ajuda).
|
||||
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)** (experimental): o mais próximo do modelo
|
||||
completo. Mas o Strata lê a maior parte dele do SSD enquanto responde, então ele escreve só 7-8.5 tokens/s num PC
|
||||
com 64 GB.
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** você configura à mão. Ele
|
||||
não está no menu do instalador.
|
||||
|
||||
Tamanhos, downloads e o que cabe onde: [docs/MODELS.md](docs/MODELS.md). Para adicionar outro modelo depois, rode
|
||||
`SETUP.bat` (Linux: `./setup.sh --setup`).
|
||||
|
||||
## Como usar
|
||||
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="A aba Monitor do app Strata ao lado de um agente de programação"><br>
|
||||
<sub>O <b>Monitor</b> do app Strata (à esquerda) enquanto um agente de programação escreve o jardim com pagode do vídeo (à direita)</sub></p>
|
||||
|
||||
- **No navegador:** abra `http://127.0.0.1:8080`. Lá tem o **Chat**, um **Monitor** ao vivo do modelo e da sua
|
||||
GPU/CPU/RAM, e o **About** com as configurações e os endereços.
|
||||
- **Seus apps e agentes de programação:** adicione um provedor "OpenAI-compatible" com a URL base
|
||||
**`http://127.0.0.1:8080/v1`**. Qualquer chave de API e qualquer nome de modelo funcionam.
|
||||
- Apps que usam a API da Anthropic: `http://127.0.0.1:8080/v1/messages` (Claude Code:
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
|
||||
- Codex CLI e outros apps que usam a OpenAI Responses API: `/v1/responses`
|
||||
([configuração](docs/DETAILS.md#the-responses-api-and-codex-cli)).
|
||||
- **Raciocínio:** escolha **off, low, medium ou high** no menu do chat ou no "reasoning effort" do seu app. Off é o
|
||||
mais rápido. High é o melhor para perguntas difíceis.
|
||||
- **Imagens:** responda sim para "Images?" na instalação. Depois clique em **Picture** no chat, ou anexe imagens no
|
||||
seu app. Placas AMD entendem imagens no Linux usando o processador; no Windows, ainda não.
|
||||
- **Do seu celular ou de outro PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Sempre defina uma chave.
|
||||
- **Uma requisição por vez:** por padrão, o Strata responde uma requisição e as outras esperam. Para responder
|
||||
várias ao mesmo tempo, defina `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Numa placa de 12 GB, isso deixa
|
||||
cada resposta mais lenta.
|
||||
- **Prompts longos:** o Strata lê a primeira mensagem de uma conversa inteira, cerca de 1 minuto a cada 30,000
|
||||
tokens. As mensagens seguintes começam em segundos.
|
||||
|
||||
Mais: [onde suas conversas ficam salvas](docs/INSTALL.md#where-things-are-stored), [a API](docs/DETAILS.md#using-it).
|
||||
|
||||
## Algo deu errado?
|
||||
|
||||
- **Meu PC travou na primeira vez que o Strata iniciou.** Isso é normal enquanto ele carrega o modelo. Espere e não
|
||||
feche a janela. Ainda travado depois de 10 minutos? Reinicie o PC, feche outros programas e tente de novo, ou
|
||||
escolha um tamanho menor.
|
||||
- **Parou durante o download ou a instalação.** Rode `START-HERE.bat` (ou `./setup.sh`) de novo. Ele continua de
|
||||
onde parou.
|
||||
- **Está muito lento e a luz do disco não para de piscar, ou aparece "the engine stopped unexpectedly".** Seu PC
|
||||
não tem RAM livre suficiente. Feche outros programas (navegadores usam muita), ou escolha um tamanho menor (Q2_0
|
||||
ou IQ2_XS).
|
||||
- **Diz que a porta 8080 já está em uso.** O Strata já está rodando. Procure a janela dele.
|
||||
|
||||
Mais problemas e como resolver: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Ainda sem solução? Abra uma
|
||||
[issue](https://github.com/Niko1221/Strata/issues) e anexe o `strata-<model>.log` da pasta do Strata. Encontrou um
|
||||
problema de segurança? Informe em particular: [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Como funciona?
|
||||
|
||||
Modelos como este normalmente rodam em servidores com centenas de gigabytes de memória de vídeo. Sua placa de vídeo
|
||||
tem 12-24 GB. O Strata faz o modelo caber **dividindo o trabalho pelo PC inteiro**. Pense numa cozinha: o que você
|
||||
usa o tempo todo fica na bancada, e o resto espera na despensa.
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Os 24,576 experts do modelo: os mais usados na placa de vídeo, todos na RAM, uma tabela de consulta no SSD"></p>
|
||||
|
||||
- **O modelo é uma equipe de 24,576 pequenos especialistas ("experts").** Cada palavra precisa de só 10 deles.
|
||||
- **Sua placa de vídeo** guarda os poucos milhares de experts usados com mais frequência. **Sua RAM** guarda todos
|
||||
eles, e **seu processador** trabalha no resto ao mesmo tempo. **Seu SSD** guarda uma grande tabela de consulta.
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Um pequeno ajudante adivinha as próximas palavras; o modelo grande confere todas de uma vez e fica com as certas"></p>
|
||||
|
||||
- **Adivinhar e conferir:** um pequeno ajudante adivinha as próximas palavras. O modelo grande confere todas de
|
||||
uma vez. Você recebe a mesma resposta, 1.6-1.8x mais cedo.
|
||||
- **Textos longos são lidos em grandes pedaços** (até 8,192 tokens por vez), a mais de 1,000 tokens por segundo.
|
||||
|
||||
A explicação mais longa: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte e seus números:
|
||||
[os detalhes](docs/DETAILS.md#how-it-works) e o [artigo](docs/paper/Strata-Paper.pdf).
|
||||
|
||||
## Créditos e licença
|
||||
|
||||
O modelo é o [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), da equipe Qwen. Ele foi
|
||||
comprimido por [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5)
|
||||
e Unsloth. O Strata usa partes do [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos os créditos:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). O Strata é de código aberto sob a [MIT License](LICENSE).
|
||||
Algumas partes e todos os modelos têm licenças próprias ([quais](docs/HOW_IT_WORKS.md#license)).
|
||||
|
||||
## Apoie o Strata
|
||||
|
||||
O Strata é gratuito e de código aberto. Se ele é útil para você, você pode apoiar o desenvolvimento:
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
|
||||
[English](README.md) · **简体中文** · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
|
||||
|
||||
<p align="center"><b>在你自己的游戏电脑上运行 1250 亿参数的 AI 模型</b><br>
|
||||
NVIDIA 或 AMD 显卡(12 GB 及以上)· Windows 或 Linux · 免费开源</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Strata 的模型写出的体素宝塔花园,在浏览器中运行"></a><br>
|
||||
<sub>体素宝塔花园,一次提示生成,在 RTX 5070 上用 Strata 运行(IQ3_S,128K 上下文)·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">完整视频(49 秒)</a></sub></p>
|
||||
|
||||
Strata 能在普通电脑上运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**。这是一个又大又聪明的
|
||||
AI 模型,通常要用服务器才能跑。它能聊天、写代码、看图片,还能配合你的应用和编程智能体一起工作。所有数据都留在你的电脑上。
|
||||
|
||||
## 速度有多快?
|
||||
|
||||
我们在两台普通的游戏电脑上做了测试。一个 token 大约相当于 ¾ 个英文单词。
|
||||
|
||||
- **写回答:** 短对话中回复出现的速度。每秒 60 个 token 已经比你的阅读速度还快。
|
||||
- **读提示:** 读入你发送内容的速度(这里是一份 32K token 的文档、代码或聊天记录)。
|
||||
|
||||
<table>
|
||||
<tr><th>NVIDIA:RTX 5070(12 GB)、Ryzen 5 7600、64 GB 内存</th><th>AMD:RX 9070 XT(16 GB)、Ryzen 9 3900X、47 GB 内存</th></tr>
|
||||
<tr><td>
|
||||
|
||||
| 规格 | 写回答 | 读提示 |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
|
||||
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
|
||||
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
|
||||
| **Coder** | 55 tokens/s | 2,180 tokens/s |
|
||||
|
||||
</td><td>
|
||||
|
||||
| 规格 | 写回答 | 读提示 |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
|
||||
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
|
||||
| **Coder** | 44 tokens/s | 1,420 tokens/s |
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
NVIDIA:Q2_0 用的是引擎 0.1.36,其他各行用的是 0.1.26(4K 回答,32K 提示)。完整表格见
|
||||
[DETAILS.md](docs/DETAILS.md#speed-measured)。显存越大的显卡越快:RTX 3090(24 GB)写回答应该能达到每秒约
|
||||
100-140 个 token。长对话和其他显卡的数据:[各模型的速度](docs/MODELS.md#how-fast-is-each-size)、
|
||||
[社区测试结果](docs/COMMUNITY_BENCHMARKS.md)。
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="请我喝杯咖啡" height="50"></a><br>
|
||||
<sub>Strata 是免费的。如果它在你的电脑上跑得不错,请我喝杯咖啡,让这个项目继续下去。</sub></p>
|
||||
|
||||
## 你需要什么
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| **显卡** | **NVIDIA** GeForce RTX 20、30、40 或 50 系列,或 **AMD** Radeon RX 7900 XT / XTX、RX 7800 XT / 7700 XT、RX 9060 XT、RX 9070 / 9070 XT、Radeon AI PRO R9700 或 RX 6800 / 6900 系列。需要 **12 GB 或以上显存**。 |
|
||||
| **内存** | 32 GB 或以上。内存大小决定能装下[哪个模型](#该选哪个模型)。64 GB 可以运行所有规格。 |
|
||||
| **硬盘** | 约 80 GB 可用空间。尽量用 SSD:第一次启动会快很多。 |
|
||||
| **系统** | Windows 10 / 11 或 Linux,以及 NVIDIA 或 AMD 的最新显卡驱动。 |
|
||||
|
||||
其他的都由安装程序搞定。两到三张显卡可以分担同一个模型([多 GPU](docs/MULTI_GPU.md))。
|
||||
|
||||
实验性支持,由社区成员在自己的机器上编写和测试:
|
||||
|
||||
- **较老的显卡**(Tesla P40 / V100、GTX 10、Radeon VII / MI50、RX 6700 XT、RX 5500 XT):[较老的 GPU](docs/OLDER_GPUS.md)。
|
||||
- **Intel Arc**,在 Linux 上从源码构建:[Intel Arc](docs/INTEL_ARC.md)。
|
||||
- **AMD Ryzen AI Max(Strix Halo)**,在 Linux 上从源码构建:[Strix Halo](docs/STRIX_HALO.md)。
|
||||
- **不支持 AVX2 的老处理器**:能用,但很慢。[较老的 CPU](docs/INSTALL.md#older-cpus-experimental)。
|
||||
|
||||
完整列表:[docs/INSTALL.md](docs/INSTALL.md#what-you-need)。
|
||||
|
||||
## 安装
|
||||
|
||||
### 让你的 AI 帮你装
|
||||
|
||||
你在用 AI 编程助手吗(Claude Code、Cursor、Codex、GitHub Copilot 等)?把下面这段粘贴给它:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
它会检查你的显卡、内存和硬盘,选出合适的模型。然后安装并启动它,再告诉你怎么连接你的应用。AI 工具也可以通过
|
||||
Strata 的 [MCP 服务器](docs/MCP_SERVER.md)来安装、启动和停止 Strata。
|
||||
|
||||
### 或者自己动手
|
||||
|
||||
[下载 Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) 并解压(或者用 `git clone`)。
|
||||
**Windows:** 双击 **`START-HERE.bat`**。**Linux:** 在 Strata 文件夹里运行 **`./setup.sh`**。
|
||||
|
||||
NVIDIA 和 AMD 的步骤完全一样。安装程序会识别你的显卡,并装好对应的引擎。它会问你几个问题:
|
||||
|
||||
- 用哪个模型、哪个规格,
|
||||
- 上下文多大(模型能记住多少文字),
|
||||
- 是否要识别图片。
|
||||
|
||||
每次直接按回车就是推荐选项。之后它会下载模型(约 70 GB)并启动。如果下载中断了,再运行一次即可:它会从中断的地方继续。
|
||||
浏览器会打开 Strata 应用,地址是 `http://127.0.0.1:8080`。
|
||||
|
||||
> **模型启动时,你的电脑可能会变慢或卡住 1-3 分钟**(第一次最久)。
|
||||
> Strata 会把 35-55 GB 加载到内存,并为显卡锁定其中一部分。这是正常的。请耐心等待,不要关闭窗口。
|
||||
> 窗口里会显示 Strata 正在做什么。
|
||||
|
||||
**下次使用时**,再运行一次 `START-HERE.bat`(或 `./setup.sh`)。它会马上启动,已下载的东西不会重复下载。关闭它的窗口就能停止模型。
|
||||
`UPDATE.bat`(`./update.sh`)只更新 Strata,不启动。更新、Docker、多张显卡、文件存放位置以及所有选项:
|
||||
[docs/INSTALL.md](docs/INSTALL.md)。
|
||||
|
||||
## 该选哪个模型
|
||||
|
||||
安装程序会根据你的内存推荐一个。同一个模型有几种规格,压缩程度不同。规格越小越快,越大越聪明一些。
|
||||
|
||||
| 你的内存 | 选择 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Coder** | 32 GB 装得下,而且专为代码打造(如果显卡是 24 GB,Q2_0 和 IQ2_XS 也能跑) |
|
||||
| **48 GB** | **IQ2_XS**(或 Q2_0,最快) | 更大的规格装不下 |
|
||||
| **64 GB** | **IQ2_XS**(推荐),或 IQ3_XXS / IQ3_S | 所有规格都装得下;IQ3_S 最好,也最慢 |
|
||||
| **96 GB 或以上** | **IQ3_S**,或 Unsloth 的 UD-IQ4_XS(约 4-bit) | 开着其他程序也能放下最大的规格 |
|
||||
|
||||
- **[Coder](docs/MODELS.md#coder):** 编程版本,去掉了一半专家。它达到完整模型 SWE-bench Verified 分数的 91%(由其作者测得),
|
||||
32 GB 内存就能装下。代码以外的能力较弱,包括中文和其他中日韩文本(#438)。这类用途请选 Q2_0、IQ2_XS 或 IQ3_S,
|
||||
它们保留了所有专家。
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15):** 一个微调版本,回答前思考的时间短得多。你能更早拿到答案,质量基本不变。
|
||||
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth 的约 4-bit 版本,质量介于 IQ3_S 和 UD-Q4_K_XL 之间。
|
||||
下载 94 GB。内存少于约 80 GB 时,Strata 回答时要从 SSD 读取其中一部分,所以会更慢(NVMe SSD 有帮助)。
|
||||
- **[Unsloth UD-Q4_K_XL](docs/MODELS.md#unsloth-ud-q4_k_xl-experimental)**(实验性):最接近完整模型。但 Strata
|
||||
回答时要从 SSD 读取其中大部分内容,所以在 64 GB 的电脑上每秒只能写 7-8.5 个 token。
|
||||
- **[OrcaRouter 的 Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** 需要手动设置,不在安装程序的菜单里。
|
||||
|
||||
规格、下载以及各配置能装下什么:[docs/MODELS.md](docs/MODELS.md)。以后想再添加模型,运行
|
||||
`SETUP.bat`(Linux:`./setup.sh --setup`)。
|
||||
|
||||
## 使用方法
|
||||
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="Strata 应用的 Monitor 标签页,旁边是一个编程智能体"><br>
|
||||
<sub>Strata 应用的 <b>Monitor</b>(左),编程智能体正在写视频里的宝塔花园(右)</sub></p>
|
||||
|
||||
- **在浏览器里:** 打开 `http://127.0.0.1:8080`。里面有 **Chat**(聊天)、实时 **Monitor**(监控模型和你的
|
||||
GPU/CPU/内存),以及 **About**(设置和地址)。
|
||||
- **你的应用和编程智能体:** 添加一个“OpenAI 兼容”的提供商,base URL 填 **`http://127.0.0.1:8080/v1`**。
|
||||
API key 和模型名随便填都行。
|
||||
- 使用 Anthropic API 的应用:`http://127.0.0.1:8080/v1/messages`(Claude Code:
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`)。
|
||||
- Codex CLI 和其他使用 OpenAI Responses API 的应用:`/v1/responses`
|
||||
([设置方法](docs/DETAILS.md#the-responses-api-and-codex-cli))。
|
||||
- **思考:** 在聊天菜单或应用的“reasoning effort”里选择 **off、low、medium 或 high**。off 最快,
|
||||
high 最适合难题。
|
||||
- **图片:** 在安装时对“Images?”选是。然后在聊天里点 **Picture**,或在你的应用里附上图片。
|
||||
AMD 显卡在 Linux 上通过处理器识别图片;在 Windows 上暂时还不行。
|
||||
- **从手机或另一台电脑访问:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`。一定要设置 key。
|
||||
- **一次处理一个请求:** 默认情况下 Strata 一次只回答一个请求,其他请求排队等待。想同时回答多个,
|
||||
设置 `"parallel": 2`([BATCHING.md](docs/BATCHING.md))。在 12 GB 显卡上,这会让每个回答变慢。
|
||||
- **长提示:** Strata 会完整读入对话的第一条消息,大约每 30,000 个 token 需要 1 分钟。之后的消息几秒内就开始回答。
|
||||
|
||||
更多:[聊天记录存在哪里](docs/INSTALL.md#where-things-are-stored)、[API](docs/DETAILS.md#using-it)。
|
||||
|
||||
## 出问题了?
|
||||
|
||||
- **Strata 第一次启动时电脑卡死了。** 加载模型时这是正常的。请耐心等待,不要关闭窗口。
|
||||
10 分钟后还卡着?重启电脑,关掉其他程序再试一次,或者换一个更小的规格。
|
||||
- **下载或安装时中断了。** 再运行一次 `START-HERE.bat`(或 `./setup.sh`)。它会从中断的地方继续。
|
||||
- **非常慢,硬盘灯一直闪,或者提示“the engine stopped unexpectedly”。** 你的电脑可用内存不够。
|
||||
关掉其他程序(浏览器很占内存),或者换一个更小的规格(Q2_0 或 IQ2_XS)。
|
||||
- **提示 8080 端口已被占用。** Strata 已经在运行了。找一下它的窗口。
|
||||
|
||||
更多问题和解决办法:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。还是解决不了?提交一个
|
||||
[issue](https://github.com/Niko1221/Strata/issues),并附上 Strata 文件夹里的 `strata-<model>.log`。发现了安全问题?
|
||||
请私下报告:[SECURITY.md](SECURITY.md)。
|
||||
|
||||
## 它是怎么工作的?
|
||||
|
||||
这类模型通常运行在拥有数百 GB 显存的服务器上。而你的显卡只有 12-24 GB。Strata 让模型装得下的办法是
|
||||
**把工作分摊到整台电脑上**。可以想象一个厨房:常用的东西放在台面上,其余的放在储藏室里。
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="模型的 24,576 个专家:最常用的在显卡上,全部在内存里,一张查找表在 SSD 上"></p>
|
||||
|
||||
- **这个模型是由 24,576 个小专家(“experts”)组成的团队。** 每个词只需要其中 10 个。
|
||||
- **你的显卡** 存放最常用的几千个专家。**你的内存** 存放全部专家,
|
||||
**你的处理器** 同时处理其余的部分。**你的 SSD** 存放一张大查找表。
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="一个小助手猜测接下来的几个词;大模型一次性全部检查,保留正确的"></p>
|
||||
|
||||
- **先猜,再检查:** 一个小助手先猜接下来的几个词,大模型再一次性检查它们。答案完全一样,但快 1.6-1.8 倍。
|
||||
- **长文本分大块读入**(每次最多 8,192 个 token),速度超过每秒 1,000 个 token。
|
||||
|
||||
更详细的解释:[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)。每个部分及其数据:
|
||||
[详细说明](docs/DETAILS.md#how-it-works)和[论文](docs/paper/Strata-Paper.pdf)。
|
||||
|
||||
## 致谢与许可证
|
||||
|
||||
模型是 Qwen 团队的 [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)。它由
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)、UkisAI(Swift 1.5)
|
||||
和 Unsloth 压缩。Strata 使用了 [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) 的部分代码。完整致谢:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits)。Strata 以 [MIT 许可证](LICENSE)开源。少数部分和每个模型
|
||||
有各自的许可证([具体是哪些](docs/HOW_IT_WORKS.md#license))。
|
||||
|
||||
## 支持 Strata
|
||||
|
||||
Strata 免费且开源。如果它对你有用,欢迎支持它的开发:
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="请我喝杯咖啡" height="50"></a></p>
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# Security
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Please report anything that should not be public until it is fixed **privately**, through GitHub's private
|
||||
vulnerability reporting: the repository's **Security** tab, **Report a vulnerability**
|
||||
([direct link](https://github.com/Niko1221/Strata/security/advisories/new)). Say what you found, how to reproduce it
|
||||
(the request, the config keys involved, the Strata version), and what an attacker gains. We answer there, fix it in
|
||||
a release, and credit you in the advisory unless you ask us not to.
|
||||
|
||||
Ordinary hardening ideas and findings that are safe to discuss in public are welcome as an
|
||||
[issue](https://github.com/Niko1221/Strata/issues) (#544 collects audit findings) or as a pull request, which we
|
||||
review like our own code.
|
||||
|
||||
Supported: the latest release. Fixes go into the next release, not into older ones.
|
||||
|
||||
## What the server exposes
|
||||
|
||||
Strata runs a model on your PC and serves it over HTTP (`serve/server.py`). Out of the box it is reachable from
|
||||
this PC only. The details and every setting are in [docs/DETAILS.md](docs/DETAILS.md) ("From other devices",
|
||||
"Host names", "Web pages without an API key", "Tools from MCP servers").
|
||||
|
||||
- **Where it listens.** `127.0.0.1` by default. `--host 0.0.0.0` (or `"host"` in `strata-<model>.json`) opens it to
|
||||
your network, and the server then warns when no API key is set.
|
||||
- **API key.** `"api_key"` in the run config (or `STRATA_API_KEY`) is required on `/v1/*` and on every endpoint
|
||||
that shows the model's state, requests or answers (`/status`, `/metrics`, `/settings`, `/mcp`, `/props`, `/slots`,
|
||||
`/api/requests`, `/config`) and on every `POST`. It is compared in constant time. Set one before you open the
|
||||
server to your network or put a tunnel in front of it.
|
||||
- **Host check (DNS rebinding, 0.1.38).** Without an API key the server answers only requests whose `Host` is a name
|
||||
it knows (`localhost`, an IP address, the address it listens on and, when it listens beyond this PC, this PC's
|
||||
name). Others get 403. `"allowed_hosts"` adds names; with an API key the key decides.
|
||||
- **Origin check (0.1.38).** Without an API key, a browser `POST` to `/v1/*` from another web site's page (it carries
|
||||
an `Origin` header) gets 403 unless that origin is Strata's own page, `localhost`, an allowed host, or listed in
|
||||
`"trusted_origins"` / `"cors_origins"`. Changing settings (`/settings`, and `/config`: the few run config keys the
|
||||
page's Model settings may change - never the network, key, MCP or program keys), `/load` and `/unload` and the
|
||||
MCP tools are accepted only as JSON from Strata's own page (or `"trusted_origins"`), so a page elsewhere cannot
|
||||
change settings or run tools.
|
||||
- **CORS** is off unless `"cors_origins"` lists origins, and then only for `/v1/*`.
|
||||
- **MCP tools** are opt-in: only the servers you put in the run config, only for requests from Strata's own page
|
||||
that ask for them. They run with your user's rights, and the model decides when to call them.
|
||||
- **The request monitor** (`/api-monitor`, which keeps the last prompts and answers in memory) is off unless
|
||||
`"api_monitor": true` is set.
|
||||
|
||||
Strata has not had an outside security audit yet.
|
||||
@@ -0,0 +1,121 @@
|
||||
# Community benchmark: 2x AMD Instinct MI50 16 GB (gfx906), Xeon E5-2666 v3
|
||||
|
||||
Measured on 2026-10-03 by [JeanP00l](https://github.com/JeanP00l) on a Linux server. This tests the Coder
|
||||
(IQ1_M) with a 131,072-token context, the layer split across two MI50 cards, and the gfx906 build of #638. That
|
||||
build is an opt-in HIP build, separate from the wave32 RDNA backend.
|
||||
|
||||
The median decode throughput was **50.1 tok/s at 4,096 prompt tokens, 47.8 tok/s at 32,768 and 45.7 tok/s at
|
||||
128,000**. Prompts were read at 321, 523 and 517 tok/s. The requests are synthetic code-explanation requests
|
||||
with greedy decoding and a 256-token output cap. They do not establish general answer quality or performance on
|
||||
other workloads.
|
||||
|
||||
## Hardware and software
|
||||
|
||||
- **GPUs:** 2x AMD Instinct MI50 16 GB (Vega 20, gfx906, wave64; HIP names them "AMD Radeon VII"). Power limit
|
||||
85 W per card, set by the owner (the cards' own limit is higher). PCIe 3.0 x16 on both. Peer access between them
|
||||
works, through the CPU's root complex (no direct bridge). Blower fans; no clock locking beyond the power limit.
|
||||
The engine's PCIe probe measured 13.8 GB/s host to device.
|
||||
- **CPU and RAM:** Intel Xeon E5-2666 v3 (10 cores, 20 threads, AVX2, no AVX-512); the engine used 9 expert-pool
|
||||
workers plus its host thread. 32 GB DDR4 quad-channel (Linux reports 31.2 GiB). 8 GiB swap file. One SATA SSD.
|
||||
- **OS and runtime:** Ubuntu 24.04.4 LTS, kernel 6.8.0-138-generic, the kernel's amdgpu driver. HIP 7.14 from the
|
||||
community image `mixa3607/rocm-gfx906:7.14-complete` (AMD's current ROCm no longer ships gfx906 libraries).
|
||||
- **Source:** [`mi50-bench-build`](https://github.com/JeanP00l/Strata/tree/mi50-bench-build) at `ea51fd3`, which is
|
||||
`main` 99f3dbd (engine 0.1.38) with #638 (at 17beb33), #639, #640 and #637 merged. #638's later commits
|
||||
(a914b13) only fix the CUDA/RDNA compile and do not change the gfx906 binary's behaviour.
|
||||
- **Build:** `-DSTRATA_HIP_GFX906=ON -DCMAKE_HIP_ARCHITECTURES=gfx906 -DSTRATA_PORTABLE=ON`, ROCm clang, Ninja,
|
||||
llama.cpp at the pinned 3cf0325.
|
||||
- **Other services:** the machine's other GPU services were stopped, so the two cards ran only Strata. CPU services
|
||||
(a database, a sync daemon) kept running. This was not a fully isolated operating system.
|
||||
|
||||
## Model and configuration
|
||||
|
||||
**Model:** [`ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF`](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF),
|
||||
IQ1_M. The repository revision was not recorded; the shard SHA-256 hashes matched the `.sha256` files downloaded
|
||||
with them:
|
||||
|
||||
- `Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00001-of-00002.gguf` `e11083ba855e7666b48ea3f2db6a9c3a20c18751a012cc24f948de91b7087fad`
|
||||
- `Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00002-of-00002.gguf` `316b46f3a2dbd68c900f43136ab9449f9dcc3725dfd8c794847c204bc161e113`
|
||||
|
||||
The native pack and the MTP draft layer were prepared with the repository's tools on 2026-09-29. The expert profile is the bundled
|
||||
`data/expert-profile-coder.bin`.
|
||||
|
||||
**Configuration** ([strata-coder.json](strata-coder.json); `/m` is the model directory, `/work` the build):
|
||||
|
||||
- context 131,072, `--kv int8 --kv-resident 32768` (32,768 KV cells per attention layer in VRAM, the rest streamed
|
||||
from RAM);
|
||||
- `layer_split 27`: layers 0-26 on GPU 0, 27-47 on GPU 1. With #639 each GPU loads only its own layers' dense
|
||||
weights;
|
||||
- expert cache `auto`, `--vram-reserve-mib 600`: 6,912 + 5,376 slots = **all 12,288 experts in VRAM**, prefilled
|
||||
from the profile, no eviction;
|
||||
- `--prefill 4096`, with the prompt path borrowing 1,193 + 1,084 cache slots;
|
||||
- MTP `--spec 4 --spec-min-p 0.5`; suffix drafting on (the default);
|
||||
- `--pcie-frac 0`, required with `STRATA_ARENA_MMAP=1` (#640, the expert arena as a read-only mapped file; 23.4
|
||||
GiB of VRAM-held experts handed back to the OS at start);
|
||||
- no vision, no calibration, no speed projection, no control vectors;
|
||||
- reasoning off (`reasoning_effort: none`), temperature 0, 256 generated tokens per speed run.
|
||||
|
||||
## Method
|
||||
|
||||
[benchmark.py](benchmark.py) is the script of the
|
||||
[RTX 5090 report](../2026-09-30-community-rtx-5090/README.md), unchanged. It builds deterministic synthetic Python
|
||||
filler, puts a different nonce near the start of each request, and counts the rendered chat prompt with Strata's
|
||||
tokenizer. Its request hashes and the output text of every run are in [results.json](results.json); the aggregates
|
||||
are in [summary.json](summary.json).
|
||||
|
||||
The server ran in a container with both cards (the repository's `serve/` and `tools/` from the source above):
|
||||
|
||||
```bash
|
||||
docker run --device=/dev/kfd --device=/dev/dri --group-add video --ipc=host --ulimit memlock=-1 \
|
||||
-e HSA_OVERRIDE_GFX_VERSION=9.0.6 -e STRATA_ARENA_MMAP=1 ... \
|
||||
python3 serve/server.py --engine strata --config strata-coder.json --port 8080
|
||||
python3 benchmark.py --root /opt/strata --pack /m/pack-coder --url http://127.0.0.1:8080 --out results/
|
||||
python3 tools/needle_bench.py --url http://127.0.0.1:8080 --lengths 32k,128k --depths 10,50,90 --out needles.json
|
||||
```
|
||||
|
||||
- One short warm-up request is excluded.
|
||||
- Three runs at each length ran serially, in increasing-length order, on the same loaded engine.
|
||||
- All nine speed requests read their whole prompt: **zero reused tokens**.
|
||||
- The expert cache was filled at start and kept between requests. Loading time is excluded.
|
||||
- TTFT is streaming, from just before the HTTP request to the first nonempty text delta, over loopback.
|
||||
- Prompt throughput is freshly read tokens / `prompt_ms`. Decode throughput is `engine_generated / decode_ms`.
|
||||
- [engine.log](engine.log) is the engine's whole log for this session, including the start-up decisions.
|
||||
|
||||
## Results
|
||||
|
||||
Each cell is the median **[minimum-maximum]** of three runs. Every request generated 256 tokens and stopped at
|
||||
the output limit. No speed request failed or was cancelled.
|
||||
|
||||
| Prompt tokens | Reused | Prompt tok/s | Decode tok/s | TTFT seconds | Total seconds |
|
||||
| ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| 4,096 | 0 | 320.9 [309.9-324.7] | 50.1 [49.3-54.0] | 12.81 [12.66-13.26] | 17.90 [17.38-18.43] |
|
||||
| 32,768 | 0 | 522.8 [522.6-523.4] | 47.8 [47.6-48.5] | 62.81 [62.75-62.84] | 68.10 [68.10-68.15] |
|
||||
| 128,000 | 0 | 516.8 [516.5-516.9] | 45.7 [44.8-51.5] | 248.09 [248.06-248.27] | 253.75 [253.03-253.84] |
|
||||
|
||||
- The decode expert-cache hit rate was 100% for every request. All experts are in VRAM; the CPU pool computes none.
|
||||
- The 4K prompt reads at about 60% of the longer ones' speed, because a 4,096-token chunk pays the fixed per-chunk
|
||||
cost once.
|
||||
- Draft acceptance varies between runs (decode 44.8-51.5 tok/s at 128K) although the text is greedy.
|
||||
|
||||
**Memory**, from [telemetry.jsonl](telemetry.jsonl) (1,699 one-second samples over 1,763 s, from start through
|
||||
the recall checks):
|
||||
|
||||
- VRAM peak: 15.42 GiB on GPU 0 and 14.70 GiB on GPU 1 (`mem_info_vram_used`, the whole card).
|
||||
- RAM: `MemAvailable` never below **25.1 GiB** of 31.2.
|
||||
- Swap: no growth during the run. The swap file held ~1.3 GiB from earlier use before the start.
|
||||
|
||||
These are sampled values; brief peaks between samples can be missed.
|
||||
|
||||
## Recall and limitations
|
||||
|
||||
The repository's unchanged `tools/needle_bench.py` found **all six needles**, at depths 10%, 50% and 90% at both
|
||||
lengths ([needles.json](needles.json)):
|
||||
|
||||
- `32k` prompts were 25,776-25,777 tokens and `128k` prompts 103,725-103,727 tokens.
|
||||
- The last 128K case reused 49,785 prompt tokens; the other five reused none.
|
||||
|
||||
**Limits of this report:**
|
||||
- One machine, one quantization, one configuration, and a small synthetic workload.
|
||||
- Long output, sampled decoding, thinking, coding-task correctness, vision, tool use, concurrency and a sustained
|
||||
thermal run were not part of this report.
|
||||
- For context, on the same machine and model, llama.cpp (3cf0325, ROCm) measured 26.9 tok/s `tg128` on 2026-09-29.
|
||||
That is a different workload, not a controlled comparison.
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Serial, fresh-prompt Strata benchmark. Run inside its prepared environment."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get(url):
|
||||
with urllib.request.urlopen(url, timeout=30) as response:
|
||||
return json.load(response)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--root", type=Path, default=Path("/workspace/Strata"))
|
||||
parser.add_argument("--pack", type=Path, default=Path("/workspace/Strata-data/packs/iq2_xs"))
|
||||
parser.add_argument("--url", default="http://127.0.0.1:18080")
|
||||
parser.add_argument("--out", type=Path, default=Path("/workspace/results"))
|
||||
parser.add_argument("--targets", default="4096,32768,128000")
|
||||
parser.add_argument("--runs", type=int, default=3)
|
||||
args = parser.parse_args()
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
sys.path[:0] = [str(args.root), str(args.root / "tools")]
|
||||
from strata_tokenizer import Tokenizer
|
||||
from serve.frontend import ChatTemplate, openai_to_messages
|
||||
directory = args.pack / "tokenizer"
|
||||
vocab = json.loads((directory / "vocab.json").read_text())
|
||||
tokens = [None] * len(vocab)
|
||||
for token, number in vocab.items():
|
||||
tokens[number] = token
|
||||
tokenizer = Tokenizer(tokens, (directory / "merges.txt").read_text().splitlines(),
|
||||
json.loads((directory / "token_type.json").read_text()))
|
||||
template = ChatTemplate(directory / "chat_template.jinja")
|
||||
initial = get(args.url + "/v1/status")
|
||||
(args.out / "initial-status.json").write_text(json.dumps(initial, indent=2) + "\n")
|
||||
rows = []
|
||||
|
||||
def request(content, maximum=256):
|
||||
return {"model": "strata", "messages": [{"role": "user", "content": content}],
|
||||
"temperature": 0, "reasoning_effort": "none", "max_tokens": maximum,
|
||||
"stream": True, "stream_options": {"include_usage": True}}
|
||||
|
||||
def count(req):
|
||||
messages, tools, kwargs = openai_to_messages(req)
|
||||
return len(tokenizer.encode(template.render(messages, tools, **kwargs), parse_special=True))
|
||||
|
||||
def perform(label, req, expected=None):
|
||||
body = json.dumps(req).encode()
|
||||
(args.out / (label + "-request.json")).write_bytes(body + b"\n")
|
||||
print("start", label, "tokens", expected, flush=True)
|
||||
chunks, texts, first, finish = [], [], None, None
|
||||
started = time.perf_counter()
|
||||
wire = urllib.request.Request(args.url + "/v1/chat/completions", data=body,
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(wire, timeout=1800) as response:
|
||||
for line in response:
|
||||
if not line.startswith(b"data: "):
|
||||
continue
|
||||
payload = line[6:].strip()
|
||||
if payload == b"[DONE]":
|
||||
break
|
||||
chunk = json.loads(payload)
|
||||
chunks.append(chunk)
|
||||
for choice in chunk.get("choices", []):
|
||||
delta = choice.get("delta", {})
|
||||
text = (delta.get("content") or "") + (delta.get("reasoning_content") or "")
|
||||
if text:
|
||||
first = first if first is not None else time.perf_counter() - started
|
||||
texts.append(text)
|
||||
finish = choice.get("finish_reason") or finish
|
||||
elapsed = time.perf_counter() - started
|
||||
metrics = get(args.url + "/metrics")
|
||||
engine = metrics["requests"][0]
|
||||
usage = next((chunk["usage"] for chunk in reversed(chunks) if "usage" in chunk), {})
|
||||
row = {"label": label, "expected_prompt_tokens": expected,
|
||||
"request_sha256": hashlib.sha256(body).hexdigest(),
|
||||
"client_ttft_s": first, "client_elapsed_s": elapsed, "finish_reason": finish,
|
||||
"usage": usage, "engine": engine, "text": "".join(texts)}
|
||||
if expected is not None and engine["prompt_tokens"] != expected:
|
||||
raise RuntimeError(f"token count mismatch: {expected} vs {engine['prompt_tokens']}")
|
||||
if not texts:
|
||||
raise RuntimeError(f"no text received for {label}")
|
||||
(args.out / (label + "-raw.json")).write_text(json.dumps({"chunks": chunks, "row": row}, indent=2) + "\n")
|
||||
rows.append(row)
|
||||
(args.out / "results.json").write_text(json.dumps(rows, indent=2) + "\n")
|
||||
print("done", label, json.dumps({"ttft_s": first, "elapsed_s": elapsed, "engine": engine}), flush=True)
|
||||
return row
|
||||
|
||||
perform("warmup", request("Reply with exactly the word READY.", 16))
|
||||
# Each nonce occurs before the filler so previous long prompts cannot be reused.
|
||||
filler = "\n".join(f"def task_{i:05d}(value: int) -> int: return (value * {(i % 97) + 1} + {i}) % 100003"
|
||||
for i in range(12000))
|
||||
ending = ("\n\nWrite a detailed explanation of the code above. Discuss deterministic integer transforms, "
|
||||
"modulo arithmetic, testing, naming, complexity, and maintainability. Write at least 600 words.")
|
||||
for target in map(int, args.targets.split(",")):
|
||||
for run in range(1, args.runs + 1):
|
||||
prefix = f"Benchmark nonce: series-{target}-trial-{run}.\nReview this synthetic Python module:\n"
|
||||
lo, hi = 0, len(filler)
|
||||
while lo < hi:
|
||||
middle = (lo + hi + 1) // 2
|
||||
if count(request(prefix + filler[:middle] + ending)) <= target:
|
||||
lo = middle
|
||||
else:
|
||||
hi = middle - 1
|
||||
req = request(prefix + filler[:lo] + ending)
|
||||
actual = count(req)
|
||||
if actual < target - 20 or actual > target:
|
||||
raise RuntimeError(f"could not construct target {target}: {actual}")
|
||||
perform(f"tokens-{target}-run-{run}", req, actual)
|
||||
measured = [row for row in rows if row["label"] != "warmup"]
|
||||
groups = {}
|
||||
for target in map(int, args.targets.split(",")):
|
||||
subset = [r for r in measured if r["label"].startswith(f"tokens-{target}-")]
|
||||
values = {
|
||||
"prompt_tokens": [r["engine"]["prompt_tokens"] for r in subset],
|
||||
"cached_tokens": [r["engine"].get("reused", 0) for r in subset],
|
||||
"output_tokens": [r["engine"]["engine_generated"] for r in subset],
|
||||
"client_ttft_s": [r["client_ttft_s"] for r in subset],
|
||||
"client_elapsed_s": [r["client_elapsed_s"] for r in subset],
|
||||
"prefill_tok_s": [(r["engine"]["prompt_tokens"] - (r["engine"].get("reused") or 0)) / (r["engine"]["prompt_ms"] / 1000) for r in subset],
|
||||
"decode_tok_s": [r["engine"]["engine_generated"] / (r["engine"]["decode_ms"] / 1000) for r in subset],
|
||||
}
|
||||
groups[str(target)] = {key: {"median": statistics.median(v), "min": min(v), "max": max(v)}
|
||||
for key, v in values.items()}
|
||||
(args.out / "summary.json").write_text(json.dumps(groups, indent=2) + "\n")
|
||||
print("summary", json.dumps(groups), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"service": "strata",
|
||||
"model": "qwen3.8-flash-next-coder",
|
||||
"loaded": true,
|
||||
"auto_load": true,
|
||||
"structured_output": {
|
||||
"formats": [
|
||||
"json_object",
|
||||
"json_schema"
|
||||
],
|
||||
"method": "prompt_and_validate",
|
||||
"constrained_decoding": false,
|
||||
"stream_buffered": true
|
||||
},
|
||||
"engine": "0.1.38",
|
||||
"started": 1791055531,
|
||||
"uptime_s": 2,
|
||||
"cache_max_tokens": 131072,
|
||||
"context": {
|
||||
"native": 131072,
|
||||
"max_positions": 131072
|
||||
},
|
||||
"concurrency": {
|
||||
"serving": 1,
|
||||
"requested": 1
|
||||
},
|
||||
"dialects": [
|
||||
"/v1/chat/completions",
|
||||
"/v1/messages"
|
||||
],
|
||||
"vision": {
|
||||
"enabled": false,
|
||||
"available": false,
|
||||
"error": null
|
||||
},
|
||||
"activity": {
|
||||
"requests": 0,
|
||||
"in_flight": 0,
|
||||
"last_request_at": null
|
||||
},
|
||||
"last_timings": null,
|
||||
"machine": {
|
||||
"at": 1791055534,
|
||||
"gpu": null,
|
||||
"ram": {
|
||||
"used_gib": 4.9,
|
||||
"total_gib": 31.2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
[
|
||||
{
|
||||
"length": "32k",
|
||||
"depth": 10,
|
||||
"prompt_tokens": 25776,
|
||||
"seconds": 52.6,
|
||||
"found": true,
|
||||
"word": "meadow-copper-504",
|
||||
"answer": "meadow-copper-504"
|
||||
},
|
||||
{
|
||||
"length": "32k",
|
||||
"depth": 50,
|
||||
"prompt_tokens": 25776,
|
||||
"seconds": 50.7,
|
||||
"found": true,
|
||||
"word": "falcon-quartz-940",
|
||||
"answer": "falcon-quartz-940"
|
||||
},
|
||||
{
|
||||
"length": "32k",
|
||||
"depth": 90,
|
||||
"prompt_tokens": 25777,
|
||||
"seconds": 50.8,
|
||||
"found": true,
|
||||
"word": "willow-cobalt-696",
|
||||
"answer": "willow-cobalt-696"
|
||||
},
|
||||
{
|
||||
"length": "128k",
|
||||
"depth": 10,
|
||||
"prompt_tokens": 103727,
|
||||
"seconds": 206.9,
|
||||
"found": true,
|
||||
"word": "falcon-saffron-138",
|
||||
"answer": "falcon-saffron-138"
|
||||
},
|
||||
{
|
||||
"length": "128k",
|
||||
"depth": 50,
|
||||
"prompt_tokens": 103725,
|
||||
"seconds": 205.9,
|
||||
"found": true,
|
||||
"word": "quartz-tundra-528",
|
||||
"answer": "quartz-tundra-528"
|
||||
},
|
||||
{
|
||||
"length": "128k",
|
||||
"depth": 90,
|
||||
"prompt_tokens": 103725,
|
||||
"seconds": 112.5,
|
||||
"found": true,
|
||||
"word": "quartz-glacier-192",
|
||||
"answer": "quartz-glacier-192"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,382 @@
|
||||
[
|
||||
{
|
||||
"label": "warmup",
|
||||
"expected_prompt_tokens": null,
|
||||
"request_sha256": "b63e57873441a996a529b246a66cf12de70a7c671777f99329354d5c195f788f",
|
||||
"client_ttft_s": 0.44287488800182473,
|
||||
"client_elapsed_s": 0.5121355729934294,
|
||||
"finish_reason": "stop",
|
||||
"usage": {
|
||||
"prompt_tokens": 19,
|
||||
"completion_tokens": 2,
|
||||
"total_tokens": 21,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
}
|
||||
},
|
||||
"engine": {
|
||||
"projection": null,
|
||||
"time": 1791055534.084382,
|
||||
"duration_s": 0.5,
|
||||
"finish": "stop",
|
||||
"prompt_tokens": 19,
|
||||
"reused": 0,
|
||||
"output_tokens": 2,
|
||||
"prompt_total": 19,
|
||||
"prompt_read": 19,
|
||||
"engine_generated": 2,
|
||||
"prompt_ms": 398.1,
|
||||
"decode_ms": 108.4,
|
||||
"decode_tok_s": 18.5,
|
||||
"hit_rate": 1.0,
|
||||
"ram_blobs": 0,
|
||||
"file_blobs": 0,
|
||||
"file_mb": 0.0,
|
||||
"drafts_offered": 3,
|
||||
"drafts_accepted": 3
|
||||
},
|
||||
"text": "READY"
|
||||
},
|
||||
{
|
||||
"label": "tokens-4096-run-1",
|
||||
"expected_prompt_tokens": 4096,
|
||||
"request_sha256": "5d94c652067354bbe72930d2bdad3886564f08cf2546b32411af02d33ad3ce05",
|
||||
"client_ttft_s": 13.26403624098748,
|
||||
"client_elapsed_s": 18.427036346998648,
|
||||
"finish_reason": "length",
|
||||
"usage": {
|
||||
"prompt_tokens": 4096,
|
||||
"completion_tokens": 256,
|
||||
"total_tokens": 4352,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
}
|
||||
},
|
||||
"engine": {
|
||||
"projection": null,
|
||||
"time": 1791055535.8394356,
|
||||
"duration_s": 18.4,
|
||||
"finish": "length",
|
||||
"prompt_tokens": 4096,
|
||||
"reused": 0,
|
||||
"output_tokens": 256,
|
||||
"prompt_total": 4096,
|
||||
"prompt_read": 4096,
|
||||
"engine_generated": 256,
|
||||
"prompt_ms": 13216.0,
|
||||
"decode_ms": 5189.0,
|
||||
"decode_tok_s": 49.3,
|
||||
"hit_rate": 1.0,
|
||||
"ram_blobs": 0,
|
||||
"file_blobs": 0,
|
||||
"file_mb": 0.0,
|
||||
"drafts_offered": 210,
|
||||
"drafts_accepted": 145
|
||||
},
|
||||
"text": "The provided Python module presents a series of deterministic integer transforms, each encapsulated within a uniquely named function. These functions, labeled from `task_00000` to `task_00112` (and potentially beyond), follow a consistent mathematical pattern: they take an integer input, apply a linear transformation of the form `(value * multiplier + offset) % modulus`, and return the result. This structure is a classic example of a linear congruential generator (LCG) component, often used in pseudo-random number generation, hashing, or cryptographic primitives, though here it appears to be a synthetic benchmark or test harness.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nEach function implements a linear congruential step: `f(x) = (a * x + c) % m`, where `a` is the multiplier, `c` is the increment, and `m` is the modulus (100003 in all cases). The modulus 100003 is a prime number, which is significant because it ensures that the multiplicative group modulo 100003 is cyclic, and for certain choices of `a` and `c`, the transformation can be a permutation of the"
|
||||
},
|
||||
{
|
||||
"label": "tokens-4096-run-2",
|
||||
"expected_prompt_tokens": 4096,
|
||||
"request_sha256": "8da8a04e70e4d916f6630729082bdfb9d6ebbb47117aad60bc0541bce198a213",
|
||||
"client_ttft_s": 12.663496508001117,
|
||||
"client_elapsed_s": 17.377855258993804,
|
||||
"finish_reason": "length",
|
||||
"usage": {
|
||||
"prompt_tokens": 4096,
|
||||
"completion_tokens": 256,
|
||||
"total_tokens": 4352,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
}
|
||||
},
|
||||
"engine": {
|
||||
"projection": null,
|
||||
"time": 1791055555.3915784,
|
||||
"duration_s": 17.4,
|
||||
"finish": "length",
|
||||
"prompt_tokens": 4096,
|
||||
"reused": 0,
|
||||
"output_tokens": 256,
|
||||
"prompt_total": 4096,
|
||||
"prompt_read": 4096,
|
||||
"engine_generated": 256,
|
||||
"prompt_ms": 12615.4,
|
||||
"decode_ms": 4740.3,
|
||||
"decode_tok_s": 54.0,
|
||||
"hit_rate": 1.0,
|
||||
"ram_blobs": 0,
|
||||
"file_blobs": 0,
|
||||
"file_mb": 0.0,
|
||||
"drafts_offered": 208,
|
||||
"drafts_accepted": 156
|
||||
},
|
||||
"text": "The provided Python module consists of a series of functions, each named with a specific pattern (`task_XXXXX`), that perform a deterministic integer transformation. Each function takes a single integer argument `value` and returns an integer result computed using a linear transformation followed by a modulo operation. The general form of each function is:\n\n```python\ndef task_XXXXX(value: int) -> int:\n return (value * A + B) % 100003\n```\n\nwhere `A` and `B` are constants specific to each function. The modulus `100003` is a prime number, which is significant in modular arithmetic because it ensures that the multiplicative group modulo 100003 is cyclic and that every non-zero element has a multiplicative inverse. This property is often exploited in cryptographic algorithms, hash functions, and pseudo-random number generators.\n\n### Deterministic Integer Transforms\n\nEach function implements a linear congruential transformation, a common technique in number theory and computer science. The transformation `value * A + B` is an affine transformation over the integers. When combined with the modulo operation, it becomes a function from the finite field (or ring) of integers modulo 10"
|
||||
},
|
||||
{
|
||||
"label": "tokens-4096-run-3",
|
||||
"expected_prompt_tokens": 4096,
|
||||
"request_sha256": "946079edb58e764050805886156dc8f877cc29e1ca8d01c84e9a1e7917d49492",
|
||||
"client_ttft_s": 12.811999237994314,
|
||||
"client_elapsed_s": 17.900370211005793,
|
||||
"finish_reason": "length",
|
||||
"usage": {
|
||||
"prompt_tokens": 4096,
|
||||
"completion_tokens": 256,
|
||||
"total_tokens": 4352,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
}
|
||||
},
|
||||
"engine": {
|
||||
"projection": null,
|
||||
"time": 1791055573.9388902,
|
||||
"duration_s": 17.9,
|
||||
"finish": "length",
|
||||
"prompt_tokens": 4096,
|
||||
"reused": 0,
|
||||
"output_tokens": 256,
|
||||
"prompt_total": 4096,
|
||||
"prompt_read": 4096,
|
||||
"engine_generated": 256,
|
||||
"prompt_ms": 12763.9,
|
||||
"decode_ms": 5114.7,
|
||||
"decode_tok_s": 50.1,
|
||||
"hit_rate": 1.0,
|
||||
"ram_blobs": 0,
|
||||
"file_blobs": 0,
|
||||
"file_mb": 0.0,
|
||||
"drafts_offered": 222,
|
||||
"drafts_accepted": 146
|
||||
},
|
||||
"text": "The provided Python module presents a series of deterministic integer transforms, each encapsulated within a uniquely named function. These functions, labeled from `task_00000` to `task_00112` (and potentially beyond), follow a consistent mathematical pattern: they take an integer input, apply a linear transformation, and return the result modulo a large prime number, specifically 100003. This structure serves as a synthetic benchmark for evaluating code generation, pattern recognition, and algorithmic consistency in automated systems.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nAt the core of each function is the expression `(value * multiplier + offset) % 100003`. This is a classic linear congruential generator (LCG) step, often used in pseudo-random number generation or cryptographic hashing, though here it is applied deterministically to a single input. The multiplier and offset vary per function, creating a unique mapping for each task. For instance, `task_00000` uses a multiplier of 1 and an offset of 0, effectively returning `value % 100003`. In contrast, `task_00097` uses a multiplier of 1"
|
||||
},
|
||||
{
|
||||
"label": "tokens-32768-run-1",
|
||||
"expected_prompt_tokens": 32768,
|
||||
"request_sha256": "e37cfc2ab9867d91130927440df24a57255a736b8d0d829d07d828e22e3de572",
|
||||
"client_ttft_s": 62.74767600699852,
|
||||
"client_elapsed_s": 68.09618598900852,
|
||||
"finish_reason": "length",
|
||||
"usage": {
|
||||
"prompt_tokens": 32768,
|
||||
"completion_tokens": 256,
|
||||
"total_tokens": 33024,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
}
|
||||
},
|
||||
"engine": {
|
||||
"projection": null,
|
||||
"time": 1791055594.1634562,
|
||||
"duration_s": 68.0,
|
||||
"finish": "length",
|
||||
"prompt_tokens": 32768,
|
||||
"reused": 0,
|
||||
"output_tokens": 256,
|
||||
"prompt_total": 32768,
|
||||
"prompt_read": 32768,
|
||||
"engine_generated": 256,
|
||||
"prompt_ms": 62608.4,
|
||||
"decode_ms": 5375.3,
|
||||
"decode_tok_s": 47.6,
|
||||
"hit_rate": 1.0,
|
||||
"ram_blobs": 0,
|
||||
"file_blobs": 0,
|
||||
"file_mb": 0.0,
|
||||
"drafts_offered": 230,
|
||||
"drafts_accepted": 152
|
||||
},
|
||||
"text": "This Python module presents a highly repetitive, synthetic collection of functions that serve as a benchmark for deterministic integer transforms. The code consists of a series of functions named `task_00000` through `task_00888` (and potentially beyond), each performing a specific linear transformation on an input integer `value` and returning the result modulo 100003.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nAt the core of each function is a linear congruential generator (LCG) style operation: `(value * multiplier + addend) % 100003`. The modulus, 100003, is a prime number. This choice of modulus is significant because it ensures that the multiplicative group of integers modulo 100003 is cyclic, which is a desirable property for cryptographic or pseudo-random number generation contexts, although here it is used for deterministic hashing or transformation.\n\nThe structure of the functions reveals a pattern in the `multiplier` and `addend` parameters. For the first 97 functions (`task_00000` to `task_00096`), the multiplier increases linearly from 1 to"
|
||||
},
|
||||
{
|
||||
"label": "tokens-32768-run-2",
|
||||
"expected_prompt_tokens": 32768,
|
||||
"request_sha256": "ed2dfaa11aecad9f4563f33bb5f5d4b8704486508700b8fa572722cc1225ee26",
|
||||
"client_ttft_s": 62.81415034100064,
|
||||
"client_elapsed_s": 68.14694995200261,
|
||||
"finish_reason": "length",
|
||||
"usage": {
|
||||
"prompt_tokens": 32768,
|
||||
"completion_tokens": 256,
|
||||
"total_tokens": 33024,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
}
|
||||
},
|
||||
"engine": {
|
||||
"projection": null,
|
||||
"time": 1791055664.4150822,
|
||||
"duration_s": 68.0,
|
||||
"finish": "length",
|
||||
"prompt_tokens": 32768,
|
||||
"reused": 0,
|
||||
"output_tokens": 256,
|
||||
"prompt_total": 32768,
|
||||
"prompt_read": 32768,
|
||||
"engine_generated": 256,
|
||||
"prompt_ms": 62675.9,
|
||||
"decode_ms": 5359.3,
|
||||
"decode_tok_s": 47.8,
|
||||
"hit_rate": 1.0,
|
||||
"ram_blobs": 0,
|
||||
"file_blobs": 0,
|
||||
"file_mb": 0.0,
|
||||
"drafts_offered": 222,
|
||||
"drafts_accepted": 147
|
||||
},
|
||||
"text": "This Python module presents a highly repetitive, synthetic collection of functions that perform deterministic integer transforms. Each function follows a strict pattern: it accepts an integer `value`, applies a linear transformation of the form `(value * multiplier + offset) % modulus`, and returns the result. The modulus is consistently set to `100003`, which is a prime number. This choice of modulus is significant because it ensures that the multiplicative group of integers modulo 100003 is cyclic, and every non-zero multiplier has a unique modular inverse. This property is often desirable in cryptographic or hashing contexts, though here it appears to be used for simple arithmetic obfuscation or benchmarking.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nThe core operation in each function is a linear congruential generator (LCG) step, commonly used in pseudo-random number generation. The formula $f(x) = (ax + c) \\pmod m$ defines an affine transformation over the finite field $\\mathbb{Z}_m$. In this module, $m = 100003$. The multipliers ($a$) and offsets ($c$) vary per function. For example, `task_00000`"
|
||||
},
|
||||
{
|
||||
"label": "tokens-32768-run-3",
|
||||
"expected_prompt_tokens": 32768,
|
||||
"request_sha256": "2a3c7342622e80c3cd8b37e062f0a2f64640c119ece8cccbf168639041eac079",
|
||||
"client_ttft_s": 62.844907227001386,
|
||||
"client_elapsed_s": 68.09568050300004,
|
||||
"finish_reason": "length",
|
||||
"usage": {
|
||||
"prompt_tokens": 32768,
|
||||
"completion_tokens": 256,
|
||||
"total_tokens": 33024,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
}
|
||||
},
|
||||
"engine": {
|
||||
"projection": null,
|
||||
"time": 1791055734.689819,
|
||||
"duration_s": 68.0,
|
||||
"finish": "length",
|
||||
"prompt_tokens": 32768,
|
||||
"reused": 0,
|
||||
"output_tokens": 256,
|
||||
"prompt_total": 32768,
|
||||
"prompt_read": 32768,
|
||||
"engine_generated": 256,
|
||||
"prompt_ms": 62706.6,
|
||||
"decode_ms": 5277.5,
|
||||
"decode_tok_s": 48.5,
|
||||
"hit_rate": 1.0,
|
||||
"ram_blobs": 0,
|
||||
"file_blobs": 0,
|
||||
"file_mb": 0.0,
|
||||
"drafts_offered": 234,
|
||||
"drafts_accepted": 153
|
||||
},
|
||||
"text": "This Python module presents a highly repetitive, synthetic collection of functions that perform deterministic integer transforms. Each function follows a strict pattern: it accepts an integer `value`, applies a linear transformation of the form `(value * multiplier + offset) % modulus`, and returns the result. The modulus is consistently set to `100003`, which is a prime number. This choice of modulus is significant because it ensures that the multiplicative group of integers modulo 100003 is cyclic, meaning that for any non-zero multiplier, the transformation is bijective (a permutation) over the set of integers modulo 100003. This property is often desirable in cryptographic contexts or pseudo-random number generation, although here it appears to be used for benchmarking or synthetic data generation.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nThe core operation in each function is a linear congruential generator (LCG) step, defined by the formula $f(x) = (ax + c) \\pmod m$. In this module, $m = 100003$. The multiplier $a$ and the increment $c$ vary per function. For example, `task_00000` uses $a"
|
||||
},
|
||||
{
|
||||
"label": "tokens-128000-run-1",
|
||||
"expected_prompt_tokens": 128000,
|
||||
"request_sha256": "3235aac3f934cf1d889c25973ca172b355509dc2522fdfb463fbefe979a9075d",
|
||||
"client_ttft_s": 248.26734286299325,
|
||||
"client_elapsed_s": 253.83500957599608,
|
||||
"finish_reason": "length",
|
||||
"usage": {
|
||||
"prompt_tokens": 128000,
|
||||
"completion_tokens": 256,
|
||||
"total_tokens": 128256,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
}
|
||||
},
|
||||
"engine": {
|
||||
"projection": null,
|
||||
"time": 1791055809.118869,
|
||||
"duration_s": 253.5,
|
||||
"finish": "length",
|
||||
"prompt_tokens": 128000,
|
||||
"reused": 0,
|
||||
"output_tokens": 256,
|
||||
"prompt_total": 128000,
|
||||
"prompt_read": 128000,
|
||||
"engine_generated": 256,
|
||||
"prompt_ms": 247841.6,
|
||||
"decode_ms": 5596.2,
|
||||
"decode_tok_s": 45.7,
|
||||
"hit_rate": 1.0,
|
||||
"ram_blobs": 0,
|
||||
"file_blobs": 0,
|
||||
"file_mb": 0.0,
|
||||
"drafts_offered": 232,
|
||||
"drafts_accepted": 152
|
||||
},
|
||||
"text": "This Python module represents a synthetic benchmark designed to test the performance and correctness of integer arithmetic operations, specifically focusing on linear transforms and modular arithmetic. The code consists of a series of functions, each named `task_XXXXX`, where `XXXXX` is a zero-padded integer index. Each function accepts a single integer argument `value` and returns an integer result.\n\n### 1. Deterministic Integer Transforms\n\nThe core logic of every function in this module is identical in structure:\n```python\nreturn (value * A + B) % M\n```\nWhere:\n- `value` is the input integer.\n- `A` is a multiplier (coefficient).\n- `B` is an additive constant.\n- `M` is the modulus, which is consistently `100003` throughout the entire module.\n\nThis represents a **linear congruential transform**. In computer science, such transforms are fundamental in pseudorandom number generation (LCG), hashing functions, and cryptographic primitives. The specific form `(value * A + B) % M` ensures that the output is always within the range `[0, M-1]`, regardless of how large the input `value` is.\n\n### 2"
|
||||
},
|
||||
{
|
||||
"label": "tokens-128000-run-2",
|
||||
"expected_prompt_tokens": 128000,
|
||||
"request_sha256": "5f4b8bc9f0afc7765320abd89960c0938e0d7cf5974e962fdbd0613fef31c8d4",
|
||||
"client_ttft_s": 248.0881588330085,
|
||||
"client_elapsed_s": 253.0268927290017,
|
||||
"finish_reason": "length",
|
||||
"usage": {
|
||||
"prompt_tokens": 128000,
|
||||
"completion_tokens": 256,
|
||||
"total_tokens": 128256,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
}
|
||||
},
|
||||
"engine": {
|
||||
"projection": null,
|
||||
"time": 1791056068.9450965,
|
||||
"duration_s": 252.7,
|
||||
"finish": "length",
|
||||
"prompt_tokens": 128000,
|
||||
"reused": 0,
|
||||
"output_tokens": 256,
|
||||
"prompt_total": 128000,
|
||||
"prompt_read": 128000,
|
||||
"engine_generated": 256,
|
||||
"prompt_ms": 247669.4,
|
||||
"decode_ms": 4967.1,
|
||||
"decode_tok_s": 51.5,
|
||||
"hit_rate": 1.0,
|
||||
"ram_blobs": 0,
|
||||
"file_blobs": 0,
|
||||
"file_mb": 0.0,
|
||||
"drafts_offered": 205,
|
||||
"drafts_accepted": 163
|
||||
},
|
||||
"text": "This Python module represents a synthetic benchmark designed to test the performance and correctness of integer arithmetic operations, specifically focusing on linear transforms and modular arithmetic. The code consists of a series of functions, each named `task_XXXXX`, where `XXXXX` is a zero-padded integer index. Each function accepts a single integer argument `value` and returns an integer result.\n\n### 1. Deterministic Integer Transforms\n\nThe core logic of each function is a linear transformation of the form:\n$$ f(x) = (a \\cdot x + b) \\pmod m $$\nwhere:\n- $x$ is the input `value`.\n- $a$ is the multiplier (coefficient).\n- $b$ is the additive constant.\n- $m$ is the modulus, which is consistently set to `100003` across all functions.\n\nThe multiplier $a$ and the constant $b$ vary deterministically based on the function's index. For example:\n- `task_00000`: $a=1, b=0$\n- `task_00001`: $a=2, b=1$\n- `task_00002`:"
|
||||
},
|
||||
{
|
||||
"label": "tokens-128000-run-3",
|
||||
"expected_prompt_tokens": 128000,
|
||||
"request_sha256": "5c3fb71db9f9f44765f28389d1032f2c8b6687d1bf5ca6f33aeb924072cfa6e4",
|
||||
"client_ttft_s": 248.0611828550027,
|
||||
"client_elapsed_s": 253.7529949669988,
|
||||
"finish_reason": "length",
|
||||
"usage": {
|
||||
"prompt_tokens": 128000,
|
||||
"completion_tokens": 256,
|
||||
"total_tokens": 128256,
|
||||
"prompt_tokens_details": {
|
||||
"cached_tokens": 0
|
||||
}
|
||||
},
|
||||
"engine": {
|
||||
"projection": null,
|
||||
"time": 1791056328.0391443,
|
||||
"duration_s": 253.4,
|
||||
"finish": "length",
|
||||
"prompt_tokens": 128000,
|
||||
"reused": 0,
|
||||
"output_tokens": 256,
|
||||
"prompt_total": 128000,
|
||||
"prompt_read": 128000,
|
||||
"engine_generated": 256,
|
||||
"prompt_ms": 247650.3,
|
||||
"decode_ms": 5720.0,
|
||||
"decode_tok_s": 44.8,
|
||||
"hit_rate": 1.0,
|
||||
"ram_blobs": 0,
|
||||
"file_blobs": 0,
|
||||
"file_mb": 0.0,
|
||||
"drafts_offered": 235,
|
||||
"drafts_accepted": 149
|
||||
},
|
||||
"text": "This Python module represents a synthetic benchmark designed to test the performance and correctness of integer arithmetic operations, specifically focusing on linear transformations combined with modular arithmetic. The code consists of a series of functions, each named `task_XXXXX`, where `XXXXX` is a zero-padded integer index. Each function accepts a single integer argument `value` and returns a new integer calculated using the formula:\n\n$$ f(value) = (value \\times A + B) \\pmod{M} $$\n\nwhere $A$ is a multiplier, $B$ is an additive constant, and $M$ is the modulus, which is consistently set to `100003` throughout the module.\n\n### Deterministic Integer Transforms and Modulo Arithmetic\n\nThe core logic of each function is a deterministic linear congruential transformation. In computer science, such transformations are fundamental to pseudorandom number generators (PRNGs), hash functions, and cryptographic primitives. The specific form $ (value \\times A + B) \\pmod{M} $ ensures that the output is always within the range $[0, M-1]$.\n\nThe modulus $M = 100003$ is a prime number. This choice is significant because"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"exe": "/work/build-906/strata",
|
||||
"args": [
|
||||
"--pack",
|
||||
"/m/pack-coder",
|
||||
"--native",
|
||||
"/m/coder/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00001-of-00002.gguf",
|
||||
"--ple-gguf",
|
||||
"/m/coder/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00002-of-00002.gguf",
|
||||
"--expert-profile",
|
||||
"/opt/strata/data/expert-profile-coder.bin",
|
||||
"--expert-cache",
|
||||
"auto",
|
||||
"--prefill",
|
||||
"4096",
|
||||
"--spec",
|
||||
"4",
|
||||
"--spec-min-p",
|
||||
"0.5",
|
||||
"--mtp",
|
||||
"/m/mtp/rt",
|
||||
"--max-context",
|
||||
"131072",
|
||||
"--kv",
|
||||
"int8",
|
||||
"--vram-reserve-mib",
|
||||
"600",
|
||||
"--kv-resident",
|
||||
"32768",
|
||||
"--pcie-frac",
|
||||
"0"
|
||||
],
|
||||
"cwd": "/opt/strata",
|
||||
"tokenizer": "/m/pack-coder/tokenizer",
|
||||
"model_name": "qwen3.8-flash-next-coder",
|
||||
"log": "/work/strata-coder.log",
|
||||
"lib_dirs": [],
|
||||
"port": 8080,
|
||||
"host": "0.0.0.0",
|
||||
"gpu": [
|
||||
0,
|
||||
1
|
||||
],
|
||||
"layer_split": "27"
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"4096": {
|
||||
"prompt_tokens": {
|
||||
"median": 4096,
|
||||
"min": 4096,
|
||||
"max": 4096
|
||||
},
|
||||
"cached_tokens": {
|
||||
"median": 0,
|
||||
"min": 0,
|
||||
"max": 0
|
||||
},
|
||||
"output_tokens": {
|
||||
"median": 256,
|
||||
"min": 256,
|
||||
"max": 256
|
||||
},
|
||||
"client_ttft_s": {
|
||||
"median": 12.811999237994314,
|
||||
"min": 12.663496508001117,
|
||||
"max": 13.26403624098748
|
||||
},
|
||||
"client_elapsed_s": {
|
||||
"median": 17.900370211005793,
|
||||
"min": 17.377855258993804,
|
||||
"max": 18.427036346998648
|
||||
},
|
||||
"prefill_tok_s": {
|
||||
"median": 320.90505253096626,
|
||||
"min": 309.9273607748184,
|
||||
"max": 324.68253087496237
|
||||
},
|
||||
"decode_tok_s": {
|
||||
"median": 50.05181144544157,
|
||||
"min": 49.3351320100212,
|
||||
"max": 54.00502077927557
|
||||
}
|
||||
},
|
||||
"32768": {
|
||||
"prompt_tokens": {
|
||||
"median": 32768,
|
||||
"min": 32768,
|
||||
"max": 32768
|
||||
},
|
||||
"cached_tokens": {
|
||||
"median": 0,
|
||||
"min": 0,
|
||||
"max": 0
|
||||
},
|
||||
"output_tokens": {
|
||||
"median": 256,
|
||||
"min": 256,
|
||||
"max": 256
|
||||
},
|
||||
"client_ttft_s": {
|
||||
"median": 62.81415034100064,
|
||||
"min": 62.74767600699852,
|
||||
"max": 62.844907227001386
|
||||
},
|
||||
"client_elapsed_s": {
|
||||
"median": 68.09618598900852,
|
||||
"min": 68.09568050300004,
|
||||
"max": 68.14694995200261
|
||||
},
|
||||
"prefill_tok_s": {
|
||||
"median": 522.8165850031671,
|
||||
"min": 522.5606236026192,
|
||||
"max": 523.3802492956216
|
||||
},
|
||||
"decode_tok_s": {
|
||||
"median": 47.767432313921596,
|
||||
"min": 47.625248823321485,
|
||||
"max": 48.50781620085268
|
||||
}
|
||||
},
|
||||
"128000": {
|
||||
"prompt_tokens": {
|
||||
"median": 128000,
|
||||
"min": 128000,
|
||||
"max": 128000
|
||||
},
|
||||
"cached_tokens": {
|
||||
"median": 0,
|
||||
"min": 0,
|
||||
"max": 0
|
||||
},
|
||||
"output_tokens": {
|
||||
"median": 256,
|
||||
"min": 256,
|
||||
"max": 256
|
||||
},
|
||||
"client_ttft_s": {
|
||||
"median": 248.0881588330085,
|
||||
"min": 248.0611828550027,
|
||||
"max": 248.26734286299325
|
||||
},
|
||||
"client_elapsed_s": {
|
||||
"median": 253.7529949669988,
|
||||
"min": 253.0268927290017,
|
||||
"max": 253.83500957599608
|
||||
},
|
||||
"prefill_tok_s": {
|
||||
"median": 516.8179839737974,
|
||||
"min": 516.4588995551998,
|
||||
"max": 516.857843499483
|
||||
},
|
||||
"decode_tok_s": {
|
||||
"median": 45.745327186305,
|
||||
"min": 44.75524475524476,
|
||||
"max": 51.5391274586781
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,104 @@
|
||||
# V100 (sm_70): a tensor-core QSA prompt attention (2026-10-03)
|
||||
|
||||
On sm_70 the prompt path ran the QSA attention on the decode kernel, one query at a time with FP32 FMAs. `prompt_attn_v70_kernel`
|
||||
(`src/kernels/cuda/qsa_prompt_attn.cu`) does the two matrix products on Volta's tensor cores (`mma.sync.m8n8k4`).
|
||||
`STRATA_PROMPT_ATTN_OLD=1` is the old path, so every number below is the same binary with and without that switch.
|
||||
|
||||
## Rig
|
||||
|
||||
- Tesla V100-PCIE-32GB (sm_70, PCIe gen3 x16), driver 550.120, one card used; a second V100 and a Quadro RTX 4000 (sm_75) are in the
|
||||
PC and hidden with `CUDA_VISIBLE_DEVICES`. Intel Core i9-7960X (AVX-512), 125 GB RAM, Linux 6.8, CUDA 12.8, GCC 13.
|
||||
- Engine: `origin/main` at `99f3dbd` (0.1.35 + 83 commits) with this change, built with
|
||||
`-DCMAKE_CUDA_ARCHITECTURES=70 -DSTRATA_EXPERIMENTAL_SM60=ON`.
|
||||
- Model: Unsloth `Qwen3.8-Flash-Next-UD-IQ4_XS` (3 shards), packed with `tools/iq_pack.py --compat-bf16`, MTP head from
|
||||
`tools/mtp_fetch.py`. Not one of setup's models (see the limits). `--max-context 204800`, `--kv int8`, `--spec 2`.
|
||||
|
||||
## Prompt speed
|
||||
|
||||
One server start per configuration (cold), a 2K warm-up request, then three prompts of random words, each answered greedily
|
||||
(temperature 0, 48 tokens). The numbers are the server's own line `strata serve: prompt N tokens = 0 reused + N read in T ms`. One run per
|
||||
cell; repeated starts of the same configuration gave the same seconds.
|
||||
|
||||
| Prompt tokens | old path | new kernel | gain | read time, old -> new |
|
||||
| ---: | ---: | ---: | ---: | ---: |
|
||||
| 7,194 | 1,010 tok/s | 1,164 tok/s | +15% | 7.1 s -> 6.2 s |
|
||||
| 28,650 | 1,063 tok/s | 1,251 tok/s | +18% | 26.9 s -> 22.9 s |
|
||||
| 114,338 | 973 tok/s | 1,123 tok/s | +15% | 117.5 s -> 101.8 s |
|
||||
|
||||
Decode is unchanged within the noise (38-51 tok/s, 48 tokens per request): it does not use this kernel.
|
||||
|
||||
## Where the time went (nsys, one V100, 28,650-token prompt, GPU kernel time of the whole capture: prefill plus 128-193 decode tokens)
|
||||
|
||||
`strata-1gpu-32k-*.nsys-rep`; the "before" capture is 0.1.35 (`d9ab843`), the "after" one is the same base with this change and the BF16 change
|
||||
of #540 / the closed #593 (BF16 GEMMs through FP16; that one only removes `magma_sgemmEx`, which is not in this table).
|
||||
|
||||
| | before | after |
|
||||
| --- | ---: | ---: |
|
||||
| QSA attention on the decode kernel (`attn_chunk_kernel<1>`) | 5.78 s (19.2%) | 0.18 s (decode steps only) |
|
||||
| `prompt_attn_v70_kernel` | - | 2.16 s (9.0%; 12% of the prompt part) |
|
||||
|
||||
## Accuracy
|
||||
|
||||
`qsa_prompt_attn_parity` (synthetic, no model; build with `-DSTRATA_PARITY_PROMPT_ATTN=ON`), V100, 32K context, 2,048 queries per chunk, output scale 3.6:
|
||||
|
||||
| KV | error vs FP64, old kernel | error vs FP64, new kernel | time per chunk, old -> new |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| int8 | 2.4e-6 | 5.1e-6 | 38.1 -> 13.4 ms (2.8x) |
|
||||
| fp16 | 1.9e-6 | 5.3e-6 | 37.3 -> 28.3 ms (1.3x) |
|
||||
|
||||
The 1,500- and 2,100-token contexts also pass (int8: 3.2x and 2.2x). The test's Q4_0 cases are skipped below sm_80, where the dispatcher keeps the
|
||||
old kernel (upstream's mode 4 is sm_80+ only).
|
||||
|
||||
## Output checks
|
||||
|
||||
- **Needle recall** (`tools/needle_bench.py --lengths 8k,32k,128k --depths 10,30,50,70,90`, greedy, thinking off, a fresh server per
|
||||
configuration; prompts of 7.9K, 32.0K and 122.7K tokens): **15 of 15 found with the old path, 15 of 15 with the new kernel.** The read times of
|
||||
the later depths are shorter because the server reuses the checkpoint of the text before the needle.
|
||||
- **Greedy output** (48 tokens, the three prompts above): identical for the 28,650- and 114,338-token prompts. For the 7,194-token prompt the
|
||||
answer starts the same and **differs later**: the kernel sums in another order and a greedy decode can follow a near tie elsewhere. A recall test
|
||||
with a code word does not tell small shifts of the output distribution apart, and a teacher-forced top-1 comparison was not run.
|
||||
|
||||
## Distribution check (teacher-forced, against the old path and against controls)
|
||||
|
||||
The method of `docs/UNSLOTH_Q4.md`: `STRATA_LOGPOS=<file> STRATA_LOGPOS_TOPK=256` on a serve engine (`--short-read 640 --adapt-every 100000`, greedy,
|
||||
`--max-context 204800`). Each prompt is a chat: a long first message (8,543 / 32,906 / 99,040 tokens of this repository's docs and code, read by the
|
||||
batched prompt path, i.e. by the kernel under test), a short reply, and a last message of ~600 tokens of other repository text that goes through the verify
|
||||
windows, where every token is scored: 617 positions per prompt. KL is old || other over the old run's top 256 plus a bucket for the rest.
|
||||
|
||||
| comparison | KL mean (8.5K / 33K / 99K) | argmax same | top-10 overlap |
|
||||
| --- | --- | --- | --- |
|
||||
| old path vs old path (a second run) | 0 / 0 / 0 | 100% | 100% |
|
||||
| new kernel vs new kernel (a second run) | 0 / 0 / 0 | 100% | 100% |
|
||||
| **old path vs new kernel** | 0.028 / 0.040 / 0.116 | 97.7 / 93.2 / 86.4% | 93.3 / 91.9 / 88.4% |
|
||||
| control A: old path, BF16 GEMMs without the FP16 detour (`STRATA_BF16_VIA_F16=0`) | 0.026 / 0.051 / 0.090 | 97.2 / 91.4 / 91.2% | 92.9 / 91.4 / 88.5% |
|
||||
| control B: old path, `--prefill 2048` instead of auto (8192) | 0.023 / 0.035 / 0.038 | 97.9 / 94.8 / 95.6% | 92.9 / 92.2 / 89.7% |
|
||||
|
||||
Median KL old vs new: 0.0007 / 0.0088 / 0.022; p99: 0.35 / 0.39 / 1.12 (heavy-tailed). The perplexity of the true tokens moves a lot under every
|
||||
change (old 365 / 127 / 24; new 346 / 192 / 12; control A 395 / 161 / 18), so it says nothing about quality here.
|
||||
|
||||
Reading: the model amplifies small numeric differences, and both controls, which change nothing but the order of the rounding, move the distribution as
|
||||
much as the kernel does. At 99K the kernel is somewhat above both controls; this is one document and 617 positions, so a real effect there is neither
|
||||
confirmed nor excluded.
|
||||
|
||||
**The kernel on the model's own data.** A temporary check (not in this PR) ran the decode kernel beside the new one on the same inputs, for every 8th batch of
|
||||
32 queries, in all 12 QSA layers, on the three prompts above (positions up to 98K, ~5 million (query, head) rows of 256): ||new - old|| / ||old|| per row is
|
||||
4.4e-7 on average and 4.8e-6 in the worst row; the largest absolute difference is 4e-5 against values up to 31. No trend over the layers or the context
|
||||
length. That is FP32 rounding level, so the differences above come from the model's sensitivity, not from the kernel's arithmetic.
|
||||
|
||||
## Limits
|
||||
|
||||
- sm_70 only (`__CUDA_ARCH__` 700-749). Q4_0 KV (`--kv q4_0`) and the QSA block scores (`block_scores_kernel`, the warp kernel below sm_80) still use their
|
||||
old kernels on Volta.
|
||||
- One request, one card. The Unsloth IQ4_XS pack is not a setup model; the GSQ-RCO quants were not run on this rig.
|
||||
- The profile above is one capture of one prompt. The distribution check is one document per length; the 99K row is the least certain.
|
||||
|
||||
## Reproduce
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -DSTRATA_ENABLE_CUDA=ON -DSTRATA_BUILD_TESTS=OFF -DSTRATA_PARITY_PROMPT_ATTN=ON \
|
||||
-DCMAKE_CUDA_ARCHITECTURES=70 -DSTRATA_EXPERIMENTAL_SM60=ON
|
||||
cmake --build build --target strata qsa_prompt_attn_parity -j
|
||||
CUDA_VISIBLE_DEVICES=0 ./build/qsa_prompt_attn_parity 32768 2048 5 # the accuracy table
|
||||
STRATA_PROMPT_ATTN_OLD=1 <start script> # the old path; without the variable: the new kernel
|
||||
python tools/needle_bench.py --url http://127.0.0.1:8081 --lengths 8k,32k,128k --depths 10,30,50,70,90
|
||||
```
|
||||
@@ -0,0 +1,70 @@
|
||||
# Community benchmark on 2x Intel Arc Pro B60
|
||||
|
||||
Measured on 2026-10-04 by LocalXPU. This is the SYCL port (`sycl/`) on two Arc Pro B60 cards, one request at a time, at an 8,192-token context. It is a short test: two prompt sizes, 2 to 3 requests each. It is not a 4K/32K/128K sweep.
|
||||
|
||||
## Hardware and software
|
||||
|
||||
- 2x Intel Arc Pro B60 24 GB (`8086:e211`, 456 GB/s each), PCIe 3.0 x8 per card; Ryzen 5 5600; 64 GB DDR4; storage type not recorded
|
||||
- Ubuntu 24.04, kernel 6.17 (`xe` driver), Level Zero V2, compute runtime NEO 26.09.37435.12, oneAPI DPC++ 2026.1.1, oneMKL 2026.1.0
|
||||
- Strata `6f32ec0` plus two `sycl/` fixes (the build fix for #559/#626 and the `cudaStreamQuery` fix in the ring wait); engine reports `0.1.39-sycl`; AOT `-DSTRATA_SYCL_AOT=bmg-g21`; source build, no Docker
|
||||
- Nothing else ran on the GPUs. The kernel log was watched for `xe` resets during every run: none.
|
||||
|
||||
## Model and configuration
|
||||
|
||||
- Coder IQ1_M (`Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M`, shard hashes checked against the Hugging Face LFS ids), `--layer-split 24`
|
||||
- Flash-Next IQ2_XS (`Qwen3.8-Flash-Next-GSQ-RCO-IQ2_XS`), `--layer-split 25`
|
||||
- Packs from `tools/iq_pack.py`, MTP layer from `tools/mtp_fetch.py` / `mtp_pack.py` / `mtp_rt.py`, profiles from `data/`
|
||||
- Context 8,192; KV int8; `--spec 4 --spec-min-p 0.5`; greedy (`temperature 0`), `enable_thinking: false`; no vision
|
||||
|
||||
`data/coder2.json` and `data/iq2b.json` are the exact server configs (engine arguments and environment); the engine's startup and stage lines are in `data/coder2.engine.txt`, `data/iq2b.engine.txt` and, for the one-card run, `data/B2.run.txt`. Served with
|
||||
`sycl/serve/server_intel.py --engine strata --config <json> --port 8085`.
|
||||
|
||||
## Method
|
||||
|
||||
Requests went through `/v1/chat/completions`, one at a time. Prompt and decode speed are the server's own `timings` object
|
||||
(`prompt_per_second`, `predicted_per_second`), per request; `bench.py` prints them (copy in `data/`, raw output in `data/c2-*.txt`, `data/i2-*.txt`).
|
||||
Model loading is not included. Three prompts:
|
||||
|
||||
- short: "Write a Python function that returns the n-th Fibonacci number" (23 prompt tokens cold, 7 fresh after the cached prefix)
|
||||
- 2K code: about 2,000 tokens of the repository's `tools/*.py` plus "write a binary search tree class" (1,922 prompt tokens, none reused); the second turn reuses the first
|
||||
- 2K prose: about 2,100 tokens of source plus "say what this code is for" (2,129 prompt tokens, none reused); the repeat reuses 2,122 of them
|
||||
|
||||
## Results
|
||||
|
||||
All experts were resident in VRAM on both cards in both runs (engine line `layer split: 100% of the experts resident`), so `STRATA_VERIFY_NO_HOST=1` was set.
|
||||
|
||||
| Configuration | Prompt tokens (fresh) | Reused | Generated | Requests | Prompt tok/s | Decode tok/s | Drafts accepted |
|
||||
| --- | ---: | ---: | ---: | ---: | --- | --- | --- |
|
||||
| Coder IQ1_M, short | 23 / 7 | 0 / 16 | 159 | 3 | 50.4 cold, 74-80 | 52.8 first, 56.6 and 56.6 | 112/138, 114/133 |
|
||||
| Coder IQ1_M, 2K code | 1,922 | 0 | 256 | 1 | 425.3 | 57.2 | 186/208 |
|
||||
| Coder IQ1_M, 2K code, next turn | 21 | 2,178 | 256 | 1 | 61.4 | 71.4 | 209/212 |
|
||||
| Coder IQ1_M, 2K prose | 2,129 | 0 | 128 | 1 | 427.9 | 40.1 | 70/98 |
|
||||
| Coder IQ1_M, 2K prose, repeat | 7 | 2,122 | 128 | 1 | 78.1 | 41.3 | 70/98 |
|
||||
| IQ2_XS, short | 23 / 7 | 0 / 16 | 112 | 2 | 50.4 cold, 81.7 | 58.6 and 61.3 | 84/88, 84/89 |
|
||||
| IQ2_XS, 2K code | 1,922 | 0 | 256 | 1 | 445.1 | 55.5 | 183/213 |
|
||||
| IQ2_XS, 2K code, next turn | 284 | 1,915 | 256 | 1 | 177.0 | 72.5 | 211/217 |
|
||||
| IQ2_XS, 2K prose | 2,129 | 0 | 104 | 1 | 436.4 | 38.8 | 56/87 |
|
||||
| IQ2_XS, 2K prose, repeat | 7 | 2,122 | 104 | 1 | 79.3 | 40.0 | 56/87 |
|
||||
|
||||
The two prose rows are slower because fewer drafts were accepted (71% and 64%), not because of the context: the 256-token code reply at the same context ran 57.2 and 55.5.
|
||||
Time to first token was not measured separately.
|
||||
|
||||
One card, Coder IQ1_M, 4,042 of 12,288 experts mirrored in pinned RAM (`STRATA_MIRROR_MIB=8192`, `ONEAPI_DEVICE_SELECTOR=level_zero:0`),
|
||||
by-hand `--tokens` run, 11-token prompt, 128 greedy tokens: **11.9 tok/s** (11.2 to 11.9 over five builds that differ only in experiments), coherent Python.
|
||||
Without the ring-wait fix this configuration stopped at layer 15 with `never rang (graph finished)` in 5 of 5 tries.
|
||||
The engine's stage table shows `wait for rings` at 186.8 ms per round, about 3.9 ms per layer. It drops to 72.9 ms and 25.1 tok/s with `kSpinMax` 2,000 in
|
||||
`sycl/include/strata/sycl_doorbell.hpp`, which suggests the GPU does not see the host's flag stores during the spin kernel and waits out the bound. That change
|
||||
risks proceeding before a slow host answers, so it is not part of this report.
|
||||
|
||||
Memory after load (`xpu-smi`, MiB, card 0 / card 1): Coder 14,592 / 17,456; IQ2_XS 20,246 / 20,573. The tightest card kept 3.7 GB free of the 24 GB with `--vram-reserve-mib 1536`.
|
||||
|
||||
## Correctness and limitations
|
||||
|
||||
- Both models, `/v1/chat/completions`: a Fibonacci function executed against the first ten values and `fn(50) == 12586269025`; a Euclid `gcd` executed on four pairs; a palindrome check (IQ2_XS); `sum(i*i for i in range(1,11))` answered `385`. All passed.
|
||||
- A three-turn conversation (a name and a number, then arithmetic on it, then a lambda using both) was answered correctly by both models, and the conversation cache reused 41 and 74 tokens.
|
||||
- No NaN, garbage or repetition loop in any output.
|
||||
- Not done: the 19-token chat-templated prompt from the docs (no token-for-token comparison with the reference text), streaming, the Anthropic route, tool calls, contexts above 2.2K, `--layer-split auto`, a run with `STRATA_VERIFY_NO_HOST=1` and experts outside VRAM.
|
||||
- Kernel tests (`ctest`, one B60): 21 of 25 pass; `iq_parity` passes for all 10 formats once its fixtures exist; `ple_parity` needs the Q2_0 shard; `s2_expert_grouped_parity` fails 4 bitwise comparisons while its host-reference check is within tolerance; `conversation_snapshot_test` aborts with `munmap_chunk(): invalid pointer`.
|
||||
- One `xe` GT reset on card 0, during the first `iq_parity` run (it did not finish in 300 s and was killed by a timeout; the same binary then passed in under 90 s). The card recovered without a reboot.
|
||||
|
||||
Paths in `data/` were shortened: `<data root>` is the directory with the models, packs and MTP layer, `<strata checkout>` the Strata source tree.
|
||||
@@ -0,0 +1,54 @@
|
||||
IDS=7734,264,12654,709,421,4523,279,307,7324,76938,1324
|
||||
strata generate: PCIe probe: 7.0 GB/s host->device (best of 7.0 7.0 7.0 7.0) -> pcie_frac 0.19 (default 0.55)
|
||||
strata generate: this CPU has no AVX-512: the expert kernels run on AVX-2 (multi-token for the i-quant gate/up rows)
|
||||
strata generate: native pack: <data root>/packs/coder-iq1_m experts (largest blob 2.66 MB), token embedding IQ4_XS in mapped host memory (322 MiB)
|
||||
strata generate: 1406 MiB of weights loaded from <data root>/packs/coder-iq1_m (302 canonical tensors skipped: served natively)
|
||||
strata generate: 300 native projection matrices, 2018.88 MiB of weights
|
||||
strata generate: the SSD is kept awake while rows are read: one page of the table after 100 ms without a read, until 60 s after the last request (STRATA_SSD_KEEPALIVE=0 turns it off)
|
||||
strata generate: profile data/expert-profile-coder.bin: 12288 ranked pairs, built for 12288 slots
|
||||
strata generate: PLE on, table 320001536 rows of <data root>/models/coder-iq1_m/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00002-of-00002.gguf
|
||||
strata mtp: draft layer loaded, 802 MiB of VRAM (experts 675, dense 111), files read in 0.45 s (1746 MiB/s)
|
||||
strata generate: experimental native Q5_K head, 521472000 bytes
|
||||
strata generate: GPU 0: Intel(R) Arc(TM) Pro B60 Graphics, compute capability 20.1
|
||||
strata generate: WARNING: this engine was compiled with CUDA 12.8 headers but loaded a CUDA 0.2 runtime (libcudart): GPU properties can read wrong and some kernels go unused. Rebuild it against one toolkit (cmake -DCUDAToolkit_ROOT=<the toolkit>, with its libcudart.so present) (#542)
|
||||
strata generate: experts streamed from the GGUF on demand (--stream-experts): no host arena; every expert must fit the VRAM cache
|
||||
strata generate: expert cache auto: 18.90 GiB free, 1536 MiB reserved (+218 MiB for the draft head) -> 6331 slots
|
||||
strata generate: expert cache 8246 slots, 15.70 GiB of VRAM; policy is
|
||||
strata generate: the GPU computes the experts in the cache; it rounds differently from the CPU,
|
||||
so a reply can differ slightly from a run without the cache (same quality:
|
||||
bench/results/2026-09-27-cache-parity).
|
||||
PROFILE, ranked by routing frequency, no eviction.
|
||||
strata generate: profile fill: 15.70 GiB in 2.5 s (6.79 GB/s, pipelined)
|
||||
strata generate: pre-filled 8246 of 8246 slots from the profile; slot 0 verified
|
||||
strata generate: 4042 of 4042 experts missing from VRAM mirrored in pinned host memory (7.72 GiB, 1.3 s); the GPU reads them over PCIe
|
||||
strata generate: R4 hit path ON - resident experts are computed on the GPU
|
||||
strata generate: 5 expert-pool workers + the host thread
|
||||
strata generate: session is up (engine 0.1.39-sycl)
|
||||
strata generate: sampling greedy
|
||||
strata generate: token graph hit path: 8246 resident experts, decided on the device
|
||||
strata generate: prompt path allocates its own buffers (no cache slots to borrow)
|
||||
strata mtp: draft head over 106299 tokens (212.9 MiB)
|
||||
strata generate: prefill 10 tokens in 1 chunks, 563.9 ms (17.7 tok/s); experts streamed 354 (354 by DMA, host 5.2 ms), resident 1689; PLE 2.7 ms
|
||||
strata verify: window up to 6 tokens, 74.0 MiB of device buffers
|
||||
strata verify: captured the 1-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 2-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 3-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 4-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 5-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 6-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata generate: window and draft graphs captured in 125 ms
|
||||
speculation 50 rounds of 6, drafts accepted 81 of 98 (0.827), 2.62 tokens per round
|
||||
window sizes T1:11 T2:8 T3:3 T4:28 T5:0 T6:0 (min draft probability 0.50)
|
||||
suffix drafts 3 windows, drafts accepted 8 of 9
|
||||
accepted per round 0:14 1:10 2:7 3:19 4:0 5:0
|
||||
verify window wait for rings 186.845 pool 10.966 host 1.142 commit 9.963 ms/round; CPU experts 2.50 distinct / 3.33 routed per layer
|
||||
pool multi gate/up 6.183 quantize 0.025 down 2.868 ms/round; 27.1 GB/s over the rows phases; CPU pool call 10.958 ms/round
|
||||
dispatch plan 0.100 activation quantize 0.254 jobs 1.514 run 9.080 ms/round
|
||||
adaptive tier 1069 experts swapped into the VRAM tier (every 4 rounds, 0.839 ms/round)
|
||||
pcie experts 0.21 distinct experts per layer read over PCIe (share 49/256 of the misses)
|
||||
mtp 10.095 ms/round drafting (49 rounds), MTP prompt 6.8 ms, 1015 MiB of VRAM
|
||||
prompt : 7734 264 12654 709 421 4523 279 307 7324 76938 1324
|
||||
output : 1608 48575 13 271 727 15336 1393 1590 198 262 413 307 2564 220 16 25 198 285 460 307 198 262 460 15336 1393 12 16 8 478 15336 1393 12 17 8 271 760 709 3294 369 264 4145 29812 7873 314 279 76938 8240 13 4213 11 424 369 524 10727 364 3349 2663 314 307 1521 424 95272 3205 942 279 1788 2663 5081 2942 13 1946 7042 14588 11 567 628 958 20781 1954 310 3436 8334 16100 2663 13 248046 198 248045 846 198 7734 264 12654 709 421 4523 279 307 7324 76938 1324 1608 48575 248046 198 248045 74455 198 248068 198 760 1156 6587 264 12654 709 421 4523 279 307 7324 76938 1324 1608 48575 13 1061 369
|
||||
decode 128 tokens in 10735.2 ms -> 11.92 tok/s
|
||||
prefill 10 tokens in 563.9 ms -> 17.73 tok/s (time to first token 809.0 ms)
|
||||
EXIT=0
|
||||
@@ -0,0 +1,41 @@
|
||||
import json,sys,re,urllib.request,time,glob
|
||||
URL="http://127.0.0.1:8085/v1/chat/completions"
|
||||
def ask(msgs,max_tokens=256):
|
||||
body={"messages":msgs,"max_tokens":max_tokens,"temperature":0,"chat_template_kwargs":{"enable_thinking":False}}
|
||||
t=time.time()
|
||||
r=urllib.request.urlopen(urllib.request.Request(URL,json.dumps(body).encode(),{"content-type":"application/json"}),timeout=900)
|
||||
d=json.load(r); d["wall"]=time.time()-t
|
||||
return d
|
||||
def show(tag,d):
|
||||
tm=d["timings"];u=d["usage"]
|
||||
print(f"[{tag}] prompt_n={tm['prompt_n']} cache_n={tm['cache_n']} prompt {tm['prompt_per_second']:.1f} tok/s | decode {tm['predicted_n']} tok {tm['predicted_per_second']:.1f} tok/s | drafts {tm.get('draft_n_accepted')}/{tm.get('draft_n')} | wall {d['wall']:.1f}s | finish {d['choices'][0]['finish_reason']}")
|
||||
def code(text):
|
||||
m=re.search(r"```python\n(.*?)```",text,re.S); return m.group(1) if m else text
|
||||
def run_tests(src,tests):
|
||||
g={}
|
||||
try:
|
||||
exec(src,g); exec(tests,g); return "PASS"
|
||||
except Exception as e: return f"FAIL {e!r}"
|
||||
mode=sys.argv[1]
|
||||
if mode=="short":
|
||||
for i in range(2):
|
||||
d=ask([{"role":"user","content":"Write a Python function that returns the n-th Fibonacci number"}],256); show(f"fib run{i}",d)
|
||||
print(d["choices"][0]["message"]["content"])
|
||||
print("fib tests:",run_tests(code(d["choices"][0]["message"]["content"]),"fn=[v for k,v in list(globals().items()) if callable(v) and not k.startswith('_') and k!='exec'][-1]\nassert [fn(i) for i in range(10)]==[0,1,1,2,3,5,8,13,21,34]; assert fn(50)==12586269025"))
|
||||
d=ask([{"role":"user","content":"Write a Python function gcd(a, b) that returns the greatest common divisor using Euclid's algorithm. Only the code."}],200); show("gcd",d)
|
||||
c=d["choices"][0]["message"]["content"]; print(c)
|
||||
print("gcd tests:",run_tests(code(c),"assert gcd(48,18)==6; assert gcd(17,5)==1; assert gcd(0,9)==9; assert gcd(270,192)==6"))
|
||||
d=ask([{"role":"user","content":"What is the output of this Python code? print(sum(i*i for i in range(1,11))) Answer with just the number."}],20); show("known",d); print(d["choices"][0]["message"]["content"], "(expected 385)")
|
||||
if mode=="long":
|
||||
src="".join(open(f).read() for f in sorted(glob.glob("<strata checkout>/tools/*.py"))[:6])[:int(sys.argv[2])]
|
||||
msgs=[{"role":"user","content":"Here is some Python source:\n\n"+src+"\n\nIn one paragraph, say what this code is for, then write a short Python function that reverses a string."}]
|
||||
for i in range(2):
|
||||
d=ask(msgs,256); show(f"long run{i}",d)
|
||||
print(d["choices"][0]["message"]["content"][:1200])
|
||||
if mode=="long2":
|
||||
fs=sorted(glob.glob("<strata checkout>/tools/*.py"))
|
||||
src="".join(open(f).read() for f in fs[6:12])[:7000]
|
||||
msgs=[{"role":"user","content":"Here is some Python source:\n\n"+src+"\n\nWrite a Python implementation of a binary search tree class with insert, search, delete and in-order traversal methods, with docstrings."}]
|
||||
d=ask(msgs,256); show("long2 BST",d)
|
||||
d2=ask(msgs+[{"role":"assistant","content":d["choices"][0]["message"]["content"]},{"role":"user","content":"Now add a height() method."}],256); show("long2 turn2",d2)
|
||||
print(d["choices"][0]["message"]["content"][:600])
|
||||
@@ -0,0 +1 @@
|
||||
{"id": "chatcmpl-9a1c5ae9d2084c8f9b34ac8c", "object": "chat.completion", "created": 1791157128, "model": "coder-iq1_m", "choices": [{"index": 0, "message": {"role": "assistant", "content": "```python\ndef fibonacci(n: int) -> int:\n \"\"\"Return the n-th Fibonacci number (0-indexed).\n \n fibonacci(0) = 0\n fibonacci(1) = 1\n fibonacci(2) = 1\n fibonacci(3) = 2\n ...\n \"\"\"\n if n < 0:\n raise ValueError(\"n must be a non-negative integer\")\n if n == 0:\n return 0\n if n == 1:\n return 1\n \n a, b = 0, 1\n for _ in range(2, n + 1):\n a, b = b, a + b\n return b\n```"}, "finish_reason": "stop"}], "usage": {"prompt_tokens": 23, "completion_tokens": 159, "total_tokens": 182, "prompt_tokens_details": {"cached_tokens": 0}}, "timings": {"cache_n": 0, "prompt_n": 23, "prompt_ms": 456.3, "prompt_per_token_ms": 19.839, "prompt_per_second": 50.4, "predicted_n": 159, "predicted_ms": 3009.9, "predicted_per_token_ms": 18.93, "predicted_per_second": 52.8, "draft_n": 138, "draft_n_accepted": 112}}
|
||||
@@ -0,0 +1,8 @@
|
||||
[long run0] prompt_n=2129 cache_n=0 prompt 427.9 tok/s | decode 128 tok 40.1 tok/s | drafts 70/98 | wall 8.2s | finish stop
|
||||
[long run1] prompt_n=7 cache_n=2122 prompt 78.1 tok/s | decode 128 tok 41.3 tok/s | drafts 70/98 | wall 3.2s | finish stop
|
||||
This code is a comprehensive integration test script for a batched inference engine (likely named "Strata") that verifies the correctness of multi-slot decoding, specifically ensuring that tokens generated in parallel slots match those generated in solo mode, even when long prompts are interleaved with other decoding operations or when requests are continued from checkpoints. The script sets up various scenarios involving different prompt lengths, slot assignments, and continuation logic to validate that the engine's internal state management, expert caching, and token generation remain consistent across different execution paths.
|
||||
|
||||
```python
|
||||
def reverse_string(s: str) -> str:
|
||||
return s[::-1]
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
[long2 BST] prompt_n=1922 cache_n=0 prompt 425.3 tok/s | decode 256 tok 57.2 tok/s | drafts 186/208 | wall 9.0s | finish length
|
||||
[long2 turn2] prompt_n=21 cache_n=2178 prompt 61.4 tok/s | decode 256 tok 71.4 tok/s | drafts 209/212 | wall 3.9s | finish length
|
||||
```python
|
||||
class BinarySearchTree:
|
||||
"""A binary search tree implementation with insert, search, delete, and in-order traversal."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize an empty binary search tree."""
|
||||
self.root = None
|
||||
|
||||
def insert(self, value):
|
||||
"""Insert a value into the binary search tree.
|
||||
|
||||
Args:
|
||||
value: The value to insert into the tree.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if self.root is None:
|
||||
self.root = _Node(value)
|
||||
return
|
||||
|
||||
current = self.root
|
||||
while True:
|
||||
if value < curr
|
||||
@@ -0,0 +1,35 @@
|
||||
[fib run0] prompt_n=7 cache_n=16 prompt 74.3 tok/s | decode 159 tok 56.6 tok/s | drafts 114/133 | wall 2.9s | finish stop
|
||||
[fib run1] prompt_n=7 cache_n=16 prompt 80.1 tok/s | decode 159 tok 56.6 tok/s | drafts 114/133 | wall 2.9s | finish stop
|
||||
```python
|
||||
def fibonacci(n: int) -> int:
|
||||
"""Return the n-th Fibonacci number (0-indexed).
|
||||
|
||||
fibonacci(0) = 0
|
||||
fibonacci(1) = 1
|
||||
fibonacci(2) = 1
|
||||
fibonacci(3) = 2
|
||||
...
|
||||
"""
|
||||
if n < 0:
|
||||
raise ValueError("n must be a non-negative integer")
|
||||
if n == 0:
|
||||
return 0
|
||||
if n == 1:
|
||||
return 1
|
||||
|
||||
a, b = 0, 1
|
||||
for _ in range(2, n + 1):
|
||||
a, b = b, a + b
|
||||
return b
|
||||
```
|
||||
fib tests: PASS
|
||||
[gcd] prompt_n=37 cache_n=0 prompt 71.7 tok/s | decode 32 tok 60.8 tok/s | drafts 23/24 | wall 1.0s | finish stop
|
||||
```python
|
||||
def gcd(a, b):
|
||||
while b:
|
||||
a, b = b, a % b
|
||||
return a
|
||||
```
|
||||
gcd tests: PASS
|
||||
[known] prompt_n=42 cache_n=0 prompt 69.5 tok/s | decode 4 tok 25.1 tok/s | drafts 4/6 | wall 0.8s | finish stop
|
||||
385 (expected 385)
|
||||
@@ -0,0 +1,66 @@
|
||||
strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 24)
|
||||
strata generate: PCIe probe: 7.0 GB/s host->device (best of 7.0 7.0 7.0 7.0) -> pcie_frac 0.19 (default 0.55)
|
||||
strata generate: this CPU has no AVX-512: the expert kernels run on AVX-2 (multi-token for the i-quant gate/up rows)
|
||||
strata generate: native pack: <data root>/packs/coder-iq1_m experts (largest blob 2.66 MB), token embedding IQ4_XS in mapped host memory (322 MiB)
|
||||
strata generate: layer split: CUDA0 loads the dense weights of layers 0-23 only
|
||||
strata generate: 741 MiB of weights loaded from <data root>/packs/coder-iq1_m (686 canonical tensors skipped: served natively)
|
||||
strata generate: 150 native projection matrices, 988.19 MiB of weights
|
||||
strata generate: the SSD is kept awake while rows are read: one page of the table after 100 ms without a read, until 60 s after the last request (STRATA_SSD_KEEPALIVE=0 turns it off)
|
||||
strata generate: layer split: CUDA1 loads the dense weights of layers 24-47 only
|
||||
strata generate: layer split: CUDA1 PCIe probe 7.0 GB/s (best of 7.0 7.0 7.0 7.0) -> pcie_frac 0.19
|
||||
strata generate: layer split: CUDA1 holds its weights; 22.04 GiB free (its session follows the split search)
|
||||
strata generate: profile data/expert-profile-coder.bin: 12288 ranked pairs, built for 12288 slots
|
||||
strata generate: PLE on, table 320001536 rows of <data root>/models/coder-iq1_m/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00002-of-00002.gguf
|
||||
strata generate: layer split: CUDA1 holds its weights, session [24, 48) and the head; 21.43 GiB free
|
||||
strata mtp: draft layer loaded, 802 MiB of VRAM (experts 675, dense 111), files read in 0.44 s (1769 MiB/s)
|
||||
strata generate: GPU 0: Intel(R) Arc(TM) Pro B60 Graphics, compute capability 20.1
|
||||
strata generate: WARNING: this engine was compiled with CUDA 12.8 headers but loaded a CUDA 0.2 runtime (libcudart): GPU properties can read wrong and some kernels go unused. Rebuild it against one toolkit (cmake -DCUDAToolkit_ROOT=<the toolkit>, with its libcudart.so present) (#542)
|
||||
strata generate: experts streamed from the GGUF on demand (--stream-experts): no host arena; every expert must fit the VRAM cache
|
||||
strata generate: expert cache auto: 21.95 GiB free, 1536 MiB reserved (+0 MiB for the draft head) -> 6144 slots
|
||||
strata generate: expert cache 6144 slots, 11.07 GiB of VRAM; policy is
|
||||
strata generate: the GPU computes the experts in the cache; it rounds differently from the CPU,
|
||||
so a reply can differ slightly from a run without the cache (same quality:
|
||||
bench/results/2026-09-27-cache-parity).
|
||||
PROFILE, ranked by routing frequency, no eviction.
|
||||
strata generate: profile fill: 11.07 GiB in 1.9 s (6.27 GB/s, pipelined)
|
||||
strata generate: pre-filled 6144 of 6144 slots from the profile; slot 0 verified
|
||||
strata generate: 3024 of 6144 experts missing from VRAM mirrored in pinned host memory (6.00 GiB, 1.0 s); the GPU reads them over PCIe
|
||||
strata generate: WARNING: 3120 experts are neither in VRAM nor mirrored; with STRATA_VERIFY_NO_HOST the device plan cannot run them and their layers' windows fall back slowly - raise STRATA_MIRROR_MIB or the free RAM, or lower --max-context
|
||||
strata generate: layer split, CUDA1: 20.65 GiB free of 23.91, room for experts 17.33 GiB
|
||||
strata generate: layer split: CUDA1 runs layers 24-47, expert cache 6144 slots (12.35 GiB), 6144 of its 6144 profiled pairs; slot 0 verified
|
||||
strata generate: layer split: CUDA0 runs layers 0-23
|
||||
strata generate: R4 hit path ON - resident experts are computed on the GPU
|
||||
strata generate: 5 expert-pool workers + the host thread
|
||||
strata generate: session is up (engine 0.1.39-sycl)
|
||||
strata generate: token graph hit path: 12288 resident experts, decided on the device
|
||||
strata serve: layer split: 100% of the experts resident, the prompt path's streamed ring 96 slots
|
||||
strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)
|
||||
strata verify: window up to 6 tokens, 74.0 MiB of device buffers
|
||||
strata serve: layer split: layers 0-23 (CUDA0), 24-47 (CUDA1), one hand-off per window
|
||||
strata verify: window up to 6 tokens, 74.0 MiB of device buffers
|
||||
strata mtp: draft head over 106299 tokens (212.9 MiB)
|
||||
strata serve: 9890 MiB of VRAM free with everything loaded
|
||||
strata verify: captured the 6-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 6-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 4-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 4-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 1-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 1-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 2-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 2-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata serve: prompt 23 tokens = 0 reused + 23 read in 456 ms (50.4 tok/s), 159 generated in 3010 ms (52.8 tok/s), drafts accepted 112 of 138, 1 checkpoints
|
||||
strata serve: suffix drafts: 1 windows, 1 of 5 drafts accepted
|
||||
strata serve: prompt 23 tokens = 16 reused + 7 read in 94 ms (74.3 tok/s), 159 generated in 2810 ms (56.6 tok/s), drafts accepted 114 of 133, 1 checkpoints
|
||||
strata serve: prompt 23 tokens = 16 reused + 7 read in 87 ms (80.1 tok/s), 159 generated in 2808 ms (56.6 tok/s), drafts accepted 114 of 133, 1 checkpoints
|
||||
strata serve: prompt 37 tokens = 0 reused + 37 read in 516 ms (71.7 tok/s), 32 generated in 526 ms (60.8 tok/s), drafts accepted 23 of 24, 1 checkpoints
|
||||
strata verify: captured the 5-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 5-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata serve: prompt 42 tokens = 0 reused + 42 read in 604 ms (69.5 tok/s), 4 generated in 160 ms (25.1 tok/s), drafts accepted 4 of 6, 1 checkpoints
|
||||
strata verify: captured the 3-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 3-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata serve: prompt 2129 tokens = 0 reused + 2129 read in 4975 ms (427.9 tok/s), 128 generated in 3189 ms (40.1 tok/s), drafts accepted 70 of 98, 1 checkpoints
|
||||
strata serve: prompt 2129 tokens = 2122 reused + 7 read in 90 ms (78.1 tok/s), 128 generated in 3101 ms (41.3 tok/s), drafts accepted 70 of 98, 1 checkpoints
|
||||
strata serve: prompt 1922 tokens = 0 reused + 1922 read in 4519 ms (425.3 tok/s), 256 generated in 4475 ms (57.2 tok/s), drafts accepted 186 of 208, 1 checkpoints
|
||||
strata serve: suffix drafts: 1 windows, 2 of 5 drafts accepted
|
||||
strata serve: prompt 2199 tokens = 2178 reused + 21 read in 342 ms (61.4 tok/s), 256 generated in 3587 ms (71.4 tok/s), drafts accepted 209 of 212, 2 checkpoints
|
||||
strata serve: suffix drafts: 37 windows, 185 of 185 drafts accepted
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"engine": "strata",
|
||||
"exe": "<data root>/strata-native.sh",
|
||||
"cwd": "<strata checkout>",
|
||||
"tokenizer": "<data root>/packs/coder-iq1_m/tokenizer",
|
||||
"model_name": "coder-iq1_m",
|
||||
"backend": "sycl",
|
||||
"log": "<data root>/logs/w2/coder2.engine.log",
|
||||
"args": [
|
||||
"--pack",
|
||||
"<data root>/packs/coder-iq1_m",
|
||||
"--native",
|
||||
"<data root>/models/coder-iq1_m/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00001-of-00002.gguf",
|
||||
"--ple-gguf",
|
||||
"<data root>/models/coder-iq1_m/Qwen3.8-Flash-Next-GSQ-RCO-IQ1_M-00002-of-00002.gguf",
|
||||
"--expert-profile",
|
||||
"data/expert-profile-coder.bin",
|
||||
"--expert-cache",
|
||||
"auto",
|
||||
"--stream-experts",
|
||||
"--prefill",
|
||||
"auto",
|
||||
"--spec",
|
||||
"4",
|
||||
"--spec-min-p",
|
||||
"0.5",
|
||||
"--mtp",
|
||||
"<data root>/mtp/rt",
|
||||
"--max-context",
|
||||
"8192",
|
||||
"--kv",
|
||||
"int8",
|
||||
"--vram-reserve-mib",
|
||||
"1536",
|
||||
"--no-prefill-borrow",
|
||||
"--layer-split",
|
||||
"24"
|
||||
],
|
||||
"env": {
|
||||
"ONEAPI_DEVICE_SELECTOR": "level_zero:0,1",
|
||||
"SYCL_CACHE_PERSISTENT": "0",
|
||||
"STRATA_MIRROR_MIB": "6144",
|
||||
"STRATA_VERIFY_DEVICE_PLAN": "1",
|
||||
"STRATA_STAGE_TRIM": "1",
|
||||
"STRATA_VERIFY_NO_HOST": "1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[long run0] prompt_n=2129 cache_n=0 prompt 436.4 tok/s | decode 104 tok 38.8 tok/s | drafts 56/87 | wall 7.6s | finish stop
|
||||
[long run1] prompt_n=7 cache_n=2122 prompt 79.3 tok/s | decode 104 tok 40.0 tok/s | drafts 56/87 | wall 2.7s | finish stop
|
||||
This code serves as a comprehensive test harness for a batch inference engine, specifically designed to verify that token generation remains consistent when multiple prompts are processed concurrently in separate slots. It compares the output tokens from parallel execution against those from sequential "solo" runs to ensure that interleaving, checkpointing, and resource sharing (such as expert cache borrowing) do not introduce drift or errors in the model's responses.
|
||||
|
||||
```python
|
||||
def reverse_string(s: str) -> str:
|
||||
return s[::-1]
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
[long2 BST] prompt_n=1922 cache_n=0 prompt 445.1 tok/s | decode 256 tok 55.5 tok/s | drafts 183/213 | wall 9.0s | finish length
|
||||
[long2 turn2] prompt_n=284 cache_n=1915 prompt 177.0 tok/s | decode 256 tok 72.5 tok/s | drafts 211/217 | wall 5.1s | finish length
|
||||
```python
|
||||
class BinarySearchTree:
|
||||
"""A binary search tree implementation with insert, search, delete, and in-order traversal."""
|
||||
|
||||
class Node:
|
||||
"""Internal node of the binary search tree."""
|
||||
|
||||
def __init__(self, value):
|
||||
"""Initialize a node with a value and no children."""
|
||||
self.value = value
|
||||
self.left = None
|
||||
self.right = None
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize an empty binary search tree."""
|
||||
self.root = None
|
||||
|
||||
def insert(self, value):
|
||||
"""Insert a value into the binary search tree.
|
||||
|
||||
Args:
|
||||
@@ -0,0 +1,27 @@
|
||||
[fib run0] prompt_n=23 cache_n=0 prompt 50.4 tok/s | decode 112 tok 58.6 tok/s | drafts 84/88 | wall 2.4s | finish stop
|
||||
[fib run1] prompt_n=7 cache_n=16 prompt 81.7 tok/s | decode 112 tok 61.3 tok/s | drafts 84/89 | wall 1.9s | finish stop
|
||||
```python
|
||||
def fibonacci(n):
|
||||
"""Return the n-th Fibonacci number (0-indexed)."""
|
||||
if n < 0:
|
||||
raise ValueError("n must be a non-negative integer")
|
||||
if n == 0:
|
||||
return 0
|
||||
if n == 1:
|
||||
return 1
|
||||
a, b = 0, 1
|
||||
for _ in range(2, n + 1):
|
||||
a, b = b, a + b
|
||||
return b
|
||||
```
|
||||
fib tests: PASS
|
||||
[gcd] prompt_n=37 cache_n=0 prompt 73.3 tok/s | decode 32 tok 61.8 tok/s | drafts 23/24 | wall 1.0s | finish stop
|
||||
```python
|
||||
def gcd(a, b):
|
||||
while b:
|
||||
a, b = b, a % b
|
||||
return a
|
||||
```
|
||||
gcd tests: PASS
|
||||
[known] prompt_n=42 cache_n=0 prompt 71.1 tok/s | decode 4 tok 25.4 tok/s | drafts 4/6 | wall 0.7s | finish stop
|
||||
385 (expected 385)
|
||||
@@ -0,0 +1,69 @@
|
||||
strata generate: layer split across 2 GPUs: CUDA0, then CUDA1 (split 25)
|
||||
strata generate: PCIe probe: 7.0 GB/s host->device (best of 7.0 7.0 7.0 7.0) -> pcie_frac 0.19 (default 0.55)
|
||||
strata generate: this CPU has no AVX-512: the expert kernels run on AVX-2 (multi-token for the i-quant gate/up rows)
|
||||
strata generate: native pack: <data root>/packs/qwen-iq2_xs experts (largest blob 1.51 MB), token embedding IQ4_XS in mapped host memory (322 MiB)
|
||||
strata generate: layer split: CUDA0 loads the dense weights of layers 0-24 only
|
||||
strata generate: 799 MiB of weights loaded from <data root>/packs/qwen-iq2_xs (670 canonical tensors skipped: served natively)
|
||||
strata generate: 156 native projection matrices, 766.85 MiB of weights
|
||||
strata generate: the SSD is kept awake while rows are read: one page of the table after 100 ms without a read, until 60 s after the last request (STRATA_SSD_KEEPALIVE=0 turns it off)
|
||||
strata generate: layer split: CUDA1 loads the dense weights of layers 25-47 only
|
||||
strata generate: layer split: CUDA1 PCIe probe 7.0 GB/s (best of 7.0 7.0 7.0 7.0) -> pcie_frac 0.19
|
||||
strata generate: layer split: CUDA1 holds its weights; 22.36 GiB free (its session follows the split search)
|
||||
strata generate: profile data/expert-profile.bin: 24576 ranked pairs, built for 24576 slots
|
||||
strata generate: PLE on, table 320001536 rows of <data root>/models/qwen-iq2_xs/Qwen3.8-Flash-Next-GSQ-RCO-IQ2_XS-00002-of-00002.gguf
|
||||
strata generate: layer split: CUDA1 holds its weights, session [25, 48) and the head; 21.93 GiB free
|
||||
strata mtp: draft layer loaded, 802 MiB of VRAM (experts 675, dense 111), files read in 0.75 s (1053 MiB/s)
|
||||
strata generate: GPU 0: Intel(R) Arc(TM) Pro B60 Graphics, compute capability 20.1
|
||||
strata generate: WARNING: this engine was compiled with CUDA 12.8 headers but loaded a CUDA 0.2 runtime (libcudart): GPU properties can read wrong and some kernels go unused. Rebuild it against one toolkit (cmake -DCUDAToolkit_ROOT=<the toolkit>, with its libcudart.so present) (#542)
|
||||
strata generate: experts streamed from the GGUF on demand (--stream-experts): no host arena; every expert must fit the VRAM cache
|
||||
strata generate: expert cache auto: 22.11 GiB free, 1536 MiB reserved (+0 MiB for the draft head) -> 12800 slots
|
||||
strata generate: expert cache 12800 slots, 16.91 GiB of VRAM; policy is
|
||||
strata generate: the GPU computes the experts in the cache; it rounds differently from the CPU,
|
||||
so a reply can differ slightly from a run without the cache (same quality:
|
||||
bench/results/2026-09-27-cache-parity).
|
||||
PROFILE, ranked by routing frequency, no eviction.
|
||||
strata generate: profile fill: 16.91 GiB in 3.8 s (4.81 GB/s, pipelined)
|
||||
strata generate: pre-filled 12800 of 12800 slots from the profile; slot 0 verified
|
||||
strata generate: 4473 of 11776 experts missing from VRAM mirrored in pinned host memory (6.00 GiB, 1.1 s); the GPU reads them over PCIe
|
||||
strata generate: WARNING: 7303 experts are neither in VRAM nor mirrored; with STRATA_VERIFY_NO_HOST the device plan cannot run them and their layers' windows fall back slowly - raise STRATA_MIRROR_MIB or the free RAM, or lower --max-context
|
||||
strata generate: layer split, CUDA1: 21.14 GiB free of 23.91, room for experts 17.93 GiB
|
||||
strata generate: layer split: CUDA1 runs layers 25-47, expert cache 11776 slots (16.11 GiB), 11776 of its 11776 profiled pairs; slot 0 verified
|
||||
strata generate: layer split: CUDA0 runs layers 0-24
|
||||
strata generate: R4 hit path ON - resident experts are computed on the GPU
|
||||
strata generate: 5 expert-pool workers + the host thread
|
||||
strata generate: session is up (engine 0.1.39-sycl)
|
||||
strata generate: token graph hit path: 24576 resident experts, decided on the device
|
||||
strata serve: layer split: 100% of the experts resident, the prompt path's streamed ring 96 slots
|
||||
strata serve: the prompt path allocates its own buffers (too few cache slots to borrow)
|
||||
strata verify: window up to 6 tokens, 56.4 MiB of device buffers
|
||||
strata serve: layer split: layers 0-24 (CUDA0), 25-47 (CUDA1), one hand-off per window
|
||||
strata verify: window up to 6 tokens, 56.4 MiB of device buffers
|
||||
strata mtp: draft head over 106299 tokens (137.9 MiB)
|
||||
strata serve: 4235 MiB of VRAM free with everything loaded
|
||||
strata verify: captured the 6-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 6-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 4-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 4-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 1-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 1-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 3-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 3-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata serve: prompt 23 tokens = 0 reused + 23 read in 456 ms (50.4 tok/s), 112 generated in 1910 ms (58.6 tok/s), drafts accepted 84 of 88, 1 checkpoints
|
||||
strata serve: prompt 23 tokens = 16 reused + 7 read in 86 ms (81.7 tok/s), 112 generated in 1827 ms (61.3 tok/s), drafts accepted 84 of 89, 1 checkpoints
|
||||
strata serve: suffix drafts: 1 windows, 1 of 3 drafts accepted
|
||||
strata serve: prompt 37 tokens = 0 reused + 37 read in 505 ms (73.3 tok/s), 32 generated in 518 ms (61.8 tok/s), drafts accepted 23 of 24, 1 checkpoints
|
||||
strata verify: captured the 5-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 5-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata serve: prompt 42 tokens = 0 reused + 42 read in 591 ms (71.1 tok/s), 4 generated in 157 ms (25.4 tok/s), drafts accepted 4 of 6, 1 checkpoints
|
||||
strata verify: captured the 2-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata verify: captured the 2-token window (upload <FIXME: Placeholder>, sync <FIXME: Placeholder>)
|
||||
strata serve: prompt 2129 tokens = 0 reused + 2129 read in 4878 ms (436.4 tok/s), 104 generated in 2680 ms (38.8 tok/s), drafts accepted 56 of 87, 1 checkpoints
|
||||
strata serve: prompt 2129 tokens = 2122 reused + 7 read in 88 ms (79.2 tok/s), 104 generated in 2602 ms (40.0 tok/s), drafts accepted 56 of 87, 1 checkpoints
|
||||
strata serve: prompt 1922 tokens = 0 reused + 1922 read in 4318 ms (445.1 tok/s), 256 generated in 4616 ms (55.5 tok/s), drafts accepted 183 of 213, 1 checkpoints
|
||||
strata serve: suffix drafts: 1 windows, 1 of 5 drafts accepted
|
||||
strata serve: prompt 2199 tokens = 1915 reused + 284 read in 1604 ms (177.0 tok/s), 256 generated in 3532 ms (72.5 tok/s), drafts accepted 211 of 217, 2 checkpoints
|
||||
strata serve: suffix drafts: 41 windows, 205 of 205 drafts accepted
|
||||
strata serve: prompt 30 tokens = 0 reused + 30 read in 454 ms (66.1 tok/s), 11 generated in 255 ms (43.2 tok/s), drafts accepted 7 of 9, 1 checkpoints
|
||||
strata serve: prompt 70 tokens = 41 reused + 29 read in 412 ms (70.3 tok/s), 3 generated in 97 ms (30.9 tok/s), drafts accepted 3 of 3, 2 checkpoints
|
||||
strata serve: prompt 108 tokens = 74 reused + 34 read in 479 ms (70.9 tok/s), 14 generated in 307 ms (45.6 tok/s), drafts accepted 8 of 11, 3 checkpoints
|
||||
strata serve: prompt 33 tokens = 0 reused + 33 read in 487 ms (67.8 tok/s), 39 generated in 813 ms (47.9 tok/s), drafts accepted 28 of 38, 1 checkpoints
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"engine": "strata",
|
||||
"exe": "<data root>/strata-native.sh",
|
||||
"cwd": "<strata checkout>",
|
||||
"tokenizer": "<data root>/packs/qwen-iq2_xs/tokenizer",
|
||||
"model_name": "qwen3.8-flash-next-iq2_xs",
|
||||
"backend": "sycl",
|
||||
"log": "<data root>/logs/w2/iq2b.engine.log",
|
||||
"args": [
|
||||
"--pack",
|
||||
"<data root>/packs/qwen-iq2_xs",
|
||||
"--native",
|
||||
"<data root>/models/qwen-iq2_xs/Qwen3.8-Flash-Next-GSQ-RCO-IQ2_XS-00001-of-00002.gguf",
|
||||
"--ple-gguf",
|
||||
"<data root>/models/qwen-iq2_xs/Qwen3.8-Flash-Next-GSQ-RCO-IQ2_XS-00002-of-00002.gguf",
|
||||
"--expert-profile",
|
||||
"data/expert-profile.bin",
|
||||
"--expert-cache",
|
||||
"auto",
|
||||
"--stream-experts",
|
||||
"--prefill",
|
||||
"auto",
|
||||
"--spec",
|
||||
"4",
|
||||
"--spec-min-p",
|
||||
"0.5",
|
||||
"--mtp",
|
||||
"<data root>/mtp/rt",
|
||||
"--max-context",
|
||||
"8192",
|
||||
"--kv",
|
||||
"int8",
|
||||
"--vram-reserve-mib",
|
||||
"1536",
|
||||
"--no-prefill-borrow",
|
||||
"--layer-split",
|
||||
"25"
|
||||
],
|
||||
"env": {
|
||||
"ONEAPI_DEVICE_SELECTOR": "level_zero:0,1",
|
||||
"SYCL_CACHE_PERSISTENT": "0",
|
||||
"STRATA_MIRROR_MIB": "6144",
|
||||
"STRATA_VERIFY_DEVICE_PLAN": "1",
|
||||
"STRATA_STAGE_TRIM": "1",
|
||||
"STRATA_VERIFY_NO_HOST": "1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
# RX 6900 XT (gfx1030): the prompt path's 16-bit GEMMs in FP16 in and out (2026-10-04)
|
||||
|
||||
On gfx103x (RDNA2) the prompt path ran its 16-bit GEMMs as rocBLAS FP16-in / FP32-out (`Gemm::f16`, `Gemm::native`) and
|
||||
BF16-in / FP32-out (`Gemm::bf16`). rocBLAS on gfx1030 has tuned kernels for FP16-in / **FP16-out** only; the other two
|
||||
fall back to generic kernels about 6.6x slower. This change runs those GEMMs FP16 in and out on gfx103x, writing the FP16
|
||||
result into the start of each FP32 row of Y and widening it there (no extra buffer), and has the kernels that produce the
|
||||
BF16 GEMMs' activation images (`gr_*`, `to_bf16`) write FP16 instead. `STRATA_HIP_PROMPT_F16=0` is the old path, so every
|
||||
number below is the same binary with and without that switch.
|
||||
|
||||
## Rig
|
||||
|
||||
- 2x AMD Radeon RX 6900 XT 16 GB (gfx1030), each PCIe 4.0 x8, no P2P used. AMD Ryzen 5 5600X, 128 GB DDR4-3200.
|
||||
- Ubuntu 26.04, kernel 7.0.0-38-generic, ROCm 10.0.0 (HIP 7.15.26333, rocBLAS 5.6).
|
||||
- Engine: `origin/main` at `6f32ec0` (0.1.39) with this change, `-DSTRATA_ENABLE_HIP=ON -DCMAKE_HIP_ARCHITECTURES=gfx1030
|
||||
-DSTRATA_PREFILL_MMQ=ON -DSTRATA_NATIVE_EXPERTS=ON`.
|
||||
- Model: Qwen3.8-Flash-Next GSQ-RCO IQ3_S (2 shards, `--native`), MTP draft layer on, `--kv int8 --kv-resident 32768
|
||||
--max-context 131072`, `--expert-cache auto`. Not one of setup's models (see the limits).
|
||||
|
||||
## Why: rocBLAS on gfx1030
|
||||
|
||||
A 7.3K-token prompt on one card (rocprofv3, 0.1.38): rocBLAS `Cijk_..._HSS_..._MT64x32x8` (FP16 in, FP32 out, a fallback
|
||||
tile) was 54% of the prompt's 15.2 s of GPU time, the BF16 products (`BSS`) another ~15%. Microbenchmark, N = 10240,
|
||||
T = 7313, K = 2560:
|
||||
|
||||
| rocBLAS GEMM on gfx1030 | TFLOPS |
|
||||
| --- | ---: |
|
||||
| FP16 in, FP32 out (HSS) | 5.6 |
|
||||
| BF16 in, any out | ~5.3 |
|
||||
| **FP16 in, FP16 out (HH, fp32 accumulate)** | **37.7** |
|
||||
|
||||
## Prompt speed
|
||||
|
||||
One card, cold server per run, the first message of each chat below read by the batched prompt path; the
|
||||
server's own line `strata serve: prompt N tokens = 0 reused + N read in T ms`. Three runs per path (two of them the
|
||||
distribution check's), all within 0.3%:
|
||||
|
||||
| Prompt tokens | old path (`STRATA_HIP_PROMPT_F16=0`) | FP16 | gain | read time, old -> FP16 |
|
||||
| ---: | ---: | ---: | ---: | ---: |
|
||||
| 9,427 | 439 tok/s | 744 tok/s | +69% | 21.5 s -> 12.7 s |
|
||||
| 34,659 | 466 tok/s | 915 tok/s | +96% | 74.4 s -> 37.9 s |
|
||||
| 105,811 | 461 tok/s | 926 tok/s | +101% | 229.5 s -> 114.3 s |
|
||||
|
||||
Decode is unchanged within the noise (44-48 tok/s on one card): it does not use these GEMMs.
|
||||
|
||||
Both cards, cold server per run, one 8,275- and one 33,586-token prompt of repository text (128 tokens answered):
|
||||
|
||||
| | old path | FP16 | gain |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| `--layer-split auto` (25 / 23 layers), 8.3K | 444 tok/s | 833 tok/s | +88% |
|
||||
| `--layer-split auto`, 33.6K | 707 tok/s | 1,356 tok/s | +92% |
|
||||
| one card + the second as expert helper (`--expert-cache-device1 auto --remote-expert-opt`), 8.3K | 432 tok/s | 798 tok/s | +85% |
|
||||
| the same, 33.6K | 454 tok/s | 887 tok/s | +95% |
|
||||
|
||||
Decode is the same within the noise in both setups (these short answers: 51-58 tok/s split, 36-42 helper).
|
||||
|
||||
## Range check
|
||||
|
||||
FP16 ends at 65504 where BF16 reaches 3.4e38, so the precondition is that nothing the prompt path feeds these GEMMs or gets
|
||||
out of them leaves FP16's range. Measured on 0.1.38 with a diagnostic counter (not part of this change) over 33.7K tokens of
|
||||
English docs, 43.3K of Chinese logs and 32.4K of C++ on one card: every BF16 GEMM's X and W and every FP16 output - **0**
|
||||
values beyond 65504, **0** infinities. Non-zero |x| < 6.1e-5 (FP16's subnormal band): 0.008-1.8% of X, 0.1-0.9% of W.
|
||||
Out-of-range values would still be finite: the activation writers saturate to +-65504 (`hf_sat`, as the FP16 images
|
||||
already did), a NaN stays a NaN.
|
||||
|
||||
## Distribution check (teacher-forced)
|
||||
|
||||
The method of `bench/results/2026-10-03-v100-prompt-attn` (`docs/UNSLOTH_Q4.md`): `STRATA_LOGPOS=<file>
|
||||
STRATA_LOGPOS_TOPK=256` on a serve engine (one card, `--short-read 640 --adapt-every 100000`, greedy). Each chat is a long
|
||||
first message of this repository's docs and source (9,427 / 34,659 / 105,811 tokens, disjoint, read by the batched prompt
|
||||
path - the code under test), a fixed assistant reply, and a last message of ~580 tokens of other repository text that is
|
||||
read through the verify windows, where every position is scored: 578 / 519 / 481 positions. KL is P || Q over P's top 256
|
||||
plus one bucket for the rest.
|
||||
|
||||
| comparison | KL mean (9K / 35K / 106K) | KL median | argmax same | top-10 overlap |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| old path vs old path (a second run) | 0 / 0 / 0 | 0 | 100% | 100% |
|
||||
| FP16 vs FP16 (a second run) | 0 / 0 / 0 | 0 | 100% | 100% |
|
||||
| old path vs FP16 | 0.032 / 0.021 / 0.019 | 0.0056 / 0.0041 / 0.0038 | 94.5 / 95.0 / 96.5% | 91.4 / 92.8 / 91.5% |
|
||||
| **reference vs old path** | 0.061 / 0.033 / 0.023 | 0.0092 / 0.0096 / 0.0055 | 93.1 / 93.3 / 93.8% | 90.6 / 92.8 / 91.9% |
|
||||
| **reference vs FP16** | 0.020 / 0.015 / 0.028 | 0.0034 / 0.0044 / 0.0038 | 95.5 / 96.5 / 95.8% | 92.7 / 93.2 / 91.6% |
|
||||
|
||||
The reference is the old path with `STRATA_PREFILL_BF16X2=1` (every BF16 activation image carries its low part too, close
|
||||
to fp32 activations; 393-411 tok/s). The FP16 path is closer to it than the old path in all three chats by median KL and
|
||||
argmax agreement, and by mean KL in two of three (in the 106K chat the FP16 path's p99 is lower, 0.20 vs 0.24, and its mean is pulled up
|
||||
by a few positions beyond that). That is the expected direction: an FP16 activation keeps 10 mantissa bits where BF16 keeps 7, which outweighs
|
||||
rounding the GEMM's output to FP16 before it is widened.
|
||||
|
||||
A rounding-order control as in the V100 report did not work here: `--prefill 2048` instead of auto (8192; the smaller
|
||||
chunk did take effect, 1,059 lent slots instead of the 8192-token ring) gave results **bit-identical** to the old path, so on
|
||||
this path the chunking does not change the arithmetic and cannot serve as a control.
|
||||
|
||||
## Where the rounding moves
|
||||
|
||||
Round-to-nearest bounds per value rounded, for values in FP16's normal range (|x| ≥ 6.1e-5; below that FP16 is
|
||||
subnormal and its relative error grows): BF16 keeps 8 significant bits (≤ 2^-8 ≈ 0.39%), FP16 keeps 11
|
||||
(≤ 2^-11 ≈ 0.049%) but ends at 65504. Both paths accumulate in fp32.
|
||||
|
||||
| | old path | FP16 path |
|
||||
| --- | --- | --- |
|
||||
| BF16-weight products: hyper-connection down / up / inject, router, shared-expert gate, indexer, `ssm_alpha` / `ssm_beta`, PLE | W: the pack's BF16; **X rounded to BF16** (≤ 0.39% per element); Y fp32 | W converted to FP16 (exact in the normal range; 0.1-0.9% of these weights lie below it); X rounded to FP16 (≤ 0.049% per element); **Y rounded to FP16** (≤ 0.049%), then widened |
|
||||
| quantized-weight products through `Gemm::native` / `Gemm::f16`, beta = 0 (X and W already FP16 in both paths) | Y fp32 | **Y rounded to FP16** (≤ 0.049%), then widened |
|
||||
|
||||
So the change removes most of the BF16 activation rounding and adds an FP16 rounding of every output. The two are not
|
||||
directly comparable - an element's rounding enters a K-long dot product, where it partly cancels or adds up depending
|
||||
on the data - which is why the distribution check above measures the net effect instead of arguing it. The reference
|
||||
(`STRATA_PREFILL_BF16X2=1`) differs from the old path in exactly the term this change shrinks (it adds the BF16 low part
|
||||
to every one of these activations), and the FP16 path comes out closer to it: by median KL and argmax agreement in all
|
||||
three chats, by mean KL in two (the 106K chat's mean is pulled up by a few positions beyond its lower p99).
|
||||
|
||||
Not shown: the reference is not an fp32 forward pass, and this is one model, one corpus and 1,578 positions. A comparison
|
||||
against an fp32 forward pass (e.g. llama.cpp on the CPU) is a follow-up for when time allows. Range is
|
||||
the cost: the range check found nothing beyond 65504 here, but a model whose activations or BF16 weights reach that far
|
||||
would need `STRATA_HIP_PROMPT_F16=0`.
|
||||
|
||||
## Output checks
|
||||
|
||||
- **Needle recall** (`tools/needle_bench.py --lengths 8k,32k,128k --depths 10,30,50,70,90`, greedy, thinking off, a fresh
|
||||
server per path, one card; prompts of 7.9K, 32.2K and 125.4K tokens): **15 of 15 found with the old path, 15 of 15 with
|
||||
FP16.** A 125K-token read takes 275 s on the old path, 139 s with FP16 (the later depths are shorter because the server
|
||||
reuses the checkpoint of the text before the needle).
|
||||
- **Repeatability:** two runs of each path give bit-identical log-probabilities (table above), and no run of this report
|
||||
(17 server starts) stalled.
|
||||
|
||||
## What changed
|
||||
|
||||
- `Gemm::set_f16_io(bool)`: off by default, so every caller other than the prompt path (e.g. `gemm_bf16_parity`) runs what
|
||||
it ran. `Prefill::init` turns it on together with `set_act_f16` when `prompt_f16()` is true for its device.
|
||||
- `prompt_f16()`: gfx103x, cached per device (a layer split can mix cards); `STRATA_HIP_PROMPT_F16=0/1` overrides.
|
||||
- With it on: `Gemm::f16` / `Gemm::native` run HH into Y's own rows (`ldc = 2 ldy` halves) and `widen_rows_f16` widens each
|
||||
row from its end; `Gemm::bf16` converts W row slices to FP16 through the existing dequantization scratch (as
|
||||
`native()` does) and takes X as the FP16 image. `bf16x2_mode()` is 0 there (the BF16 low part has no FP16 meaning).
|
||||
- No new device memory, no host sync, no fallback branch; CUDA builds are unchanged (`#if defined(__HIPCC__)`).
|
||||
|
||||
## Limits
|
||||
|
||||
- One machine, one model (IQ3_S, packed from the GSQ-RCO GGUF, not one of setup's), one corpus for the distribution check.
|
||||
- gfx1031/1032 (the same rocBLAS family) are enabled by the same `gfx103` prefix but were not run.
|
||||
@@ -0,0 +1,55 @@
|
||||
# 2x RX 6900 XT (gfx1030): a helper GPU's experts and the PCIe share (2026-10-04)
|
||||
|
||||
With a second GPU holding experts (`--expert-cache-device1`, `--remote-expert-opt`), the verify window's GPU plan counted
|
||||
the experts that helper holds among the layer's misses and moved part of them - the PCIe share, `pcie_frac` of the misses
|
||||
- to the primary card over PCIe, where the primary computed them while the helper sat idle for them. The plan runs before
|
||||
`RemoteExperts::begin()` claims its rows, and `begin()` only takes the rows the plan left alone. #854 makes the plan treat
|
||||
a helper's experts as it treats the peer tier's: not a miss, never in the PCIe share. The numbers below are the engine's
|
||||
own `STRATA_DECODE_TIMING=1` report per request.
|
||||
|
||||
## Rig
|
||||
|
||||
- 2x AMD Radeon RX 6900 XT 16 GB (gfx1030), each PCIe 4.0 x8 through the CPU; no P2P. The engine's probe measured 14.1
|
||||
GB/s host to device and set `pcie_frac` to 0.39 (0.55 is the default without a probe). AMD Ryzen 5 5600X (6 cores,
|
||||
AVX2, no AVX-512; 5 expert-pool workers plus the host thread), 128 GB DDR4-3200.
|
||||
- Ubuntu 26.04, kernel 7.0.0-38-generic, ROCm 10.0.0 (HIP 7.15.26333).
|
||||
- Model: Qwen3.8-Flash-Next GSQ-RCO IQ3_S (2 shards, `--native`), MTP draft layer on, `--kv int8 --kv-resident 32768
|
||||
--max-context 131072`. Configuration [strata-helper.json](strata-helper.json) (`/m` the model directory, `/work` the
|
||||
build; `HIP_VISIBLE_DEVICES=1,0` makes the card on PCI bus 0f the primary): `--expert-cache auto
|
||||
--expert-cache-device1 auto --remote-expert-opt --spec 4 --spec-min-p 0.5`. The primary's cache holds 4,861 experts
|
||||
(9.26 GiB), the helper 8,163 more (15.46 GiB); 24,576 experts in all.
|
||||
- Engines, all `-DSTRATA_ENABLE_HIP=ON -DCMAKE_HIP_ARCHITECTURES=gfx1030 -DSTRATA_PREFILL_MMQ=ON`:
|
||||
- *baseline*: `origin/main` at `6f32ec0` (0.1.39) with #835 (`6add1a7`; #835 changes the prompt path's GEMMs only,
|
||||
not the decode path measured here - its prompt tok/s are therefore higher than the fix's below);
|
||||
- *baseline, same base* (2026-10-05): `6f32ec0` unchanged;
|
||||
- *fix*: `6f32ec0` with #854 (`ffca1b0`).
|
||||
|
||||
## Method
|
||||
|
||||
A fresh server per arm. Two requests of about 5,000 prompt tokens each (two offsets into one text), "Summarize the
|
||||
document above in about 300 words", sampled (temperature 1.0, top-p 0.95, top-k 20), up to 600 tokens; the engine's
|
||||
`strata decode timing` line per request. Milliseconds per verify window and its parts are the measure; tok/s also
|
||||
depends on how many drafts a sampled answer accepts and varies more.
|
||||
|
||||
## Results
|
||||
|
||||
| arm | ms / window | of which: GPU-reach wait | per-layer host | PCIe entries / layer-window | CPU entries / layer-window | decode tok/s |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| baseline (`6add1a7`), probed share 0.39 | 59.6 / 58.5 | 43.4 / 43.7 | 8.6 / 8.3 | 4.00 / 3.99 | 0.83 / 0.43 | 40.6 / 39.3 |
|
||||
| baseline, same base (`6f32ec0`), probed share 0.39 | 60.4 / 57.8 | 43.7 / 43.0 | 8.8 / 8.3 | 4.07 / 3.91 | 0.95 / 0.38 | 41.0 / 37.5 |
|
||||
| **fix, probed share 0.39** | **37.2 / 33.8** | 19.5 / 18.6 | 9.9 / 9.1 | 0.26 / 0.06 | 1.40 / 0.57 | **65.6 / 67.7** |
|
||||
| fix, probed share 0.39, run 2 (2026-10-05) | 35.2 / 33.8 | 18.8 / 18.7 | 9.3 / 9.1 | 0.20 / 0.06 | 1.17 / 0.56 | 63.0 / 64.4 |
|
||||
| fix, `--pcie-frac 0` | 36.5 / 35.1 | 18.8 / 18.8 | 10.1 / 9.5 | 0 / 0 | 1.61 / 0.84 | 66.9 / 67.1 |
|
||||
| baseline (`6add1a7`), `--pcie-frac 0` (the workaround) | 34.6 / 34.1 | 18.2 / 18.7 | 9.5 / 9.3 | 0 / 0 | - | 60.5 / 67.7 |
|
||||
|
||||
"GPU-reach wait" is the host waiting for the GPU to reach the next layer: with the share on, the primary spent 24 ms more
|
||||
per window reading and computing about 4 experts per layer-window that the helper also held (the "PCIe entries"
|
||||
column), and the helper waited. With the fix the share keeps only experts no GPU holds: 0.06-0.26 per layer-window here,
|
||||
and the probed share costs nothing against `--pcie-frac 0` while taking a little off the CPU (1.40 -> 1.61 CPU entries
|
||||
without it). Before the fix, `--pcie-frac 0` was the way around it.
|
||||
|
||||
## Limits
|
||||
|
||||
One machine, one model, two prompts per arm; one server run per arm, two for the fix with the probed share (one on each day). The change
|
||||
is host code and backend-independent, but only HIP and only one helper were run; three or four helpers go through the same
|
||||
loop and were not tested. The prompt path does not use this plan and is unaffected by the change.
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"exe": "/work/build-hip/strata",
|
||||
"args": [
|
||||
"--pack",
|
||||
"/m/packs/iq3_s",
|
||||
"--native",
|
||||
"/m/gguf/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00001-of-00002.gguf",
|
||||
"--ple-gguf",
|
||||
"/m/gguf/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00002-of-00002.gguf",
|
||||
"--expert-profile",
|
||||
"/work/data/expert-profile.bin",
|
||||
"--expert-cache",
|
||||
"auto",
|
||||
"--prefill",
|
||||
"auto",
|
||||
"--spec",
|
||||
"4",
|
||||
"--spec-min-p",
|
||||
"0.5",
|
||||
"--mtp",
|
||||
"/m/mtp/rt",
|
||||
"--max-context",
|
||||
"131072",
|
||||
"--kv",
|
||||
"int8",
|
||||
"--kv-resident",
|
||||
"32768",
|
||||
"--expert-cache-device1",
|
||||
"auto",
|
||||
"--remote-expert-opt"
|
||||
],
|
||||
"cwd": "/work",
|
||||
"tokenizer": "/m/packs/iq3_s/tokenizer",
|
||||
"model_name": "qwen3.8-flash-next-iq3_s",
|
||||
"log": "/work/logs/strata.log",
|
||||
"lib_dirs": [
|
||||
"/opt/rocm/lib"
|
||||
],
|
||||
"port": 8089,
|
||||
"backend": "hip",
|
||||
"gpu": 1,
|
||||
"gpus_asked": true,
|
||||
"env": {
|
||||
"HIP_VISIBLE_DEVICES": "1,0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
# Windows mapped expert-page release
|
||||
|
||||
Measured October 4, 2026, against upstream `6f32ec070f23ced9f50e704d854d775da52591ab` (0.1.39).
|
||||
The same patched executable was run with `STRATA_FILE_RELEASE=0` and `STRATA_FILE_RELEASE=1`.
|
||||
Releasing file-backed expert pages after GPU copies substantially increased available physical RAM on this
|
||||
machine. Prefill was slightly slower. This is a memory-pressure improvement, not a general speed claim.
|
||||
|
||||
## What changes
|
||||
|
||||
Windows `FileExpertSource::release()` uses `VirtualUnlock` on complete interior pages of its read-only file views.
|
||||
Windows documents that unlocked pages are removed from the process working set even when the call returns
|
||||
`FALSE` with `ERROR_NOT_LOCKED`. Other errors are reported. The mapping stays valid and later reads fault in the
|
||||
original bytes. Shared boundary pages are retained. The code resolves file spans directly, so it does not trim
|
||||
resident complements, CUDA-pinned buffers or the temporary buffers used to assemble GGUF experts.
|
||||
|
||||
Existing cache-upload release calls use this implementation. Two additional calls release pages after prefill's
|
||||
lent cache slots have finished refilling, in serving and one-shot generation. Other platforms are unchanged.
|
||||
`STRATA_FILE_RELEASE=1` enables it; unset, `0`, or any other value leaves it disabled. The switch is read once
|
||||
per engine process, so restart the engine when changing it.
|
||||
|
||||
This builds on the existing working-set handling in [#467](https://github.com/Niko1221/Strata/issues/467) and the
|
||||
mapped-arena work in [#640](https://github.com/Niko1221/Strata/pull/640). Multi-GPU placement already exists upstream;
|
||||
this patch does not implement it. See Microsoft's [VirtualUnlock documentation](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualunlock).
|
||||
|
||||
## Configuration
|
||||
|
||||
| Component | Measured configuration |
|
||||
| --- | --- |
|
||||
| System | Windows 11 Pro build 26200, Core i9-14900HX, 31.77 GiB usable RAM, 2 x 16 GB DDR5-5600 |
|
||||
| GPU 0 | RTX 4080 Laptop, 12,282 MiB VRAM; also drives the desktop |
|
||||
| GPU 1 | RTX 3090, 24,576 MiB VRAM, Thunderbolt eGPU |
|
||||
| Links | Engine H2D probes approximately 12.3-12.9 GB/s and 2.4 GB/s respectively; no CUDA peer read/write |
|
||||
| Driver | NVIDIA 616.56 |
|
||||
| Storage | Model and pack on an internal WD PC SN560 NVMe |
|
||||
| Build | MSVC 19.29.30157, CUDA 13.1.80, Release, AVX2 portable, SM 86/89, native experts enabled |
|
||||
| Model | `ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF`, IQ2_XS, with MTP |
|
||||
| Placement | GPUs `0,1`, layer split 12, stage-weight trim, mapped experts |
|
||||
| Runtime | Context 32768, int8 KV, 15 pool workers, auto expert cache and prefill, spec 4, minimum draft probability 0.5 |
|
||||
|
||||
GPU 0 held 6,144 expert pairs (about 8.00 GiB), GPU 1 held 13,381 (about 18.15 GiB); 19,525 of 24,576 pairs in total.
|
||||
Prefill selected 8192 and borrowed approximately 3.77 GiB of expert cache on each GPU. The native pack used
|
||||
`experts.bin` (35,454,976,000 bytes), `dense.bin` (1,538,035,200 bytes), and the GGUF PLE table on disk.
|
||||
Power was AC; the existing power plan, drivers and system-managed pagefile were not changed.
|
||||
|
||||
Model revision: `ed59f92082b1e93c0e96d60a8b11aab089b52f09`.
|
||||
MTP checkpoint revision: `de4b8e4d43b917e7706784d8bb445c9af86a3540`.
|
||||
llama.cpp revision: `3cf03257f219afbe7334045ff7c6a06ac68c627d`.
|
||||
The two downloaded GGUF shards were fully SHA-256 verified:
|
||||
|
||||
| Shard | Bytes | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| 1 | 39,225,954,592 | `92cee27ae5bbadcd732416a0f7a7f0acc092399dbbe8f5a5efa707c2ec0a49d7` |
|
||||
| 2 | 28,800,138,432 | `316b46f3a2dbd68c900f43136ab9449f9dcc3725dfd8c794847c204bc161e113` |
|
||||
|
||||
The measured prototype executable had SHA-256
|
||||
`9118b02dcc0ad4bd8398ba033c641feb93800e0eeecea59ee42be141e4a186fc`.
|
||||
It predates the submitted patch's decision to default the switch to OFF. Explicit `0` and `1` have the same
|
||||
behavior in both versions; the numbers below are from the prototype, not a benchmark of the final rebuild.
|
||||
|
||||
## Measurements
|
||||
|
||||
Each arm had one excluded short warmup followed by three fresh prompts at each length, with 256 output tokens,
|
||||
temperature zero and thinking disabled. Prompts ask for explanations of synthetic integer-processing code.
|
||||
The harness checks that all prompt tokens were processed, no prefix tokens were reused, the response completed,
|
||||
and exactly one request was recorded by the idle dedicated server. Actual prompt lengths were 1023, 4096 and 16384.
|
||||
|
||||
Medians of three requests per cell:
|
||||
|
||||
| Prompt | Prefill OFF / ON, tok/s | Decode OFF / ON, tok/s | Client TTFT OFF / ON, s |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 1K | 263.68 / 253.83 | 77.35 / 77.65 | 3.93 / 4.07 |
|
||||
| 4K | 583.51 / 558.24 | 81.99 / 94.17 | 7.06 / 7.38 |
|
||||
| 16K | 1045.85 / 1000.16 | 80.88 / 84.93 | 15.73 / 16.45 |
|
||||
|
||||
An independent Windows monitor sampled host RAM, process working sets, commit, pagefile use and physical-disk
|
||||
counters approximately once per second. The following window includes warmup and gaps between requests, from
|
||||
the first request's start to the last request's finish. GiB means bytes divided by 2^30.
|
||||
|
||||
| Metric | Release OFF | Release ON |
|
||||
| --- | ---: | ---: |
|
||||
| Available RAM, median | 0.512 GiB | 12.187 GiB |
|
||||
| Available RAM, minimum | 0.277 GiB | 6.110 GiB |
|
||||
| Engine working set, median | 23.379 GiB | 11.946 GiB |
|
||||
| Engine nonprivate working set, median | 20.460 GiB | 8.689 GiB |
|
||||
| System commit, median | 51.880 GiB | 51.973 GiB |
|
||||
| Engine private commit, median | 36.807 GiB | 36.804 GiB |
|
||||
| Pagefile use, median | 1.233 GiB | 1.218 GiB |
|
||||
| Internal physical-disk reads, total | 16.750 GiB | 16.809 GiB |
|
||||
|
||||
The benefit was available physical RAM. Commit was effectively unchanged; this does not solve a commit-limit
|
||||
failure or eliminate the need for pagefile capacity. Physical reads were also effectively unchanged.
|
||||
Nonprivate working-set bytes include file mappings and shared images, not only model pages. Physical-disk
|
||||
counters are system-wide and omit about 1.1-1.3 seconds at the sampled interval edges. The external disk was idle.
|
||||
|
||||
These were sequential OFF-then-ON runs on one machine, with warm engines and retained OS file-cache history.
|
||||
The adaptive expert cache remained enabled. The OS cache was not flushed; this is not a cold-SSD experiment.
|
||||
Three repetitions provide observed ranges, not confidence intervals. Code-explanation outputs differed between
|
||||
the two arms despite identical measured requests; speculative decode rates are workload-dependent. Prefill
|
||||
throughput dropped 3.7-4.4%, and median whole-request time changed by +3.5%, -1.1% and +2.9% at 1K, 4K and 16K.
|
||||
These observations do not establish a decode-speed improvement, output equivalence, or results on other hardware.
|
||||
|
||||
## Reproduction and checks
|
||||
|
||||
`benchmark.py` and `monitor_windows.py` are the scripts used for these measurements (the raw per-trial export and the harness
|
||||
self-tests were left out of the repository). The switch was called `STRATA_ARENA_RELEASE` in the prototype that measured this;
|
||||
it is `STRATA_FILE_RELEASE` here, because the old name already means the opposite elsewhere (`=0` keeps experts a GPU holds).
|
||||
The benchmark needs the repository's normal server/tokenizer dependencies; the Windows monitor also needs psutil.
|
||||
|
||||
Start a dedicated server on `127.0.0.1:18080` using a configuration with the runtime settings above, the patched
|
||||
engine, and `"env": {"STRATA_FILE_RELEASE": "0"}`. Wait until the engine is loaded and idle. In another terminal,
|
||||
run the monitor against the server process ID, and then the benchmark (replace the uppercase placeholders):
|
||||
|
||||
```powershell
|
||||
python bench/results/2026-10-04-windows-mapped-release/monitor_windows.py --server-pid SERVER_PID --out off-host.jsonl --stop-file off-monitor.stop
|
||||
```
|
||||
|
||||
```powershell
|
||||
python bench/results/2026-10-04-windows-mapped-release/benchmark.py --pack PACK_DIR --config CONFIG_JSON --label release-off --out results-off --targets 1024,4096,16384 --runs 3 --max-tokens 256
|
||||
```
|
||||
|
||||
Stop the monitor by creating its stop file. Unload and stop the server, change only the release value to `1`,
|
||||
restart, and repeat with new ON output paths. Keep available VRAM, background workloads and model files stable.
|
||||
For host summaries, select samples with `min(started_epoch_s) <= epoch_s <= max(finished_epoch_s)` from the
|
||||
benchmark's results, including warmup. Compute sample medians/minima and physical-disk last-minus-first counters.
|
||||
Sum `windows_memory` counters for `strata.exe` processes only; nonprivate working set is total minus private.
|
||||
|
||||
The C++ tests cover canonical and variable native layouts, full-page alignment, preserved boundary pages,
|
||||
three release/read cycles with byte-for-byte checks, invalid indices, a closed source, and three GGUF roles
|
||||
across two shards. Build with `STRATA_BUILD_TESTS=ON` and `STRATA_NATIVE_EXPERTS=ON`, then run
|
||||
`ctest --test-dir BUILD_DIR -R "^(file_expert_source_test|expert_layout_test)$" --output-on-failure` in fresh
|
||||
processes with the switch unset, `0`, and `1`.
|
||||
|
||||
The final submitted source was rebuilt with the toolchain above. Both C++ tests passed with the switch unset,
|
||||
`0`, and `1`. That rebuilt engine also completed a warmup and one fresh
|
||||
request each at 1023 and 4096 prompt tokens, generating 256 tokens per measured request on both GPUs with release
|
||||
enabled. This was an inference smoke test, not a replacement for the prototype's OFF/ON performance comparison.
|
||||
Its executable SHA-256 is `9f1e96c36b2ddb530422ee37734606491c0f4c3c9b720095c68d63953e89913b`.
|
||||
|
||||
Additional prototype checks with release ON completed 30 requests without runtime errors: 21 of 24 objective
|
||||
checks passed, with three JSON-only checks failing because of Markdown fences. All 24 objective responses and
|
||||
statuses matched the official engine with the same GPU placement, including those three failures. Six prose
|
||||
requests were unscored. Needle retrieval passed near 1K/4K/16K tokens at 50% depth. The functional battery was not
|
||||
run on the release-OFF prototype, and these limited checks do not prove general model quality or token parity.
|
||||
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Serial fresh-prompt benchmark, adapted from the community MI50/RTX 5090 harness.
|
||||
|
||||
Start a dedicated Strata server separately. This script never starts or stops it.
|
||||
Expert adaptation and the OS file cache stay warm between requests; prefix reuse
|
||||
is rejected. Engine file-tier counters describe decode, not physical disk reads.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import platform
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def write_json(path, value):
|
||||
path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def get(url, timeout=30):
|
||||
with urllib.request.urlopen(url, timeout=timeout) as response:
|
||||
return json.load(response)
|
||||
|
||||
|
||||
def digest(value):
|
||||
return hashlib.sha256(json.dumps(value, sort_keys=True).encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def executable_record(config):
|
||||
"""Resolve the configured binary exactly as serve/server.py does, without executing it."""
|
||||
executable = Path(config["exe"])
|
||||
if not executable.is_absolute():
|
||||
executable = Path(config.get("cwd") or ".") / executable
|
||||
executable = executable.resolve()
|
||||
checksum = hashlib.sha256()
|
||||
with executable.open("rb") as source:
|
||||
for block in iter(lambda: source.read(1024 * 1024), b""):
|
||||
checksum.update(block)
|
||||
return {"path": str(executable), "sha256": checksum.hexdigest(), "size_bytes": executable.stat().st_size}
|
||||
|
||||
|
||||
def engine_provenance_record(path, executable):
|
||||
"""Keep compact build provenance and verify its executable artifact, when recorded."""
|
||||
path = path.resolve()
|
||||
raw = path.read_bytes()
|
||||
source = json.loads(raw.decode("utf-8-sig"))
|
||||
artifacts = source.get("artifacts", {})
|
||||
if isinstance(artifacts, list):
|
||||
entries = [(entry["file"], entry) for entry in artifacts]
|
||||
elif isinstance(artifacts, dict):
|
||||
entries = list(artifacts.items())
|
||||
else:
|
||||
raise ValueError("Provenance artifacts must be a list or dictionary")
|
||||
matches = [entry for name, entry in entries
|
||||
if Path(name).name.casefold() == Path(executable["path"]).name.casefold()]
|
||||
if len(matches) > 1:
|
||||
raise ValueError("Provenance has multiple artifacts matching the configured executable")
|
||||
expected = matches[0]["sha256"].lower() if matches else None
|
||||
if expected is not None and expected != executable["sha256"]:
|
||||
raise ValueError("Provenance executable SHA-256 does not match the configured executable")
|
||||
toolchain_keys = ("cmake", "ninja", "msvc", "windows_sdk", "nvcc", "cuda", "cuda_architectures",
|
||||
"cxx_standard", "cuda_standard", "build_type", "portable_avx2", "msvc_runtime",
|
||||
"parallel_jobs", "tests_built")
|
||||
toolchain = {key: value for key, value in source.get("toolchain", {}).items() if key in toolchain_keys}
|
||||
# Release BUILD.json records CUDA and target architectures at the top level.
|
||||
toolchain.update({key: source[key] for key in ("cuda", "archs", "ptx", "portable") if key in source})
|
||||
return {"path": str(path), "sha256": hashlib.sha256(raw).hexdigest(),
|
||||
"version": source.get("strata_version", source.get("version")), "commit": source.get("commit"),
|
||||
"fingerprint": source.get("engine_source_fingerprint", source.get("source_fingerprint")),
|
||||
"toolchain": toolchain, "repository": source.get("fork_repository"),
|
||||
"llama_cpp_commit": source.get("llama_cpp_pinned_commit"),
|
||||
"executable_hash_verified": expected is not None, "recorded_executable_sha256": expected}
|
||||
|
||||
|
||||
def validate_completion(before, after, chunks, expected, finish, done, text):
|
||||
if not done:
|
||||
raise RuntimeError("The stream ended without [DONE]")
|
||||
if not text:
|
||||
raise RuntimeError("The response contained no text")
|
||||
if finish not in ("length", "stop"):
|
||||
raise RuntimeError(f"Unsuccessful finish reason: {finish!r}")
|
||||
if after["totals"]["requests"] != before["totals"]["requests"] + 1:
|
||||
raise RuntimeError("Request counter mismatch; another client or engine restart invalidated this trial")
|
||||
if after["live"]["state"] != "idle" or after["live"].get("queued", 0):
|
||||
raise RuntimeError("The dedicated server was not idle after the request")
|
||||
engine = after["requests"][0]
|
||||
usage_chunk = next((chunk for chunk in reversed(chunks) if "usage" in chunk), None)
|
||||
if not usage_chunk or not usage_chunk.get("timings"):
|
||||
raise RuntimeError("The final stream chunk lacks usage or engine timings")
|
||||
usage, timings = usage_chunk["usage"], usage_chunk["timings"]
|
||||
if engine.get("reused") != 0 or usage["prompt_tokens_details"]["cached_tokens"] != 0:
|
||||
raise RuntimeError("Fresh prompt reused cached tokens; restart this arm before resuming")
|
||||
if engine["prompt_tokens"] != expected or usage["prompt_tokens"] != expected:
|
||||
raise RuntimeError(f"Prompt token count mismatch; expected {expected}")
|
||||
if engine.get("prompt_total") != expected or engine.get("prompt_read") != expected:
|
||||
raise RuntimeError("The engine did not report reading the complete prompt")
|
||||
if engine["finish"] != finish or engine["output_tokens"] != usage["completion_tokens"]:
|
||||
raise RuntimeError("Stream usage or finish reason does not match /metrics")
|
||||
if timings["prompt_n"] != expected or timings["predicted_n"] != usage["completion_tokens"]:
|
||||
raise RuntimeError("Final stream timings do not match the completed request")
|
||||
for name, timing_name in (("prompt_ms", "prompt_ms"), ("decode_ms", "predicted_ms")):
|
||||
if engine[name] <= 0 or abs(engine[name] - timings[timing_name]) > 0.11:
|
||||
raise RuntimeError(f"Invalid or mismatched engine timing: {name}")
|
||||
if engine["engine_generated"] <= 0:
|
||||
raise RuntimeError("The engine reported no generated tokens")
|
||||
return engine, usage, timings
|
||||
|
||||
|
||||
def perform(url, out, session, label, attempt, req, expected, interval=1.0, timeout=1800):
|
||||
stem = f"{label}-attempt-{attempt}"
|
||||
body = json.dumps(req, ensure_ascii=False).encode("utf-8")
|
||||
(out / f"{stem}-request.json").write_bytes(body + b"\n")
|
||||
row = {"label": label, "attempt": attempt, "session": session, "status": "failed",
|
||||
"expected_prompt_tokens": expected, "request_sha256": hashlib.sha256(body).hexdigest(),
|
||||
"started_epoch_s": time.time(), "raw_stream": f"{stem}-sse.jsonl",
|
||||
"telemetry": f"{stem}-telemetry.jsonl", "text": "", "reasoning": ""}
|
||||
chunks, texts, reasoning, telemetry_errors = [], [], [], []
|
||||
stop = threading.Event()
|
||||
monitor = None
|
||||
started = None
|
||||
|
||||
def sample(output):
|
||||
metrics = get(url + "/metrics")
|
||||
hardware = dict(metrics["hardware"])
|
||||
if hardware.get("ram_total") is not None and hardware.get("ram_used") is not None:
|
||||
hardware["ram_available"] = hardware["ram_total"] - hardware["ram_used"]
|
||||
sample_row = {"epoch_s": time.time(), "metrics_time": metrics.get("time"),
|
||||
"live": metrics["live"], "hardware": hardware,
|
||||
"completed_requests": metrics["totals"]["requests"]}
|
||||
output.write(json.dumps(sample_row) + "\n")
|
||||
output.flush()
|
||||
|
||||
def monitor_loop(output):
|
||||
while not stop.wait(interval):
|
||||
try:
|
||||
sample(output)
|
||||
except Exception as error:
|
||||
telemetry_errors.append(f"{type(error).__name__}: {error}")
|
||||
output.write(json.dumps({"epoch_s": time.time(), "error": telemetry_errors[-1]}) + "\n")
|
||||
output.flush()
|
||||
break
|
||||
|
||||
try:
|
||||
before = get(url + "/metrics")
|
||||
row["metrics_before"] = before
|
||||
if before["live"]["state"] != "idle" or before["live"].get("queued", 0):
|
||||
raise RuntimeError("The dedicated server must already be loaded and idle")
|
||||
if expected + req["max_tokens"] > before["engine"]["max_context"]:
|
||||
raise RuntimeError("Prompt plus output cap exceeds the configured context")
|
||||
with (out / row["telemetry"]).open("w", encoding="utf-8") as telemetry:
|
||||
sample(telemetry)
|
||||
monitor = threading.Thread(target=monitor_loop, args=(telemetry,), daemon=True)
|
||||
monitor.start()
|
||||
try:
|
||||
with (out / row["raw_stream"]).open("w", encoding="utf-8") as raw:
|
||||
started = time.perf_counter()
|
||||
first, finish, done = None, None, False
|
||||
wire = urllib.request.Request(url + "/v1/chat/completions", data=body,
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(wire, timeout=timeout) as response:
|
||||
for line in response:
|
||||
raw.write(json.dumps({"elapsed_s": time.perf_counter() - started,
|
||||
"line": line.decode("utf-8")}, ensure_ascii=False) + "\n")
|
||||
raw.flush()
|
||||
if not line.startswith(b"data:"):
|
||||
continue
|
||||
payload = line[5:].strip()
|
||||
if payload == b"[DONE]":
|
||||
done = True
|
||||
break
|
||||
chunk = json.loads(payload)
|
||||
chunks.append(chunk)
|
||||
if chunk.get("error"):
|
||||
raise RuntimeError(f"Stream error: {chunk['error']}")
|
||||
for choice in chunk.get("choices", []):
|
||||
delta = choice.get("delta", {})
|
||||
content = delta.get("content") or ""
|
||||
thought = delta.get("reasoning_content") or ""
|
||||
if (content or thought) and first is None:
|
||||
first = time.perf_counter() - started
|
||||
texts.append(content)
|
||||
reasoning.append(thought)
|
||||
finish = choice.get("finish_reason") or finish
|
||||
row.update(client_ttft_s=first, client_elapsed_s=time.perf_counter() - started,
|
||||
finish_reason=finish, stream_done=done)
|
||||
after = get(url + "/metrics")
|
||||
row["metrics_after"] = after
|
||||
engine, usage, timings = validate_completion(before, after, chunks, expected,
|
||||
finish, done, "".join(texts + reasoning))
|
||||
row.update(engine=engine, usage=usage, timings=timings,
|
||||
prefill_tok_s=expected / (engine["prompt_ms"] / 1000),
|
||||
decode_tok_s=engine["engine_generated"] / (engine["decode_ms"] / 1000))
|
||||
finally:
|
||||
stop.set()
|
||||
monitor.join()
|
||||
try:
|
||||
sample(telemetry)
|
||||
except Exception as error:
|
||||
telemetry_errors.append(f"{type(error).__name__}: {error}")
|
||||
if telemetry_errors:
|
||||
raise RuntimeError("Telemetry failed: " + "; ".join(telemetry_errors))
|
||||
row["status"] = "ok"
|
||||
except urllib.error.HTTPError as error:
|
||||
row["error"] = {"type": type(error).__name__, "message": str(error), "http_status": error.code,
|
||||
"body": error.read().decode("utf-8", errors="replace")}
|
||||
except Exception as error:
|
||||
row["error"] = {"type": type(error).__name__, "message": str(error)}
|
||||
finally:
|
||||
stop.set()
|
||||
if monitor is not None and monitor.is_alive():
|
||||
monitor.join()
|
||||
row.update(text="".join(texts), reasoning="".join(reasoning), finished_epoch_s=time.time())
|
||||
if started is not None and "client_elapsed_s" not in row:
|
||||
row["client_elapsed_s"] = time.perf_counter() - started
|
||||
write_json(out / f"{stem}-record.json", row)
|
||||
write_json(out / f"{stem}-chunks.json", chunks)
|
||||
return row
|
||||
|
||||
|
||||
def summary(rows, targets):
|
||||
groups = {}
|
||||
for target in targets:
|
||||
selected = [r for r in rows if r["label"].startswith(f"tokens-{target}-")]
|
||||
good = [r for r in selected if r["status"] == "ok"]
|
||||
fields = ("client_ttft_s", "client_elapsed_s", "prefill_tok_s", "decode_tok_s")
|
||||
groups[str(target)] = {"successful_runs": len(good), "failed_attempts": len(selected) - len(good),
|
||||
**{field: {"median": statistics.median(v), "min": min(v), "max": max(v)}
|
||||
for field in fields if (v := [r[field] for r in good])}}
|
||||
return groups
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[3])
|
||||
parser.add_argument("--pack", type=Path, required=True)
|
||||
parser.add_argument("--config", type=Path, help="Server configuration, saved with relevant fields only")
|
||||
parser.add_argument("--engine-provenance", type=Path,
|
||||
help="Build provenance JSON or release BUILD.json; requires --config")
|
||||
parser.add_argument("--url", default="http://127.0.0.1:18080")
|
||||
parser.add_argument("--model", default="strata")
|
||||
parser.add_argument("--label", required=True, help="Configuration label, such as iq2-xs-3090-resident")
|
||||
parser.add_argument("--out", type=Path, required=True, help="A separate directory for each configuration")
|
||||
parser.add_argument("--targets", default="1024,4096,16384")
|
||||
parser.add_argument("--runs", type=int, default=3)
|
||||
parser.add_argument("--max-tokens", type=int, default=256)
|
||||
parser.add_argument("--timeout", type=float, default=1800)
|
||||
parser.add_argument("--telemetry-interval", type=float, default=1.0)
|
||||
parser.add_argument("--resume", action="store_true", help="Keep completed trials and preserve failed attempts")
|
||||
args = parser.parse_args()
|
||||
targets = [int(value) for value in args.targets.split(",")]
|
||||
if not targets or min(targets) <= 0 or len(set(targets)) != len(targets):
|
||||
parser.error("Targets must be unique positive token counts")
|
||||
if args.runs < 1 or args.max_tokens < 1 or args.timeout <= 0 or args.telemetry_interval <= 0:
|
||||
parser.error("Runs, output cap, timeout, and telemetry interval must be positive")
|
||||
if args.engine_provenance and not args.config:
|
||||
parser.error("--engine-provenance requires --config to identify the executable")
|
||||
args.root, args.pack, args.out = args.root.resolve(), args.pack.resolve(), args.out.resolve()
|
||||
url = args.url.rstrip("/")
|
||||
sys.path[:0] = [str(args.root), str(args.root / "tools")]
|
||||
from strata_tokenizer import Tokenizer
|
||||
from serve.frontend import ChatTemplate, openai_to_messages
|
||||
|
||||
directory = args.pack / "tokenizer"
|
||||
vocab = json.loads((directory / "vocab.json").read_text(encoding="utf-8"))
|
||||
tokens = [None] * len(vocab)
|
||||
for token, number in vocab.items():
|
||||
tokens[number] = token
|
||||
tokenizer = Tokenizer(tokens, (directory / "merges.txt").read_text(encoding="utf-8").splitlines(),
|
||||
json.loads((directory / "token_type.json").read_text(encoding="utf-8")))
|
||||
template = ChatTemplate(directory / "chat_template.jinja")
|
||||
|
||||
def request(content, maximum=args.max_tokens):
|
||||
return {"model": args.model, "messages": [{"role": "user", "content": content}],
|
||||
"temperature": 0, "reasoning_effort": "none", "max_tokens": maximum,
|
||||
"stream": True, "stream_options": {"include_usage": True}}
|
||||
|
||||
def count(req):
|
||||
messages, tools, kwargs = openai_to_messages(req)
|
||||
return len(tokenizer.encode(template.render(messages, tools, **kwargs), parse_special=True))
|
||||
|
||||
initial = get(url + "/metrics")
|
||||
if initial["live"]["state"] != "idle" or initial["live"].get("queued", 0):
|
||||
raise RuntimeError("Start a dedicated, loaded and idle server before this benchmark")
|
||||
config = {}
|
||||
executable = None
|
||||
if args.config:
|
||||
source = json.loads(args.config.read_text(encoding="utf-8-sig"))
|
||||
executable = executable_record(source)
|
||||
keys = ("exe", "cwd", "args", "gpu", "layer_split", "split_skip_if_fits", "backend", "tokenizer", "log",
|
||||
"model_name", "parallel", "expert_profile_save", "expert_profile_save_every")
|
||||
config = {key: source[key] for key in keys if key in source}
|
||||
compute_variables = {"CUDA_VISIBLE_DEVICES", "CUDA_DEVICE_ORDER", "STRATA_STAGE_TRIM", "STRATA_SPLIT_OWN",
|
||||
"STRATA_ARENA_PIN_GIB", "STRATA_RESIDENT_PIN", "STRATA_RESIDENT_HEADROOM_GIB",
|
||||
"STRATA_ARENA_MMAP", "STRATA_PREFILL_HELP", "STRATA_FETCH_THREADS", "STRATA_LOOKAHEAD",
|
||||
"STRATA_DECODE_TIMING", "STRATA_SPLIT_TIMING", "STRATA_VERIFY_PROFILE",
|
||||
"STRATA_SPLIT_MISS_MS", "STRATA_PF_FUSED", "STRATA_FILE_RELEASE"}
|
||||
config["env"] = {key: value for key, value in source.get("env", {}).items()
|
||||
if key in compute_variables}
|
||||
revision = subprocess.run(["git", "-c", f"safe.directory={args.root.as_posix()}", "rev-parse", "HEAD"],
|
||||
cwd=args.root, capture_output=True, text=True, check=True).stdout.strip()
|
||||
provenance = engine_provenance_record(args.engine_provenance, executable) if args.engine_provenance else None
|
||||
manifest = {"label": args.label, "frontend_source_commit": revision, "pack": str(args.pack),
|
||||
"config": config, "engine_executable": executable,
|
||||
"engine_provenance": provenance, "engine_reported_version": initial["engine"].get("version"),
|
||||
"targets": targets, "runs": args.runs, "max_tokens": args.max_tokens,
|
||||
"model": args.model, "server_model": initial["engine"]["model"],
|
||||
"max_context": initial["engine"]["max_context"],
|
||||
"hardware_static": initial.get("hardware_static"),
|
||||
"tokenizer_sha256": {name: hashlib.sha256((directory / name).read_bytes()).hexdigest()
|
||||
for name in ("vocab.json", "merges.txt", "token_type.json", "chat_template.jinja")},
|
||||
"script_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
||||
"method": "Serial warm-engine trials; default adaptive expert state retained; zero prompt reuse required",
|
||||
"disk_counter_note": "file_mb is the decode file tier, possibly OS-cached; disk_read_mb is system-wide physical I/O"}
|
||||
manifest_path = args.out / "manifest.json"
|
||||
if manifest_path.exists():
|
||||
if not args.resume:
|
||||
raise RuntimeError("Output already has a manifest; use a new output directory or --resume")
|
||||
if digest(json.loads(manifest_path.read_text(encoding="utf-8"))) != digest(manifest):
|
||||
raise RuntimeError("Resume manifest differs; use a new output directory")
|
||||
elif args.resume:
|
||||
raise RuntimeError("Cannot resume: output has no manifest")
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
write_json(manifest_path, manifest)
|
||||
rows_path = args.out / "results.json"
|
||||
rows = json.loads(rows_path.read_text(encoding="utf-8")) if args.resume and rows_path.exists() else []
|
||||
session = uuid.uuid4().hex[:12]
|
||||
write_json(args.out / f"session-{session}.json", {"session": session, "epoch_s": time.time(),
|
||||
"python": sys.version, "platform": platform.platform(), "initial_metrics": initial,
|
||||
"initial_status": get(url + "/v1/status"), "resumed": args.resume})
|
||||
|
||||
def run(label, req):
|
||||
if any(row["label"] == label and row["status"] == "ok" for row in rows):
|
||||
print("skip completed", label, flush=True)
|
||||
return
|
||||
attempt = sum(row["label"] == label for row in rows) + 1
|
||||
print("start", args.label, label, "tokens", count(req), flush=True)
|
||||
row = perform(url, args.out, session, label, attempt, req, count(req),
|
||||
args.telemetry_interval, args.timeout)
|
||||
rows.append(row)
|
||||
write_json(rows_path, rows)
|
||||
write_json(args.out / "summary.json", summary(rows, targets))
|
||||
print(row["status"], label, json.dumps({key: row.get(key) for key in
|
||||
("client_ttft_s", "prefill_tok_s", "decode_tok_s", "error")}), flush=True)
|
||||
if row["status"] != "ok":
|
||||
raise RuntimeError(f"Benchmark stopped after failed trial {label}; evidence was retained")
|
||||
|
||||
run(f"warmup-{session}", request(f"Warmup {session}. Reply with exactly the word READY.", 16))
|
||||
filler = "\n".join(f"def task_{i:05d}(value: int) -> int: return (value * {(i % 97) + 1} + {i}) % 100003"
|
||||
for i in range(12000))
|
||||
ending = ("\n\nWrite a detailed explanation of the code above. Discuss deterministic integer transforms, "
|
||||
"modulo arithmetic, testing, naming, complexity, and maintainability. Write at least 600 words.")
|
||||
for target in targets:
|
||||
for trial in range(1, args.runs + 1):
|
||||
# Checkpoints cover a complete turn or >=16K prompt tokens by default;
|
||||
# this early differing nonce prevents a whole checkpoint from matching.
|
||||
prefix = f"Benchmark nonce: series-{target}-trial-{trial}.\nReview this synthetic Python module:\n"
|
||||
low, high = 0, len(filler)
|
||||
while low < high:
|
||||
middle = (low + high + 1) // 2
|
||||
if count(request(prefix + filler[:middle] + ending)) <= target:
|
||||
low = middle
|
||||
else:
|
||||
high = middle - 1
|
||||
req = request(prefix + filler[:low] + ending)
|
||||
if not target - 20 <= count(req) <= target:
|
||||
raise RuntimeError(f"Could not construct target {target}: {count(req)}")
|
||||
run(f"tokens-{target}-run-{trial}", req)
|
||||
print("Complete:", args.out / "summary.json", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,139 @@
|
||||
"""Record Windows host memory and physical-disk counters during a local experiment."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
import json
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
import psutil
|
||||
|
||||
|
||||
class PerformanceInformation(ctypes.Structure):
|
||||
_fields_ = [("cb", wintypes.DWORD)] + [
|
||||
(name, ctypes.c_size_t) for name in (
|
||||
"CommitTotal", "CommitLimit", "CommitPeak", "PhysicalTotal",
|
||||
"PhysicalAvailable", "SystemCache", "KernelTotal", "KernelPaged",
|
||||
"KernelNonpaged", "PageSize",
|
||||
)
|
||||
] + [(name, wintypes.DWORD) for name in (
|
||||
"HandleCount", "ProcessCount", "ThreadCount",
|
||||
)]
|
||||
|
||||
|
||||
performance = ctypes.WinDLL("psapi", use_last_error=True).GetPerformanceInfo
|
||||
performance.argtypes = [ctypes.POINTER(PerformanceInformation), wintypes.DWORD]
|
||||
performance.restype = wintypes.BOOL
|
||||
|
||||
|
||||
class ProcessMemoryCounters(ctypes.Structure):
|
||||
_fields_ = [("cb", wintypes.DWORD), ("page_fault_count", wintypes.DWORD)] + [
|
||||
(name, ctypes.c_size_t) for name in (
|
||||
"peak_working_set_bytes", "working_set_bytes", "peak_paged_pool_bytes",
|
||||
"paged_pool_bytes", "peak_nonpaged_pool_bytes", "nonpaged_pool_bytes",
|
||||
"commit_bytes", "peak_commit_bytes", "private_commit_bytes",
|
||||
"private_working_set_bytes",
|
||||
)
|
||||
] + [("shared_commit_bytes", ctypes.c_ulonglong)]
|
||||
|
||||
|
||||
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
kernel.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
|
||||
kernel.OpenProcess.restype = wintypes.HANDLE
|
||||
kernel.CloseHandle.argtypes = [wintypes.HANDLE]
|
||||
kernel.CloseHandle.restype = wintypes.BOOL
|
||||
process_memory = ctypes.WinDLL("psapi", use_last_error=True).GetProcessMemoryInfo
|
||||
process_memory.argtypes = [wintypes.HANDLE, ctypes.POINTER(ProcessMemoryCounters), wintypes.DWORD]
|
||||
process_memory.restype = wintypes.BOOL
|
||||
|
||||
|
||||
def extended_memory(process):
|
||||
handle = kernel.OpenProcess(0x1000, False, process.pid) # PROCESS_QUERY_LIMITED_INFORMATION
|
||||
if not handle:
|
||||
if not process.is_running():
|
||||
raise psutil.NoSuchProcess(process.pid)
|
||||
raise ctypes.WinError(ctypes.get_last_error())
|
||||
try:
|
||||
info = ProcessMemoryCounters()
|
||||
info.cb = ctypes.sizeof(info)
|
||||
if not process_memory(handle, ctypes.byref(info), info.cb):
|
||||
raise ctypes.WinError(ctypes.get_last_error())
|
||||
return {name: getattr(info, name) for name, _ in info._fields_ if name != "cb"}
|
||||
finally:
|
||||
kernel.CloseHandle(handle)
|
||||
|
||||
|
||||
def sample(server):
|
||||
info = PerformanceInformation()
|
||||
info.cb = ctypes.sizeof(info)
|
||||
if not performance(ctypes.byref(info), info.cb):
|
||||
raise ctypes.WinError(ctypes.get_last_error())
|
||||
processes = []
|
||||
if server is not None:
|
||||
try:
|
||||
descendants = [server, *server.children(recursive=True)]
|
||||
except psutil.NoSuchProcess:
|
||||
descendants = []
|
||||
for process in descendants:
|
||||
try:
|
||||
with process.oneshot():
|
||||
processes.append({
|
||||
"pid": process.pid,
|
||||
"name": process.name(),
|
||||
"memory": process.memory_info()._asdict(),
|
||||
"windows_memory": extended_memory(process),
|
||||
"cpu_seconds": process.cpu_times()._asdict(),
|
||||
"io_counters": process.io_counters()._asdict(),
|
||||
})
|
||||
except psutil.NoSuchProcess:
|
||||
continue
|
||||
battery = psutil.sensors_battery()
|
||||
return {
|
||||
"epoch_s": time.time(),
|
||||
"memory": psutil.virtual_memory()._asdict(),
|
||||
"pagefile": psutil.swap_memory()._asdict(),
|
||||
"commit_bytes": info.CommitTotal * info.PageSize,
|
||||
"commit_limit_bytes": info.CommitLimit * info.PageSize,
|
||||
"commit_peak_bytes": info.CommitPeak * info.PageSize,
|
||||
"system_cache_bytes": info.SystemCache * info.PageSize,
|
||||
"physical_disks": {
|
||||
name: counters._asdict()
|
||||
for name, counters in psutil.disk_io_counters(perdisk=True).items()
|
||||
},
|
||||
"ac_power": battery.power_plugged if battery is not None else None,
|
||||
"processes": processes,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--server-pid", type=int)
|
||||
parser.add_argument("--out", type=Path, required=True)
|
||||
parser.add_argument("--stop-file", type=Path, required=True)
|
||||
parser.add_argument("--interval", type=float, default=1.0)
|
||||
parser.add_argument("--samples", type=int, help="Optional bounded diagnostic run")
|
||||
args = parser.parse_args()
|
||||
if args.interval <= 0 or (args.samples is not None and args.samples < 1):
|
||||
parser.error("Interval and sample limit must be positive")
|
||||
if args.stop_file.exists():
|
||||
parser.error("The stop file already exists; choose a new experiment path")
|
||||
server = psutil.Process(args.server_pid) if args.server_pid is not None else None
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
with args.out.open("x", encoding="utf-8") as output:
|
||||
count = 0
|
||||
while not args.stop_file.exists():
|
||||
started = time.monotonic()
|
||||
output.write(json.dumps(sample(server)) + "\n")
|
||||
output.flush()
|
||||
count += 1
|
||||
if args.samples is not None and count >= args.samples:
|
||||
break
|
||||
if server is not None and not server.is_running():
|
||||
break
|
||||
time.sleep(max(0, args.interval - (time.monotonic() - started)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,288 @@
|
||||
{
|
||||
"kind": "clean-rotation-only-q2-native-same-binary-ab",
|
||||
"date": "2026-10-04",
|
||||
"source_commit": "18a30ad775bce86e99a54e6dc552ccb72c66d16e",
|
||||
"source_archive_sha256": "a726e0be8b03c40d1c5c09b49da37e28ba6e34ef3fd247bb85adf043de927a88",
|
||||
"engine_sha256": "6ed9db1dcc04b94e9816b73df30730395ff44e42f4def3b46722199aea5d881d",
|
||||
"upstream_base": "6f32ec070f23ced9f50e704d854d775da52591ab",
|
||||
"ggml_commit": "3cf03257f219afbe7334045ff7c6a06ac68c627d",
|
||||
"hardware": {
|
||||
"cpu": "AMD Ryzen 9 7950X 16-Core Processor",
|
||||
"memory": "MemTotal: 130975976 kB",
|
||||
"gpu": "NVIDIA RTX PRO 6000 Blackwell Workstation Edition, 97887, 595.91.07",
|
||||
"kernel": "7.0.0-34-generic",
|
||||
"compiler": "g++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"
|
||||
},
|
||||
"build": {
|
||||
"type": "Release",
|
||||
"cuda": "13.2",
|
||||
"cuda_architectures": "120"
|
||||
},
|
||||
"model": {
|
||||
"repo": "ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF",
|
||||
"revision": "ed59f92082b1e93c0e96d60a8b11aab089b52f09",
|
||||
"verification": "Both contents verified against the pinned Hugging Face LFS SHA-256 values.",
|
||||
"files": [
|
||||
{
|
||||
"path": "Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00001-of-00002.gguf",
|
||||
"size": 37623740192,
|
||||
"sha256": "69820c02ec7d0b45ef2ebb19d6620299db749fe2aded7f39f93c6b88b199b720"
|
||||
},
|
||||
{
|
||||
"path": "Q2_0/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00002-of-00002.gguf",
|
||||
"size": 28800138432,
|
||||
"sha256": "316b46f3a2dbd68c900f43136ab9449f9dcc3725dfd8c794847c204bc161e113"
|
||||
}
|
||||
]
|
||||
},
|
||||
"model_hash_note": "Both downloaded/reused shard contents were verified against the pinned publisher LFS hashes; size/inode/mtime are checked for stability before each arm.",
|
||||
"layout": {
|
||||
"layers": 48,
|
||||
"experts_per_layer": 512,
|
||||
"blob_bytes": 1382400,
|
||||
"gate_up_type": "Q2_0",
|
||||
"down_type": "Q2_0"
|
||||
},
|
||||
"model_formats": "Published GSQ-RCO Q2_0 model with its matching IQ4_NL PLE shard; FP16 KV.",
|
||||
"pack_command_template": [
|
||||
"python",
|
||||
"{source}/tools/iq_pack.py",
|
||||
"--gguf",
|
||||
"{native}",
|
||||
"--out",
|
||||
"{pack}"
|
||||
],
|
||||
"pack_hashes": {
|
||||
"native_experts_sha256": "015e53d3ae36324f172be4942bcf129539b343e2eb4fcd4f25ca5eff180bfa41",
|
||||
"index_sha256": "c758314d495a1261d06a806b1c415596dfabe62e0cbf44ce592dc84c53229ccd",
|
||||
"dense_sha256": "a43d7d0b69525335f4b000d5197a1d3aefcd3c0014fc3e12b3455e0a5d827ba2"
|
||||
},
|
||||
"qualification": "Storage parity on the standard Q2_0 model; no Q8 PLE reader extension or model conversion is required.",
|
||||
"cache_note": "The 12000-slot cache is deliberately smaller than this 96 GB GPU allows to exercise RAM/GPU exchanges. This is not the fastest full-GPU Q2 configuration.",
|
||||
"resident_complement_gib": 16.19,
|
||||
"input_tokens": 1024,
|
||||
"output_tokens": 1024,
|
||||
"allocated_context": 40960,
|
||||
"prompt_task": "Fixed-length storage parity and timing, not a model quality evaluation.",
|
||||
"temperature": 0,
|
||||
"input_token_ids_sha256": "9f8ae9eec9b56e2a04adf499638c0c1c275be8fc34cf627e632a26d3711f65a7",
|
||||
"eos_note": "EOS matching uses the out-of-vocabulary sentinel 2147483647 so all arms generate the requested count even if a normal end-of-turn token occurs.",
|
||||
"args_template": [
|
||||
"--pack",
|
||||
"{pack}",
|
||||
"--native",
|
||||
"{native}",
|
||||
"--expert-cache",
|
||||
"12000",
|
||||
"--resident-budget-gib",
|
||||
"32",
|
||||
"--expert-profile",
|
||||
"{source}/data/expert-profile.bin",
|
||||
"--adapt-every",
|
||||
"4",
|
||||
"--adapt-swaps",
|
||||
"96",
|
||||
"--adapt-decay",
|
||||
"0.7",
|
||||
"--pcie-frac",
|
||||
"-1",
|
||||
"--spec",
|
||||
"2",
|
||||
"--mtp",
|
||||
"{mtp}",
|
||||
"--mtp-max-t",
|
||||
"1",
|
||||
"--suffix-draft",
|
||||
"0",
|
||||
"--kv",
|
||||
"fp16",
|
||||
"--max-context",
|
||||
"40960",
|
||||
"--prompt-cache",
|
||||
"0",
|
||||
"--conversation-cache-mib",
|
||||
"0",
|
||||
"--prefill",
|
||||
"1024",
|
||||
"--no-prefill-borrow",
|
||||
"--vram-reserve-mib",
|
||||
"2048",
|
||||
"--ple-io",
|
||||
"ram",
|
||||
"--ple-row-cache",
|
||||
"1048576",
|
||||
"--eos-ids",
|
||||
"2147483647",
|
||||
"--stats",
|
||||
"--native-dense-gguf",
|
||||
"{model}/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00001-of-00002.gguf",
|
||||
"--native-dense-gguf",
|
||||
"{model}/Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00002-of-00002.gguf"
|
||||
],
|
||||
"environment": {
|
||||
"STRATA_ADAPT_NOWAIT": "0",
|
||||
"STRATA_DECODE_TIMING": "1"
|
||||
},
|
||||
"profile_sha256": "8f59b4aa8873209dff11c11e37bcda9529a1335b724a1afeea37bf6388975baf",
|
||||
"process_policy": "A fresh native engine per arm; no prompt or conversation reuse.",
|
||||
"decoding": "Target-only: --mtp-max-t 1 --suffix-draft 0. Required MTP runtime loaded, zero offered drafts.",
|
||||
"output_tokens_identical": true,
|
||||
"first_differing_output_index": null,
|
||||
"token_hash_encoding": "SHA-256 of UTF-8 compact JSON array, no whitespace or trailing newline.",
|
||||
"token_ids_file": "exchange-rotation-token-ids.json",
|
||||
"token_ids_file_sha256": "2c904cf4a7f41650ec06f54ffa773636114efed8cff059d825e5431f5aa666aa",
|
||||
"arms": [
|
||||
{
|
||||
"rotation": 0,
|
||||
"output_tokens": 1024,
|
||||
"decode_tokens_per_second": 104.89546306634843,
|
||||
"startup_seconds": 23.845058003003942,
|
||||
"wall_seconds": 10.330669555987697,
|
||||
"timings": {
|
||||
"generated": 1024,
|
||||
"prompt_tokens": 1024,
|
||||
"prompt_ms": 567.9,
|
||||
"decode_ms": 9762.1,
|
||||
"finish": "length",
|
||||
"drafts_accepted": 0,
|
||||
"drafts_offered": 0,
|
||||
"reused": 0,
|
||||
"hits": 454042,
|
||||
"lookups": 480592,
|
||||
"ram_blobs": 41285,
|
||||
"file_blobs": 0,
|
||||
"file_mb": 0.0,
|
||||
"prompt_read": 1024,
|
||||
"offloaded": 10928
|
||||
},
|
||||
"rotation_active": false,
|
||||
"rotated_blocks": 0,
|
||||
"resident_exchanges": 3802,
|
||||
"avoided_memcpy_payload_bytes": 0,
|
||||
"engine_info": {
|
||||
"version": "0.1.39",
|
||||
"context": 40960,
|
||||
"kv": "fp16",
|
||||
"kv_resident": 0,
|
||||
"expert_slots": 12000,
|
||||
"expert_cache_mib": 15820,
|
||||
"expert_slots_primary": 12000,
|
||||
"expert_cache_primary_mib": 15820,
|
||||
"spec": 2,
|
||||
"mtp_max": 1,
|
||||
"lookup": 0,
|
||||
"vram_free_mib": 74168,
|
||||
"cvec": 0,
|
||||
"arena_mib": 16579,
|
||||
"pool_workers": 15,
|
||||
"pcie_frac": "0.55",
|
||||
"spec_min_p": "0.00",
|
||||
"conversation_cache_mib": 0,
|
||||
"conversation_cache_slots": 4,
|
||||
"conversation_cache_min_free_mib": 2560,
|
||||
"tail_role_token": -1,
|
||||
"vram_elastic": 0,
|
||||
"engine": "0.1.39"
|
||||
},
|
||||
"diagnostics": [
|
||||
"strata decode timing: 1024 windows, avg T 1.00, 1.00 tokens/window, 9.53 ms/window = verify 8.73 (GPU-reach wait 7.11 + per-layer host 1.28 [plan 0.03 actq 0.02 jobs 0.00 CPU 1.15] + stage 0.01) + commit/emit 0.01 + draft 0.00; per layer-window: CPU experts 0.54 (0.54 entries), VRAM hits 9.24, PCIe 0.22",
|
||||
"strata serve: resident RAM: 16.19 GiB of experts in RAM, 3802 exchanged with the VRAM tier, 0 blob reads from the file"
|
||||
],
|
||||
"output_token_ids_sha256": "b384e4b16b5bd1c41f4739d053eb50d100a9cbfdb18fc5b76e31d188015961be"
|
||||
},
|
||||
{
|
||||
"rotation": 1,
|
||||
"output_tokens": 1024,
|
||||
"decode_tokens_per_second": 113.44249218975028,
|
||||
"startup_seconds": 13.035045650991378,
|
||||
"wall_seconds": 9.620121874992037,
|
||||
"timings": {
|
||||
"generated": 1024,
|
||||
"prompt_tokens": 1024,
|
||||
"prompt_ms": 592.7,
|
||||
"decode_ms": 9026.6,
|
||||
"finish": "length",
|
||||
"drafts_accepted": 0,
|
||||
"drafts_offered": 0,
|
||||
"reused": 0,
|
||||
"hits": 454042,
|
||||
"lookups": 480592,
|
||||
"ram_blobs": 41285,
|
||||
"file_blobs": 0,
|
||||
"file_mb": 0.0,
|
||||
"prompt_read": 1024,
|
||||
"offloaded": 10928
|
||||
},
|
||||
"rotation_active": true,
|
||||
"rotated_blocks": 3802,
|
||||
"resident_exchanges": 3802,
|
||||
"avoided_memcpy_payload_bytes": 5255884800,
|
||||
"engine_info": {
|
||||
"version": "0.1.39",
|
||||
"context": 40960,
|
||||
"kv": "fp16",
|
||||
"kv_resident": 0,
|
||||
"expert_slots": 12000,
|
||||
"expert_cache_mib": 15820,
|
||||
"expert_slots_primary": 12000,
|
||||
"expert_cache_primary_mib": 15820,
|
||||
"spec": 2,
|
||||
"mtp_max": 1,
|
||||
"lookup": 0,
|
||||
"vram_free_mib": 74168,
|
||||
"cvec": 0,
|
||||
"arena_mib": 16579,
|
||||
"pool_workers": 15,
|
||||
"pcie_frac": "0.55",
|
||||
"spec_min_p": "0.00",
|
||||
"conversation_cache_mib": 0,
|
||||
"conversation_cache_slots": 4,
|
||||
"conversation_cache_min_free_mib": 2560,
|
||||
"tail_role_token": -1,
|
||||
"vram_elastic": 0,
|
||||
"engine": "0.1.39"
|
||||
},
|
||||
"diagnostics": [
|
||||
"FileExpertSource: exchange buffer rotation enabled: 96 buffers, 1382400 bytes each; no host commit memcpy",
|
||||
"strata decode timing: 1024 windows, avg T 1.00, 1.00 tokens/window, 8.82 ms/window = verify 8.41 (GPU-reach wait 6.83 + per-layer host 1.23 [plan 0.03 actq 0.02 jobs 0.01 CPU 1.10] + stage 0.01) + commit/emit 0.01 + draft 0.00; per layer-window: CPU experts 0.54 (0.54 entries), VRAM hits 9.24, PCIe 0.22",
|
||||
"strata serve: resident RAM: 16.19 GiB of experts in RAM, 3802 exchanged with the VRAM tier, 0 blob reads from the file",
|
||||
"strata serve: exchange rotation: 3802 blocks, 5255884800 host memcpy bytes avoided (cumulative payload)"
|
||||
],
|
||||
"output_token_ids_sha256": "b384e4b16b5bd1c41f4739d053eb50d100a9cbfdb18fc5b76e31d188015961be"
|
||||
}
|
||||
],
|
||||
"validation": [
|
||||
{
|
||||
"name": "configure",
|
||||
"exit_code": 0,
|
||||
"seconds": 2.0173747100052424
|
||||
},
|
||||
{
|
||||
"name": "build",
|
||||
"exit_code": 0,
|
||||
"seconds": 74.66783442400629
|
||||
},
|
||||
{
|
||||
"name": "ctest",
|
||||
"exit_code": 0,
|
||||
"seconds": 0.36451481201220304
|
||||
},
|
||||
{
|
||||
"name": "rotation-gpu",
|
||||
"exit_code": 0,
|
||||
"seconds": 0.4641803269914817
|
||||
},
|
||||
{
|
||||
"name": "memcheck",
|
||||
"exit_code": 0,
|
||||
"seconds": 0.5145799079909921
|
||||
}
|
||||
],
|
||||
"comparison_exit_code": 0,
|
||||
"limitations": [
|
||||
"One off/on pair; no general speedup or speculative-path token-parity claim.",
|
||||
"GPU cache deliberately capped to exercise exchanges; Q2 can fit fully on this 96 GB card.",
|
||||
"EOS ignored for exact output length; not an answer-quality measurement.",
|
||||
"AMD and Windows GPU execution untested."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"input":[248045,846,198,8441,8129,318,1856,10895,1590,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,13,198,760,50245,8129,7276,18541,13017,321,7262,198,3601,314,8129,13,271,2427,38453,494,220,16,13,8984,1132,23173,6572,24959,18101,539,12258,13,13262,24217,2980,279,2468,3798,13,2233,16308,11,1970,11,59429,466,15673,13,248046,198,248045,74455,198,248068,271,248069,271],"off":[16,220,17,220,18,220,19,220,20,220,21,220,22,220,23,220,24,220Line truncated
|
||||
+19
-2
@@ -8,10 +8,11 @@ endif()
|
||||
# maintainers; gfx1101 (RX 7800 XT, #254) and gfx1200 (RX 9060 XT, #256) by their owners. gfx1102 (RX 7600) has the
|
||||
# same LDS limit and dot4 instruction and passed ctest (#192), but no model run has been reported yet. RDNA2 gfx1030 (RX 6800 / 6900) has the
|
||||
# same LDS limit and wave32 but an older dot4 instruction (v_dot4_i32_i8, hip_compat/intrinsics.hpp); a community
|
||||
# report ran it (#311), the maintainers have not.
|
||||
# report ran it (#311), the maintainers have not. gfx1151 (Ryzen AI Max / Strix Halo, Radeon 8060S, RDNA3.5) is the
|
||||
# same wave32 / 64 KiB LDS / sudot4 family as gfx1100, on a unified-memory APU; experimental.
|
||||
set(_strata_hip_validated gfx1100 gfx1201)
|
||||
set(_strata_hip_community gfx1101 gfx1200)
|
||||
set(_strata_hip_unvalidated gfx1102 gfx1030)
|
||||
set(_strata_hip_unvalidated gfx1012 gfx1102 gfx1030 gfx1031 gfx1034 gfx1151)
|
||||
# CMake hands HIP a ';' list, but a -DCMAKE_HIP_ARCHITECTURES typed by hand (or ROCm's own Windows tooling) may use
|
||||
# spaces, which foreach(IN LISTS) would otherwise treat as one element.
|
||||
string(REPLACE " " ";" _strata_hip_norm "${CMAKE_HIP_ARCHITECTURES}")
|
||||
@@ -44,6 +45,13 @@ string(REPLACE ";" "," STRATA_HIP_ARCHS "${STRATA_HIP_ARCH_LIST}")
|
||||
enable_language(HIP)
|
||||
find_package(hip CONFIG REQUIRED)
|
||||
find_package(hipblas CONFIG REQUIRED)
|
||||
# Older distro hipBLAS has no workspace API. The compatibility shim uses
|
||||
# rocBLAS directly for that version; newer hipBLAS keeps its existing path.
|
||||
set(STRATA_HIP_BLAS_TARGETS roc::hipblas)
|
||||
if(hipblas_VERSION VERSION_LESS "1.0")
|
||||
find_package(rocblas CONFIG REQUIRED)
|
||||
list(APPEND STRATA_HIP_BLAS_TARGETS roc::rocblas)
|
||||
endif()
|
||||
find_package(hipblaslt CONFIG QUIET)
|
||||
|
||||
if(NOT TARGET hip::host)
|
||||
@@ -67,6 +75,15 @@ add_library(strata_hip_runtime INTERFACE)
|
||||
target_include_directories(strata_hip_runtime BEFORE INTERFACE
|
||||
"${STRATA_HIP_COMPAT_INCLUDE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/include")
|
||||
target_compile_definitions(strata_hip_runtime INTERFACE STRATA_USE_HIP=1 "STRATA_HIP_ARCHS=\"${STRATA_HIP_ARCHS}\"")
|
||||
option(STRATA_GFX1012_PORTABLE_DOT "Use the portable signed-byte dot control on gfx1012" OFF)
|
||||
if(STRATA_GFX1012_PORTABLE_DOT)
|
||||
target_compile_definitions(strata_hip_runtime INTERFACE STRATA_GFX1012_PORTABLE_DOT=1)
|
||||
endif()
|
||||
# #313: the RDNA3 WMMA GEMM's intrinsics only compile for gfx11 targets and the host pass does not define
|
||||
# __gfx11xx__, so the build supplies the macro for gfx11 configs (the kernels also gate on the device at runtime)
|
||||
if(CMAKE_HIP_ARCHITECTURES MATCHES "gfx11")
|
||||
target_compile_definitions(strata_hip_runtime INTERFACE STRATA_WMMA_GFX11=1)
|
||||
endif()
|
||||
target_link_libraries(strata_hip_runtime INTERFACE hip::host)
|
||||
# The shim renames the CUDA runtime to HIP, force-included into every host and device source. On Windows the host
|
||||
# compiler is ROCm's clang++ too (tools/hip/build_windows.bat: CMake refuses to mix cl.exe with Clang HIP), which takes
|
||||
|
||||
Binary file not shown.
+11
-7
@@ -14,7 +14,7 @@ Work through the steps in order. Tell the user what you are doing in plain words
|
||||
user the key. Never put a tunnel or port forward in front of a server without a key.
|
||||
- Do not change the user's system beyond what setup does (setup installs Python for the user account if needed,
|
||||
and everything else inside the Strata folder and its `Strata-data` folder). Ask before installing drivers.
|
||||
- The model download is ~70 GB (up to 111 GB for Unsloth's 4-bit). Confirm the user is fine with that before you
|
||||
- The model download is ~70 GB (94 GB for Unsloth's UD-IQ4_XS, 111 GB for UD-Q4_K_XL). Confirm the user is fine with that before you
|
||||
start, especially on a metered connection.
|
||||
- Setup is long-running (an hour or more on a slow connection). Run it in the background or with a long timeout and
|
||||
poll its output; do not kill it because it is quiet for a while. It is resumable: running the same command again
|
||||
@@ -44,11 +44,12 @@ Requirements (details: [INSTALL.md](INSTALL.md#what-you-need)):
|
||||
|
||||
- **GPU:** NVIDIA RTX 20, 30, 40 or 50 series, or AMD Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT,
|
||||
RX 9070 / 9070 XT, Radeon AI PRO R9700, RX 6800 / 6900 series; 12 GB of VRAM or more (an NVIDIA card with 8 GB runs,
|
||||
slowly). GTX 10 series and older, and integrated GPUs, are not supported.
|
||||
slowly). GTX 10 series and older, and integrated GPUs, are not supported; Pascal / Volta cards (P40, V100) and some
|
||||
older AMD cards have experimental paths the user opts into ([OLDER_GPUS.md](OLDER_GPUS.md)).
|
||||
- **Driver:** NVIDIA 580 or newer. AMD on Linux: the kernel's amdgpu driver; on Windows: a current AMD Adrenalin
|
||||
driver. If the driver is missing or too old, tell the user to update it (NVIDIA App / nvidia.com/drivers, or AMD
|
||||
Software) and restart; do not install drivers yourself unless they ask.
|
||||
- **RAM:** 32 GB or more (see step 3). **Disk:** ~80 GB free, ideally on an NVMe SSD. **CPU:** x86-64 with AVX2.
|
||||
- **RAM:** 32 GB or more (see step 3). **Disk:** ~80 GB free, ideally on an NVMe SSD. **CPU:** x86-64 with AVX2. Without AVX2 (Xeon E5 v1/v2 and older) setup still installs, as an experimental and slow build it compiles on the PC (10-20 minutes): tell the user that before starting ([INSTALL.md](INSTALL.md#older-cpus-experimental)).
|
||||
- **OS:** Windows 10/11 or Linux (Ubuntu 22.04/24.04 are fully automatic).
|
||||
|
||||
If the PC does not meet them, say which part is missing and stop.
|
||||
@@ -72,7 +73,7 @@ By the PC's RAM (ask the user whether they mainly want it for code - then the Co
|
||||
| 32 GB | `--family coder` (size IQ1_M) | for code; with a 24 GB card Q2_0 and IQ2_XS also run (setup picks the low-RAM mode) |
|
||||
| 48 GB | `--family qwen --model IQ2_XS` | or `Q2_0` (fastest) |
|
||||
| 64 GB | `--family qwen --model IQ2_XS` (recommended) | `IQ3_XXS` / `IQ3_S` are slower and a bit better |
|
||||
| 96 GB+ | `--family qwen --model IQ3_S` | `--family unsloth --model UD-Q4_K_XL` is experimental: NVIDIA only, NVMe SSD, 7-8.5 tokens/s on 64 GB |
|
||||
| 96 GB+ | `--family qwen --model IQ3_S` | `--family unsloth --model UD-IQ4_XS` (~4-bit, 94 GB, part of the experts read from the SSD under ~80 GB of RAM); `--model UD-Q4_K_XL` is experimental: NVIDIA only, NVMe SSD, 7-8.5 tokens/s on 64 GB |
|
||||
|
||||
`--family swift` (Swift 1.5, a fine-tune that thinks shorter; sizes Q2_0, IQ2_XS, IQ3_XXS) is the alternative to
|
||||
`qwen`. With `--yes` and no `--model`, setup picks the recommended size for the RAM itself. More: [MODELS.md](MODELS.md).
|
||||
@@ -92,7 +93,7 @@ The flags (all of them: `START-HERE.bat --help`):
|
||||
| --- | --- |
|
||||
| `--yes` | take the recommended answer to every question (no prompts) |
|
||||
| `--family qwen\|swift\|coder\|unsloth` | the model version |
|
||||
| `--model Q2_0\|IQ2_XS\|IQ3_XXS\|IQ3_S\|IQ1_M\|UD-Q4_K_XL` | the size (the Coder is IQ1_M, Unsloth UD-Q4_K_XL) |
|
||||
| `--model Q2_0\|IQ2_XS\|IQ3_XXS\|IQ3_S\|IQ1_M\|UD-IQ4_XS\|UD-Q4_K_XL` | the size (the Coder is IQ1_M, Unsloth UD-IQ4_XS or UD-Q4_K_XL) |
|
||||
| `--context N` | context in tokens; default by VRAM: 32768 under 14 GB, 65536 under 20 GB, else 131072 |
|
||||
| `--vision yes\|no\|gpu\|cpu` | read pictures; `--yes` leaves images off. AMD cards: `cpu` |
|
||||
| `--gpu N` / `--gpus 0,1` / `--gpus all` | one card, or several sharing the model (default: the card with the most VRAM) |
|
||||
@@ -132,7 +133,7 @@ Notes:
|
||||
## 6. Start the server
|
||||
|
||||
Setup prints the start script it wrote (`start script: run-<model>.bat`). The name is the family tag plus the size,
|
||||
lower case: `run-iq2_xs`, `run-swift-iq2_xs`, `run-coder-iq1_m`, `run-unsloth-ud-q4_k_xl`.
|
||||
lower case: `run-iq2_xs`, `run-swift-iq2_xs`, `run-coder-iq1_m`, `run-unsloth-ud-iq4_xs`.
|
||||
|
||||
```
|
||||
Windows (PowerShell): Start-Process -FilePath ".\run-iq2_xs.bat" (opens its own window)
|
||||
@@ -168,10 +169,13 @@ line when it is ready; the engine log is `strata-<model>.log` in the Strata fold
|
||||
- **Any OpenAI-compatible app or agent:** base URL `http://127.0.0.1:8080/v1`, any API key (or the configured one),
|
||||
any model name.
|
||||
- **Anthropic-compatible apps:** `http://127.0.0.1:8080/v1/messages`.
|
||||
- **Codex CLI and other Responses API apps:** `http://127.0.0.1:8080/v1/responses` (stateless; Codex's
|
||||
`config.toml`: [DETAILS.md](DETAILS.md#the-responses-api-and-codex-cli)).
|
||||
- **Claude Code:** `ANTHROPIC_BASE_URL=http://127.0.0.1:8080`, `ANTHROPIC_MODEL` set to a Claude model name it knows
|
||||
(Strata ignores the name), and any `ANTHROPIC_AUTH_TOKEN` (or the configured key).
|
||||
- **Thinking level:** `"reasoning_effort": "none" | "low" | "medium" | "high"` (default high).
|
||||
- Strata answers one request at a time. API details: [DETAILS.md](DETAILS.md#using-it).
|
||||
- Strata answers one request at a time; `"parallel": N` in the model's config (or setup `--parallel N`) decodes up to
|
||||
N together ([BATCHING.md](BATCHING.md)). API details: [DETAILS.md](DETAILS.md#using-it).
|
||||
|
||||
## 9. When something fails
|
||||
|
||||
|
||||
+105
-6
@@ -5,7 +5,8 @@ covers the build on Linux (on Windows a ready-made engine, see [Windows](#window
|
||||
RX 9070 / 9070 XT / Radeon AI PRO R9700 (RDNA4, gfx1201; see [RDNA4](#rdna4-gfx1201)). The RX 7800 XT / 7700 XT
|
||||
(gfx1101) and the RX 9060 XT (gfx1200) were validated by their owners (see [Community-validated
|
||||
cards](#community-validated-cards)); the RX 6800 / 6900 series (RDNA2, gfx1030) builds and runs too, reported by a community machine and not yet validated by the maintainers (see [RDNA2](#rdna2-gfx1030)). Setup chooses it by itself on a PC with no NVIDIA card Strata can use (`--backend hip` on a PC with both); the
|
||||
install steps for users are in [INSTALL.md](INSTALL.md#amd-cards). Other AMD architectures, wave64, and mixed
|
||||
install steps for users are in [INSTALL.md](INSTALL.md#amd-cards). gfx906 (Instinct MI50 / MI60, Radeon VII; wave64) has a separate
|
||||
opt-in build, see [gfx906](#gfx906-instinct-mi50--mi60-radeon-vii-wave64-built-from-source). The Ryzen AI Max "Strix Halo" APU (gfx1151, RDNA3.5, unified memory) is built from source on Linux: see [STRIX_HALO.md](STRIX_HALO.md). Other AMD architectures and mixed
|
||||
AMD/NVIDIA execution in one run are not supported.
|
||||
|
||||
The backend maps the CUDA-shaped runtime and BLAS calls to HIP/hipBLAS, uses
|
||||
@@ -46,7 +47,9 @@ the kernel's amdgpu driver (no ROCm install needed):
|
||||
architectures above; the engine is compiled for each of them (cards of two families, e.g. gfx1100 + gfx1201, need
|
||||
a system ROCm 7: AMD's wheels hold one family). A split pays only when no single card holds the model's experts
|
||||
(see RDNA4 below).
|
||||
- **Limits for now:** images only through the CPU encoder (`--vision cpu`, 0.1.32), no calibration. The Monitor
|
||||
- **Limits for now:** images only through the CPU encoder (`--vision cpu`, 0.1.32). Setup does not offer the tuning
|
||||
(calibration) on AMD yet: its controls are being checked on HIP one at a time (#566). Since 0.1.39 a tuning run by
|
||||
hand (`./setup.sh --calibrate`) is saved for the AMD card it ran on and reused when setup runs again. The Monitor
|
||||
shows the card's load, VRAM, temperature and power from Linux sysfs (0.1.32).
|
||||
|
||||
The rest of setup is the same as on NVIDIA: the model download, the start script, the server.
|
||||
@@ -61,9 +64,9 @@ On a PC with no NVIDIA card Strata can use, the AMD card is chosen by itself; wi
|
||||
Adrenalin Edition](https://www.amd.com/en/support/download/drivers.html)). Nothing else: no ROCm or HIP SDK
|
||||
install, no compiler, no admin rights.
|
||||
- **Detection:** setup reads the display adapters Windows lists (their PCI ids; the VRAM size from the display
|
||||
driver's registry entry). An integrated Radeon is listed as not supported.
|
||||
driver's registry entry). An integrated Radeon is listed as not supported, except the Ryzen AI Max "Strix Halo" (Radeon 8060S / 8050S / 8040S, gfx1151: [STRIX_HALO.md](STRIX_HALO.md)); a Strix Point (890M / 880M, gfx1150), Krackan (860M / 840M, gfx1152) or Phoenix / Hawk Point (780M / 760M, gfx1103) Radeon is named as what it is and not supported.
|
||||
- **Engine:** the ready-made `strata-windows-x64-hip.zip` from the release (built by `tools\hip\build_windows.bat`
|
||||
for gfx1100, gfx1101, gfx1102, gfx1200, gfx1201 and gfx1030) goes into `engine\`. It carries the ROCm libraries the
|
||||
for gfx1100, gfx1101, gfx1102, gfx1200, gfx1201, gfx1030 and, from 0.1.40, gfx1151 (Strix Halo, unvalidated on Windows)) goes into `engine\`. It carries the ROCm libraries the
|
||||
engine loads (`engine\rocm\bin`: the HIP runtime, hipBLAS / rocBLAS / hipBLASLt with their kernels for these cards,
|
||||
amd_comgr and the Microsoft C++ runtime; ROCm 10.2.0a20260930 from AMD's TheRock builds, licenses in
|
||||
`engine\rocm\licenses`). The HIP runtime works through the AMD driver's own components, so the driver is the one
|
||||
@@ -198,6 +201,13 @@ compositor fails with "Failed to pin framebuffer with error -12".
|
||||
The installer supports this backend (see "Install with setup" above). Images run through the CPU encoder for now (`--vision cpu`).
|
||||
Setup installs one AMD card, or several with `--gpus` (the engine's layer split; see RDNA4 below).
|
||||
|
||||
**Opt-in switches for RDNA2 prompts and hangs (0.1.40, off by default):** `STRATA_DENSE_MMQ=1` (#820, HIP) runs the
|
||||
prompt's dense GGUF projections through the int8 MMQ kernels instead of dequantize + hipBLAS: on the RX 6900 XT a 4K
|
||||
prompt read 436 -> 757 tok/s (greedy text diverges from the default after a few dozen tokens: the q8_1 activation
|
||||
rounding). `STRATA_HIP_ADAPT_KERNEL_COPY=1` (#884) copies the adaptive tier's swaps with a kernel instead of the SDMA
|
||||
engine, a workaround for the gfx1030 hang seen with the MMQ prompt path and adaptive swaps (untested on the reporter's
|
||||
machine). Windows HIP: the doorbell kernels fence their store (#697), and the shared-expert fork is off on HIP (#816).
|
||||
|
||||
## RDNA4 (gfx1201)
|
||||
|
||||
The RX 9070 / 9070 XT and the Radeon AI PRO R9700 run the same kernels as gfx1100: wave32, 64 KiB of LDS per
|
||||
@@ -345,8 +355,84 @@ run it; the report below is from a community machine: an RX 6900 XT 16 GB (gfx10
|
||||
[AMD_HIP_PERFORMANCE.md](AMD_HIP_PERFORMANCE.md) cost 8.6 and 13.6 tok/s here (30 with the defaults): keep the
|
||||
defaults on a 16 GB card.
|
||||
- **hipBLASLt:** ROCm's hipBLASLt ships no gfx1030 kernels, so there is no table and the plain hipBLAS path runs.
|
||||
- **Not validated:** gfx1031 / gfx1032 (the same `dp4a` path, no hardware report), setup's own build path and the
|
||||
`gfx103X-all` wheels on gfx1030, images, answer-quality benchmarks.
|
||||
- **Prompt GEMMs in FP16 (#835):** rocBLAS on gfx1030 has tuned kernels for FP16-in / FP16-out GEMMs only; the prompt
|
||||
path's FP16-in / FP32-out and BF16 products ran generic kernels about 6.6x slower (5.6 and ~5.3 TFLOPS against 37.7 at
|
||||
N = 10240, T = 7313, K = 2560). `STRATA_HIP_PROMPT_F16=1` (opt-in, off by default; the engine prints a tip on gfx103x) runs the 16-bit prompt GEMMs FP16 in and out;
|
||||
unset is the old path. `STRATA_DBG_NAN=1` also counts the non-finite FP16 outputs (a sum past 65504). The output is not bit-identical to it (an FP16 rounding of each GEMM's output replaces the BF16 rounding of
|
||||
the BF16 GEMMs' activations); what the distribution check showed and did not show is in
|
||||
[bench/results/2026-10-04-rdna2-fp16-prompt](../bench/results/2026-10-04-rdna2-fp16-prompt/README.md). Measured on a
|
||||
second community machine - 2x RX 6900 XT 16 GB (one card for these numbers), Ryzen 5 5600X (6 cores, AVX2), 128 GB,
|
||||
Ubuntu 26.04, ROCm 10.0.0, engine 0.1.39 built by hand with `-DSTRATA_PREFILL_MMQ=ON` as setup does, GSQ-RCO IQ3_S:
|
||||
a 9.4K / 34.7K / 105.8K-token prompt reads at 744 / 915 / 926 tok/s instead of 439 / 466 / 461; decode is unchanged.
|
||||
On that machine the HIP ctest passes 60 of 65 with and without this change: 2 skipped (`hip_prompt_attn_wmma`, no
|
||||
matrix cores; `hip_prefill_hipblaslt_gemm`, no hipBLASLt table) and 3 that fail for reasons outside the engine
|
||||
(`ple_parity` needs a Q2_0 PLE file that is not on that machine, `expert_multi_test` refuses the CPU without AVX-512,
|
||||
`platform_memory_test` cannot `mlock` at the shell's default `ulimit -l`).
|
||||
- **gfx1031** (RX 6700 XT, #524): setup knows it (the `gfx103X-all` wheels, unvalidated); its reporter runs it daily
|
||||
on one card.
|
||||
- **Not validated:** gfx1032 (the same `dp4a` path, no hardware report), setup's own build path and the
|
||||
`gfx103X-all` wheels on gfx1030, images, answer-quality benchmarks. RDNA1 (gfx1012, RX 5500 XT) builds by hand:
|
||||
[OLDER_GPUS.md](OLDER_GPUS.md#amd-building-gfx906-and-gfx1012).
|
||||
|
||||
## gfx906 (Instinct MI50 / MI60, Radeon VII): wave64, built from source
|
||||
|
||||
gfx906 is wave64 and has no WMMA and no packed byte arithmetic, so the wave32 backend above refuses it. A separate opt-in build compiles the CUDA sources as HIP through a small compat layer
|
||||
(`include/strata/platform/hip_compat/`), with a CUDA warp mapped to a logical half of the 64-lane wavefront
|
||||
(32-wide shuffles, a ballot of its own half). The hot kernels have wave64 layouts of their own (below). Setup does
|
||||
not build it yet: build by hand, and run `serve/server.py` with a config as on any other card.
|
||||
|
||||
**ROCm.** AMD's current ROCm releases no longer ship gfx906 libraries. The build and the measurements below used
|
||||
HIP 7.14 from the community image [`mixa3607/rocm-gfx906:7.14-complete`](https://hub.docker.com/r/mixa3607/rocm-gfx906)
|
||||
(rocBLAS/hipBLAS with gfx906 kernels), on the kernel's amdgpu driver (Ubuntu 24.04, kernel 6.8). hipBLASLt is not
|
||||
used.
|
||||
|
||||
```sh
|
||||
# inside the image, with this checkout at /src and llama.cpp at the pinned commit in third_party/llama.cpp
|
||||
apt-get update && apt-get install -y cmake ninja-build git python3 build-essential
|
||||
cmake -S . -B build-906 -G Ninja -DSTRATA_HIP_GFX906=ON -DCMAKE_HIP_ARCHITECTURES=gfx906 \
|
||||
-DSTRATA_GGML_DIR=/src/third_party/llama.cpp \
|
||||
-DCMAKE_C_COMPILER=/opt/rocm/llvm/bin/clang -DCMAKE_CXX_COMPILER=/opt/rocm/llvm/bin/clang++
|
||||
ninja -C build-906 strata
|
||||
```
|
||||
|
||||
`STRATA_HIP_GFX906` turns on the CUDA targets and refuses `STRATA_ENABLE_HIP` (one HIP build at a time). Run the
|
||||
container with `--device=/dev/kfd --device=/dev/dri --group-add video --ipc=host --ulimit memlock=-1`; on some boards
|
||||
`HSA_OVERRIDE_GFX_VERSION=9.0.6` is needed for the runtime to accept the card.
|
||||
|
||||
**What differs from the CUDA build** (all under `STRATA_HIP_GFX906`; each A/B switch restores the CUDA layout):
|
||||
|
||||
| | gfx906 | switch |
|
||||
|---|---|---|
|
||||
| grouped native experts | mode 7: signs as `dp4a(g ^ m, u) - dp4a(m, u)` (no byte SIMD), grids and activations in LDS, one weight load per group; the IQ formats only (Unsloth's K-quant / Q5_1 / Q8_0 experts take the CUDA layout) | `STRATA_EXP_MODE=2` (previous AMD layout) |
|
||||
| MMVQ | one wavefront per row, 64-lane butterfly, no LDS | `STRATA_MMVQ_WAVE=0` |
|
||||
| router top-10 | one wavefront, register argmax | - |
|
||||
| verify-window routing (256-expert router) | the window's tokens in one multi-column BF16 projection + one top-k, ~3 ms of a ~55 ms window | `STRATA_ROUTE_PER_TOKEN=1` |
|
||||
| hyper-connection read | norm loads in flight, 8 lanes per row for `up` | `STRATA_GR_FAST=0` |
|
||||
| `gr_down_multi`, opt-in | split along K (8 rows x 5 slices of 2048), ~3% per window; sums in another order than the single-token read, so off by default | `STRATA_GR_SPLIT=1` |
|
||||
| IQ4 table lookup | llama.cpp's `v_perm_b32` sequence (HIP's `__byte_perm` is a scratch array) | - |
|
||||
| QSA prompt attention | the FP32 kernel (no tensor cores / WMMA) | - |
|
||||
|
||||
Every switch above was checked bitwise against the layout it replaces: the parity tests on synthetic data,
|
||||
`native_expert_bench` on real GGUF rows (one mode against another), and the greedy text of a fixed request.
|
||||
|
||||
**Measured** on 2x Instinct MI50 16 GB (85 W power limit each, PCIe 3.0 x16 both, peer access between them),
|
||||
Xeon E5-2666 v3 (10 cores, AVX2), 32 GB DDR4, SATA SSD; Coder IQ1_M (`--native` pack), 131,072-token context,
|
||||
`--kv int8 --kv-resident 32768`, `--layer-split 27`, `--prefill 4096`, MTP `--spec 4 --spec-min-p 0.5`,
|
||||
`--pcie-frac 0`, `STRATA_ARENA_MMAP=1`. Measured on the port this build was cut from (builds on the 0.1.30 and 0.1.33 bases, the same
|
||||
kernels, plus the layer-split weight trim and the mapped arena, which are separate pull requests):
|
||||
|
||||
- all 12,288 experts held in VRAM across the two cards;
|
||||
- a 17,043-token agent prompt (Claude Code's first request, tools included) read in 43.5 s (392 tok/s), decode
|
||||
on it 57.7-59.3 tok/s;
|
||||
- 25 GB of the 32 GB RAM available while serving;
|
||||
- six repeats of that request with a 6,000-token output cap and a 900-second two-client load test run six times
|
||||
(407 requests, 202,940 tokens): no stalls, no errors, no GPU faults.
|
||||
|
||||
For comparison on the same machine: llama.cpp (3cf0325, ROCm) on the same Coder IQ1_M measured 26.9 tok/s `tg128`.
|
||||
|
||||
**Not done:** setup.py detection and an automatic build, Windows, images (`--vision`), MI60 and Radeon VII (the
|
||||
same gfx906 ISA; not run), a single-card run, the tensor-split experiment (the halves of every layer on two cards;
|
||||
it works but is not part of this build).
|
||||
|
||||
## Tuning table
|
||||
|
||||
@@ -367,6 +453,19 @@ prompt speed with and without it before keeping it.
|
||||
Shipped tables:
|
||||
|
||||
- `gfx1100-hipblaslt-100100.txt`, `gfx1100-hipblaslt-100200.txt`: RX 7900 XTX.
|
||||
- `gfx1100-hipblaslt-100401.txt`: RX 7900 XTX, calibrated with the packaged ROCm 10.0.0
|
||||
(`rocm/dev-ubuntu-24.04:10.0.0-full`, hipBLASLt 1.4.1) on a Ryzen 7 9800X3D, over the 26 dense GEMM
|
||||
geometries of the shipped gfx1100 table. Without it, that stack reads a prompt through plain hipBLAS: on
|
||||
Swift 1.5 IQ3_XXS at 262144 ctx, fresh-prompt prefill measured 682 -> 1,038 tok/s at 1.7K tokens and
|
||||
857 -> 1,524 tok/s at 6.5K (medians of 3, decode unchanged), with `hip_prefill_hipblaslt_gemm` reporting
|
||||
`fallbacks=0`. setup uses it only when the installed hipBLASLt reports 1.4.1.
|
||||
- `gfx1100-hipblaslt-100500.txt`: RX 7900 XTX (gfx1100, 24 GB), calibrated with a ROCm 10.2.0a20261003 nightly
|
||||
SDK (`libamdhip64.so.7.17.26392`, hipBLASLt 1.5.0, version number 100500). Same 16 dense GEMM geometries at
|
||||
T=4096 and T=8192 as the other gfx1100 tables (32 rows), calibrated with `tune_hipblaslt` run against that
|
||||
library. On this ROCm the engine refuses the 100100/100200 tables (version mismatch) and prefills on plain
|
||||
hipBLAS; with this table a 0.1.38-lineage nightly engine prefills a 131071-token prompt at 1687 tok/s vs 926
|
||||
without it (median of 3 clean runs each, greedy; decode unchanged). Per-run data and the calibration command:
|
||||
the `2026-10-04-community-rx7900xtx-hipblaslt-100500` folder of PR #745.
|
||||
- `gfx1201-hipblaslt-100500.txt`: Radeon AI PRO R9700 (gfx1201, 32 GB), calibrated with ROCm 10.2.0a20260914
|
||||
(AMD's `gfx120X-all` nightly, hipBLASLt 1.5.0, library build `d3164197`). 16 dense GEMM geometries at T=4096 and
|
||||
T=8192, 32 rows. setup uses it only when the installed hipBLASLt reports 1.5.0 (it is found in `/opt/rocm`
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# Batched DMA submission (experimental)
|
||||
|
||||
With `--pcie-mode dma`, `STRATA_DMA_BATCH=1` submits a verifier group's independent
|
||||
expert uploads through `cudaMemcpyBatchAsync` on CUDA 13.0 or later. The default
|
||||
is `0`, retaining the existing per-expert copies. The normal kernel-copy mode
|
||||
is unchanged. This is an experiment to reduce host submission overhead, not a
|
||||
demonstrated increase in model tokens per second.
|
||||
|
||||
`STRATA_DMA_BATCH=2` also sets CUDA 13.4's
|
||||
`cudaMemcpyFlagPreferOverlapWithCompute`. It is a scheduling hint, not a
|
||||
guarantee of overlap. On CUDA 13.0–13.3 this mode behaves like `1`. Older CUDA
|
||||
builds and HIP retain individual copies. A single upload or a group larger than
|
||||
128 also uses individual copies.
|
||||
|
||||
The sources must be immutable pinned host blobs, and destinations must be
|
||||
disjoint. `expert_source.cpp` already restricts the DMA group to pinned experts.
|
||||
The helper resolves device-visible aliases for registered host allocations;
|
||||
Windows registered host pointers need not equal their GPU aliases. No extra
|
||||
VRAM or weight conversion is introduced. All copies remain on the existing
|
||||
stream, before the callback that signals flag B. A submission error fails the
|
||||
engine rather than signalling readiness for stale weights.
|
||||
|
||||
## Tests
|
||||
|
||||
With the normal CUDA build and `STRATA_BUILD_TESTS=ON`:
|
||||
|
||||
```sh
|
||||
cmake --build build --target dma_batch_parity
|
||||
ctest --test-dir build -R dma_batch --output-on-failure
|
||||
```
|
||||
|
||||
The test checks bytes and cross-stream event ordering for 0, 1, 4, 17, 64, 128,
|
||||
and 129 uploads, at 37 bytes, 4 KiB and 2 MiB per upload, in all three modes.
|
||||
Windows also runs the same cases with `VirtualAlloc` plus `cudaHostRegister`.
|
||||
It prints interleaved diagnostic timings separately for submission and total
|
||||
transfer time. These times overlap; they must not be added. On Windows it also
|
||||
reports submitting-thread cycles using `QueryThreadCycleTime`. Cycle counts
|
||||
are neither whole-process CPU utilization nor an energy measurement.
|
||||
|
||||
The 63-case matrix passed with both host allocation methods on CUDA 13.4 and
|
||||
13.0, and with the individual-copy fallback on CUDA 12.6, all using the same
|
||||
RTX 4070 Laptop and driver 616.56. This checks the older-toolkit fallback;
|
||||
it is not a HIP runtime test.
|
||||
|
||||
## Initial measurement
|
||||
|
||||
RTX 4070 Laptop 8 GB, Ryzen 9 7940HS, 64 GB DDR5-5600, Windows, CUDA 13.4:
|
||||
16 independent registered-host uploads of 2 MiB each, five measured repetitions
|
||||
after warmup, alternating mode order:
|
||||
|
||||
| Path | Median host submission | Median total transfer |
|
||||
| --- | ---: | ---: |
|
||||
| Individual copies | 0.0936 ms | 2.5589 ms |
|
||||
| Batch | 0.0162 ms | 2.5248 ms |
|
||||
| Batch with overlap hint | 0.0155 ms | 2.5206 ms |
|
||||
|
||||
Batch submission latency fell 82.7%; total transfer latency fell only 1.3%.
|
||||
Submission latency is measured wall time around the API calls, not whole-process
|
||||
CPU utilization or energy consumption. The initial full-model screen on Swift
|
||||
1.5 IQ3_XXS at 64K allocated context did not establish a generation-speed gain:
|
||||
one coding request measured 30.3 tokens/s with individual DMA and 30.0 with
|
||||
batching. Its generated lengths differed, so total task times cannot isolate
|
||||
the transfer change. This evidence does not justify changing the default.
|
||||
|
||||
A separate CUDA 13.4 repetition with submitting-thread cycle measurements gave
|
||||
449,824 cycles for individual copies and 67,939 for the batch, about 84.9% fewer.
|
||||
Submission wall time was 0.1127 vs 0.0170 ms, while total transfer time was
|
||||
2.5584 vs 2.5309 ms (about 1.1% lower). These are separate runs from the initial
|
||||
table, not measurements to combine into a larger speedup.
|
||||
|
||||
## Model comparison on the isolated patch
|
||||
|
||||
An A-B-B-A comparison used only this patch on v0.1.39, with `--pcie-mode dma`.
|
||||
Each fresh engine ran the same nine requests, including tool use, prompt reuse
|
||||
and an independently checked interval-merging implementation. The coding request
|
||||
used high reasoning, seed 4242, temperature 1, top-p 0.95, top-k 20 and a
|
||||
3,072-token output limit. Context allocation was 65,536, INT8 KV with 20,480
|
||||
resident cells, 1,040 cached experts, 7 workers plus the caller, PCIe fraction
|
||||
0.35, and MTP 5 with English drafting and min-p 0.5.
|
||||
|
||||
| Run | Batch mode | Code decode tok/s | Output tokens including reasoning | Request wall time |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| A1 | 0 | 29.7 | 928 | 32.98 s |
|
||||
| B1 | 1 | 31.4 | 1,478 | 48.86 s |
|
||||
| B2 | 1 | 30.3 | 1,314 | 45.12 s |
|
||||
| A2 | 0 | 30.2 | 1,078 | 37.45 s |
|
||||
|
||||
All 36 requests passed. All four implementations passed 155 independent cases
|
||||
and generator, invalid-input and non-mutation checks. Outputs differed even
|
||||
between the two controls, and draft acceptance also differed. These runs do
|
||||
not isolate a model speedup or show better time to a verified answer. The
|
||||
byte-parity tests establish transfer correctness, not deterministic generated
|
||||
text or general agent reliability.
|
||||
|
||||
A fifth run with batching enabled passed the same nine requests plus a
|
||||
57,750-token prompt, auxiliary-call restoration, and three 1,024-square image
|
||||
color checks including image-state restoration. An OpenAI-compatible HTTP
|
||||
smoke request also passed. The long prompt read took 78.414 seconds; restoring
|
||||
57,745 tokens after the side call reduced prompt processing to 0.301 seconds.
|
||||
This validates the existing state cache with the patch, not a new cache speedup.
|
||||
The five clean-patch runs passed 53 recorded requests plus the HTTP check.
|
||||
Observed free physical VRAM stayed at least 347 MiB via NVML, and available
|
||||
system RAM stayed above 4.30 GiB. The engine's separate CUDA free-memory warning
|
||||
reported zero, so the NVML figure must not be read as a verified WDDM budget.
|
||||
Linux and HIP runtime validation remains untested.
|
||||
|
||||
## Design reference
|
||||
|
||||
The batching idea follows NVIDIA NCCL's
|
||||
[copy-engine implementation](https://github.com/NVIDIA/nccl/blob/12df1a11afad322be5a204a2db890161cbf8131d/src/ce_coll.cc)
|
||||
and the [CUDA `cudaMemcpyBatchAsync` contract](https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__MEMORY.html).
|
||||
The helper and tests here are new;
|
||||
no NCCL source was transplanted. NCCL's multi-GPU performance claims are not
|
||||
used as predictions for this single-GPU path.
|
||||
@@ -0,0 +1,224 @@
|
||||
# Several requests at once (batch slots)
|
||||
|
||||
By default Strata serves **one request at a time**: the others wait in the server's queue. With `"parallel": N`
|
||||
(the engine's `--batch N`, also spelled `--slots N`) the engine keeps up to N conversations open and decodes them
|
||||
**together**: every verify window then carries one token of each conversation, so the dense weights, the shared
|
||||
expert, the head and every routed expert two conversations share are read once per window for all of them.
|
||||
Combined with a layer split across several GPUs and `--batch-groups`, the cards also work on different
|
||||
conversations at the same time instead of waiting for each other.
|
||||
|
||||
It is opt-in and changes nothing when the options are absent (#465; the engine part is PR #559).
|
||||
|
||||
## Turning it on
|
||||
|
||||
One GPU: add `"parallel": 2` to the model's config (`strata-<model>.json`) and restart, or run setup with
|
||||
`--parallel 2`. Setup recommends it only where it does not cost speed (below); any number you ask for is kept as
|
||||
asked, with a note when it is more than setup would recommend.
|
||||
|
||||
```
|
||||
"parallel": 2
|
||||
```
|
||||
|
||||
On one GPU with MTP (`--mtp` and `--spec`), `--batch-mtp` (in the config's `args`, or `STRATA_BATCH_MTP=1` in the
|
||||
server's environment) lets each batch slot verify one MTP proposal per window. It is opt-in; without it the batch
|
||||
behaviour described below is exactly the one without MTP. It needs VRAM per slot for the draft state and buffers, so
|
||||
check the engine's free-memory log before using it on a smaller card. If it cannot run (one slot, no `--mtp`, a layer
|
||||
split or helper GPU) the engine says so and batches as usual. RTX PRO 5000 owners measured +31% to +39% total
|
||||
throughput with 2 to 4 clients (a RX R9700 run too); it has not been validated with a layer split.
|
||||
|
||||
With a layer split, the engine options go into the config's `args`:
|
||||
|
||||
```
|
||||
"args": [ ..., "--batch", "8", "--batch-groups", "4", "--trim-stage-weights" ],
|
||||
"layer_split": "12,24,36"
|
||||
```
|
||||
|
||||
| Option | What it does |
|
||||
| --- | --- |
|
||||
| `"parallel": N` / `--batch N` / `--slots N` (2..8 normally) | 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: 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. |
|
||||
| `--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 by
|
||||
default (a window holds 8 rows), 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`).
|
||||
|
||||
### What a slot costs, and what setup recommends
|
||||
|
||||
Every slot's session takes VRAM that the expert cache would otherwise hold: 0.56 GiB at a 32K context with 8-bit
|
||||
KV, more with a longer context unless the KV cache streams (`--kv-resident`: then only the attention's 32K window
|
||||
stays in VRAM, and each slot's whole KV cache takes pinned RAM - 1.6 GB at 128K). On a card whose experts mostly run
|
||||
on the CPU, a batch also reads about as many distinct experts as the requests one by one (different conversations
|
||||
route to different experts), so the gain is in **latency** (nobody waits for a whole answer), and a request alone
|
||||
runs slower (the smaller expert cache): 11-24 % on a 12 GB card, see the measurements below.
|
||||
|
||||
So setup recommends `"parallel"` **only where the experts mostly fit in VRAM**: the expert cache (each card's VRAM
|
||||
less ~5 GB, every card of a layer split counted) must still hold at least half of the model's experts beside the
|
||||
slots, and the slots may take at most a fifth of it, up to 4 slots. With Q2_0 at 32K that is 3 slots on a 24 GB
|
||||
card, 4 from 32 GB or on a split such as 2 x 16 GB; IQ3_S needs 32 GB or a split. Everywhere else (any 12 or 16 GB
|
||||
card alone) it stays at one at a time and setup says: "parallel N reduces waiting for several users but costs
|
||||
about 10-25% speed per request on this card". `--parallel N` is honoured as asked either way.
|
||||
|
||||
## How the server uses the slots
|
||||
|
||||
- **One request alone** runs on the usual solo path (verify windows with MTP drafts): the fastest single stream.
|
||||
- **When a second request arrives**, the first is stopped (`STOP`) and continues in a batch slot with its prompt
|
||||
plus what it generated so far - the engine's prompt cache holds exactly that, so nothing is read again - and the
|
||||
new request is admitted next to it. By default, a request in a slot decodes **without MTP drafts** (one token per window).
|
||||
With `--batch-mtp`, each slot verifies one MTP proposal alongside its current token.
|
||||
- **A request left alone in a slot** (the others finished, nobody waits) goes back to the solo path: the slot is
|
||||
stopped, the engine copies its sessions back and decodes with MTP drafts again (at most twice per request; with
|
||||
`--prompt-cache 0` it stays in the slot; `STRATA_PARALLEL_SOLO=0` turns it off). The draft layer's own K/V was
|
||||
built for another conversation then, but measured it accepted as many drafts (140 of 172) as a draft layer that
|
||||
read the conversation (140 of 173).
|
||||
- **More requests than slots** wait for a free one (`/metrics` -> `live.slots` shows each slot: idle, reading or
|
||||
decoding, its tokens and tok/s; `live.running` the requests in flight).
|
||||
- **Each admission** reads the request's prompt through the usual prompt path (prompt cache and conversation
|
||||
checkpoints included) and produces its first token there; the state is then copied into the slot. Admissions
|
||||
are taken one at a time, and **the slots decode between the prompt's chunks**: after each chunk (`--prefill`,
|
||||
2048-8192 tokens) they decode for half as long as the chunk took (`STRATA_BATCH_DECODE_SHARE`, default 0.5), so
|
||||
a long prompt slows the others down instead of stopping them. The chunks are the ones one uninterrupted read
|
||||
takes, so the prompt's arithmetic is unchanged.
|
||||
- **A long prompt gives way to a short one** (#656's cooperative preemption): when a request with a prompt under
|
||||
half as long is waiting, the server sends `BYIELD`; at its next chunk boundary the long read stops, the part read
|
||||
so far is copied into a slot, the short request is admitted, and the long one then goes on from its slot with the
|
||||
same chunks (at most twice per request).
|
||||
- **Each slot is a conversation cache.** A finished slot keeps what it holds (the prompt, the answer, and the
|
||||
checkpoint at the prompt's last turn boundary); the next turn of that conversation goes to that slot and the
|
||||
engine copies its state back (50-60 ms for a short conversation) instead of reading the history again - also for
|
||||
a client that drops the reply's thinking from the history (the checkpoint matches up to the new turn). A new
|
||||
conversation takes an empty slot, else the one used longest ago.
|
||||
- Slots are assigned so that consecutive requests land in different pipeline groups (`--batch-groups`).
|
||||
- A client that disconnects stops its slot (`BSTOP`); the others go on.
|
||||
|
||||
## Exactness
|
||||
|
||||
A batch row's arithmetic is the single-token window's, so with greedy decoding **every conversation of a batch
|
||||
produces exactly the tokens it produces alone** - verified token by token for 8 concurrent conversations of 150
|
||||
tokens, with and without the pipeline (`tools/batch_test.py`), and on one RTX 5070 for 4 conversations, for a long
|
||||
prompt read while two others decode, for a prompt that gave way and went on, for a next turn continued from its
|
||||
slot (from all it holds, and from its turn checkpoint without the reply's thinking), and for a request stopped in
|
||||
its slot and continued on the solo path (`tools/batch_interleave_test.py`). These
|
||||
settings make the comparison exact:
|
||||
|
||||
- `STRATA_IQ_MT_MIN=1` (the multi-token CPU expert kernels for every group, as for the solo path's own
|
||||
exactness tests: by default an expert's rows round differently alone than in a group, so the output depends on
|
||||
how many rows of a window share an expert - which differs between a batch and a request alone),
|
||||
- `--pcie-frac 0`: the PCIe share of the missed experts is chosen per window from the window's misses, so the
|
||||
same expert can run on the GPU in one window and on the CPU in another, which rounds differently, and
|
||||
- `--adapt-every 1000000` (the VRAM tier fixed), and `--no-prefill-borrow` while a prompt is read beside decoding
|
||||
slots: their windows then see the expert cache without the slots the prompt borrowed (those experts run on the
|
||||
CPU).
|
||||
|
||||
With the default settings the outputs stay coherent but drift apart after some tokens, as two solo runs whose
|
||||
windows differ can. Measured on the RTX 5070 (Q2_0, 4 conversations of 200 tokens, `tools/batch_test.py` without
|
||||
the settings above): one equal to its solo run, the others apart from token 0, 99 and 174 - the first token already
|
||||
differed for one, because the adaptive VRAM tier had moved experts between the solo runs and the batch; all four
|
||||
texts read as well as their solo runs.
|
||||
|
||||
Sampled requests (temperature, top_p, top_k, min_p, seed) are drawn row by row with the solo window's
|
||||
counter-based draw (Philox(seed, position)).
|
||||
|
||||
## Limits (for now)
|
||||
|
||||
- By default, batch windows carry no MTP drafts: a conversation in a slot decodes one token per window (the solo
|
||||
path keeps its drafts, which is why a request alone is not put in a slot, and goes back to it when left alone).
|
||||
- Grouped MTP currently uses one proposal per slot and requires one GPU; it does not support a layer split.
|
||||
- Repetition / frequency / presence penalties are not applied in batch windows.
|
||||
- A prompt shorter than one chunk is read in one piece (the slots wait for it); a read gives way only at a chunk
|
||||
boundary, and not for pictures.
|
||||
- Admissions are one at a time: two new long prompts are read one after the other.
|
||||
- `--batch-groups` needs every stage on its own GPU; a pipelined slot is not kept as a conversation cache.
|
||||
- The slot sessions take VRAM (above) and, with KV streaming, pinned RAM.
|
||||
|
||||
## Measured
|
||||
|
||||
One RTX 5070 (12 GB), Ryzen 5 7600, 64 GB DDR5, Q2_0, 32K context, through the HTTP server: C different requests
|
||||
sent at once (an 800-word essay each, 256 tokens per answer, greedy, thinking off), median of 3 rounds:
|
||||
|
||||
| Concurrent | Setting | Total tok/s | vs one at a time | Per request tok/s | First token: median / last of the round |
|
||||
| ---: | --- | ---: | ---: | ---: | ---: |
|
||||
| 1 | one at a time (default; what setup recommends on this card) | 74.3 | - | 83.6 | 0.4 s / 0.4 s |
|
||||
| 1 | `"parallel": 2` | 67.3 | -9 % | 74.8 | 0.4 s / 0.4 s |
|
||||
| 1 | `"parallel": 4` | 57.9 | -22 % | 63.8 | 0.4 s / 0.4 s |
|
||||
| 2 | one at a time | 71.6 | - | 77.3 | 2.1 s / 4.1 s |
|
||||
| 2 | `"parallel": 2` | 61.0 | -15 % | 32.6 | 0.5 s / 0.9 s |
|
||||
| 2 | `"parallel": 4` | 54.6 | -24 % | 28.7 | 0.6 s / 0.7 s |
|
||||
| 4 | one at a time | 70.7 | - | 79.6 | 6.0 s / 11.2 s |
|
||||
| 4 | `"parallel": 2` | 61.2 | -13 % | 32.2 | 4.5 s / 9.3 s |
|
||||
| 4 | `"parallel": 4` | 63.1 | -11 % | 16.9 | 1.0 s / 1.8 s |
|
||||
|
||||
On this card the slots buy **waiting time, not speed**: the fourth of four requests starts after 1.8 s instead of
|
||||
11.2 s, but together they decode 11-24 % slower than one after the other, and a request alone loses 11 % (2 slots)
|
||||
or 24 % (4 slots), because the slots' sessions (0.56 GiB each) come out of the expert cache and most experts run
|
||||
on the CPU: a batch window over 4 conversations reads 24 CPU experts per layer against ~8 for one, so it costs about
|
||||
what the 4 tokens cost one after the other (`strata batch:` in the engine log: 54 ms per 4-row window, ~20 ms per
|
||||
1-row window). This is why setup leaves a 12 GB card at one at a time. Cards that hold most experts in VRAM, and a
|
||||
layer split, are where the slots also add speed (below).
|
||||
|
||||
A 4-GPU layer split (4 x 16 GB, PCIe Gen3), IQ3_S, `--batch 8 --batch-groups 4 --trim-stage-weights`, through
|
||||
the HTTP server, 400 tokens per answer, temperature 0.7 (PR #559):
|
||||
|
||||
| Concurrent requests | Per request | Total |
|
||||
| ---: | ---: | ---: |
|
||||
| 1 | 123 tok/s (solo path) | 120 tok/s |
|
||||
| 2 | 57 tok/s | 113 tok/s |
|
||||
| 4 | 51 tok/s | 205 tok/s |
|
||||
| 8 | 45 tok/s | 360 tok/s |
|
||||
|
||||
With the patches below on engine 0.1.38 and parking on, through the service: 8 requests at temperature 0 -> 369
|
||||
tok/s, at 0.7 -> 358 tok/s.
|
||||
|
||||
`--trim-stage-weights` alone raised the share of experts held in VRAM on that machine from 76-85 % to 84-100 %
|
||||
per card.
|
||||
|
||||
## Together with conversation parking
|
||||
|
||||
`--conversation-cache-mib N --conversation-cache-slots K` (DETAILS.md) works with the layer split too: a request
|
||||
whose conversation was parked is restored on every stage before its admission, so an agent and its sub-agents, or
|
||||
several chats that alternate, come back without reading their history again. Measured on the same 4-GPU split,
|
||||
two long conversations alternating through the HTTP server: the first turns took 3.9 s and 5.7 s to the first token
|
||||
(their prompts read), the follow-ups 0.53 s and 0.46 s.
|
||||
|
||||
## Testing
|
||||
|
||||
Four scripts drive a built engine or a running server; each exits non-zero on a failure. `serve/test_parallel.py`
|
||||
tests the server's side with a scripted engine (no GPU).
|
||||
|
||||
| Script | What it checks |
|
||||
| --- | --- |
|
||||
| `tools/batch_test.py` | the same prompts alone (`GEN`) and together in the batch slots (`BGEN`): every slot's greedy tokens equal its solo tokens; prints the aggregate rate. `--batch-groups` in `--extra` tests the pipeline, `--keys "temperature=0.7"` the sampled rows. |
|
||||
| `tools/batch_interleave_test.py` | a long prompt read while two slots decode, a prompt that gives way (`BYIELD`) and goes on, and a next turn continued from its slot: each equal to its solo tokens. |
|
||||
| `tools/parking_test.py` | a follow-up to a conversation decodes the same tokens whether its state stayed live or came back from the parking cache (with a layer split: every stage's image). |
|
||||
| `tools/early_close_test.py` | a client that stops reading a streamed answer early (alone, and with a second request running) does not leave its tokens to the next request (server). |
|
||||
|
||||
For exact comparisons pass `--pcie-frac 0 --adapt-every 1000000` (and the scripts set `STRATA_IQ_MT_MIN=1`):
|
||||
|
||||
```
|
||||
python3 tools/batch_test.py --exe engine/strata --config strata-<model>.json --batch 8 --n 8 \
|
||||
--extra "--layer-split 12,24,36 --trim-stage-weights --batch-groups 4 --pcie-frac 0 --adapt-every 1000000"
|
||||
python3 tools/batch_interleave_test.py --exe engine/strata --config strata-<model>.json \
|
||||
--extra "--pcie-frac 0 --adapt-every 1000000 --no-prefill-borrow"
|
||||
python3 tools/parking_test.py --exe engine/strata --config strata-<model>.json \
|
||||
--extra "--layer-split 12,24,36 --conversation-cache-mib 8192 --conversation-cache-slots 4 --pcie-frac 0"
|
||||
STRATA_KEY=<key> python3 tools/early_close_test.py http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
## Engine protocol (`--serve`)
|
||||
|
||||
On top of `GEN` / `GENI`:
|
||||
|
||||
| Line | Direction | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `BGEN <slot> <max_new> [keys] <ids>` | in | read the prompt (as `GEN 1`), then continue in `<slot>` |
|
||||
| `BGENI <slot> <max_new> [keys] <file> <ids>` | in | the same with images |
|
||||
| `BADM <slot> <1/0>` | out | after the admission's `DONE`: 1 = it continues in the slot, 0 = it ended |
|
||||
| `BT <slot> <id>` | out | a token of that slot |
|
||||
| `BDONE <slot> <generated> <stop/length/cancel> <ms>` | out | the slot is free again (it keeps its conversation) |
|
||||
| `BSTOP <slot>` | in | end that slot at its next window |
|
||||
| `BYIELD <slot>` | in | the prompt being read gives way at its next chunk boundary; its part read waits in `<slot>` (the admission's own, or a free slot for a solo request) |
|
||||
| `YIELDED <slot> <tokens>` | out | before the `DONE cancel` of a read that gave way: the request is sent again later and goes on from there |
|
||||
| `INFO ... batch_slots=N` | out | the slots the engine runs (only with `--batch`) |
|
||||
|
||||
`tools/batch_test.py` drives the engine directly: the same prompts alone, then together, compared token by token,
|
||||
and the aggregate rate.
|
||||
@@ -15,6 +15,12 @@ and their limits. Report what you actually measured and label estimates separate
|
||||
- [2026-09-30: RTX 5090, Core Ultra 9 285K, 64 GB RAM](../bench/results/2026-09-30-community-rtx-5090/README.md):
|
||||
Strata 0.1.29, original Flash-Next IQ2_XS, 131,072-token context; three runs
|
||||
each at 4,096, 32,768, and 128,000 prompt tokens, plus six recall checks.
|
||||
- [2026-10-03: 2x AMD Instinct MI50 16 GB (gfx906), Xeon E5-2666 v3, 32 GB RAM](../bench/results/2026-10-03-community-2x-mi50/README.md):
|
||||
the gfx906 build (#638) with #639 and #640, Coder IQ1_M, 131,072-token context, layer split across both cards;
|
||||
three runs each at 4,096, 32,768, and 128,000 prompt tokens, plus six recall checks.
|
||||
- [2026-10-04: 2x Intel Arc Pro B60 24 GB, Ryzen 5 5600, 64 GB RAM](../bench/results/2026-10-04-community-2x-arc-pro-b60/README.md):
|
||||
the SYCL port at `6f32ec0` with two fixes, Coder IQ1_M and Flash-Next IQ2_XS, 8,192-token context, layer split across both cards;
|
||||
short prompts and 2K-token prompts, plus one run on a single B60. Intel's SYCL engine, no 4,096/32,768/128,000-token sweep.
|
||||
|
||||
## What to record
|
||||
|
||||
|
||||
+404
-27
@@ -71,7 +71,7 @@ attention reads in VRAM (`--kv-resident 32768`), so more experts fit on the GPU.
|
||||
(1,589 -> 3,872 experts in VRAM); at 128K about +6%. The attention reads exactly the same values (only where the KV lives changes); it
|
||||
costs ~13.7 KB of RAM per context token (1.7 GB at 128K). Existing installs: run `START-HERE.bat --setup` once to turn
|
||||
it on. Setup turns it on when the RAM has room for it; `--kv-streaming on|off` overrides that (on past the RAM test with a
|
||||
note; never with `--kv k8v4` or under WSL, which cannot stream).
|
||||
note; never under WSL, which cannot stream).
|
||||
|
||||
**4-bit KV cache (engine 0.1.8, optional):** `START-HERE.bat --setup` asks above 8K context (or pass `--kv q4_0`). It
|
||||
halves the KV cache's memory with a Hadamard rotation before 4-bit rounding (PR #21), about 4% faster at 128K, but it
|
||||
@@ -81,8 +81,8 @@ Details: [`bench/results/2026-09-27-kv-q4`](../bench/results/2026-09-27-kv-q4/RE
|
||||
**Hybrid K8V4 KV cache (engine 0.1.25, optional, PR #120):** `--kv k8v4` (`START-HERE.bat --setup --kv k8v4`) keeps
|
||||
the keys at 8 bits and stores the values as rotated 4-bit: 23% less KV memory than 8-bit, so more experts fit in
|
||||
VRAM. RTX 3090, the Coder at 198K context: 99 instead of 85 tokens/s output, the same needle results, prompts 2-5%
|
||||
slower. It does not stream its KV cache (KV streaming is on by default from 64K), so it pays off mostly on large
|
||||
cards at long contexts.
|
||||
slower. It streams its KV cache like the other formats (`--kv-resident N`): on an RTX 2060 SUPER 8 GB at 128K with
|
||||
20,480 resident cells, +780 expert slots over resident K8V4, and it scores better than 4-bit KV on long documents.
|
||||
|
||||
**Reproducible greedy output (0.1.30, opt-in, `STRATA_IQ_MT_MIN=1`):** with the IQ models, the CPU computes an
|
||||
expert for one token with ggml's dot product and for several tokens with Strata's multi-token kernels, which round
|
||||
@@ -90,7 +90,9 @@ slightly differently. How many tokens share an expert depends on the drafts in a
|
||||
at temperature 0 can end in a different (equally good) answer when the drafting, the cache state or a resumed
|
||||
conversation differ (issue #152). `STRATA_IQ_MT_MIN=1` (in the config's `env`) uses the multi-token kernels for
|
||||
every group: the answer then no longer depends on the drafting. Measured on a Ryzen 7600 (AVX-512): IQ3_S decode
|
||||
-1..-3%, the other models the same; the default stays the fastest rule. Through the server, two more things carry
|
||||
-1..-3%, the other models the same; the default stays the fastest rule. On an Intel CPU of Alder Lake or later
|
||||
without AVX-512, where the AVX-2 kernel gathers the IQ3_S grid, `STRATA_IQ3S_MT1=1` (opt-in) gives IQ3_S the multi-token
|
||||
kernel for one token, which is the faster one there; it changes a lone token's rounding, so it is off by default. Through the server, two more things carry
|
||||
over from one request to the next (#410): the adaptive tier moves experts between RAM and VRAM (the GPU and the CPU
|
||||
round an expert differently), and the prompt cache resumes a repeated prompt and reads only its tail through the
|
||||
decode path. For byte-identical repeats add `--prompt-cache 0 --adapt-swaps 0 --pcie-frac 0` to the engine's args
|
||||
@@ -107,11 +109,21 @@ English/code subset from before (40,525 ids, ~110 MiB less VRAM, English answers
|
||||
almost no drafts). `--draft-vocab cyrillic` takes the English/code subset plus the whole Cyrillic script (58,963
|
||||
ids): the shipped subsets hold 142 of the vocabulary's 18,580 Cyrillic tokens, so Ukrainian or Russian answers got
|
||||
1.4 tokens a round; with it 2.1, and 83 -> 109 tokens/s (RTX 5090, the NVFP4 fork), English unchanged.
|
||||
`--draft-vocab fr` (0.1.39, #597) takes the English/code subset plus the 5,686 tokens that cover 99% of a French
|
||||
Wikipedia corpus (46,211 ids, `tools/draft_vocab.py --corpus`): 23.6% of French text's tokens were outside the
|
||||
English/code subset, 0.7% are outside this one. Drafts accepted in French answers 0.51 -> 0.60 (IQ3_XXS, RTX 5070,
|
||||
8 prompts x 2 passes; English 0.61 -> 0.63 and code 0.77 -> 0.78, no loss), and 141 -> 158 tok/s in French on an
|
||||
RTX 5090 (IQ3_S, the reporter's measurement).
|
||||
`tools/draft_vocab.py` builds and inspects subsets. When the start stops with "the draft head does not fit" (a
|
||||
12 GB card with a long context, #474), the engine says how much the head needs, how much VRAM is free and which
|
||||
smaller subset fits, and the server's start error repeats it; setup suggests `--draft-vocab en` on cards under
|
||||
14 GB (only a suggestion: nothing changes unless you pass it).
|
||||
|
||||
**Serving without the draft layer (`--mtp` is optional):** `serve` runs without `--mtp`. Drafts then come from the
|
||||
suffix/prompt-lookup drafter only (or one token per round), every token is still verified against the model, and the
|
||||
draft layer's VRAM (~0.7-1 GiB with its head) goes to the expert cache: 1,000 -> 1,678 slots in one A/B on an 8 GB
|
||||
card. The conversation cache (`--conversation-cache-mib`) stays on: a parked conversation then carries no draft K/V.
|
||||
|
||||
**Low-RAM mode (engine 0.1.26, chosen by setup):** normally all of a model's experts are copied into RAM (23-50 GB,
|
||||
pinned) and the GPU holds a copy of the most-used ones. On a PC whose RAM cannot hold them beside the system (the
|
||||
experts plus ~10 GB), setup instead maps them from one file in the model's folder (`--mmap-experts`, the pack's
|
||||
@@ -131,6 +143,15 @@ other ~18 GB), a 32 GB PC with a 12-16 GB GPU the Coder; IQ3_XXS on a 32 GB PC s
|
||||
as far as the RAM allows, their experts are kept in RAM too (so a prompt reads nothing from the SSD either).
|
||||
- The cache still follows the conversation (`--adapt-every`): a swap copies the evicted expert back from VRAM into the
|
||||
RAM place of the one that replaces it, so the RAM copy keeps holding exactly what the GPU does not.
|
||||
- `--adapt-async 1` (opt-in, `--serve`): the swaps of a round advance between decode windows on a helper thread
|
||||
(copy back, copy in, move into RAM) instead of one window waiting for the whole round. Not with `--batch` or
|
||||
`--peer-device` (the blocking tier runs there). It is not bit-exact from run to run: which window first computes a
|
||||
swapped-in expert on the GPU (which rounds differently from the CPU) depends on when its copy lands.
|
||||
It stays on the blocking tier (said in the log) when the exchange buffers are not page-locked, and with
|
||||
`--pipeline-windows`; the stats line reports ms per round.
|
||||
- `STRATA_EXCHANGE_ROTATE=1` (opt-in): an adaptive swap hands buffer ownership over instead of copying the evicted
|
||||
blob into the RAM copy (equal-size blobs, fully page-locked copy). Same tokens, fewer host copies; it works with
|
||||
`--adapt-async 1` too. Details and the measurement: [EXCHANGE_ROTATION.md](EXCHANGE_ROTATION.md).
|
||||
- The answers are the plain mapped mode's for the same expert placement: the bytes are the file's. With a page-locked
|
||||
copy the GPU also takes its usual share of the misses over PCIe (`--pcie-frac`), as with enough RAM; `--pcie-frac 0`
|
||||
(or `STRATA_RESIDENT_PIN=0`) gives the mapped mode's exact tokens.
|
||||
@@ -143,7 +164,25 @@ other ~18 GB), a 32 GB PC with a 12-16 GB GPU the Coder; IQ3_XXS on a 32 GB PC s
|
||||
(`--mmap-experts`): the cards together hold more of the experts, and two users measured it 1.3-1.6x faster than
|
||||
one card, but the OS file cache can fill the RAM to 0 free during long prompts. `--yes` keeps one GPU. A config
|
||||
with `--resident-experts` started with `--gpus` switches to `--mmap-experts` with a note, and the engine runs that
|
||||
pair as `--mmap-experts` with a warning instead of refusing it.
|
||||
pair as `--mmap-experts` with a warning instead of refusing it. **Since 0.1.40 (#642, #848)** setup requires an
|
||||
engine that runs the resident variant on a split, so it keeps both cards, resident, as for any config:
|
||||
[MULTI_GPU.md](MULTI_GPU.md#using-it).
|
||||
|
||||
**A mapped arena for small RAM (Linux, opt-in, 0.1.39, PR #640):** `STRATA_ARENA_MMAP=1` maps a native pack's expert
|
||||
arena read-only from the pack's `experts.bin` instead of reading it into locked RAM, for a PC whose GPUs hold most
|
||||
experts but whose RAM is small (2x 16 GB GPUs with 32 GB of RAM: ~1 GB -> 25 GB available while serving). The first
|
||||
start writes `experts.bin` (when the drive has room for it), later starts map it; the pages of the experts a GPU holds
|
||||
are handed back to the OS. Run it with `--pcie-frac 0` (the GPUs get no mapped alias). Without the variable nothing
|
||||
changes.
|
||||
|
||||
**Releasing mapped expert pages on Windows (opt-in):** `STRATA_FILE_RELEASE=1` lets `FileExpertSource` trim the
|
||||
full file-backed pages of experts after their GPU uploads complete, including the slots lent to prefill and then
|
||||
refilled. It works with `experts.bin` and the direct GGUF views; shared boundary pages and private/pinned buffers
|
||||
are left alone. Unset or `0` keeps the previous behavior. On one 32 GB Windows 11 laptop with an RTX 4080 Laptop
|
||||
and a Thunderbolt RTX 3090, IQ2_XS with `--mmap-experts --layer-split 12 --trim-stage-weights` raised median available
|
||||
RAM from 0.51 to 12.19 GiB across 1K/4K/16K prompt trials, with 3.7-4.4% lower prefill throughput. It did not reduce
|
||||
committed memory or physical SSD reads. This is a working-set hint, not an unmap or a guarantee that the OS drops
|
||||
its file cache. See the [configuration, measurements and limits](../bench/results/2026-10-04-windows-mapped-release/README.md).
|
||||
|
||||
**Low-RAM mode without `experts.bin` (engine 0.1.31):** for the native packs (IQ2_XS, IQ3_XXS, IQ3_S, the Coder, Swift,
|
||||
Q2_0 packed by `tools/iq_pack.py`; not the canonical Q2_0 pack setup makes for AVX-512 CPUs) the mapped mode no longer
|
||||
@@ -157,22 +196,64 @@ engine fetches a layer's missing experts on 8 threads (`STRATA_FETCH_THREADS`) w
|
||||
|
||||
**A RAM budget (engine 0.1.31, `--resident-budget-gib N`):** the resident variant for a model whose experts do not all
|
||||
fit: the N GiB of experts the GPU cache does not hold that the expert profile ranks hottest are copied into RAM at
|
||||
start (locked; page-locked when the driver allows the whole budget), and the rest are read from the files through the
|
||||
OS file cache. It implies `--mmap-experts` and leaves 4 GB of free RAM (a larger N is clamped to that less 256 MiB,
|
||||
with a message; #403: a clamped budget no longer fails the safety check that follows, and a budget that cannot be
|
||||
kept at all is a warning, with every expert read from the files). Setup sets N with `--resident-budget-gib N`. With
|
||||
start (locked; page-locked when the driver allows the whole budget), and the rest are read from the files.
|
||||
It implies `--mmap-experts` and leaves 4 GiB of headroom. On Windows, available commit capacity also
|
||||
limits the budget; a larger N is clamped to the smaller limit less 4 GiB and a 256 MiB margin, with a message.
|
||||
A clamped budget no longer fails the safety check that follows (#403). A budget that cannot be kept at all is a
|
||||
warning, with every expert read from the files. Setup sets N with `--resident-budget-gib N`. With
|
||||
the GGUF read in place it also warms the next layer's likely experts: while the CPU works on a layer, a thread applies
|
||||
the next layer's router to this layer's input and asks the OS for the pages of the predicted experts that neither the
|
||||
GPU nor the RAM budget holds (only pages - the experts computed are the same; `STRATA_LOOKAHEAD=0` turns it off). This
|
||||
is what runs [Unsloth's UD-Q4_K_XL](UNSLOTH_Q4.md) (72 GiB of experts) on a 64 GB PC: 7-8.5 tokens/s at N = 40 on an
|
||||
RTX 5070, against ~3 tokens/s before these changes.
|
||||
|
||||
**Switches added in 0.1.40 (all off unless noted; none changes the default output):**
|
||||
- `--kv-grow` (or `STRATA_KV_GROW=1`; `--no-kv-grow` turns it off): the K/V takes VRAM only for the cells the requests
|
||||
reach, and the expert cache holds the rest, giving slots back as the context grows. It needs one GPU, an expert
|
||||
profile, the whole K/V in VRAM (no KV streaming) and every expert in RAM (not the resident low-RAM mode); otherwise,
|
||||
and with `--batch`, `--vram-elastic` or `--peer-device`, the engine says so and stays off.
|
||||
- `--host-core last` (or `STRATA_HOST_CORE=last`, Windows): the host thread runs on the last physical core and the
|
||||
workers take the first. Windows sends a GPU's interrupts to the first core, where a host spinning on the GPU's flags
|
||||
waits for them (`--host-core first` is the default; the startup log names the cores).
|
||||
- `STRATA_ADAPT_LAG=2` (#764): a window takes the adaptive tier's swaps once they are two windows old (default 1,
|
||||
as 0.1.39). `STRATA_PREFILL_EQUAL=1` (#693): a prompt segment is read in chunks of equal size, not full chunks and a
|
||||
short last one (changes the rounding). `STRATA_OWNED_PRICE=exact` (#796): the cache sizing prices the prompt path's
|
||||
own buffers by their real allocation instead of 0.1.39's rule.
|
||||
- `STRATA_Q2_BITPLANE=1`: a bit-plane row kernel for Q2_0 on CPUs with AVX2 and no AVX-512 (changes the last bits).
|
||||
`STRATA_NO_AVXVNNI=1` turns the AVX-VNNI forms of the i-quant, IQ4_NL and Q2_0 rows off on CPUs that have them
|
||||
(Alder Lake, Sapphire Rapids and later); they give the same bits as the AVX2 forms, 4-25% faster rows.
|
||||
- Draft layer: `--lookup-chain-min M` (the shortest context match `--lookup-chain` extends, default 3),
|
||||
`--mtp-hnorm stream` (one norm per stream, as llama.cpp's MTP graph) and `--mtp-draft-vocab FILE` (a token subset
|
||||
of your own instead of `<mtp>/draft_vocab.bin`).
|
||||
- Elsewhere: HIP `STRATA_DENSE_MMQ=1` and `STRATA_HIP_ADAPT_KERNEL_COPY=1` in [AMD_HIP.md](AMD_HIP.md#model-and-serving-configuration);
|
||||
`STRATA_KEEP_EMPTY_TURNS=1` and `STRATA_TOPK_STREAM=0` in [TROUBLESHOOTING.md](TROUBLESHOOTING.md).
|
||||
|
||||
|
||||
**Read-ahead at start (Linux):** the weights, the native dense matrices, the GPU cache's fill from the profile, the
|
||||
resident RAM copy and the MTP draft files are asked for ahead of their reads (madvise / posix_fadvise WILLNEED in
|
||||
128 KiB steps), so the drive sees a deep queue instead of one page fault at a time. Measured on a Gen3 NVMe (RTX 5090,
|
||||
32 GB, Q2_0 resident at 262K): ready in 70 s instead of ~920 s; the fill went from 39 MB/s to 3.2 GB/s.
|
||||
`STRATA_READ_AHEAD=0` turns it off; `STRATA_FILL_AHEAD=N` sets how many fill pairs are asked for ahead (default 256).
|
||||
|
||||
**How much came from where:** with `--stats` the engine prints the tiers of the decode (`expert tiers`: blobs from the
|
||||
RAM copy, blobs and MB from the files, the time spent reading them; `routing prefetch`: how many of the file reads had
|
||||
been warmed). The server log has the same per request (`expert tiers: GPU ... hits ...; RAM ... blobs, files ...
|
||||
blobs ... MB read`), and `GET /metrics` lists `ram_blobs`, `file_blobs` and `file_mb` for each recent request (with
|
||||
engine 0.1.31 or newer). It also lists each request's speculative drafts, `drafts_offered` and `drafts_accepted`
|
||||
(`null` when the engine did not report them), and their sums since the server started in `totals` (#457).
|
||||
Its `hit_rate` is the VRAM share of the experts looked up while answering: experts the GPU reads over PCIe
|
||||
(`--pcie-frac`) are not in it, so a higher `--pcie-frac` raises it even when decoding gets slower. `pcie_share`
|
||||
(engine 0.1.39 or newer, #588) is their share of all routed experts, and the server log and the Monitor tab show it
|
||||
beside the hit rate.
|
||||
|
||||
**Where a decode window's time goes (profiling, #610):** start the server with `STRATA_DECODE_TIMING=1` (and
|
||||
`STRATA_VERIFY_PROFILE=1` for the GPU's side) in the environment. After each request the engine log then has one
|
||||
`strata decode timing:` line - windows, tokens per window, and per window the verify time split into the wait for the
|
||||
GPU, the CPU expert pool (plan, activation quantization, jobs) and the stage, plus commit and draft - and one
|
||||
`strata decode GPU stages (ms/window):` line with the GPU time of each stage (GDN and QSA layers, the VRAM expert
|
||||
hits, the router, the head, ...). The GPU profile times every stage with events, so it slows the decode a little:
|
||||
use it to compare, not to measure speed. This works with every pack; `--gpu-stages` (a one-token replay of
|
||||
per-layer graphs) refuses a native (IQ) pack, which has no such graphs.
|
||||
|
||||
Time to first token is prompt length / prompt speed: with Q2_0 about 4 s at 4K, 25 s at 32K, under 2 minutes at 128K
|
||||
and 4.5 minutes at 262K (engine 0.1.13 made long prompts about twice as fast, below).
|
||||
@@ -208,7 +289,9 @@ card gains depends on its PCIe link (the experts stream over it), so they are st
|
||||
More VRAM matters more than a faster GPU: every extra GB holds ~700 more experts, and every expert on the GPU is one the
|
||||
CPU does not have to compute. A 3090's 24 GB takes most of the CPU work away. (Since 0.1.14 the expert profile ranks
|
||||
all 24,576 experts; before, the cache stopped at 8,000, about 10-14 GB. `tools/make_profile.py` builds a profile from
|
||||
your own prompts: run the engine once with `--dump-routing trace.bin`, see the tool's help.)
|
||||
your own prompts: run the engine once with `--dump-routing trace.bin`, see the tool's help. The shipped profile already
|
||||
ranks every pair, so a trace only changes the order with `--reorder` (0.1.39, #587): your trace's pairs first, then the
|
||||
base's, then the rest.)
|
||||
|
||||
## Which model?
|
||||
|
||||
@@ -276,8 +359,8 @@ Everything else is installed for you the first time.
|
||||
| --- | --- |
|
||||
| GPU | NVIDIA **RTX 20, 30, 40 or 50 series**, **12 GB VRAM or more** (8 GB runs, slowly). Measured on an RTX 5070 and an RTX 3090; RTX 20 (Turing, since 0.1.27) was tested by a contributor on an RTX 2070. Or AMD **Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700, RX 6800 / 6900 series**: [AMD_HIP.md](AMD_HIP.md). |
|
||||
| RAM | **64 GB** recommended (see the table above). |
|
||||
| CPU | x86-64 with AVX2 (any Intel/AMD desktop CPU from the last ~8 years). AVX-512 (Ryzen 7000/9000) is a bit faster. |
|
||||
| Disk | ~70-80 GB free for the model, ~6 GB for the MTP layer (+1 GB with images). **Q2_0 on an AVX-512 CPU** also writes a one-time ~40 GB copy of its experts for the fast CPU kernel. An NVMe SSD is strongly recommended. |
|
||||
| CPU | x86-64 with AVX2 (any Intel/AMD desktop CPU from the last ~8 years). AVX-512 (Ryzen 7000/9000) is a bit faster. Older CPUs without AVX2 are experimental and slow: [Older CPUs](INSTALL.md#older-cpus-experimental). |
|
||||
| Disk | ~70-80 GB free for the model, ~6 GB for the MTP layer (+1 GB with images). **Q2_0 on an AVX-512 CPU** also writes a one-time ~40 GB pack for the fast CPU kernel (34 GB of experts in its layout, plus the dense weights). An NVMe SSD is strongly recommended. |
|
||||
| OS | Windows 10/11, or Linux (Ubuntu 22.04/24.04 get everything installed automatically). |
|
||||
|
||||
What the first start installs: in this folder `.venv/`, `engine/` and `third_party/`; the model files (`models/`,
|
||||
@@ -414,6 +497,18 @@ the OS file cache, so loading again takes seconds while that RAM is not needed e
|
||||
16 GB with Q2_0 in the low-RAM mode: unloading takes ~0.3 s, and a request to an unloaded model answered after
|
||||
4.6 s (text) or 14.7 s (a picture, image encoder on the CPU).
|
||||
|
||||
**Giving part of the VRAM back while it keeps serving (#533, opt-in, one NVIDIA GPU).** With `"vram_elastic": true`
|
||||
in the config (the engine flag `--vram-elastic`), the expert cache is allocated in 512 MiB segments
|
||||
(`"vram_segment_mib"`), and `POST /v1/vram` with `{"reserve_mib": 8000}` shrinks it between requests until that much
|
||||
VRAM is free for another program; `{"reserve_mib": null}` grows it back towards its full size, keeping the reserve
|
||||
the engine started with (`--vram-reserve-mib`), and `{"reserve_mib": 0}` takes all of it back. A request that is
|
||||
running finishes first. The experts of the segments given back are computed on the CPU, like any expert outside the
|
||||
cache, so answers keep coming, slower; growing back puts the same experts in the same slots. Nothing resizes on its
|
||||
own. Not with a layer split, the helper caches, `--peer-device` or the resident low-RAM mode. Measured on an RTX 5070
|
||||
12 GB with Q2_0 (4.8 GiB cache): `{"reserve_mib": 6000}` took 78 ms and freed 4.3 GiB (the cache keeps 0.5 GiB for
|
||||
the prompt path), decode 44 -> 33 tok/s; growing back took 92 ms and the answers were token for token the ones before
|
||||
the shrink. Without the flag nothing changes (the same answers as without it).
|
||||
|
||||
**Keep what the expert cache learned across restarts (opt-in, engine 0.1.36, #477):** a start fills the GPU's expert
|
||||
cache from the shipped profile, and the adaptive tier (`--adapt-every`) then moves in the experts your requests use.
|
||||
With `"expert_profile_save": "expert-profile-learned.bin"` in `strata-<model>.json` the engine saves that as a
|
||||
@@ -435,9 +530,11 @@ The server listens on `http://127.0.0.1:8080` (change with `--port` in setup, or
|
||||
| --- | --- |
|
||||
| OpenAI Chat Completions (stream and non-stream, tools) | `POST /v1/chat/completions` |
|
||||
| Anthropic Messages (stream and non-stream, tools) | `POST /v1/messages` |
|
||||
| OpenAI Responses (stream and non-stream, tools; stateless, [below](#the-responses-api-and-codex-cli)) | `POST /v1/responses` |
|
||||
| Model list / health | `GET /v1/models`, `GET /models`, `GET /health` |
|
||||
| Model properties | `GET /props` (also accepts `?model=<loaded-model-id>`) |
|
||||
| What the model is doing right now | `GET /status`, `GET /slots` (single slot, busy or idle) |
|
||||
| Save / restore the conversation to a file (session files, below) | `POST /slots/0?action=save\|restore` |
|
||||
| Everything the Monitor tab shows (engine, live state, last requests, hardware) | `GET /metrics` |
|
||||
| The MCP servers, their state and tools ([below](#tools-from-mcp-servers)) | `GET /mcp` |
|
||||
|
||||
@@ -473,6 +570,32 @@ print(r.choices[0].message.content)
|
||||
part of the thinking the client sees and counts as output tokens. `"reasoning_budget_tokens": N` in
|
||||
`strata-<model>.json` sets it for every request; a request's own value wins, and `0` means no budget. Off by default;
|
||||
Anthropic's `"thinking": {"budget_tokens": N}` still only chooses the level, as above.
|
||||
- **A reply stuck on one token is ended (0.1.39, #606).** When a reply repeats the same token 256 times in a row, the
|
||||
server ends it there with `finish_reason` `"length"` and says so in its window: a model in a loop, or a broken
|
||||
state that answers one token forever (#606 saw 36,689 tokens of `!`). `"repeat_stop_tokens": N` in
|
||||
`strata-<model>.json` sets the run length; `0` turns it off (for a request that really wants one token many times).
|
||||
- **Repeated reasoning (opt-in, #728).** The single-token guard above does not see a model that repeats whole
|
||||
passages. `"reasoning_loop_recovery"` in `strata-<model>.json` is `false` (the default), `"stop"` or `"recover"`
|
||||
(`true` means `"recover"`). Every 512 output tokens, at a complete character and parser boundary, the reasoning is
|
||||
measured over its last 2,000 words and punctuation marks (counting passages over the last 30,000 words). If at
|
||||
least 25% belong to 12-word passages seen three times, `"stop"` ends the reply there as `"length"` and says so in
|
||||
the server window. `"recover"` stops and drains that generation, then goes on once from all its generated token
|
||||
ids with the template's low-effort sentence in place of the xhigh one in the first system message (a splice of
|
||||
token ids: the rest of the prompt is not decoded or re-encoded). The task stays the same; no answer or `</think>`
|
||||
is inserted, and both passes share the original output limit. On recovery only, temperature is raised to at least
|
||||
1.0 and presence penalty to at least 1.5; a client's top-p, top-k and seed are never changed. `/metrics` records
|
||||
`reasoning_recoveries` and the coverage. This is a policy, not a numerical engine fix or a guarantee of an answer.
|
||||
`"recover"` needs the exact xhigh sentence in the first system message and skips requests with images; re-reading
|
||||
the changed prefix costs prompt time. Either mode can mistake repeated useful code or checks for a loop, so keep
|
||||
the recorded answer quality alongside the completion rate when testing it.
|
||||
- **Changing the effort without re-reading the prompt (opt-in, 0.1.39, #458).** The effort's instruction is the
|
||||
first thing in the prompt, so a request that only changes the effort (an agent's "think harder" switch, `none` for
|
||||
a quick tool step) reads the whole conversation again. `"effort_position": "end"` in `strata-<model>.json` renders
|
||||
every request's prompt start as the default effort's and puts a `low` / `medium` effort in a short system turn
|
||||
right before the answer (no thinking: the empty thinking block, as always); the engine (0.1.39+, which the server
|
||||
checks) keeps its checkpoint in front of that turn, so the next request reuses the conversation whatever its
|
||||
effort. The default (`"start"`) prompt is unchanged. The model sees a level that is not the default in another
|
||||
place than it was trained with; how well it follows it there is not measured yet.
|
||||
- **Anthropic requests that don't ask for thinking (opt-in, 0.1.32, #278).** By default a `/v1/messages` request
|
||||
with no `"thinking"`, effort or budget thinks as the model's template does. `"anthropic_thinking": "on_request"` in
|
||||
`strata-<model>.json` renders such a request without thinking - Anthropic's own rule, and what Claude Code's short
|
||||
@@ -482,6 +605,15 @@ print(r.choices[0].message.content)
|
||||
long prompt the stream sends keep-alives, so agents do not time out; the server window prints progress every
|
||||
15 s, and `GET /status` says what it is doing (`reading the prompt`, `answering`, tokens so far). Closing the
|
||||
connection or pressing stop in your app really stops the model, so the next request starts at once.
|
||||
- **Prefill progress in the stream (opt-in).** `"return_progress": true` puts that progress on the stream instead of
|
||||
sending only the keep-alive, as one extra field on a chunk with an empty delta: `prompt_progress` with `total`,
|
||||
`cache`, `processed` and `time_ms`. Those are llama.cpp's four fields and mean the same there (`time_ms` is the time
|
||||
since the prompt started reading, and the work still to do is `(total-cache) - (processed-cache)`), so a client that
|
||||
draws a prefill bar for llama.cpp draws one here too. It is off unless the request asks, as it is in llama.cpp.
|
||||
The engine says one line per `--prefill` chunk, so that chunk is the step: measured on two RTX 3090s with
|
||||
`--prefill auto` (8192 tokens), a 42,131 token prompt read in 15 s sent six of them. A prompt shorter than one chunk
|
||||
sends nothing, and that is on purpose: its only line arrives once the prompt is read, because the last tokens go
|
||||
through the verify windows rather than the batched path, so it stops up to `--short-read` tokens short of the end.
|
||||
- **Chat apps.** Any app with an "OpenAI-compatible" provider works: base URL `http://127.0.0.1:8080/v1`, any API key.
|
||||
- **OpenCode** (#543). A starting point for `opencode.jsonc` (in your project, or `~/.config/opencode/`); the field
|
||||
names are OpenCode's, so check its config docs if your version differs:
|
||||
@@ -520,6 +652,7 @@ print(r.choices[0].message.content)
|
||||
- **Claude Code** (Strata 0.1.17 or newer): set `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` and
|
||||
`ANTHROPIC_MODEL` to a Claude model name it knows (it refuses names it doesn't; Strata ignores the name), plus any
|
||||
`ANTHROPIC_AUTH_TOKEN` (or your `api_key`, if you set one).
|
||||
- **Codex CLI** (0.1.39): see [the Responses API](#the-responses-api-and-codex-cli) below.
|
||||
- **Context.** Chosen in setup (8K-262K). Requests longer than that are refused, never silently cut. A request whose
|
||||
`max_tokens` would run past the context is refused too (400); agents that always ask for their full output cap
|
||||
can instead get it shortened to the room left: add `"fit_max_tokens": true` to `strata-<model>.json` (or pass
|
||||
@@ -527,6 +660,14 @@ print(r.choices[0].message.content)
|
||||
- **Model aliases** (0.1.32). `"aliases": ["qwen", "local-model"]` in `strata-<model>.json` lists the model under
|
||||
those names too in `/v1/models` (each with its own `id`, and in the model's `aliases`), like llama-server's
|
||||
`--alias`; a request naming one is answered under that name. Any other name is still served, as before.
|
||||
- **Model settings in the web page (0.1.39, #564).** The About tab's Model settings card shows and changes a few of
|
||||
the keys above in `strata-<model>.json`: the `sampling` defaults (temperature, top_p, top_k, min_p),
|
||||
`reasoning_budget_tokens`, `fit_max_tokens`, `anthropic_thinking`, `effort_position`, `aliases`, `idle_unload_s`,
|
||||
`lazy_load`, `engine_silence_s`, `api_monitor`, `open_browser` and `--vram-reserve-mib`. An empty field removes the
|
||||
key (its default). Every other key of the file stays as it is, the earlier file is kept as
|
||||
`strata-<model>.json.bak`, and the model uses the change from its next start. Only Strata's own page can save
|
||||
(JSON, the API key when one is set, as for the Chat settings); the network, key, MCP and program keys are not
|
||||
editable there.
|
||||
- **From other devices on your network.** The server listens on your PC only (`127.0.0.1`) unless you say otherwise:
|
||||
run setup with `START-HERE.bat --setup --host 0.0.0.0 --api-key some-long-secret` (or add `"host": "0.0.0.0"` and
|
||||
`"api_key": "..."` to `strata-<model>.json`). The server window then prints this PC's addresses
|
||||
@@ -563,7 +704,8 @@ print(r.choices[0].message.content)
|
||||
other page, and `Origin: null`, gets **403**. Clients that send no `Origin` (curl, the OpenAI and Anthropic SDKs,
|
||||
other servers) are not affected. With
|
||||
an API key, the key decides. `POST /unload` and `POST /load` take `Content-Type: application/json` from Strata's
|
||||
own page (or no `Origin`), like `/settings`.
|
||||
own page (or no `Origin`), like `/settings`. `POST /slots/0?action=save|restore` keeps the Host and API-key checks
|
||||
and also takes only JSON from no `Origin`, Strata's own page or a trusted origin - also when an API key is set.
|
||||
|
||||
**Conversation cache.** A request that continues a chat reads only the part after what the engine already holds: the
|
||||
live session, or one of the checkpoints it keeps in RAM (up to 6, ~118 MB each, taken at the start of each new
|
||||
@@ -572,8 +714,15 @@ and pictures. The oldest checkpoint - in practice the end of the system prompt,
|
||||
shares - is kept for good while the rest rotates by least recent use, so a NEW chat that shares that prefix starts
|
||||
reading after it instead of from token 0. A prompt read from the start is also checkpointed at the end of its system
|
||||
prompt when that is 2,048 tokens or more (engine 0.1.20; PR #62 + #65), so that root exists for agent clients with long
|
||||
system prompts and tool lists. Engine options: `--prompt-cache N` (0 = off), `--prompt-cache-every N`,
|
||||
system prompts and tool lists. Claude Code stamps its system prompt with a billing header that changes on every
|
||||
request (`cch=...`) and every session (the 4th part of `cc_version=`); the server pins both stamps (to `f`s, as
|
||||
llama.cpp does), so the system prompt and the tool list in front of it are the same prompt on every turn. Engine options: `--prompt-cache N` (0 = off), `--prompt-cache-every N`,
|
||||
`--prompt-cache-root N` (0 = no system-prompt checkpoint), `--turn-token ID`.
|
||||
A one-shot request that no later request continues (a classification call, a probe) can send
|
||||
`"strata_checkpoint": false` in its body: it saves no checkpoint at its last turn nor every 16K tokens, so what
|
||||
follows the reused prefix (or the root) is read in one run, and its session is not kept or parked for a next request.
|
||||
It still starts from a checkpoint it matches, and still saves the system-prompt root when that reaches
|
||||
`--prompt-cache-root`. Without the field (or with `true`) nothing changes.
|
||||
|
||||
**Multiple conversations (opt-in).** Add `--conversation-cache-mib 8192
|
||||
--conversation-cache-slots 4` to the engine arguments to park up to four conversations
|
||||
@@ -581,13 +730,20 @@ in a bounded 8 GiB host-RAM cache. This preserves controller/worker histories wh
|
||||
their requests alternate; it does not execute requests concurrently. No client session
|
||||
ID is required: only exact token/image prefixes with matching steering mode are reused.
|
||||
The default budget is 0 (disabled); `--prompt-cache 0` also disables parking.
|
||||
The initial shared-core integration supports a single session GPU: combining
|
||||
enabled parking with `--layer-split` is rejected before model loading. Ordinary
|
||||
upstream layer-split checkpoints remain available with parking disabled. FP16,
|
||||
With `--layer-split`, a parked conversation holds one image per stage (each later stage's
|
||||
session and its share of the conversation checkpoints), every image is validated before
|
||||
anything is overwritten, and each is restored on its own GPU. Verified on a 4-GPU split:
|
||||
a 9,276-token conversation restored in 51 ms, and its follow-up decodes exactly the tokens
|
||||
it decodes when the conversation never left (`tools/parking_test.py`). FP16,
|
||||
INT8, Q4_0 and identity-layout K8V4 snapshots are supported; the K8V4 draft ring
|
||||
remains INT8, as in upstream. Windows/HIP and multi-GPU runtime coverage must be
|
||||
reported separately from Linux/CUDA evidence.
|
||||
|
||||
The web page's Monitor tab has a **Conversation cache** card (0.1.39, #596): the parked conversations against the
|
||||
slots and the RAM budget, how many were parked, restored and evicted, and the last switch (read from the engine's
|
||||
log), and for every setup how many prompt tokens the cache gave back - in the last request and since the start.
|
||||
`/metrics` has the same under `"conversation_cache"`.
|
||||
|
||||
Snapshots contain running state, checkpoints, used K/V pages, and draft-layer K/V.
|
||||
They add host RAM, not another model or VRAM allocation. The byte budget also counts
|
||||
an incoming snapshot during a switch. After a restore, unchanged K/V pages can be
|
||||
@@ -612,12 +768,127 @@ rather than permission to continue with partial state. Indexer spare keys and th
|
||||
moving spare row are preserved, including checkpoint rewinds.
|
||||
The engine log reports parking, restoration, bytes, evictions, individual snapshot
|
||||
sizes and K/V bytes reused during capture. `STRATA_SNAPSHOT_FULL_CAPTURE=1` disables
|
||||
retention for diagnostic comparisons. Snapshots are not
|
||||
persisted across restarts.
|
||||
retention for diagnostic comparisons. Parked snapshots are not
|
||||
persisted across restarts; the session files below are.
|
||||
|
||||
**Current limits (v1):** one request at a time, and one conversation cached at a time (switching between two chats
|
||||
re-reads the other one unless the opt-in cache above is enabled); images only when set up with them (below); no video. **Temperature / top_p / top_k / min_p /
|
||||
seed** are honored per request (OpenAI and Anthropic fields); with the default adaptive expert tier a sampled result
|
||||
**Session files (disk).** The conversation the engine holds can be saved to a file and restored later, also after a
|
||||
restart of the same engine version, so a long prompt is not read again. The server exposes the save and restore
|
||||
requests of llama-server's slot API, for its single slot 0, when started with `--slot-save-path DIR` (also
|
||||
`"slot_save_path"` in the config); NAME must be a plain file name inside DIR. There is no erase action and the file
|
||||
format is Strata's own, not llama.cpp's:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://127.0.0.1:8080/slots/0?action=save" -H "Content-Type: application/json" -d '{"filename": "chat1.bin"}'
|
||||
# {"id_slot": 0, "filename": "chat1.bin", "n_saved": 63025, "n_written": 1198691396, "timings": {"save_ms": 709.5}}
|
||||
curl -X POST "http://127.0.0.1:8080/slots/0?action=restore" -H "Content-Type: application/json" -d '{"filename": "chat1.bin"}'
|
||||
# {"id_slot": 0, "filename": "chat1.bin", "n_restored": 63025, "n_read": 1198691396, "timings": {"restore_ms": 898.2}}
|
||||
```
|
||||
|
||||
DIR becomes one absolute path at start (`--slot-save-path` relative to the server's working directory, the config's
|
||||
`slot_save_path` relative to the config's `"cwd"`), created with mode 0700 when missing. It should be private to the
|
||||
user that runs Strata: the files hold the conversation's token IDs and state, are created with mode 0600 on Linux
|
||||
(on Windows they inherit the folder's permissions), and nothing deletes them - about 1.2 GB per 63K-token
|
||||
conversation. Before it writes, a save checks that the disk has room for the whole new file plus
|
||||
`--session-min-free-mib` (an engine argument, in the config's `args`; default 4096, 0 = no check) - also when it
|
||||
replaces a file, whose space comes back only after the rename. This is a preflight, not a quota or a reservation.
|
||||
NAME may
|
||||
not contain a path, a drive, a stream (`:`), a Windows device name (`NUL`, `CON.bin`, `COM1`...), a control character,
|
||||
a leading dot or a trailing dot or space.
|
||||
|
||||
The request must be `Content-Type: application/json` (else `415`) and come from no browser page, Strata's own or a
|
||||
trusted origin (another site's `Origin` gets `403`, also with an API key); the Host and API-key checks apply as
|
||||
everywhere. Errors: `501` without `--slot-save-path` or with parallel requests; `400` for a slot other than 0, an unknown action, a refused
|
||||
file name, or a file the engine refuses as invalid (not a session file, corrupt, another model or configuration, over
|
||||
this session's limits; the session is as it was); `404` for a restore of a missing file; `503` while the model is not
|
||||
loaded, when the RAM to read the file is not there or an allocation failed; `507` when the disk has no room (the
|
||||
free-space reserve, or the OS reports no space or quota); `500` for any other I/O failure (permissions, read, write,
|
||||
flush, rename) and when the engine ended (a restore transfer failure, below, an engine that said nothing for
|
||||
`engine_silence_s`, or one that answered out of protocol) - the next request starts it again. The status follows the
|
||||
engine's category (`error.kind`: `invalid`, `memory`, `storage`, `io`), never the words of the message. A save that
|
||||
failed after its new file had replaced the old one says so with `error.published: true` (below). A save or restore waits for the running request
|
||||
(the same queue), shows in `/status` and counts as activity for the idle unload. A later request whose messages
|
||||
continue the restored conversation reuses the restored state or its checkpoint; a short tail may be read again (35
|
||||
tokens in the measurement below). Only the deepest checkpoint is saved, so an edit further back reads more again.
|
||||
Clients still send their messages (and images): the file holds engine state and token/image identity, not a chat
|
||||
export. Underneath, `strata --serve` takes `SAVE <path>` and `RESTORE <path>` on stdin between requests and answers
|
||||
`SAVED <tokens> <bytes> <ms>`, `RESTORED <tokens> <bytes> <ms>`, `SERR <kind> <published 0|1> <reason>` (failed,
|
||||
the engine and the session as they were) or `FATAL <reason>` (then exits). On the way, `SESSION <done> <total>` follows
|
||||
every block of the file that moved (at most 16 MiB, the last partial block and a small file included), and
|
||||
`SWAIT <phase> <seconds>` comes before a step that blocks in one call (`fingerprint`, `capture`, `flush`, `publish`,
|
||||
`validate`, `transfer`): that step is allowed those seconds - 60 plus one per 4 MiB it concerns, at most 3600 - by the
|
||||
engine's watchdog and by the server, then it counts as stuck. The next line clears the allowance. The server checks
|
||||
every line: a malformed or unknown one, counts that are negative or go back, an allowance outside 1..3600 or a time
|
||||
that is not a finite number end the engine as out of step.
|
||||
|
||||
One file holds the running state, the deepest checkpoint, every QSA layer's K/V up to the conversation's length and
|
||||
the draft layer's K/V. Format v1, little-endian, fixed-width integers, IEEE-754 floats (a big-endian build does not
|
||||
compile): a 64-byte header (magic `STRSESS\x01`, version u32, header size u32, model fingerprint u64, config
|
||||
fingerprint u64, payload length u64, two reserved u64 that must be 0, a hash of the first 56 bytes), the payload
|
||||
(geometry, layer range, cvec flag, the live state, the checkpoints, the K/V layers; every array preceded by its u64
|
||||
count), the payload hash and the end marker `STRSEND\x01`. The hash is a 64-bit function with xxHash64-style rounds,
|
||||
not the standard XXH64 stream; it detects accidental corruption and does not authenticate a file: restore only files
|
||||
this engine wrote. An unknown version is refused; a new format gets a new version number.
|
||||
|
||||
A file is bound to the model inputs and to the settings that change what the saved bytes mean. The model fingerprint
|
||||
samples (size, first and last MiB) every file the engine loads, by its role: the GGUF shards (also
|
||||
`--native-dense-gguf`, the head shards and `--embd-gguf`), the PLE shard, the pack's index/dense/embedding files and
|
||||
`native_experts.txt`, every file the expert source resolved - the pack's `experts.bin`, or for a pack read in place each
|
||||
layer's gate/up/down GGUF as `native_experts.txt` names it, also one outside the CLI shards - and the MTP's files; not
|
||||
other files in those folders, and not the path, so a moved model folder still matches. A
|
||||
change in the middle of a file that keeps its size is not seen: do not change model files while their sessions are
|
||||
kept. The config fingerprint covers the engine version string (another version is refused; two builds of the same version
|
||||
are not told apart - there is no build hash), CUDA or HIP,
|
||||
`--kv`, `STRATA_KV_ROT`, `--kv-resident`, `--max-context`, `--mtp-window`, the resolved rope configuration (type,
|
||||
base, factor, freq scale, original context, the YaRN knobs - the cached K is post-RoPE), the loaded control vector (a
|
||||
digest of the tables uploaded: every file's content times its exact scale, the mode, the layer range and the
|
||||
direction) and the arithmetic switches (`--native-*`, `--no-ple`, the A/B arms). Sampling, seeds, draft tuning and
|
||||
the expert tier are not in it: they change what comes next, not what the saved cells hold.
|
||||
|
||||
A save writes a temporary file with a new hidden name beside `path` (created exclusively, never an existing file or
|
||||
link), flushes it to the disk, renames it over `path` (`MoveFileExW` with write-through on Windows, which promises no transaction
|
||||
on every filesystem) and, on POSIX, flushes the folder (Windows has no folder flush; the file flush and the
|
||||
write-through rename are all it does). A save that fails before the rename keeps the old file at `path` and removes
|
||||
only its own temporary file. Once the rename is done the old file is gone: if the folder flush then fails, the save
|
||||
fails with `published` set - the new file's bytes are complete and flushed, but its name may not survive a power loss.
|
||||
A filesystem that cannot flush a folder (`EINVAL`) is not a failure; the engine logs it. A restore opens `path` without following a symbolic link (or a Windows reparse point) and refuses
|
||||
anything but a regular file with one name; it checks the size, the header, both fingerprints (before the payload is
|
||||
parsed; the first 16 MiB block, header included, is already read), that the parse's peak (the image, the read buffer, the per-segment overhead) fits in RAM above the parking
|
||||
floor (`--conversation-cache-min-free-mib`), the file's size against the largest this session can restore, the
|
||||
geometry and layer range before any state array, and every count against the bytes left and this session's exact
|
||||
limits (context and cells, checkpoints, layers, each running-state array, each K/V part) before allocating it, the payload hash
|
||||
and then the usual snapshot validation - all before any device write, and a refusal leaves the current session as it
|
||||
was. A transfer failure after the device writes began ends the engine (`FATAL`) rather than decode from a partial
|
||||
state; the server reports `500` and starts it again. A restore does not park the outgoing session. Not supported with
|
||||
`--layer-split`, `--peer-device`, `--batch` (the config's `"parallel"`, #465; the server answers `501`) or
|
||||
`--prompt-cache 0` (the RAM conversation cache need not be on). On Linux the file
|
||||
moves with `O_DIRECT` in 16 MiB blocks when the filesystem takes it (buffered I/O otherwise, or with
|
||||
`STRATA_SESSION_BUFFERED=1`); on Windows with buffered I/O. The engine has been run on Linux/CUDA only. An earlier
|
||||
revision's CPU file-I/O test passed as a 32-bit Windows executable under Wine; the current code has not been built
|
||||
for Windows, and the Windows engine, HIP and AMD cards have not been run.
|
||||
|
||||
A session file saves conversation state, not all of the process's execution history. Exact future token replay
|
||||
across restarts is not guaranteed: expert residency and CPU/GPU rounding can change later output. In a 63K test the
|
||||
first 32-token continuation after a restore matched the process that kept running; on the next continuation three
|
||||
restored processes agreed with one another but differed from that process from the 28th token on. A refused,
|
||||
corrupted restore in between did not change the restored processes' continuation. The cause of this divergence has
|
||||
not been isolated.
|
||||
|
||||
Measured on an RTX 4070 Ti (12 GB), Ryzen 9 5900X, 64 GB RAM, NVMe ext4, IQ3_XXS, a 63,025-token conversation
|
||||
(engine 0.1.38 with this change, binary sha256 `3bbe4fc3...`): file 1.20 GB, save 0.65 s including the flushes to
|
||||
disk (10 more saves of the 1.20 GB state after a further turn: 0.59-0.61 s replacing one file, 0.80-1.18 s to new
|
||||
names), restore 1.05 s in a new engine process, then the next 32-token turn in 1.14 s with the same 32 output tokens as
|
||||
the same turn without a restart (1.00 s); the cold first turn takes 25.5 s. The save reported 73 `SESSION` lines, never
|
||||
more than 16 MiB apart. A symbolic link, a second hard link and a file with one flipped payload byte were refused
|
||||
(the last also while a restored conversation was live, which then went on to answer); a fresh engine with another
|
||||
`--rope-freq-base` refused the file and then answered; a save with a RAM floor above the machine's RAM was refused as
|
||||
`memory` before any copy, the file it would have replaced untouched. The times are single runs.
|
||||
|
||||
**Current limits (v1):** one request at a time unless `"parallel": N` is set (opt-in batch slots, up to N requests
|
||||
decoded together: [BATCHING.md](BATCHING.md)), and one conversation cached at a time (switching between two chats
|
||||
re-reads the other one unless the opt-in cache above is enabled, or each conversation keeps its own batch slot); images only when set up with them (below); no video. **Temperature / top_p / top_k / min_p /
|
||||
seed** are honored per request (OpenAI and Anthropic fields), and so are stop strings (OpenAI `stop`, a string or up
|
||||
to 4; Anthropic `stop_sequences`): the answer ends before the first one, which is not sent, and the engine stops
|
||||
there (`finish_reason` "stop"; `stop_reason` "stop_sequence" with `stop_sequence` set to the one found); with the default adaptive expert tier a sampled result
|
||||
is not reproducible run to run - for seed-reproducible output add `--adapt-every 100000` (static residency) to the
|
||||
engine arguments. The run config's optional `sampling` block sets the defaults for requests that leave the fields out
|
||||
(`"sampling": {"temperature": 1.0, "top_p": 0.95, "top_k": 20}`); a request's own fields always win, and with no
|
||||
@@ -631,6 +902,65 @@ without penalties, so more of its guesses are now rejected. Requests without pen
|
||||
|
||||
---
|
||||
|
||||
### The Responses API and Codex CLI
|
||||
|
||||
`POST /v1/responses` (0.1.39, #451) speaks OpenAI's newer Responses API, which Codex CLI uses (it no longer
|
||||
speaks Chat Completions). It runs on the same path as `/v1/chat/completions`, so the thinking levels, the thinking
|
||||
budget, the conversation cache and the same API key, Host and Origin checks apply.
|
||||
|
||||
It is **stateless**: nothing is stored, so the client sends the whole conversation in `input` every time (Codex does,
|
||||
with `store: false`). `previous_response_id`, `conversation`, `background` and the retrieve/delete/cancel endpoints
|
||||
are refused with an error that says so.
|
||||
|
||||
| Request | What Strata does |
|
||||
| --- | --- |
|
||||
| `input` as a string, or as items | `message` items (`user`, `assistant`, `system`, `developer`; text and images), `reasoning`, `function_call`, `function_call_output`, `custom_tool_call(_output)` |
|
||||
| `instructions` | The system message (with leading `developer` messages; later ones become user messages, as on the chat path) |
|
||||
| `tools` | `function` tools, `namespace` tools (the model sees `namespace.name`; calls come back with `namespace` and `name`), `custom` tools (one free-form `input` string). Hosted tools (`web_search`, `file_search`, ...) are left out: the model cannot run them |
|
||||
| `tool_choice` | `"none"` hides the tools; anything else lets the model choose (it cannot be forced) |
|
||||
| `reasoning.effort` | `none`/`minimal`, `low`, `medium`, `high`/`xhigh`; without it the model's default (high) |
|
||||
| `max_output_tokens` | The output cap (thinking included). Running out ends the response `incomplete` (`max_output_tokens`) |
|
||||
| `text.format` | `json_schema` and `json_object` use the [JSON response formats](#json-response-formats) (checked, not constrained) |
|
||||
| `temperature`, `top_p`, `reasoning_budget_tokens`, ... | As on the chat path |
|
||||
|
||||
The model's thinking comes back as a `reasoning` output item with `reasoning_text` content (streamed as
|
||||
`response.reasoning_text.delta`). This model writes no separate summaries, so `summary` is empty. With
|
||||
`"include": ["reasoning.encrypted_content"]` the item also carries `encrypted_content`: an opaque string (base64, not
|
||||
encrypted; the client already holds the text). Send the reasoning items back with the rest of the conversation, as
|
||||
Codex does: their thinking goes back into the prompt, so it matches what the model wrote and the conversation cache
|
||||
is reused.
|
||||
|
||||
With `"stream": true` the events are the official ones, in order: `response.created`, `response.in_progress`, then
|
||||
for each output item `response.output_item.added`, its deltas (`response.reasoning_text.delta`,
|
||||
`response.output_text.delta`, `response.function_call_arguments.delta`), its `...done` events and
|
||||
`response.output_item.done`, and at the end `response.completed`, `response.incomplete` or `response.failed`.
|
||||
While a long prompt is read, a `response.in_progress` event goes out every 15 s, which Codex counts as activity.
|
||||
Errors before the answer starts have the Responses form (`{"error": {"message", "type", "param", "code"}}`); after
|
||||
it started they arrive as `response.failed`.
|
||||
|
||||
**Codex CLI.** In `~/.codex/config.toml` (Windows: `%USERPROFILE%\.codex\config.toml`):
|
||||
|
||||
```toml
|
||||
model = "strata" # any name; Strata answers with its model
|
||||
model_provider = "strata"
|
||||
model_context_window = 32768 # the context you chose in setup: Codex compacts before it gets there
|
||||
show_raw_agent_reasoning = true # show the model's thinking (it writes no summaries)
|
||||
# model_reasoning_effort = "medium" # none, low, medium or high; default: the model's (high)
|
||||
|
||||
[model_providers.strata]
|
||||
name = "Strata (local)"
|
||||
base_url = "http://127.0.0.1:8080/v1"
|
||||
wire_api = "responses"
|
||||
stream_idle_timeout_ms = 600000 # a first, long prompt can take minutes to read
|
||||
# env_key = "STRATA_API_KEY" # only if the server has an api_key: the variable holding it
|
||||
```
|
||||
|
||||
Then run `codex` (or `codex exec "..."`) as usual. Codex warns `Model metadata for ... not found` for a local model
|
||||
name; that is expected. Measured with Codex CLI 0.160.0 and Q2_0 on an RTX 5070: its first prompt (instructions and
|
||||
tool descriptions) was 9,443 tokens, read in 10 s; in a tool loop, each later turn reused about 96% of the prompt from
|
||||
the cache and read only the new part in 1-2 s. On Windows, Codex's sandbox rejected every shell command in that test
|
||||
until it was started with `-c 'windows.sandbox="unelevated"'` (a Codex setting, not Strata's).
|
||||
|
||||
## Tools from MCP servers
|
||||
|
||||
The chat page can give the model tools from [MCP](https://modelcontextprotocol.io) servers, as LM Studio and Claude
|
||||
@@ -743,11 +1073,34 @@ helper (`strata-vision`, from llama.cpp's `mtmd` library) and adds it to your st
|
||||
| Encoder on | Time per picture | Cost |
|
||||
| --- | --- | --- |
|
||||
| **GPU** (recommended) | **0.1-0.5 s** (up to 1,024 image tokens) | ~1.4 GB of VRAM is kept free for it, so the expert cache is smaller: text output is a few % slower (table below) |
|
||||
| CPU | 10-30 s (pictures are scaled down to ~300 image tokens) | nothing on the GPU |
|
||||
| CPU | about 3 s at 300 image tokens on 8 cores (6-13 s on 4 threads); more tokens take longer, in proportion (#767, #625) | nothing on the GPU |
|
||||
|
||||
A picture becomes up to 1,024 tokens of the context (a 640x480 photo: 300). The same picture sent again, as chat apps
|
||||
do on every turn, is encoded only once.
|
||||
|
||||
**More image tokens (0.1.39, #625):** `--vision-tokens N` at setup (`START-HERE.bat --setup --vision cpu
|
||||
--vision-tokens 768`) sets the most tokens a picture becomes - `"max_tokens"` in the `"vision"` section of
|
||||
`strata-<model>.json`, which you can also edit by hand. More tokens keep more detail (small text, charts, screenshots)
|
||||
and take longer to encode, on the CPU most of all; a setup run again keeps the value.
|
||||
|
||||
**A minimum number of image tokens (0.1.40, #767):** `"min_tokens": N` in the `"vision"` section of
|
||||
`strata-<model>.json` (edit it by hand; a setup run again keeps it) is passed to the encoder as `--min-tokens N`
|
||||
(mtmd's `image_min_tokens`): a small picture is scaled up to at least N tokens. llama.cpp's mtmd prints that Qwen-VL
|
||||
models want at least 1,024 for grounding tasks (pointing, counting small items); the CPU default stays at 300 at most
|
||||
and no minimum, because a larger minimum changes the image answers and costs encode time (about 3 s at 300 tokens on
|
||||
8 cores, in proportion to the tokens).
|
||||
|
||||
**A Q8_0 encoder (#625):** `"mmproj"` in the `"vision"` section can point to another mmproj file of this model, for
|
||||
example a Q8_0 one (llama.cpp's `convert_hf_to_gguf.py --mmproj --outtype q8_0` makes one): the encoder's library
|
||||
reads quantized weights, the file is smaller (590 MiB against 865 MiB for the BF16 one, from `llama-quantize
|
||||
mmproj-Qwen3.8-Flash-Next-BF16.gguf mmproj-Qwen3.8-Flash-Next-Q8_0.gguf Q8_0`), and on the CPU it uses less RAM
|
||||
(about 280 MiB less in the encoder) and can encode faster than BF16 at the default 300 tokens. Setup downloads the
|
||||
BF16 file, and a setup run again keeps a file of your own that still exists. **Recommended for `--vision cpu`.**
|
||||
Accuracy against BF16, per image token: the cosine of the embeddings is 0.997-0.999 on average (#625's report, with
|
||||
a community Q8_0 file, at 300, 768 and 1,024 tokens), and 0.9988 and 0.9987 on two pictures with the file made by
|
||||
the command above (300 tokens; the worst single token 0.94-0.96). Encode time above 768 tokens is the same as BF16's
|
||||
within about 3%. Numbers on more pictures (charts, small text) are welcome in #625.
|
||||
|
||||
**A spare GPU for the encoder (0.1.33, #408):** with a card the engine doesn't use, add `"cuda_device": 2` (numbered
|
||||
like `nvidia-smi`) to the `"vision"` section of `strata-<model>.json`: the encoder then runs on that card alone. Lower
|
||||
`--vram-reserve-mib` in `"args"` to 700 as well, so the engine's cards keep that VRAM for the expert cache. The
|
||||
@@ -869,9 +1222,10 @@ the document, +0.4% on the chat. Details: `bench/results/2026-09-27-esp/`.
|
||||
| Slower than the tables | The monitor plugged into the GPU and other GPU programs take VRAM from the expert cache; RAM running below its rated speed (enable EXPO/XMP in the BIOS) slows the CPU half. |
|
||||
| `this server was started without the vision encoder` | The model was set up for text only: run setup again with `--vision gpu`. |
|
||||
| A picture is refused or `cannot read the image` | The file is not a picture Pillow can open (JPEG, PNG, WebP, GIF, BMP, TIFF, AVIF work). |
|
||||
| Pictures are slow (10-30 s) | The encoder runs on the CPU: run setup again with `--vision gpu` (needs ~1.4 GB of VRAM). |
|
||||
| Pictures are slow (3-30 s) | The encoder runs on the CPU: run setup again with `--vision gpu` (needs ~1.4 GB of VRAM). |
|
||||
| A request never finishes: "reading the prompt", GPU "100%" at low power | The GPU ran out of VRAM (engines before 0.1.9 could end with ~30 MiB free at large contexts). Run `START-HERE.bat` once to get engine 0.1.9 or newer; the log then says `... MiB of VRAM free with everything loaded` (a few hundred) and names the `--vram-reserve-mib` to add if it is low. |
|
||||
| Generation stops mid-answer, GPU "100%", one CPU core busy | Fixed in engine 0.1.12 (issue #29, a race in the CPU expert pool on big-VRAM cards). Since then a request that stops moving ends with an error instead of hanging (after 2 minutes; 1 minute from 0.1.13): the log says `no progress for ... s ... (issue #29)` with where it stopped, and the next request starts the engine again. If you see that line, please open an issue with it. Engine 0.1.13 adds a stall report under it (what every expert-pool thread and the GPU handshake were doing, memory and page faults) and, on Windows, a `strata-stall-<pid>.dmp` file with every thread's stack: attach both. (`STRATA_WATCHDOG_S` sets the time in seconds; 0 turns it off.) Engine 0.1.14 fixes the stall those reports found (issue #31: with the IQ packs the host could wait forever inside the NVIDIA driver while copying experts in a verify window; the experts are now copied by a GPU kernel, `--pcie-mode dma` restores the old way). |
|
||||
| `no progress for 60 s ... reading the prompt` on Linux, and the stall report says `threads waiting on the disk (state D): 16 ...` | The engine waits for the drive, not a deadlock: the n-gram table is read at random (`--ple-io direct`), which a rotational disk cannot keep up with (#605). The engine warns at start when the table is on one; `--ple-io ram` (Linux, needs RAM for the table) or the model on an SSD fixes it. Setup adds `--ple-io ram` itself on a rotational disk when the RAM holds the table (0.1.39). |
|
||||
| `the engine said nothing for ... s during the request` or `... did not finish the request after it was stopped (STOP)` | Issue #481: the engine and the server lost step (the engine waits for its next command, the server for the request's end; GPU at 0 %, nothing in the log). The server ends the engine after 300 s without a line from it during a request (while a prompt is read: each chunk may take three times the previous one's time, the first one up to its tokens at 50 tok/s more), the request ends with an error and the next request starts the engine again. `"engine_silence_s": 600` in `strata-<model>.json` sets the time (0 = wait forever, as before). If you see it, please add the end of the engine log to #481. |
|
||||
| `out of memory: cudaFuncSetAttribute` in the log (IQ3_XXS, long prompt) | Fixed in engine 0.1.15: CUDA loaded a kernel's code when it was first needed, and mid-prompt there was no VRAM left for it. Run `START-HERE.bat` (Windows) or `./setup.sh` (Linux) once to update. |
|
||||
| Anything else | The engine log is `strata-<model>.log` in this folder. |
|
||||
@@ -887,12 +1241,32 @@ the document, +0.4% on the chat. Details: `bench/results/2026-09-27-esp/`.
|
||||
the conversation while you chat).
|
||||
- **RAM:** all 24,576 experts, pinned. The CPU computes the experts that are not on the GPU **in place**, at the same time
|
||||
as the GPU works on the cached ones (AVX-512 / AVX2 kernels, ggml's for the i-quants).
|
||||
- **SSD:** the 28.8 GB n-gram table, read a few rows per token through the OS cache.
|
||||
- **SSD:** the 28.8 GB n-gram table, a few rows per token, read unbuffered past the OS cache (`--ple-io direct`, the
|
||||
default, made for SSDs; on a rotational disk `--ple-io ram` keeps the table in RAM, #605). The engine reads the table
|
||||
in the format the GGUF has it: IQ4_NL (the default table), Q4_0, Q5_0, Q5_1, Q8_0, FP8 (E4M3 with a scale) or BF16.
|
||||
Measured on 4,000 random rows against the checkpoint's own BF16 table, the mean per-row error is Q8_0 0.53%, FP8 2.64%,
|
||||
Q5_1 3.78%, Q5_0 4.25%, IQ4_NL 7.60%, Q4_1 7.80%, Q4_0 8.55% (a Q8_0 table is 54 GB, an IQ4_NL one 28.8 GB). On the Q2_0 model, 700 teacher-forced tokens, the mean KL against the BF16 table is 0.0117 (Q5_0), 0.0121 (Q5_1), 0.0125
|
||||
(Q8_0), 0.0128 (FP8), 0.0140 (Q4_0) and 0.0146 (IQ4_NL), with the perplexity within 1% of BF16's either way: any change to the
|
||||
table moves the 2-bit model by about 0.012, so the formats are hard to tell apart. Setup does not offer another table;
|
||||
it only changes which GGUF the engine is given.
|
||||
- **Speculation:** the model's own MTP layer drafts up to 3 tokens; one pass over all 48 layers checks them. 2.4-3.2
|
||||
tokens per pass on average. When the reply repeats the context (code edits, quoted text), **prompt lookup** (engine
|
||||
0.1.7) drafts up to 5 tokens from the earlier copy, but only where its measured acceptance and cost say it pays:
|
||||
code edits 6-11% faster, other text unchanged. The drafts are checked like the MTP's, so the output is the same.
|
||||
- **Prompts** are processed in 2,048-token chunks with the experts streamed to the GPU over PCIe.
|
||||
- **Prompts** are processed in chunks of up to 8,192 tokens (`--prefill auto`; 32,768 opt-in) with the experts
|
||||
streamed to the GPU over PCIe. Since engine 0.1.39b (#583) the streamed ring is sized in bytes for the pack (a
|
||||
native pack with bigger blobs gets fewer ring slots and keeps more cache slots) and `--prefill auto` picks the
|
||||
largest chunk that keeps that ring full. It only gives up ring slots where 0.1.39's rule held the chunk at 4,096
|
||||
tokens or less; from 6,144 on it keeps 0.1.39's ring and only looks for a bigger chunk beside it (shrinking the
|
||||
ring there measured slower: the Coder -12%), and a prompt that fits 0.1.39's chunk keeps 0.1.39's ring (one
|
||||
chunk: a smaller ring only slowed it). RTX 5070, 32K prompts against `STRATA_RING_BYTES=0`, 2-3 interleaved
|
||||
rounds: IQ3_XXS +18.5% at a 1,500-slot cache and +9% in another memory state, the Coder +3%, IQ3_S unchanged; 4K
|
||||
prompts unchanged (-0.7% to +1.1%, bit-identical). Q2_0 (a fixed `--prefill 2048`) is not affected. It changes
|
||||
the bits of a long prompt against 0.1.39 (the experts go through a different
|
||||
mix of cached and streamed groups). Measured quality, teacher-forced over the next 2,001 tokens of a long document
|
||||
against the FP16 prompt path (IQ3_XXS, `--prefill auto`): 8K prompt KL 0.042 (0.1.39: 0.054), top-1 agreement
|
||||
93.5% (92.2%); 32K prompt KL 0.020 (0.019), top-1 95.3% (95.0%) - the same band as before. `STRATA_RING_BYTES=0`
|
||||
restores 0.1.39's ring, loan and chunk choice.
|
||||
|
||||
The full story, with measurements, bottlenecks and what comes next: **[docs/paper/Strata-Paper.pdf](paper/Strata-Paper.pdf)**.
|
||||
|
||||
@@ -940,7 +1314,10 @@ times out, the server keeps ownership and reports an error rather than claiming
|
||||
|
||||
`POST /v1/chat/completions` accepts `response_format: {"type":"json_object"}` or
|
||||
`{"type":"json_schema","json_schema":{"name":"answer","strict":true,"schema":{"type":"object","properties":{"answer":{"type":"integer"}},"required":["answer"],"additionalProperties":false}}}`.
|
||||
The schema must describe an object at its root. Local `#` references work; remote references are refused.
|
||||
The schema must accept only JSON objects at its root: `"type":"object"`, or an `anyOf`/`oneOf` whose branches are all
|
||||
object schemas (an `allOf` with an object member, or a local `$ref` to one, also counts), as apps written for
|
||||
llama.cpp's `json_schema` send. A root that also allows an array, string, number, boolean or null is refused. Local
|
||||
`#` references work; remote references are refused.
|
||||
`json_schema` is checked with the Python package `jsonschema` when it is installed (`python -m pip install
|
||||
"jsonschema>=4.23,<5"`; setup does not add it); without it the answer is only checked to be one JSON object, and the
|
||||
server says so once.
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Avoid duplicate expert slots across helper and primary GPUs
|
||||
|
||||
## Why this helps
|
||||
|
||||
On a PC with two small GPUs, helper experts start out separate from the primary
|
||||
cache. Later, the adaptive primary cache can promote an expert that the helper
|
||||
already holds. Both cards then spend memory on the same expert.
|
||||
|
||||
We observed about 1,900-2,000 duplicate slots during sustained requests.
|
||||
This opt-in change keeps helper-owned experts out of primary promotion candidates,
|
||||
in both the server and command-line generation paths.
|
||||
|
||||
## Use
|
||||
|
||||
Set `STRATA_DISJOINT_ADAPT=1` before starting the engine. It is off by default.
|
||||
It leaves the existing peer-tier check and expert arithmetic unchanged.
|
||||
The check is live: each promotion candidate is asked of the helper's own cache (`RemoteExperts::holds`), so an expert
|
||||
the helper's tier swaps in or out later is followed (a mask copied at load would go stale). Needs the helper-share
|
||||
change (#854) for `holds`.
|
||||
|
||||
## Validation environment
|
||||
|
||||
- Two modified RTX 3080 20 GB cards; CUDA SM86, CUDA 13.1, Linux.
|
||||
- Intel Xeon E5-2686 v4 (18 cores / 36 threads), about 94 GiB usable RAM.
|
||||
- PCIe 3.0, no GPU peer-to-peer access; 17 expert-pool workers.
|
||||
- Qwen3.8-Flash-Next IQ3_S, 131,072-token context, INT8 KV,
|
||||
32,768 resident KV cells.
|
||||
- Medium thinking: 2,048-token reasoning cap, 8,192-token output cap.
|
||||
- Physical GPU power limits stayed at 250 W and 280 W. No power increase.
|
||||
- Historical performance tests used an isolated engine based on official
|
||||
v0.1.38 plus reviewed architectds/Strata changes (best, 05c0f36).
|
||||
This PR contains only our change, ported to official main (99f3dbd).
|
||||
The historical timings are NOT a measured speedup of this pure-upstream port.
|
||||
|
||||
## Historical benefit
|
||||
|
||||
Same binary and helper capacity, six complete JSON tasks per arm:
|
||||
|
||||
| 32K cached request | Original | Ownership guard |
|
||||
| --- | ---: | ---: |
|
||||
| Generation | 81.5 tokens/s | 94.1 tokens/s |
|
||||
| Complete request | 12.997 s | 11.417 s |
|
||||
|
||||
Generation improved by 15.5%; request time fell by 12.2%.
|
||||
The tasks returned complete 100-record arrays and correct source facts.
|
||||
All 18 checks passed across the original, guarded, and larger-cache arms.
|
||||
The larger-helper arm is not included in the comparison above.
|
||||
These repetitive tasks do not represent all agent workloads.
|
||||
|
||||
## Validation and remaining work
|
||||
|
||||
- Standalone ownership test passes, including disabled, empty, unowned,
|
||||
owned, and out-of-range entries, plus candidate filtering.
|
||||
- Linux compile/link against pure official main (99f3dbd) passed. No engine
|
||||
was started and no production settings were changed.
|
||||
- Pure-upstream GPU performance and session/vision/cancellation regression
|
||||
checks still need to be run. Keep this PR as a draft until then.
|
||||
- Historical combined builds passed those safety checks, but that is not a
|
||||
substitute for validating this independent port.
|
||||
@@ -0,0 +1,139 @@
|
||||
# Resident expert exchange buffer rotation
|
||||
|
||||
When the resident RAM complement exchanges an expert with the GPU cache, the
|
||||
evicted expert lands in a temporary host buffer. The existing path then copies
|
||||
those bytes into the promoted expert's old RAM slot. Rotation removes that last
|
||||
host copy: the temporary buffer becomes resident, and the old RAM slot becomes
|
||||
the next temporary buffer.
|
||||
|
||||
## Enable
|
||||
|
||||
Rotation is **off by default**. Set `STRATA_EXCHANGE_ROTATE=1` before starting the
|
||||
engine with your existing arguments:
|
||||
|
||||
```bash
|
||||
export STRATA_EXCHANGE_ROTATE=1
|
||||
```
|
||||
|
||||
```powershell
|
||||
$env:STRATA_EXCHANGE_ROTATE = '1'
|
||||
```
|
||||
|
||||
Only the exact value `1` opts in. Set it to `0` or remove it to restore the copy
|
||||
path. This setting does not select resident RAM mode or change any preset.
|
||||
It applies only when all layers have equal-size expert blocks, the resident
|
||||
complement is fully pinned and GPU-mapped, and the exchange buffers can also be
|
||||
pinned and mapped. Other layouts keep the copy path and log why rotation could
|
||||
not be enabled. Configurations without a resident complement do not use it.
|
||||
|
||||
The startup diagnostic confirms activation:
|
||||
|
||||
```text
|
||||
FileExpertSource: exchange buffer rotation enabled: ...; no host commit memcpy
|
||||
```
|
||||
|
||||
Serving diagnostics report the cumulative rotated block count and avoided
|
||||
`memcpy` payload bytes. This counts the copied payload once, not read plus write
|
||||
traffic, and does not include the GPU transfers, which still occur.
|
||||
|
||||
## Ownership and synchronization
|
||||
|
||||
The original allocations own their memory until `FileExpertSource::close()`.
|
||||
Rotation updates slot ownership and keeps each host pointer paired with its GPU
|
||||
alias. Atomic slot IDs let background residency queries observe those immutable
|
||||
pairs. Compute readers and GPU transfers must still finish before the existing
|
||||
serialized commit; rotation adds no overlapping compute or new synchronization
|
||||
policy. Reserve the maximum exchange capacity before rotation starts: growing a
|
||||
live exchange arena is rejected because it can now hold resident experts.
|
||||
|
||||
## Tests
|
||||
|
||||
The ownership test can run without CUDA:
|
||||
|
||||
```bash
|
||||
g++ -std=c++20 -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer \
|
||||
-pthread -Iinclude tests/core/exchange_storage_test.cpp -o /tmp/exchange_storage_test
|
||||
/tmp/exchange_storage_test
|
||||
```
|
||||
|
||||
With an existing CUDA build configured:
|
||||
|
||||
```bash
|
||||
cmake --build build --target strata exchange_storage_test file_expert_source_test
|
||||
ctest --test-dir build -R '^(exchange_storage_test|file_expert_source_test)$' --output-on-failure
|
||||
./build/file_expert_source_test --rotation-gpu
|
||||
compute-sanitizer --tool memcheck --error-exitcode 71 ./build/file_expert_source_test --rotation-gpu
|
||||
```
|
||||
|
||||
The CPU test checks byte preservation, pointer/alias ownership, guards, invalid
|
||||
operations and concurrent residency queries across 12,304 exchanges. The CUDA
|
||||
fixture performs 64 exchanges in each of three modes: copy with pinned RAM,
|
||||
rotation with pinned RAM, and requested rotation with pageable RAM (fallback).
|
||||
It checks exact bytes, GPU alias reads, file fallback, capacity, and close/reopen.
|
||||
The GPU fixture is explicit; the ordinary CTest invocation does not run it.
|
||||
|
||||
On 2026-10-04, the clean patch on upstream
|
||||
`6f32ec070f23ced9f50e704d854d775da52591ab` built on Linux with GCC 15.2 and CUDA
|
||||
13.3 for an RTX 4090. Both CTest tests, the ASan/UBSan ownership test, and the
|
||||
CUDA fixture passed. Compute Sanitizer reported **0 errors**. AMD and Windows
|
||||
GPU execution were not tested for this patch.
|
||||
|
||||
## One native token-identical A/B
|
||||
|
||||
Measured on 2026-10-04 using the clean rotation build at
|
||||
`18a30ad775bce86e99a54e6dc552ccb72c66d16e`, on upstream
|
||||
`6f32ec070f23ced9f50e704d854d775da52591ab`. The model is the standard
|
||||
GSQ-RCO **Q2_0**, with its matching IQ4_NL PLE shard. Both shards were verified
|
||||
against the publisher's SHA-256 hashes at the pinned revision in the receipt.
|
||||
The native pack uses the existing `tools/iq_pack.py`; no compatibility conversion
|
||||
or Q8 PLE reader extension is needed.
|
||||
|
||||
Both arms used the same Release binary (GCC 13.3, CUDA 13.2), RTX PRO 6000
|
||||
Blackwell 96GB, Ryzen 9 7950X and 128GB installed RAM. A fresh engine per arm
|
||||
generated exactly 1,024 tokens after the same 1,024-token counting prompt.
|
||||
The context allocation was 40,960, with 12,000 GPU expert slots, a 16.19 GiB
|
||||
pinned/mapped resident complement, FP16 KV, greedy target-only decoding and
|
||||
zero offered drafts, reused prompt tokens or file-tier blob reads during
|
||||
generation. Only `STRATA_EXCHANGE_ROTATE` changed. Transfer completion is
|
||||
awaited before admission.
|
||||
|
||||
The GPU cache was deliberately capped at 12,000 slots (15.45 GiB) to exercise
|
||||
resident RAM exchanges. Q2 can fit entirely on this 96GB GPU; this is a storage
|
||||
path comparison, not the fastest full-GPU configuration. It does not change
|
||||
the project's presets or enable resident mode for existing users.
|
||||
|
||||
| Rotation | Decode tokens/s | Resident exchanges | Avoided host-copy payload | Output IDs |
|
||||
|---|---:|---:|---:|---|
|
||||
| `0` | 104.90 | 3,802 | 0 bytes | 1,024, identical |
|
||||
| `1` | 113.44 | 3,802 | 5,255,884,800 bytes | 1,024, identical |
|
||||
|
||||
Decode rates exclude startup and prefill. The observed throughput change was
|
||||
+8.1% in this one pair; repeated timing or a general speedup is not established.
|
||||
The out-of-vocabulary EOS sentinel `2147483647` forces the exact output length,
|
||||
so this is not a model-quality evaluation. Speculative-path token parity,
|
||||
AMD and Windows GPU execution remain untested.
|
||||
|
||||
The same Blackwell binary passed both CTest checks and the three-mode CUDA
|
||||
fixture; Compute Sanitizer reported 0 errors. The
|
||||
[receipt](../bench/results/exchange-rotation-ab.json) contains source, binary,
|
||||
model, pack and profile hashes, arguments, timings and diagnostics. The
|
||||
[raw IDs](../bench/results/exchange-rotation-token-ids.json) retain the input
|
||||
and both outputs. To check the token comparison without a model:
|
||||
|
||||
```bash
|
||||
python - <<'PY'
|
||||
import hashlib, json
|
||||
from pathlib import Path
|
||||
p = Path('bench/results')
|
||||
r = json.loads((p / 'exchange-rotation-ab.json').read_text())
|
||||
raw = (p / r['token_ids_file']).read_bytes()
|
||||
assert hashlib.sha256(raw).hexdigest() == r['token_ids_file_sha256']
|
||||
d = json.loads(raw)
|
||||
assert len(d['input']) == len(d['off']) == len(d['on']) == 1024
|
||||
assert d['off'] == d['on']
|
||||
for name, arm in zip(('off', 'on'), r['arms']):
|
||||
sha = hashlib.sha256(json.dumps(d[name], separators=(',', ':')).encode()).hexdigest()
|
||||
assert sha == arm['output_token_ids_sha256']
|
||||
print('1,024 identical output token IDs')
|
||||
PY
|
||||
```
|
||||
+46
-8
@@ -5,9 +5,9 @@ AMD graphics card. The short version is in the [README](../README.md#install); a
|
||||
this for you with [AI_SETUP.md](AI_SETUP.md).
|
||||
|
||||
> **On this page:** [What you need](#what-you-need) · [Windows](#windows) · [Linux](#linux) ·
|
||||
> [AMD cards](#amd-cards) · [Several cards](#two-or-three-cards) · [Docker](#docker-linux) · [Updating](#updating) ·
|
||||
> [Where things are stored](#where-things-are-stored) · [Setup's questions](#setups-questions) ·
|
||||
> [Tuning](#tuning-for-your-pc) · [All options](#options-without-questions)
|
||||
> [AMD cards](#amd-cards) · [Several cards](#two-or-three-cards) · [Docker](#docker-linux) ·
|
||||
> [Older CPUs](#older-cpus-experimental) · [Updating](#updating) · [Where things are stored](#where-things-are-stored) ·
|
||||
> [Setup's questions](#setups-questions) · [Tuning](#tuning-for-your-pc) · [All options](#options-without-questions)
|
||||
|
||||
## What you need
|
||||
|
||||
@@ -15,7 +15,7 @@ this for you with [AI_SETUP.md](AI_SETUP.md).
|
||||
| --- | --- |
|
||||
| **GPU** | **NVIDIA** RTX 20, 30, 40 or 50 series, **12 GB VRAM or more** (8 GB runs, slowly). Measured on an RTX 5070 and an RTX 3090; RTX 20 (Turing, since 0.1.27) was tested by a contributor on an RTX 2070. **AMD** Radeon RX 7900 XT / XTX, RX 9070 / 9070 XT and Radeon AI PRO R9700 (validated), RX 7800 XT / 7700 XT and RX 9060 XT (validated by their owners), RX 6800 / 6900 series (community-reported), with 12 GB of VRAM or more. See [AMD cards](#amd-cards). |
|
||||
| **RAM** | Enough for the size you pick ([which model](MODELS.md#pick-by-ram)); **64 GB** runs every size. A big GPU makes up for less RAM - the [low-RAM mode](MODELS.md#a-big-graphics-card-and-little-ram). |
|
||||
| **CPU** | x86-64 with AVX2 (any Intel/AMD desktop CPU from the last ~8 years). AVX-512 (Ryzen 7000/9000) is a bit faster. |
|
||||
| **CPU** | x86-64 with AVX2 (any Intel/AMD desktop CPU from the last ~8 years). AVX-512 (Ryzen 7000/9000) is a bit faster. Older CPUs without AVX2 are experimental and slow: [Older CPUs](#older-cpus-experimental). |
|
||||
| **Disk** | ~70-80 GB free for the model, ~6 GB for the MTP layer (+1 GB with images). **Q2_0 on an AVX-512 CPU** also writes a one-time ~40 GB copy of its experts for the fast CPU kernel. On Linux with an AMD card, ROCm takes ~10 GB more when setup installs it. An NVMe SSD is strongly recommended: it makes the first start much faster. |
|
||||
| **OS** | Windows 10/11, or Linux (Ubuntu 22.04/24.04 get everything installed automatically). |
|
||||
| **Driver** | **NVIDIA:** a current driver, version 580 or newer ([nvidia.com/drivers](https://www.nvidia.com/drivers) or the NVIDIA App). **AMD:** on Linux the kernel's amdgpu driver (no ROCm install needed); on Windows a current AMD Software: Adrenalin Edition driver ([amd.com/support](https://www.amd.com/en/support)). |
|
||||
@@ -69,8 +69,8 @@ On Linux setup uses a system ROCm 7 when there is one, or installs ROCm into `.v
|
||||
sudo), and compiles the engine on your PC for the card (10-20 minutes, once; it needs a C++ compiler and git:
|
||||
`sudo apt install build-essential git`). Several AMD cards share the model with `--gpus`, as on NVIDIA.
|
||||
|
||||
What differs from NVIDIA for now: pictures are read by the image encoder on the CPU (`--vision cpu`, 10-30 s per
|
||||
picture), `--calibrate` is NVIDIA-only, and Unsloth's 4-bit model needs an NVIDIA card. Measurements per card, the
|
||||
What differs from NVIDIA for now: pictures are read by the image encoder on the CPU (`--vision cpu`, about 3 s per
|
||||
picture at the default 300 image tokens on 8 cores), `--calibrate` is NVIDIA-only, and Unsloth's 4-bit model needs an NVIDIA card. Measurements per card, the
|
||||
build by hand and the tuning tables: [AMD_HIP.md](AMD_HIP.md).
|
||||
|
||||
## Two or three cards
|
||||
@@ -121,6 +121,35 @@ The same idea, in a container (NVIDIA cards).
|
||||
to a network. The image has a `HEALTHCHECK` on `/health`, so `docker ps` shows the container
|
||||
healthy once the model is loaded, and `GET /v1/status` says what it is running.
|
||||
|
||||
## Older CPUs (experimental)
|
||||
|
||||
Strata's ready-made engine needs AVX2 (Intel Haswell 2013, AMD Zen 2017 or newer). Since 0.1.39 an older CPU - AVX
|
||||
only (Sandy Bridge / Ivy Bridge, Xeon E5 v1/v2, AMD Bulldozer) or SSE4.2 only (Nehalem / Westmere, Xeon X5600) - runs
|
||||
as an **experimental** build that setup compiles on that PC. Run setup as usual: it says the CPU has no AVX2, warns
|
||||
that this is experimental and slow, and compiles the engine (10-20 minutes, once; Windows needs the Visual Studio Build
|
||||
Tools and the CUDA Toolkit, which setup offers to install). It does not stop. The ready-made engine still refuses such
|
||||
a CPU, and says so.
|
||||
|
||||
What differs in that build (`STRATA_ISA_FLOOR=avx` or `none`, chosen from the CPU):
|
||||
|
||||
- ggml-cpu is compiled for AVX or SSE4.2 instead of AVX2, and the CPU's share of the experts runs on its kernels.
|
||||
Strata's own AVX2 / AVX-512 kernels stay in the program but are used only on a CPU that has them.
|
||||
- Only the i-quant models' packs run (every model setup installs on such a CPU); the AVX-512 Q2_0 pack does not.
|
||||
- Speed: the GPU part is unchanged and the CPU part is slower, so a GPU with more VRAM (more experts cached there)
|
||||
helps most. With the older paths forced on a Ryzen 5 7600 + RTX 5070 and the expert cache held at 1500 slots, greedy
|
||||
decode went from 26 to 11-17 tok/s with the AVX build and to 3.5-3.8 tok/s with the SSE4.2 build (Coder and IQ3_XXS,
|
||||
10 prompts each, one run). An old Xeon with DDR3 will be slower than these.
|
||||
- Untested on a real old CPU by us. It was checked here by forcing the older paths on a Ryzen 5 7600
|
||||
(`STRATA_FORCE_ISA=avx` or `sse`): the answers were sane and passed the engine's checks, but not the same token for
|
||||
token as the normal build (ggml-cpu rounds the CPU experts differently, as the AVX2 kernel does from the AVX-512
|
||||
one). Contributors ran earlier versions of the same approach on a Xeon E5-2680 (2.87 tok/s with a GTX 1080 Ti)
|
||||
and on Xeon E5-2687W / X5690 machines.
|
||||
- AMD cards: Linux only (setup compiles the AMD engine there anyway); NVIDIA on Windows and Linux.
|
||||
- Two-socket Xeons: memory on the other socket is slow; keep the RAM on one CPU if you can.
|
||||
|
||||
By hand: `cmake ... -DSTRATA_ISA_FLOOR=avx` (or `none`) builds it; `STRATA_ISA_FLOOR=avx` set for setup builds it on
|
||||
any PC (to try it). Problems and results on real hardware are welcome as GitHub issues.
|
||||
|
||||
## Updating
|
||||
|
||||
**`UPDATE.bat`** (Linux: `./update.sh`) updates Strata without starting the model - for when the GPU is busy with
|
||||
@@ -145,12 +174,19 @@ says so and starts the engine you had.
|
||||
kept, only their names. Another browser or a private window starts empty; clearing the site's data deletes them.
|
||||
- **How the model starts:** `strata-<model>.json` in the Strata folder (context, GPUs, host, API key, ...), written
|
||||
by setup; next to it `run-<model>.bat` / `.sh`, the log `strata-<model>.log` and, when you use "Use for other
|
||||
apps too", `strata-<model>.shared-settings.json`.
|
||||
apps too", `strata-<model>.shared-settings.json`. Running setup again for the same model (another context, say)
|
||||
rewrites the keys setup writes (`exe`, `args`, `port`, `gpu`, `host`, `api_key`, `vision`, ...) and keeps the
|
||||
ones you added (`sampling`, `mcp_servers`, `mcp`, `cors_origins`, ...); the earlier file is kept as
|
||||
`strata-<model>.json.bak` (0.1.39). Engine options you added to `"args"` by hand are not carried over: setup names
|
||||
them, and you add them again.
|
||||
- **The model files** (`models/`, `packs/`, `mtp/`, 70-120 GB): in **`Strata-data` next to the Strata folder**, or
|
||||
wherever `--data-dir` put them.
|
||||
- **Where that data folder is:** `%APPDATA%\Strata\settings.json` on Windows, `~/.config/strata/settings.json` on
|
||||
Linux ([details](DETAILS.md#before-you-start)).
|
||||
- **The program itself:** `.venv/`, `engine/` and `third_party/` in the Strata folder.
|
||||
- **Where github.com cannot be reached:** a build from source downloads llama.cpp's source (ggml, gguf-py) from
|
||||
GitHub at the commit `LLAMA_CPP_COMMIT` in `setup.py` names. Put that commit's tree in `third_party/llama.cpp`
|
||||
yourself (from a mirror or another PC) and setup uses it instead (#585).
|
||||
|
||||
## Setup's questions
|
||||
|
||||
@@ -161,7 +197,7 @@ says so and starts the engine you had.
|
||||
covering factor; `--rope-scaling`/`--rope-scale` override) ([details](DETAILS.md): "Context extension past
|
||||
262K").
|
||||
- **Images?** Whether it should also read pictures. The image encoder (0.9 GB) runs on the GPU (0.1-0.5 s per
|
||||
picture, ~1.4 GB of VRAM kept free for it) or on the CPU (10-30 s per picture, nothing on the GPU); with AMD cards
|
||||
picture, ~1.4 GB of VRAM kept free for it) or on the CPU (about 3-13 s per picture, nothing on the GPU); with AMD cards
|
||||
on the CPU for now. [Details](DETAILS.md#images-vision).
|
||||
- **Experimental speed projection?** Off unless you say yes - [read what it does](DETAILS.md#experimental-speed-projection-experimental-off-by-default)
|
||||
first. It changes how the model answers, and only the original model offers it.
|
||||
@@ -187,6 +223,8 @@ START-HERE.bat --port 8081 another port
|
||||
START-HERE.bat --gpu 1 another GPU (setup picks the one with the most VRAM)
|
||||
START-HERE.bat --gpus 0,2 several GPUs sharing the model
|
||||
START-HERE.bat --vram-reserve-mib 2048 leave 2 GB of VRAM free for other programs (remembered)
|
||||
START-HERE.bat --no-browser do not open the chat page when the model is ready (remembered;
|
||||
--browser undoes it)
|
||||
START-HERE.bat --setup --backend hip the AMD engine on a PC that also has an NVIDIA card
|
||||
START-HERE.bat --setup --host 0.0.0.0 --api-key <secret> reachable from other devices, with a key
|
||||
START-HERE.bat --calibrate tune the engine for this PC (about 5-10 minutes), then start
|
||||
|
||||
+652
@@ -0,0 +1,652 @@
|
||||
# Strata on an Intel Arc
|
||||
|
||||
Strata's engine is CUDA. On an Intel Arc it runs as **Strata's own engine, ported to SYCL** (`sycl/`, the
|
||||
section "The engine itself on Intel" below), behind the same Strata server: OpenAI and Anthropic APIs, streaming,
|
||||
tool calls, MCP and the web app all unchanged. llama.cpp's SYCL backend is the comparison point in the tables (it
|
||||
runs the same GGUF at about a third of the speed).
|
||||
|
||||
Everything Intel-specific lives in `sycl/`; no shared file of upstream's is changed, so upstream merges stay clean.
|
||||
`sycl/setup_intel.py` and `sycl/serve/server_intel.py` wrap upstream's `setup.py` and `serve/server.py` from
|
||||
outside.
|
||||
|
||||
Written for and measured on an **Arc Pro B70 (32 GB)** running the Coder (IQ1_M) on Ubuntu 24.04, in a
|
||||
PCIe 3.0 x8 slot (the card trains at Gen3 x8 there; it can do Gen5 x16).
|
||||
|
||||
## What you get (2026-10-01)
|
||||
|
||||
| | NVIDIA (Strata engine) | Intel Arc, Strata SYCL port | Intel Arc, llama.cpp |
|
||||
|---|---|---|---|
|
||||
| models | all four | the Coder IQ1_M; the original IQ2_XS too (experts beyond VRAM in the host mirror: 58-64 tok/s) | the Coder (fits VRAM) |
|
||||
| model files | the same GGUFs | the same GGUFs + a native pack | the same GGUFs |
|
||||
| where the model lives | experts in RAM, hot ones on the card | every expert in VRAM (`--stream-experts`: no host copy), shard 2's lookup table read from the SSD by row | all of shard 1 on the card, shard 2 paged from disk |
|
||||
| RAM needed | 32-64 GB | little (23 GB is fine) | little (23 GB is fine) |
|
||||
| decode, Coder IQ1_M | 44-51 tok/s on an RTX 5070 | **78.2 tok/s** (19-token prompt, 256 greedy tokens), **75.7** after a 2,184-token prompt, 69 after 40K, 66.6 after 128K, 55.9 after 256K; 48-72 through the API | 23-25 tok/s |
|
||||
| prompt reading | ~1,870 tok/s | **790 tok/s** at 2,184 tokens, 1,117 at 40K, 888 at 128K, 757 at 256K | ~150 tok/s (424 at 105K) |
|
||||
| speculative decoding (MTP) | yes | yes (the base checkpoint's draft layer; 70-85% of drafts accepted on code) | no |
|
||||
| images | yes | not yet | not yet |
|
||||
| context | up to 262K | 256K measured, 56 tok/s decode there (`--kv-resident`: the KV in pinned host memory, the attended window in VRAM) | 131K measured ceiling |
|
||||
|
||||
Decode speed with speculative decoding depends on the text: code drafts well, prose less so (see "Speed depends
|
||||
on the text"). The SYCL numbers are greedy runs of the engine (as in "How to run it by hand" below, 256 new
|
||||
tokens) on engine 0.1.31-sycl (2026-10-01; 0.1.32-0.1.38-sycl reproduce them exactly);
|
||||
"Decode round 2" below lists what each change bought.
|
||||
|
||||
## Setup
|
||||
|
||||
Build the engine and its runtime image first ("How to build it" below), then:
|
||||
|
||||
python3 sycl/setup_intel.py [setup.py's options, e.g. --model IQ2_XS --context 32768 --port 8085]
|
||||
|
||||
This is upstream's `setup.py`, run with the Intel steps swapped in (it imports setup.py and replaces those steps;
|
||||
setup.py itself is unchanged). The model choice, download, pack, tokenizer, MTP draft layer and the context and KV
|
||||
questions are setup's own. What changes:
|
||||
|
||||
- **GPU check:** the Arc is found in sysfs (vendor 8086 under `xe` or `i915`) and offered through setup's AMD
|
||||
path. That is the path that builds locally and has no images.
|
||||
- **Engine step:** it uses the SYCL build (`build-sycl-aot/strata`, run in the `strata-sycl-dev` image by
|
||||
`sycl/serve/strata-sycl.sh`) instead of compiling CUDA or HIP.
|
||||
- **RAM rule:** this does not apply. The CUDA engine keeps every expert in RAM; the port streams them from the
|
||||
GGUF into VRAM (`--stream-experts`), so RAM only decides the KV streaming. `--check` lists what fits by VRAM.
|
||||
- **The config:** it uses the container's paths and `"backend": "sycl"`. The VRAM reserve is 1,024 MiB up to
|
||||
32K and 2,048 MiB with 4,096-token prompt chunks above that. KV streaming (`--kv-resident 32768`) is on from
|
||||
64K up when the RAM holds the KV. A `model_switcher` or `sampling` block from an earlier config is kept.
|
||||
`run-<model>.sh` starts `sycl/serve/server_intel.py`.
|
||||
|
||||
If a future `setup.py` drops a step this relies on, it stops with a message instead of writing a wrong config.
|
||||
The models, packs and checkout must sit under the folder `strata-sycl.sh` mounts at `/work` (the one above the
|
||||
checkout, or `STRATA_SYCL_ROOT`).
|
||||
|
||||
Then `run-<model>.sh` (or `sycl/setup_intel.py` again) starts the model. The first start of a 30 GB model
|
||||
takes about two minutes. `--port N` and `--host 0.0.0.0` work as in upstream's setup.
|
||||
|
||||
## Things that matter on this GPU
|
||||
|
||||
- **`SYCL_CACHE_PERSISTENT` must be 0.** The persistent JIT cache segfaults on Xe2 during the first
|
||||
compile. The start script sets it; if you run llama-server by hand, do too.
|
||||
- **The whole model goes on the card** (`--n-gpu-layers 999`), except `per_layer_token_embd.weight`,
|
||||
the single 28.8 GB tensor of shard 2. `--override-tensor per_layer_token_embd=CPU` keeps it in host
|
||||
memory, mmapped and paged from the SSD by row, which is how the model's authors serve it. Host RSS
|
||||
stays around 2 GB.
|
||||
- **Thinking.** The model reasons before it answers. Strata's web app has the setting; for an API
|
||||
client, put `{"reasoning_effort": "none"}` in `strata-<model>.shared-settings.json` next to the
|
||||
config, or send `chat_template_kwargs: {"enable_thinking": false}` per request. Without one of
|
||||
those a short `max_tokens` is spent entirely inside the think block and the answer looks empty.
|
||||
- **`/health` says 503 while loading**; the server polls `/props` instead.
|
||||
|
||||
## How much context fits
|
||||
|
||||
The architecture keeps this small: only every fourth layer is full attention (12 of 48); the other
|
||||
36 are gated-delta-net layers with a fixed-size recurrent state that does not grow with context.
|
||||
Per token, the 12 attention layers keep 2 KV heads x 256 x (K + V) at q8_0, plus the
|
||||
sparse-attention indexer's keys. **Measured: about 20 KiB per token** (VRAM grows 0.6 GB per 32k
|
||||
of context with everything else unchanged).
|
||||
|
||||
| context | VRAM in use, model loaded | headroom on 32 GB | status |
|
||||
|---|---|---|---|
|
||||
| 32,768 | 28.4 GB | 3.5 GB | measured, the default |
|
||||
| 65,536 | 29.0 GB | 2.9 GB | measured, loads |
|
||||
| 98,304 | 29.6 GB | 2.3 GB | measured, loads |
|
||||
| 131,072 | 30.3 GB | 1.5 GB | **measured, the practical ceiling**: a 104,798-token prompt (79% of the window) read in 247 s at 424 tok/s, then answered |
|
||||
| 163,840 | ~31.0 GB | ~0.9 GB | not attempted: under the 1.2 GB safety margin |
|
||||
| 262,144 | ~32.6 GB | none | **does not fit - asking for it took the host down** |
|
||||
|
||||
**Do not ask for more than fits.** On this driver a GPU allocation past VRAM does not fail: the xe
|
||||
driver evicts buffers into host RAM, the kernel runs out of memory, and the machine livelocks until
|
||||
its hardware watchdog resets it. A 262K request did exactly that on 2026-09-29, and llama.cpp's own
|
||||
"failed to fit params" check fired too late to prevent it. Compute the KV size first and leave
|
||||
1.5 GB free.
|
||||
|
||||
Prompt reading gets faster with size, because the work batches better: ~150 tok/s on a 2.7k-token
|
||||
prompt, 424 tok/s on a 105k-token one. So a full 128k window costs about five minutes to read,
|
||||
not fifteen.
|
||||
|
||||
## The engine itself on Intel: the SYCL port (`sycl/`)
|
||||
|
||||
Everything above runs the model through llama.cpp. This is the other half: Strata's own engine - the 50 CUDA
|
||||
kernels and the host code that drives them (streams, events, graph capture, pinned memory) - built for the
|
||||
Arc with oneAPI. It is a migration of the tree, not a new backend: the engine has no backend seam to slot
|
||||
into.
|
||||
|
||||
**Layout.** `sycl/src` and `sycl/include` mirror the tree and hold only the files the port changes; the
|
||||
SYCL build (`sycl/CMakeLists.txt`) takes every other source from the original location. No upstream file
|
||||
is edited. `sycl/include/dpct/` is the vendored SYCLomatic helper library, so the port builds without the
|
||||
migration tool.
|
||||
|
||||
**How it was made, so it can be redone.**
|
||||
|
||||
1. `sycl/tools/Dockerfile` - the dev image: the llama.cpp SYCL image plus SYCLomatic (`dpct` 2025.3),
|
||||
ninja, and the CUDA 12.8 headers that `sycl/tools/get-cuda-headers.sh` pulls out of NVIDIA's pip wheels
|
||||
(dpct parses CUDA; it needs the headers, not the toolkit).
|
||||
2. `sycl/tools/migrate.sh` - writes a compilation database for the 86 CUDA-touching translation units and
|
||||
runs dpct over them. 85 migrate; dpct reports no line it could not migrate, and about 1,400 advisory
|
||||
notes.
|
||||
3. `sycl/tools/fixups.py` - what dpct got wrong or could not do, as an idempotent script with a reason per
|
||||
item. The ones that mattered:
|
||||
- CUDA's null stream means the default stream; dpct turned it into a null `sycl::queue*`. Every
|
||||
stream cast now goes through `strata::q_of()` (`sycl/include/strata/sycl_queue.hpp`).
|
||||
- `__ldg((const float*) p)` came out as `*p`, reading one byte of a float scale (two sites, s_gemv).
|
||||
- `__fadd_rn(a, b ? c : d)` lost its parentheses (two sites).
|
||||
- the ggml lookup tables were threaded through kernel parameters with the wrong table per template;
|
||||
they are plain `static const` arrays read from device code, as ggml-sycl does.
|
||||
- helper headers renamed since dpct 2025.3 (`entangle`, `chunked_partition`), graph introspection and
|
||||
`cudaGraphUpload` (no SYCL equivalents), `%globaltimer` (the stage profiler reads zeros).
|
||||
4. `sycl/tools/build.sh` - configure + build with icpx inside the image. Two compiler flags are load-bearing:
|
||||
`-fp-model=precise` (icpx defaults to a fast FP model) and `-cl-fp32-correctly-rounded-divide-sqrt`
|
||||
for the device compiler - the Arc's fp32 divide is not correctly rounded by default (OpenCL allows
|
||||
2.5 ulp), and Strata's quantizers are byte-exact against ggml through `amax / 127`. Measured: without
|
||||
it `quantize_act_parity` has 303k mismatches, with it none.
|
||||
|
||||
**Where it stands (2026-09-29).** The whole tree builds and links: the `strata` binary and 22 kernel
|
||||
parity tests. On the B70, 19 of the 22 pass byte-for-byte or within their tolerances; the other three
|
||||
need model fixtures (`iq_parity`, `ple_parity`) or the tensor-core kernel below (`qsa_prompt_attn`).
|
||||
|
||||
| parity test | result |
|
||||
|---|---|
|
||||
| bf16_gemv, cvec, dequant_s2, elementwise, gdn, gr, kv_q4, kv_q8, kv_stream, qsa, quantize_act, rope, router_top10, s2_gemv, s2_gemv_q8, s_gemv, s_gemv_q8k, sampler, shared_expert | pass |
|
||||
| iq_parity, ple_parity | need fixtures (a `logs/iq_fixture` directory, a Q2_0 shard) |
|
||||
| qsa_prompt_attn_parity | needs the tensor-core kernel (below) |
|
||||
| kv_hybrid_parity (0.1.27, hybrid K8V4 KV) | appends, gathers and attention pass; its last step is that same tensor-core kernel |
|
||||
|
||||
**The engine end to end (2026-09-29, later the same day).** It generates. Prompt "Write a Python function
|
||||
that returns the n-th Fibonacci number", greedy, 64 tokens:
|
||||
|
||||
> `<think>` The user wants a Python function that returns the n-th Fibonacci number. Let me consider the
|
||||
> options: 1. **Recursive approach** - Simple but O(2^n) time complexity, very slow for large n. 2.
|
||||
> **Iterative approach** - O(n) time, O(1) ...
|
||||
|
||||
Measured on the same card, same 2,185-token prompt, 64 greedy tokens (AOT build, `--spec 2`,
|
||||
`--no-prefill-borrow`), against llama.cpp's SYCL build serving the same GGUF:
|
||||
|
||||
| | llama.cpp SYCL | Strata SYCL port |
|
||||
|---|---|---|
|
||||
| prompt reading | 138-319 tok/s | **560 tok/s** (693 at 2,000 tokens, 180 at 300) |
|
||||
| decode, suffix drafter only (`--spec 2`) | 24.2-26.0 tok/s | 20.2-20.8 tok/s |
|
||||
| decode with the MTP draft layer (`--spec 4 --mtp`) | - | **42.7 tok/s** at the 2,185-token context (81% accepted, 2.9 tokens/round); 45.3 on a short prompt |
|
||||
|
||||
Prompt reading is where Strata's design pays (oneMKL GEMM over dequantised experts, the whole model on
|
||||
the card). Decode is at ~80%: a speculative round costs ~53 ms whatever its size and the suffix drafter
|
||||
is accepted 9% of the time, so nearly every round yields one token; `--spec 4` (16.9 tok/s) was worse
|
||||
than `--spec 2` (20.8) for that reason. The kernels themselves run at 130-160 GB/s of weights on a card
|
||||
that streams 600 GB/s (`sycl/probe/bw.cpp`, incompressible data) - the same class llama.cpp reaches -
|
||||
and five variants of the hot Q6_K matvec (lanes per row, rows per warp, unroll, 16-byte loads, software
|
||||
pipelining; `mmvq_bench`) all landed in that band. The draft source was the lever: the base
|
||||
Qwen3.8-Flash-Next checkpoint's MTP layer (`tools/mtp_fetch.py fetch`, `mtp_pack.py --experts q2_0`,
|
||||
`mtp_rt.py`; 4.9 GB downloaded, 809 MiB of VRAM) drafts for the Coder fine-tune at 87% acceptance:
|
||||
3.4 tokens per 53 ms round, 45 tok/s, the same greedy tokens. The `mtp.cpp` path ran on the first try
|
||||
under the same flags.
|
||||
|
||||
Without AOT the runtime JIT-compiles every kernel on first use, ~47 s the first time a process runs;
|
||||
`SYCL_CACHE_PERSISTENT=1 SYCL_CACHE_DIR=<dir>` keeps that across runs (15 MB).
|
||||
|
||||
How to run it by hand (a greedy test run, the way every number in this section was measured; inside the
|
||||
`strata-sycl-dev` image, AOT build in `build-sycl-aot/`):
|
||||
|
||||
```
|
||||
STRATA_VERIFY_DEVICE_PLAN=1 STRATA_VERIFY_NO_HOST=1 \
|
||||
build-sycl/strata --pack <iq pack> --native <shard1> --ple-gguf <shard2> \
|
||||
--expert-profile data/expert-profile-coder.bin --expert-cache auto --stream-experts \
|
||||
--prefill auto --spec 4 --spec-min-p 0.5 --max-context 8192 --tokens <ids> --max-new 64 --greedy
|
||||
```
|
||||
|
||||
- `--stream-experts` (this port): no resident host copy of the experts; every one of the 12,288 goes from
|
||||
the GGUF into the VRAM cache through a small staging ring (`GgufExpertSource`). Upstream needs 32 GB of
|
||||
RAM for this model; with the flag the engine runs on 23 GiB.
|
||||
- `STRATA_VERIFY_DEVICE_PLAN=1`: the GPU plans each layer itself (upstream's E-6; off by default there).
|
||||
- `STRATA_VERIFY_NO_HOST=1` (this port): the host waits for the whole window graph instead of per-layer
|
||||
rings. Only valid with every expert resident, which is the case on a 32 GB card.
|
||||
- `--no-prefill-borrow`: the prompt path must not lend expert slots (a lent expert is served from the host
|
||||
behind a flag the GPU does not see reliably here; long prompts hung without it).
|
||||
|
||||
What the port had to get right beyond compiling (each is an entry in `sycl/tools/fixups.py` or a flag):
|
||||
|
||||
- **Host-mapped flags.** Strata's decode is a GPU/CPU handshake through host-mapped memory. `volatile`
|
||||
device loads do not bypass the caches on Intel; system-scope atomics do, and they are the only thing
|
||||
measured to work (`sycl/probe/doorbell.cpp`, six variants). Host-to-device visibility *during* a kernel
|
||||
stays unreliable on this platform, which is why the all-resident path waits for the window instead.
|
||||
- **Bounded spins.** A device spin that never sees its flag is not a hang of one process: the xe driver
|
||||
times the queue out and resets the GT node by node - a window graph has 2,400 - and the card stays
|
||||
wedged until a reboot (twice). Every spin is capped (`kSpinMax`).
|
||||
- **32-lane sub-groups.** The kernels are written for warps; dpct pinned 134 of 289 launches, the rest
|
||||
would run at Xe2's default 16 (`-fsycl-default-sub-group-size=32`).
|
||||
- **Synchronous copies.** `cudaMemcpy` blocks; dpct's default-queue `memcpy` did not wait. With the
|
||||
streaming ring that filled the expert cache from overwritten buffers: non-deterministic residuals, NaN by
|
||||
layer 5. Found with the per-layer residual ladder (`STRATA_VERIFY_DEBUG=1` prints R after every layer).
|
||||
- `native_expert_parity` hand-ported: the GPU native expert kernel matches ggml's float reference on real
|
||||
IQ1_M rows (rel 1.1e-2, the same class as the CPU path).
|
||||
|
||||
Profiling: `sycl/tools/Dockerfile.unitrace` builds the dev image with Intel's unitrace; `unitrace -d` around
|
||||
the engine plus `sycl/rank_kernels.py` gives device time per kernel. `mmvq_bench` and `native_expert_parity
|
||||
NATIVE_BENCH=1` time the two hot kernel families in isolation (warm the clocks first: a 5 ms run measures
|
||||
the ramp, not the kernel).
|
||||
|
||||
**After the 0.1.25-0.1.27 merge (2026-09-30, same card, same flags, AOT).** Fibonacci prompt (19 tokens),
|
||||
64 greedy tokens: decode 46.5 tok/s (85% of drafts accepted). The 2,184-token prompt: 566 tok/s prompt,
|
||||
38.3 tok/s decode (77% accepted on that text); the draft layer's prompt pass is now upstream's batched one
|
||||
(38.6 ms for 2,184 tokens, from 105 ms for 19 before it). Parity: 19 of 22, byte-exact `quantize_act` back.
|
||||
|
||||
**Speed work, 2026-09-30 (after the 0.1.27 merge).** What moved the numbers, measured on the B70 with the
|
||||
same prompts as above (outputs identical before and after each change):
|
||||
|
||||
| change | effect |
|
||||
|---|---|
|
||||
| every window graph and the drafter's graphs captured at load (`STRATA_WARM_GRAPHS=0`: as before) | the first request no longer pays 250-290 ms of captures |
|
||||
| the commit graph left running while the drafter's round runs on its own queue (`Verifier::commit(wait=false)` + `commit_finish`) | decode 38.3 -> 43.0 tok/s at the 2,184-token context, 46.5 -> 49.8 short |
|
||||
| upstream 0.1.29 merged (GDN recurrence pipelining, the block-scores read, sampler) | 2,184-token prompt 765 -> 792 tok/s, decode at that context 43.1 -> 45.2 |
|
||||
| the draft layer's batched prompt pass takes prompts under 64 rows (it fell back to per-6-token graphs) | its prompt cost on 19 tokens 106 -> 16 ms |
|
||||
| one device module per kernel (`-fsycl-device-code-split=per_kernel`) | the first launch in the prompt path (the embedding gather) 245 -> 1 ms |
|
||||
| the expert dequant writes each thread's run of FP16 values as one vector store (it wrote them one 2-byte store at a time) | dequant per expert 0.085 -> 0.030 ms; prompt 496 -> 575 tok/s at 2,184 tokens, 720 -> 841 at 8,000 |
|
||||
| SWAR sign compare/subtract in the expert dots, a local-memory resident-plan kernel, a split-K fused down kernel (bit-identical to the single-token one) | parity-clean, no measurable decode change; kept |
|
||||
| the PLE row reader's default queue depth 16 -> 64 blocking O_DIRECT threads (`STRATA_IO_THREADS`; 128 and 256 change nothing: the drive tops out near 85k IOPS on the 27k random 4 KB reads a 2,184-token prompt needs) | the rows of that prompt 466 -> 330 ms |
|
||||
| a short first prompt chunk (`STRATA_PREFILL_FIRST`, default 256 tokens) so the GPU starts while the remaining rows are still being read | 2,184 tokens: 711 -> 765 tok/s, 8,000: 857 -> 986; time to first token about 150 ms less. The chunk boundary moves rounding in the expert GEMMs, so a long greedy continuation can diverge late (token 45 of 64 on the test prompt) |
|
||||
|
||||
Draft policy sweep (2,184-token prompt, 128 tokens): windows of 6 (`--spec 4`) at 41.6-43.5 tok/s; `--spec 2`
|
||||
33.6, `--spec 6` 35-37; `--spec-min-p` 0.3-0.7 within noise. Profiles (unitrace, `strata-sycl-dev:metrics` with
|
||||
Intel's metrics libraries and `dev.xe.observation_paranoid=0`): the expert dot kernels are ALU-bound (77% XVE
|
||||
active), the dense projections memory-latency bound (92-128 GB/s at 84-93% occupancy), a decode round is 80-85%
|
||||
kernel time and ~5 us of launch gap per node over 2,400-2,600 nodes.
|
||||
|
||||
**XMX.** oneMKL's FP16 GEMMs already run on the XMX units (30-60 TFLOP/s in `xmx_gemm_bench`); the prompt path is
|
||||
bound by the dequant that feeds them, not by the products. Two joint_matrix kernels were written and are correct
|
||||
but lose to the existing paths on this card, so both stay opt-in: `xmx_gemm_iq` (a fused dequant + GEMM straight
|
||||
from the quantized rows, 4-5x slower than dequant + oneMKL) and `qsa_prompt_attn_xmx` (the port of the mma.sync
|
||||
prompt attention; `qsa_prompt_attn_parity` passes at 1e-6 of scale, 3x slower than the FP32 fallback per chunk;
|
||||
`STRATA_PROMPT_ATTN_XMX=1`). Where the prompt time goes now (2,184 / 8,000 tokens): dequant 28% / 27%, GEMMs
|
||||
22% / 20%, the per-layer host grouping 9.5% / 9%, attention 7% / 14%, embeddings + PLE rows 13% / 5%.
|
||||
|
||||
A trap worth knowing: with a native pack `--prefill-until N` does not feed the rest of the prompt through the
|
||||
token loop (it is skipped for native packs), so the tokens after N are dropped and the model free-runs. Compare
|
||||
output tokens between paths, never only timings.
|
||||
|
||||
**An 80,000-token prompt (2026-09-30).** Where the time goes changes completely at this scale:
|
||||
|
||||
- **The VRAM plan.** `--expert-cache auto` fills the card down to `--vram-reserve-mib` *before* the KV state
|
||||
(about 1 GB in INT8 at 81,920 cells) and the prompt chunk buffers (about 2 GB at `--prefill 8192`) exist. With
|
||||
the 1,536 MiB default that put 30.95 of the B70's 32.6 GB in use, the driver started migrating buffers and the
|
||||
run never finished. `--max-context 81920 --kv int8 --vram-reserve-mib 3072 --prefill 4096` peaks at 28.8 GB.
|
||||
(80k ids also exceed Linux's 128 KB single-argument limit: `--tokens-file`.)
|
||||
- **Streamed experts.** The reserve costs cache slots (10,348 of 12,288 at 3 GB), and the prompt path streams the
|
||||
missing experts from the GGUF for every chunk. Two port bugs sat on this path and are fixed: the stream plan
|
||||
held `GgufExpertSource::blob()` pointers across ~1,900 reads of a 512-slot ring (so blobs were overwritten
|
||||
before they were copied: the 75 s / 1,065 tok/s measured first was computed partly with the wrong experts), and
|
||||
the stream-all walk hangs on this card in its first large chunk (the copy engine stops on a barrier). The stager
|
||||
threads now read the blobs themselves, and the port uses the per-layer routed-only walk (`STRATA_PREFILL_RING=8`
|
||||
upstream, the port's default; `STRATA_PREFILL_STREAM_ALL=1` restores the other for debugging).
|
||||
- **Correct result: 80,000 tokens in 101 s = 790 tok/s.** GPU time: expert down GEMM 26.0 s, attention 15.4 s,
|
||||
dequant 12.7 s, QSA block selection 9.3 s (0.2 s at 8k: it scans every block of the context per query), gate/up
|
||||
GEMM 8.9 s, gather 6.1 s, the per-layer grouping sync 5.2 s, GDN recurrence 4.8 s. The PLE rows are free at this
|
||||
scale (97.6% row-cache hits).
|
||||
- **Decode after such a prompt, and the fix.** Without lending cache slots to the prompt path (`--no-prefill-borrow`,
|
||||
which the port used from the start) the reserve evicts ~1,900 experts for good and decode after the prompt runs at
|
||||
2 tok/s. Borrowing lends ~940 slots to the prompt path and refills them in about a second afterwards: 80,000
|
||||
tokens in 75 s = **1,062 tok/s, decode 35-40 tok/s** after it, at `--vram-reserve-mib 2048 --prefill 4096`.
|
||||
It costs ~1 s on short prompts (2,184 tokens: 610 vs 792 tok/s), so the port borrows by default only above a
|
||||
32K context (`--prefill-borrow` / `--no-prefill-borrow` decide explicitly).
|
||||
|
||||
**Long contexts (2026-10-01, engine 0.1.32-sycl, setup's flags: `--kv-resident 32768 --vram-reserve-mib 2048
|
||||
--prefill 4096`, the prompt path borrowing cache slots - the default above 32K).** Same text repeated to length,
|
||||
then 256 greedy tokens:
|
||||
|
||||
| context | KV | prompt | decode after | peak VRAM | experts in the host mirror |
|
||||
|---|---|---|---|---|---|
|
||||
| 128K | int8 | 888 tok/s (144 s) | **66.6 tok/s** | 29.9 GB | none |
|
||||
| 128K | q4_0 | 848 tok/s (151 s) | 62.8 tok/s | 29.7 GB | none |
|
||||
| 128K | k8v4, KV in VRAM | **1,006 tok/s** (127 s) | **66.8 tok/s** | 30.7 GB | 103 (0.2 GB) |
|
||||
| 256K | int8 | 757 tok/s (338 s) | **55.9 tok/s** | 30.2 GB | none |
|
||||
| 256K | q4_0 | 737 tok/s (347 s) | 51.7 tok/s | 30.0 GB | none |
|
||||
| 256K | k8v4, KV in VRAM | 823 tok/s (311 s) | 52.2 tok/s | 30.9 GB | 865 (1.7 GB) |
|
||||
|
||||
Borrowing keeps every expert in VRAM (the prompt buffers live in lent cache slots, refilled after the prompt).
|
||||
Without it (`--no-prefill-borrow`) the buffers keep their own VRAM and ~1,000-2,300 experts move to the pinned host
|
||||
mirror: the prompt is 2-5% faster, decode after it 10-23% slower (0.1.31 without borrowing: 128K int8 934 / 59.5,
|
||||
q4_0 887 / 53.4, k8v4 1,026 / 54.4; 256K int8 784 / 48.7, q4_0 763 / 43.2, k8v4 838 / 42.3 tok/s).
|
||||
|
||||
KV streaming (`--kv-resident`) keeps the whole KV in pinned host memory and only the attended window in VRAM, so
|
||||
the KV pushes no experts out; setup.py turns it on from 64K up and keeps INT8 (the faster of the
|
||||
two at every size here). Without it the KV pushes experts out and decode after the prompt fell to 4-9 tok/s at
|
||||
128K-256K (2026-09-30). `--kv k8v4` does not support streaming yet: setup keeps its KV in VRAM, which pushes up to
|
||||
more experts out; those come from the pinned host mirror over PCIe (planned item 2), so decode holds at 42 tok/s at
|
||||
256K (it was 3.8 before the mirror). k8v4 reads prompts fastest (no streaming copies) and works through the FP32
|
||||
attention fallback (its dedicated prompt kernel is the XMX one below).
|
||||
|
||||
**XMX prompt attention v2.** 64-cell chunks, vector-packed K^T and V, hi+lo Q in one accumulator per scale group,
|
||||
one accumulator update per chunk: 1.4-1.5x faster than v1 and correct in all modes (`qsa_prompt_attn_parity`,
|
||||
and `kv_hybrid_parity` passes completely with it), but still ~2x slower than the FP32 fallback (13.4 vs 5.5 ms per
|
||||
chunk, INT8, 32K context). This attention is gather-bound: each query position selects its own ~2,000 cells, so
|
||||
the K/V fetch dominates and only 12 of the 16 matrix rows are real heads. Opt-in: `STRATA_PROMPT_ATTN_XMX=1`
|
||||
(64-cell chunks, 120 KB of local memory) or `=32`.
|
||||
|
||||
**XMX v2 in the full matrix, and why a grouped kernel is not next (2026-09-30).** With `STRATA_PROMPT_ATTN_XMX=1`
|
||||
(stage timing on, ~8-10% overhead) the long prompts run 23-33% slower than with the FP32 attention: 128K int8 666 vs
|
||||
960 tok/s, k8v4 596 vs 983; 256K k8v4 506 vs 752, int8 536 vs 718 (q4_0 does not use the XMX kernel). The obvious
|
||||
fix, gathering the union of neighbouring positions' cells once, depends on how much their selections overlap.
|
||||
Measured on the last chunk of an 80K prompt (first QSA layer, `STRATA_DUMP_SEL=<file>`): the union of 8 consecutive
|
||||
positions is 3.3x one position's 2,051 cells (16: 5.1x), and only 12% of a selection is shared by all 8. Grouping
|
||||
would cut the K/V gather to ~40% but multiply the arithmetic by 3-5x: at best 5-8 s of an 80K prompt. Not built.
|
||||
|
||||
**Serving the port (2026-09-30).** `serve/server.py --engine strata` runs the SYCL engine unchanged through
|
||||
`sycl/serve/strata-sycl.sh`, an `exe` that starts the binary inside the oneAPI runtime image with the serve pipes
|
||||
attached (paths in the config's `args` are the container's, the data root mounted at `/work`). A config:
|
||||
|
||||
```json
|
||||
{"engine": "strata", "exe": "<repo>/sycl/serve/strata-sycl.sh",
|
||||
"args": ["--pack", "/work/pack", "--native", "<shard 1>", "--ple-gguf", "<shard 2>",
|
||||
"--expert-profile", "data/expert-profile-coder.bin", "--expert-cache", "auto", "--stream-experts",
|
||||
"--prefill", "auto", "--spec", "4", "--spec-min-p", "0.5", "--mtp", "/work/mtp/rt",
|
||||
"--max-context", "32768", "--kv", "int8", "--vram-reserve-mib", "1024"],
|
||||
"sampling": {"temperature": 0.6, "top_p": 0.95, "top_k": 20, "repetition_penalty": 1.05}, ...}
|
||||
```
|
||||
|
||||
Measured through the OpenAI API with those sampling defaults: 36 tok/s decode on a first turn, 33 on a follow-up
|
||||
(which reuses the conversation's cached prompt), against 24-26 for llama.cpp on the same card. After the decode work
|
||||
(2026-10-01 build): 48-72 tok/s on a ~190-token answer, prompt included (the first request after a start is the slower
|
||||
one); 55-59 tok/s over 5,000-18,000-token answers on the 2026-09-30 build. The reserve matters:
|
||||
with `--stream-experts` there is no host copy of the experts, so any expert left out of VRAM is read from the SSD
|
||||
and computed on the CPU whenever it is routed. At 1,536 MiB the cache came up 128 experts short and decode fell to
|
||||
5-10 tok/s; 1,024 MiB fits all 12,288 with 2 GB of VRAM still free.
|
||||
|
||||
**Logprobs (2026-10-02).** `/v1/chat/completions` takes OpenAI's `"logprobs": true` and `"top_logprobs": 0..20`,
|
||||
streamed or not, through `sycl/serve/server_intel.py`; the reply carries `choices[0].logprobs.content[]` (`token`,
|
||||
`logprob`, `bytes`, `top_logprobs`), and with thinking on the thinking tokens under `reasoning_content`. The engine
|
||||
(the port only) gets `logprobs=K` on its GEN line and writes an `LP logprob id:logprob ...` line after each `T`
|
||||
line, from the verify window's head logits - the distribution the token was taken from, before temperature,
|
||||
penalties and sampling. Upstream's server ignores the lines, and an engine never asked writes none. Each token
|
||||
costs one 1 MB logits row copy when asked. Measured (IQ2_XS, "capital of France"): "Paris" at -0.0019 (p 0.998), the
|
||||
same answer and no measurable time without it. Not on `/v1/completions` or the Anthropic endpoint yet.
|
||||
|
||||
**The Monitor tab on Intel.** `sycl/serve/server_intel.py` is `serve/server.py` with two additions made at run
|
||||
time:
|
||||
|
||||
- **GPU readings.** When NVML has no card, it plugs `sycl/serve/xe_telemetry.py` into `serve/telemetry.py`'s
|
||||
`gpu_reader`. That reads the Arc through sysfs (temperature, power and its cap, the PCIe link the card
|
||||
trained at). Load and VRAM come from `/run/gpustat.json`: xe reports them per client in `/proc/*/fdinfo`,
|
||||
which only root can read, so a small root sampler writes them (`sycl/tools/gpustat.py`; install it with its
|
||||
`gpustat.service` as its header says).
|
||||
- **A Model menu.** A run config may name a `model_switcher` (an RPC taking `{"mode": m}`) for a host that swaps
|
||||
models on one card. The web app's header then gets a Model menu (`sycl/serve/web/switcher.js`, injected into
|
||||
the page).
|
||||
|
||||
**Speed depends on the text.** Decode with speculative decoding tracks how often the draft layer guesses right.
|
||||
The served engine at 32K context: 45-51 tok/s on the test prompt (continuing a Fibonacci function, 77-85%
|
||||
of drafts accepted), 30-39 tok/s on a chat answer with prose (55-72%); sampling and the repetition penalty cost
|
||||
nothing measurable. The engine used to abort at exit in serve mode (a queue wait in a destructor after the
|
||||
runtime's teardown began); it now exits directly once its requests are done.
|
||||
|
||||
**Planned (2026-09-30), in order.** Ranked by payoff on this card; the first three were taken first.
|
||||
|
||||
1. Expert dot products on XMX in integer mode: a decode window (up to 6 tokens) fits one INT8 DPAS (1-8 rows), the
|
||||
i-quant grids decode to small integers, the activations are already INT8. Today: scalar dp4a, ALU-bound (77%).
|
||||
**Parked**: three `joint_matrix` versions (opt-in `STRATA_EXPERT_XMX=1`, do not enable) were 1.4x and 2-3x slower
|
||||
than dp4a, and the third (B filled in place with `joint_matrix_apply`) hung the GPU. At 1-6 rows the grid decode
|
||||
and the packed-B layout cost more than the DPAS saves.
|
||||
2. Experts missing from VRAM read from pinned host memory over PCIe instead of the SSD (the 256K decode collapse).
|
||||
**Done**: at start-up every expert without a VRAM slot is read into a pinned host mirror (`STRATA_MIRROR_MIB`, by
|
||||
default free RAM less 4 GiB), and the device-built verify plan points the expert kernels straight at it. Before,
|
||||
that plan dropped non-resident experts, so output with misses was wrong. Forced test (`--expert-cache 8000`,
|
||||
1,879 experts / 3.6 GiB out of VRAM): decode 2.6 -> 40.9 tok/s (full cache 43.3), prompt 451 -> 640 tok/s
|
||||
(776), output tokens identical to the full-cache run. About 3.6 GiB of VRAM can go to context for ~6% of decode.
|
||||
3. KV streaming (`--kv-resident`) from 64K up: only the attended window of the KV in VRAM. **Done**: 256K decodes at
|
||||
31 tok/s (from 4-5).
|
||||
4. QSA block selection on XMX: every query against every pooled block, a dense product that grows with the context.
|
||||
5. The hot decode kernels re-tuned for Xe2's native 16-wide sub-groups (twice the registers per thread).
|
||||
**Tried, no gain**: SIMD16 builds of the multi-column mmvq and the wide Q6_K kernel (`STRATA_MMVQ_SG`: 16 all,
|
||||
1 IQ4_XS only, 2 short outputs only; default 32). Alone (`mmvq_sg_bench`) SIMD16 is up to 1.45x faster on IQ4_XS
|
||||
and 1.3x on the 640-row shared-expert projections, 10-25% slower on the large K-quant ones; in the engine no
|
||||
setting beats SIMD32 beyond run-to-run noise (SIMD32 itself lands at 39 or 45 tok/s). Output tokens identical.
|
||||
The same bench shows the real headroom: the dense decode kernels stream 100-280 GB/s of a 608 GB/s card
|
||||
(Q6_K, the most common type, ~146 GB/s at 2-4 columns).
|
||||
**Found and fixed: misaligned loads.** A Q6_K block is 210 bytes, so every block's `ql`/`qh` runs start only
|
||||
2-byte aligned, and the B70 splits a misaligned 16-byte load into pieces. The same weights repacked at a
|
||||
224-byte stride ran 2.3-4.7x faster (`q6k_align_bench`). Without changing the layout, the wide Q6_K kernel now
|
||||
does two aligned 16-byte loads and a shift (`load16_a2`; both stay inside the block) and takes every Q6_K
|
||||
shape and window width (it used to be gated to n_out >= 4096, 1-4 columns): 2560->10240 at 1/2/4/6 columns
|
||||
160/145/141/101 -> 493/432/331/273 GB/s, the 248K-row head 150 -> 407 GB/s at 1 column. Output tokens
|
||||
identical. **Coder decode 44.9 -> 54 tok/s** (2k prompt, greedy). `STRATA_MMVQ_A2=0` restores the old path.
|
||||
Still misaligned: IQ4_XS (136 B, 8-aligned, ~100 GB/s), IQ4_NL (18 B), Q8_0 (34 B); Q4_K/Q5_K are aligned
|
||||
but use 4-byte loads in the multi kernel (230-280 GB/s).
|
||||
|
||||
**Decode round 2 (2026-09-30), from a unitrace of 256 decoded tokens.** Each step measured on its own, 19-token prompt,
|
||||
256 tokens, two runs each, output tokens identical throughout:
|
||||
|
||||
| | decode |
|
||||
|---|---|
|
||||
| after the Q6_K alignment fix | 53.9 tok/s |
|
||||
| + `resident_plan` grouping in parallel (thread 0 alone took 73 us per layer, ~3.5 ms a round) | 57.1 |
|
||||
| + the GR down kernel reads the activations directly (the 60 KB SLM tile per group capped occupancy) | 58.7 |
|
||||
| + wide 16-byte-load kernels for Q4_K, Q5_K, IQ4_XS (codebook in registers) | 62.6 |
|
||||
| + wide kernels for Q8_0 (MTP dense, shexp down of layer 0) and IQ4_NL (shexp down); Q6_K's activation loads aligned | 65.6 |
|
||||
| + the GR down projection sliced by columns (each group stages one 128-column slice of xn, 4 lanes per row) + a fixed-order reduce | 70.6 |
|
||||
| + IQ4_NL expert dots (the down projection in 39 of 48 layers): codebook in registers, weights as aligned 4-byte loads, bitwise the same ints (2026-10-01) | 76.1 |
|
||||
| + GR norm per (token, stream), bitwise the same; aligned weight loads in the IQ3_XXS/IQ3_S/IQ2_S gate/up dots | **77.9** |
|
||||
|
||||
The last step changes the output: identical for 146 tokens, then a near-tie after a comma goes the other way (the new
|
||||
kernels sum in a different order). Q8_0 kernels 4-6x (189 -> 31 us at 2560 x 10240, 2 columns), IQ4_NL 640 -> 2560
|
||||
28 -> 6.6 us. The aligned-load helper only loads its second chunk when the address is unaligned, so it never reads a
|
||||
16-byte chunk without a needed byte and cannot cross a page at the end of an allocation. `STRATA_MMVQ_WIDE_32=0`
|
||||
restores the old Q8_0/IQ4_NL kernels. The sliced GR down kernel (GR read 108.6 -> 76.5 us at 6 tokens, sums equal to
|
||||
~1e-7) flips that near-tie back: its output is the original one. `STRATA_GR_DOWN_SLICED=0` restores the direct kernel.
|
||||
|
||||
Kernel level: IQ4_XS 107 -> 323 GB/s at 2 columns (61 -> 212 at 6), Q4_K/Q5_K 1.3-1.6x; the GR read 135 -> 110 us at
|
||||
6 tokens (bitwise equal). Switches to the old paths: `STRATA_PLAN_PARALLEL=0`, `STRATA_GR_DOWN_DIRECT=0`,
|
||||
`STRATA_MMVQ_WIDE_K=0`. (`iq_parity` reports 10 "missing fixture" failures with and without the change: the oracle
|
||||
fixtures are not in the port's tree.)
|
||||
|
||||
**Two-speed runs, explained (2026-09-30).** Identical greedy runs decode at either ~45 or ~39 tok/s. A per-gather
|
||||
trace of the PLE reader (`STRATA_PLE_TRACE=1`) shows the slow runs pay one 226 ms PLE read stall in the first
|
||||
decode round, after the window graphs are captured; every other read and round matches the fast runs. Prompt time
|
||||
plus decode time is the same in both modes (4.95-5.17 s): the stall lands either in the prompt's PLE wait or in the
|
||||
first decode round, so it is a once-per-process cost, not lost throughput. Ruled out: NVMe APST, the I/O scheduler
|
||||
(`none`), CPU starvation (93% idle during decode), I/O thread count. Compare runs on time to first token + decode.
|
||||
6. INT8 prompt GEMMs: experts dequantized to INT8, oneMKL/oneDNN INT8 on XMX (half the dequant bytes, 2x rate).
|
||||
7. Fewer graph nodes per decode round (~2,500 at ~5 us): norm+rope, scores+top-k, gate+quantize fused.
|
||||
8. Wider speculation (two draft branches per verify window): the kernels are latency-bound, so it is nearly free.
|
||||
9. A model bigger than VRAM: the original Qwen3.8-Flash-Next IQ2_XS (35.5 GB of experts; the same path suits Q2_0
|
||||
and Swift 1.5) on the B70 with 23 GB of RAM. ~24 GB of experts in VRAM, the rest (~10-12 GB) in the pinned host
|
||||
mirror the device plan reads over PCIe (item 2). The port has the IQ2_XS/IQ3_XXS/Q2_0 expert kernels. Open: whether
|
||||
a 10+ GB pinned mirror fits beside everything else in 23 GB, and decode with that share of experts on a Gen3 x8
|
||||
link (3.6 GB mirrored measured 40.9 tok/s; expect less). Needs the 68 GB download.
|
||||
**Done (2026-10-01)**: shard 1 downloaded (39.2 GB; shard 2 is byte-identical to the Coder's, so a hard link),
|
||||
a native pack (`tools/iq_pack.py`, 6 s), the original model's expert profile, the same MTP draft layer. 18,329 of
|
||||
24,576 experts in VRAM (24.6 GiB), 6,247 in the pinned host mirror (8.4 GiB, 9 s to fill), host RAM never below
|
||||
12 GB free. **Decode 50.8 tok/s** on the 19-token prompt and **60.6** after 2,184 tokens, prompt 549 tok/s;
|
||||
coherent, correct answers on both. Q2_0 and Swift 1.5 (similar size) should behave the same; IQ3_XXS/IQ3_S
|
||||
(43-50 GB of experts) would need 19-26 GB mirrored, more than 23 GB of RAM allows.
|
||||
|
||||
**Keeping up with upstream.** A merge of upstream `main` into `b70` leaves the copies in `sycl/` behind
|
||||
wherever upstream touched a file they mirror. They are refreshed by re-migration, not by hand (done for
|
||||
0.1.25-0.1.27, 2026-09-30):
|
||||
|
||||
1. Merge upstream into `b70`. No shared file should conflict: the Intel code is all in `sycl/`. Then check that
|
||||
`sycl/setup_intel.py --check` and `sycl/serve/server_intel.py --engine mock` still run against the new
|
||||
setup.py and server.py.
|
||||
2. Migrate the *old* upstream tree (a `git archive` of the pre-merge commit) with `migrate.sh` into
|
||||
`sycl-base`, and the merged tree into `sycl-new`; about 3 minutes each in the dev image.
|
||||
3. `sycl/tools/normalize.sh <dir> <commit>` on both: dpct's file names, the unchanged files, the
|
||||
message serials, then `fixups.py`. Two runs of dpct on the same source now differ only in the kernel
|
||||
name hashes it generates.
|
||||
4. For every file present in both: `git merge-file sycl/F sycl-base/F sycl-new/F`. Files upstream did not
|
||||
change are left alone. Copies dpct never produced (`verify.cpp`, `mtp.cpp`, `remote_experts.cpp`
|
||||
include no CUDA header directly) take upstream's diff by hand; new parity tests are copied in and
|
||||
listed in `sycl/CMakeLists.txt`.
|
||||
5. A fixup whose pattern upstream changed shows up as a compile error (the PTX gate became an `#elif`
|
||||
under upstream's `__HIPCC__` guard); extend the fixup, re-run it, rebuild.
|
||||
|
||||
**The 0.1.31 merge (2026-10-01).** Upstream 0.1.29 -> 0.1.31 (132 commits) by the steps above: 45 migrated files
|
||||
changed for real (27 more only in dpct's kernel-name hashes), 38 conflicts resolved, verify.cpp/mtp.cpp/
|
||||
conversation_state.cpp ported by hand. A symbol audit (the port's feature identifiers counted before and after) is
|
||||
part of the procedure now: it caught a dropped mirror hook and four doorbell waits that had lost their spin bound
|
||||
(upstream renamed the loops; `fixups.py` covers both spellings). What the merge needed beyond conflicts:
|
||||
|
||||
- **The expert kernels.** Upstream sends the i-quant experts to new multi kernels (one warp per row, 8 rows a
|
||||
group); the port's grid is sized for its own kernels (8 lanes a row, 32 rows a group), so only a quarter of each
|
||||
expert's rows were written and the output was end-of-text tokens. The port's kernels stay the default (they also
|
||||
serve upstream's new Q4_K/Q5_K/Q5_1/Q8_0 experts); `STRATA_EXPERT_SPLIT=1` runs upstream's, with their grid.
|
||||
- **Prompt-slot borrowing hung** in the first chunk of any prompt with it on (40K, 128K; GPU busy) - fixed after
|
||||
the 0.1.32 merge, see "Prompt-slot borrowing: the hang" below.
|
||||
- **Upstream's new tests found three dpct mistranslations from the first migration.** dpct writes `__fadd_rn(a, b)`
|
||||
as `a + b` without parentheses, so `__fadd_rn(sum, c ? x : y)` became `sum + c ? x : y` (replaced, not added): in
|
||||
the native QSA indexer's per-token pooled key (the decode path: blocks completed while generating got wrong
|
||||
pooled keys; prompt blocks come from the batched kernel and were right), in the native QSA scores (an opt-in
|
||||
path) and in the s2 activation rounding (every value 0; the Q2_0 s2 pack, unused here). Fixed, and `fixups.py`
|
||||
now parenthesises. `qsa_parity`'s batch-vs-sequential test passes; decode output after long prompts changed.
|
||||
- **Upstream's new kernels against the port's on the B70** (decode tok/s, Coder 19 / 2,184-token prompts, IQ2_XS
|
||||
19): port defaults 78.1 / 75.7 / 58.6; `STRATA_EXPERT_SPLIT=1` 65.6-69.5 / 67.2 / 49.1 (-11 to -16%);
|
||||
`STRATA_GR_V3=1` 73.2 / 72.4 (-4 to -6%). Both stay opt-in.
|
||||
- **Lanes per row of the port's expert kernels** (`STRATA_GU_LANES` / `STRATA_DOWN_LANES`, 4/8/16/32 at run time):
|
||||
gate/up 8 or 16 x down 4 or 8 all decode in 37.7-38.6 ms per verify round (2 runs each, Coder, both prompts); tok/s
|
||||
differences between them are draft acceptance (each split rounds differently). The default (8 / 8) stays.
|
||||
- **Speed after the merge:** Coder 78.2 / 75.7 tok/s (as before), IQ2_XS 58.6 / 64.2 (from 50.8 / 60.6).
|
||||
- **Still failing:** (`iq_multi_parity` on IQ2_XS: fixed later, see "Two model-specific bugs"), `s2_expert_grouped_parity` (the s2 path), `kv_hybrid_parity`'s last step
|
||||
(needs the unported tensor-core prompt kernel). `ple_parity` and `native_expert_parity` need model files.
|
||||
|
||||
**The 0.1.32 merge (2026-10-01).** Upstream 0.1.31 -> 0.1.32 (89 commits) by the same steps; the hash-only
|
||||
differences are now canonicalized before `git merge-file` (the port's kernel-name hashes are kept), which left 10
|
||||
files with real conflicts. What it needed:
|
||||
|
||||
- **Upstream's async commit** (`set_commit_async` / `wait_commit`) maps onto the port's own deferred commit
|
||||
(`commit(n, err, false)` + `commit_finish`); `wait_commit` is `commit_finish`.
|
||||
- **Upstream's new hyper-connection read variants** (split / staged, chosen per card by a bit-for-bit self-test at
|
||||
start) are not used by the port, which keeps its sliced down / split norm read; the self-test segfaulted on the
|
||||
B70, so on SYCL it runs only with `STRATA_HC_CHECK=1` (open). The port's split-norm kernel was renamed
|
||||
(`gr_norm_split_port_kernel`): upstream now has one of the same name.
|
||||
- Two kernel names collided after hash canonicalization (renamed), and fused_gr's per-block shared-memory query is
|
||||
a fixup now.
|
||||
- **Result:** every output identical to 0.1.31 (Coder 19 / 2,184-token prompts and IQ2_XS, 256 greedy tokens),
|
||||
same speeds (Coder 77.7-78.1 / 75.7 tok/s, IQ2_XS 58.5), 40K prompt 1,201 tok/s then 65.1 tok/s decode.
|
||||
`kv_hybrid_parity` and `qsa_prompt_attn_parity` pass (the tests now turn on the XMX prompt attention they
|
||||
check); `iq_multi_parity` (IQ2_XS) and `s2_expert_grouped_parity` as before.
|
||||
|
||||
**The 0.1.33 merge (2026-10-01).** Upstream 0.1.32 -> 0.1.33 (17 commits): no shared file conflicted (the Intel
|
||||
code is all in `sycl/`); six engine files changed upstream and were merged by the same re-migration (only those six:
|
||||
the other files dpct produced differently were left alone). Two conflicts: `--resident-cpu-experts` (upstream's new
|
||||
`resident_cpu_explicit` beside the port's `--stream-experts`) and the prompt attention's compute-capability check
|
||||
(the port keeps its XMX dispatch). `sycl/setup_intel.py` and `sycl/serve/server_intel.py` ran unchanged against the
|
||||
new setup.py and server.py. Every output identical (Coder, IQ2_XS, the 40K prompt with borrowing); `gr_parity` and
|
||||
`qsa_prompt_attn_parity` pass.
|
||||
|
||||
**Prompt-slot borrowing: the hang (fixed 2026-10-01).** From 0.1.31 on, a prompt that borrowed cache slots stopped
|
||||
in its first full chunk with the GPU at 100% and the host waiting on the compute queue. It was never a borrowing
|
||||
bug: borrowing is just the only way the Coder streams experts during a prompt (every expert is resident otherwise),
|
||||
and the streamed path's stager deadlocked. Its threads read experts into a ring of 16 page-locked buffers and, to
|
||||
reuse one, waited on the copy queue's event for the DMA that last read it (the CUDA form, `cudaEventSynchronize`).
|
||||
Under the Level Zero v2 adapter that event, once a host thread had waited on it, no longer released the other
|
||||
queue's barrier that also listed it, and the GPU waited forever - only once a layer streamed more than 16 experts
|
||||
(the ring wrapped). The v1 adapter (`SYCL_UR_USE_LEVEL_ZERO_V2=0`) and a 256-buffer ring both ran; so did a sync
|
||||
after every phase (`STRATA_PREFILL_SYNC=1`, kept as a debug switch). The fix: the copy queue writes a sequence
|
||||
number into page-locked host memory after each DMA and the stager polls it - no host thread waits on a queue's
|
||||
event. Output is bit-identical to the run without borrowing; borrowing is the default above 32K again.
|
||||
|
||||
**Two model-specific bugs found with Swift 1.5 (fixed 2026-10-01).** UkisAI's Swift 1.5 IQ2_XS (setup's `swift`
|
||||
family, the same GSQ-RCO layout) decoded token 0 forever: NaN logits from layer 13 on.
|
||||
|
||||
- **`--stream-experts` read the wrong shard.** Since upstream 0.1.31, `ExpertLayout::gguf_file` is per layer AND role
|
||||
(`3 * layer + role`; a shard boundary can fall inside a layer). The port's `GgufExpertSource` still indexed it by
|
||||
layer, so a layer in shard 2 was read from shard 1 at shard 2's offsets: garbage IQ1_M scales, infinities after the
|
||||
fp16 dequantization. The original model keeps every expert in shard 1 and never noticed; Swift's layers 13-47 are
|
||||
in shard 2. `STRATA_DBG_NAN=1` now also reports the experts' fp16 GEMM inputs (activations, dequantized gate/up).
|
||||
- **IQ2_XS products were garbage** (the long-failing `iq_multi_parity` IQ2_XS case): ggml's `vec_dot_iq2_xs` reads
|
||||
its four 16-bit codes through a `uint16_t*` to an `int2`, type punning the SYCL device compiler does not honour;
|
||||
the dequantizer, which reads the codes directly, was exact. The codes are extracted by shifts now (the dot and the
|
||||
expert kernels' `Split<17>`); the test checks against a CPU decode from ggml's tables too and passes. No model here
|
||||
uses IQ2_XS tensors, Swift included - this was not Swift's bug, only found on the way.
|
||||
|
||||
Swift 1.5 IQ2_XS after both: correct answers, 51-118 words of thinking on short questions, 48 tok/s raw decode.
|
||||
|
||||
**Load time (2026-10-02).** Starting a model is mostly the expert cache's fill from the GGUF (`--stream-experts`):
|
||||
the Coder's 23.4 GiB of experts took 64-76 s, the IQ2_XS's 24.9 GiB 91 s, because the fill read each expert's
|
||||
three slices, copied the blob and waited for the copy before the next read. It is a pipeline now: the slots are
|
||||
admitted in profile order first (the same placement), the reads go in file order by up to 8 threads into
|
||||
page-locked batches of 64, and a batch's copies run while the next one is read. Cold page cache: 76.2 s -> 18.8 s
|
||||
(0.33 -> 1.34 GB/s); a whole Coder start from the engine's launch to its first token 82 s -> 26 s, the IQ2_XS
|
||||
120 s -> 41 s (its 8.2 GB host mirror is ~9 s of the rest). Output identical. `STRATA_FILL_SERIAL=1` is the old fill.
|
||||
|
||||
**The stream-all prompt walk is back (2026-10-03).** It copies every expert the VRAM cache does not hold, layer by
|
||||
layer ahead of the compute, instead of only the routed ones. It hung on the B70 until the stager and PLE fixes of
|
||||
2026-10-02 and runs since. It now runs by default when the VRAM holds more than 90% of the (layer, expert) pairs;
|
||||
past that it would copy several times the routed experts (the IQ2_XS keeps a quarter of them in the RAM mirror: a
|
||||
2,184-token prompt fell from ~560 to 254 tok/s with it). `STRATA_PREFILL_STREAM_ALL=1` / `=0` force it on or off.
|
||||
Coder, outputs identical either way:
|
||||
|
||||
| prompt | routed-only | stream-all |
|
||||
|---|---|---|
|
||||
| 40K int8 | 1,106 tok/s | 1,160 |
|
||||
| 128K int8, KV streaming | 897 | 920 |
|
||||
| 128K k8v4 | 1,006 | 1,041 |
|
||||
| 256K int8, KV streaming | 760 | 780 |
|
||||
|
||||
Measured alongside: the 2,184-token prompt reads 784-799 tok/s on both the 0.1.35+PR and the 0.1.38 build (three
|
||||
runs each), so the 825 seen once on 0.1.35 was session variance, not a regression. The prompt path at 40K (Coder,
|
||||
GPU timeline): expert dequant 15.7%, expert GEMMs 23.4%, host grouping 6.5%, gather 5.7%, combine 2.4%, prompt
|
||||
attention 19.2%, QSA select 6.8%, DeltaNet recurrence 5.8%, hyper-connection reads 4.6%. Upstream's sm_90 cluster
|
||||
kernels (greedy sampler, QSA top-k) would save next to nothing here: the sampler reads ~6 MB a round against a 13 ms
|
||||
token. Q4_0 tensor-core prompt attention (#452) is not worth porting: the port's XMX prompt attention is 2-3x slower
|
||||
than the plain kernel it uses for every KV format.
|
||||
|
||||
**The 0.1.38 merge (2026-10-03).** Upstream 0.1.35 -> 0.1.38 (83 commits) merged the seven PRs the port had taken
|
||||
early, so their header forks in `sycl/include` are gone; the merge base for the 3-way merge was the port's own "main +
|
||||
PRs" migration. New in the port with it: the DeltaNet output norm without its dead FP32 store, two prompt-path
|
||||
fixes (the fused layout's buffers, the streamed walk's resident lookup), `STRATA_GR_DOWN_MAX4=1` (opt-in; neutral on
|
||||
the B70: 2,184-token prompt 793 vs 788 tok/s, decode 76.2 vs 76.3). Stubbed in the port: `--peer-device` (a second
|
||||
GPU as an expert-cache tier: upstream's code is CUDA calls; `open` refuses with a message) and the fused int8 prompt
|
||||
kernels (#136, part of the MMQ library the port does not build). Off on SYCL: the sm_90 thread-block-cluster greedy
|
||||
sampler and QSA top-k (no SYCL counterpart; the callers take the plain kernels). `STRATA_GDN_KEYHEAD=1` stays opt-in
|
||||
(still 8% slower here: 727 vs 788 tok/s). `qsa_prompt_attn_parity` now also tests upstream's Q4_0 tensor-core mode
|
||||
(#452), which the port does not have: those cases report "refused" (the int8 and fp16 XMX cases pass); a `--kv
|
||||
q4_0` prompt takes the older kernel, as before. Outputs identical (Coder, IQ2_XS, 40K); 128K int8 KV streaming
|
||||
892 / 66.8 tok/s.
|
||||
|
||||
**0.1.35 and seven open upstream PRs (2026-10-02).** Upstream main 0.1.33 -> 0.1.35 merged as before, then seven open
|
||||
PRs ported into `sycl/` ahead of upstream (one dpct run of main + all of them, 3-way merged into only the files they
|
||||
touch; `sycl/tools/merge_upstream.py`). Their header changes live in `sycl/include` until upstream merges them.
|
||||
|
||||
| PR | what | on the B70 |
|
||||
|---|---|---|
|
||||
| #385 (sergqwer) | stager: a buffer's first job of a generation waits for the previous DMA from it | race fix; same output |
|
||||
| #463 (constantindjonkam) | decode waits for an adaptive swap before reading the residency table | determinism fix |
|
||||
| #453 (architectds) | the drafter's batched K/V on the KV-streaming ring | 128K int8: 888 -> 895 tok/s prompt, 66.6 -> 67.0 decode |
|
||||
| #374 (sergqwer) | the first chunk's PLE rows read beside layer 0 | part of the 2,184-token prompt's 784 -> 825 tok/s |
|
||||
| #363 (BlueKingMuch) | the PCIe expert call: group stride, fused SwiGLU + q8_1 | IQ2_XS decode +1.7%; re-done on the port's lane kernels |
|
||||
| #407 (sergqwer) | `--adapt-tuned` (opt-in) | neutral here; stays opt-in |
|
||||
| #413 (BlueKingMuch) | DeltaNet recurrence per key head | bit-identical but 8% slower prompt here: off (`STRATA_GDN_KEYHEAD=1`) |
|
||||
|
||||
#374 needed two port fixes: its next-chunk read assumed every chunk is the full chunk length (the port's first chunk
|
||||
is 256 tokens: the second chunk's rows were read from the wrong place), and its host wait on the PLE upload's event,
|
||||
now inside the layer loop, deadlocked the prompt past ~4K tokens under the Level Zero v2 adapter (the stager's bug
|
||||
again): the upload is marked by a polled sequence number now. #413's gate is an NVIDIA SM-count rule and its parity
|
||||
test an SM-holding NVIDIA bench (not built). Outputs identical to 0.1.33 (Coder 19 / 2,184 tokens, IQ2_XS, 40K).
|
||||
|
||||
**Not ported yet (2026-10-01).**
|
||||
|
||||
- Three kernels carry inline PTX (`mma.sync` tensor-core matrix ops, `ldmatrix`, `cp.async`):
|
||||
`qsa_prompt_attn`, `qsa_select`'s block scores, `native_qsa_score`. The SYCL build takes the "older card"
|
||||
fallback the CUDA build uses below sm_80. `qsa_prompt_attn` also has an XMX version (`joint_matrix`, opt-in
|
||||
`STRATA_PROMPT_ATTN_XMX=1`): correct, but slower than the fallback (see "XMX prompt attention v2").
|
||||
- The ggml MMQ prefill path (`moe_mmq.cu`) needs llama.cpp's ggml-cuda sources; not built.
|
||||
- AOT device code is what runs: `AOT=bmg-g31 BUILD_DIR=.../build-sycl-aot` (the JIT build costs ~47 s of
|
||||
compiling on the first window).
|
||||
|
||||
## Not done
|
||||
|
||||
- Images, on both Intel engines. Strata's vision path encodes with `strata-vision` into embeddings the CUDA
|
||||
engine reads; neither the SYCL port nor the llama.cpp config wires it yet.
|
||||
- Speculative decoding on the llama.cpp path (the GGUF carries no draft layer llama.cpp can use). The SYCL
|
||||
port has it (MTP draft layer, `--mtp`).
|
||||
|
||||
## Measured, 2026-10-01, Arc Pro B70, Coder IQ1_M, 32K context, INT8 KV: the SYCL port (engine 0.1.31-sycl)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| decode | 78.2 tok/s on a 19-token prompt, 75.7 after a 2,184-token prompt (256 greedy tokens, MTP + suffix drafts) |
|
||||
| prompt | 780 tok/s on 2,184 tokens; 934 tok/s at 128K, 784 at 256K |
|
||||
| through the API | 48-72 tok/s on a ~190-token chat answer, prompt included (the first request after a start is the slower one) |
|
||||
| VRAM | all 12,288 experts resident, ~1.9 GB free with everything loaded (`--vram-reserve-mib 1024`) |
|
||||
| RAM | no host copy of the experts (`--stream-experts`) |
|
||||
|
||||
## Measured, 2026-09-29, Arc Pro B70, Coder IQ1_M, 32K context, q8_0 KV: llama.cpp
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| load | ~100 s |
|
||||
| VRAM | 28.4 of 32 GB |
|
||||
| decode | 23-25 tok/s; GPU 92% busy at 165 W, CPU idle |
|
||||
| prefill | 149 tok/s on a 2,701-token prompt; 424 tok/s on a 104,798-token prompt at 131k context |
|
||||
| quality | correct code on every test; matches the NVIDIA path token for token in spirit, not measured |
|
||||
@@ -0,0 +1,145 @@
|
||||
# Intel Arc (experimental)
|
||||
|
||||
Strata 0.1.39 includes an **experimental Intel Arc engine**: Strata's own engine ported to SYCL (Intel oneAPI).
|
||||
maxfridbe wrote it in [#423](https://github.com/Niko1221/Strata/pull/423), with fixes from the people testing it.
|
||||
The code is in `sycl/`, and the port's own notes, measurements and maintenance procedure are in
|
||||
[docs/INTEL.md](INTEL.md). This page covers what you need, how to build it, and what has been tested.
|
||||
|
||||
**Experimental means:** the Strata maintainers have no Intel GPU. We compile the port and run its kernel tests, but
|
||||
we have not run it on an Arc. Every result on Arc hardware below comes from the community. The NVIDIA and AMD
|
||||
engines are unchanged: the Intel build is a separate CMake target, off by default.
|
||||
|
||||
There is **no ready-made Intel engine** in the release zips. You build it from source on Linux.
|
||||
|
||||
## What has been run, and by whom
|
||||
|
||||
| | Hardware | Result | Reported in |
|
||||
|---|---|---|---|
|
||||
| Port author | Arc Pro B70 32 GB, Ubuntu 24.04 | Coder IQ1_M: 70-78 tok/s decode, ~790 tok/s prompt; IQ2_XS: 51-64 tok/s; 256K context measured | #423, [INTEL.md](INTEL.md) |
|
||||
| Community | 2x Arc Pro B70, `--layer-split` | Flash-Next IQ3_XXS 66 tok/s decode, 394 tok/s prompt (with the `stage_room` fix that is now in 0.1.39) | #423 |
|
||||
| Community | Arc Pro B50 16 GB | Coder IQ1_M ~23 tok/s, IQ2_XS ~25-27 tok/s, up to 128K | #423 |
|
||||
| Community | Arc B580 12 GB, WSL2 | IQ2_XS ~21 tok/s, Coder ~15 tok/s; **device loss also seen** | #423 |
|
||||
| Community | 2x Arc Pro B60 24 GB, `--layer-split` | Coder IQ1_M: 56.6 tok/s decode, 428 tok/s prompt (2,129 tokens); Flash-Next IQ2_XS: 58.6-61.3 tok/s decode, 436 tok/s prompt; all experts in VRAM; 8K context | [bench/results/2026-10-04-community-2x-arc-pro-b60](../bench/results/2026-10-04-community-2x-arc-pro-b60/README.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 (needs the ring-wait fix from the same report) | same |
|
||||
| Strata maintainers | no Arc | compile check and kernel tests on a CPU device only (below) | this release |
|
||||
|
||||
The 0.1.39 port re-migrates 0.1.38's port onto the 0.1.39 engine sources (the #606 NaN fix, the #649 verify
|
||||
trace, the new prompt paths). 0.1.39's `sycl/` did not compile against 0.1.39's own engine sources (#784: the
|
||||
`ThreadAffinity` type of #626 and the layer range of `NativeDense::load` from #559), and its ring waits never saw a
|
||||
slow layer as still running (#866, #867). Both are fixed in 0.1.40, and the two B60 reports below were measured with
|
||||
exactly those two fixes on top of 0.1.39. **No one has run an unpatched 0.1.39 port on an Arc**, and the other rows were
|
||||
measured on earlier versions.
|
||||
|
||||
The two B60 rows ran `6f32ec0` plus two small `sycl/` fixes (the compile fix and the ring-wait fix), AOT `bmg-g21`, on Ubuntu 24.04 with
|
||||
`xe`, Level Zero V2, NEO 26.09.37435.12 and oneAPI 2026.1.1, without Docker. Host: Ryzen 5 5600, 64 GB RAM, PCIe 3.0 x8 per card.
|
||||
Full flags, per-request timings and engine logs are in the report linked in the table.
|
||||
|
||||
## What was tested here (0.1.39)
|
||||
|
||||
- **Compiles:** Ubuntu 24.04 (WSL2), Intel oneAPI DPC++ 2026.1.1 + oneMKL 2026.1. The whole `sycl/` project
|
||||
builds with 0 errors: the `strata` engine plus all 157 targets (the kernel parity tests and benches). The build is
|
||||
SPIR-V (JIT). The AOT build (`STRATA_SYCL_AOT`) was not built here, because it needs `ocloc`.
|
||||
- **Kernel parity tests on a CPU** (`ONEAPI_DEVICE_SELECTOR=opencl:cpu`, Intel's OpenCL CPU runtime, AMD Ryzen 5 7600):
|
||||
14 of 25 pass. That is the same set that PR #423's own 0.1.38 port passes on that device: the failures are tight
|
||||
float tolerances on the CPU's math (rel 3e-6 against a 1e-6 limit), model fixtures that are not present, and two
|
||||
tests that time out on a CPU. This is a check that the kernels compile and compute, not a test of an Arc.
|
||||
- **Setup:** `--backend sycl` warns, then hands over to `sycl/setup_intel.py` on Linux (unit tests:
|
||||
`tools/test_setup_sycl.py`).
|
||||
- **Unchanged:** the CUDA engine's greedy output, checked byte-identical against the gated 0.1.39 build (Q2_0 and Coder).
|
||||
The HIP build is not affected (the option is off by default).
|
||||
|
||||
**Not tested by anyone yet:** Windows (no native build path; see below), Arc on WSL2 for the 0.1.39 port, the Alchemist
|
||||
A-series, integrated Arc GPUs (the B390 / Panther Lake in #515; the shared-memory planning does not exist yet), and
|
||||
images (not wired on Intel).
|
||||
|
||||
## What you need
|
||||
|
||||
- **Linux**, e.g. Ubuntu 24.04, with Intel's GPU driver (the `xe` or `i915` kernel driver plus the compute runtime /
|
||||
Level Zero; on Ubuntu, `intel-opencl-icd libze1 libze-intel-gpu1`, or Intel's
|
||||
[client GPU guide](https://dgpu-docs.intel.com/driver/client/overview.html)).
|
||||
- **Intel oneAPI**: the DPC++ compiler (`icpx`, 2025.3 or newer; 2026.1 is what was built here) and **oneMKL**. About 5 GB.
|
||||
- `cmake` 3.24+, `ninja`, `git` (the build fetches ggml unless you point `STRATA_GGML_DIR` at a llama.cpp checkout),
|
||||
Python 3.
|
||||
- For `setup --backend sycl` today: **Docker**. `sycl/setup_intel.py` runs the engine in the `strata-sycl-dev`
|
||||
image built from `sycl/tools/Dockerfile`. Note that the Dockerfile starts from a community llama.cpp SYCL image
|
||||
(`ghcr.io/snailium/...`), not an Intel or Strata image.
|
||||
- VRAM: the port keeps the experts on the card (`--stream-experts`). A 32 GB card holds the Coder IQ1_M or IQ2_XS.
|
||||
Smaller cards mirror part of the experts in RAM and are slower.
|
||||
|
||||
## Build (Linux)
|
||||
|
||||
Install oneAPI from Intel's apt repository (this is what was used here):
|
||||
|
||||
```sh
|
||||
wget -qO- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB \
|
||||
| sudo gpg --dearmor -o /usr/share/keyrings/oneapi-archive-keyring.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" \
|
||||
| sudo tee /etc/apt/sources.list.d/oneAPI.list
|
||||
sudo apt update
|
||||
sudo apt install intel-oneapi-compiler-dpcpp-cpp intel-oneapi-mkl-devel ninja-build cmake git
|
||||
```
|
||||
|
||||
Then build the engine. Either command works: the first goes through the top-level CMake option, the second
|
||||
configures the `sycl/` project directly.
|
||||
|
||||
```sh
|
||||
source /opt/intel/oneapi/setvars.sh
|
||||
cmake -S . -B build-sycl -G Ninja -DCMAKE_C_COMPILER=icx -DCMAKE_CXX_COMPILER=icpx -DSTRATA_ENABLE_SYCL=ON
|
||||
# or: cmake -S sycl -B build-sycl -G Ninja -DCMAKE_C_COMPILER=icx -DCMAKE_CXX_COMPILER=icpx
|
||||
cmake --build build-sycl --target strata
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `-DSTRATA_SYCL_AOT=bmg-g31` (Arc Pro B70) or `bmg-g21` (B580 / B570 / Pro B60) compiles the GPU code ahead of
|
||||
time. This needs `ocloc` (Intel's `intel-ocloc` package). Without it, the first start JIT-compiles every kernel,
|
||||
which takes about 47 s.
|
||||
- `-DSTRATA_SYCL_PARITY=OFF` skips the kernel tests (on by default). Run them with
|
||||
`ctest --test-dir build-sycl` on the card.
|
||||
|
||||
`setup_intel.py` looks for `build-sycl-aot/strata` or `build-sycl/strata` in the checkout. With the
|
||||
top-level option the engine is at `build-sycl/sycl/strata`, so either use `-S sycl` or set `STRATA_SYCL_BIN`.
|
||||
|
||||
## Setup and running
|
||||
|
||||
```sh
|
||||
./setup.sh --backend sycl [setup's usual options, e.g. --model IQ2_XS --context 32768]
|
||||
```
|
||||
|
||||
This prints the experimental warning and continues with `sycl/setup_intel.py`. That script finds the Arc in sysfs,
|
||||
uses the SYCL engine you built, and writes the config and `run-<model>.sh`. It still downloads and packs the model
|
||||
the usual way. To run the engine by hand (no Docker), see "How to run it by hand" in [INTEL.md](INTEL.md).
|
||||
|
||||
Things that matter on an Arc (details in INTEL.md):
|
||||
|
||||
- **Do not ask for more VRAM than the card has.** On the `xe` driver, an allocation past VRAM can push buffers into
|
||||
RAM until the machine runs out of memory and stalls. Leave about 1.5 GB free.
|
||||
- `SYCL_CACHE_PERSISTENT=0`: the persistent JIT cache crashed on Xe2 during the first compile.
|
||||
- Two cards: `ONEAPI_DEVICE_SELECTOR=level_zero:*` (the image pins `level_zero:0`; `strata-sycl.sh` now passes the
|
||||
variable through) and `--layer-split`.
|
||||
- **Arc Pro B60:** the PCI id is `8086:e211` (`lspci -nn`, the kernel's `xe` id list files it with the BMG-G21 cards), `sycl-ls` prints
|
||||
`Intel(R) Arc(TM) Pro B60 Graphics 20.1.0`, and `ocloc ids bmg-g21` prints 20.1.0, so `-DSTRATA_SYCL_AOT=bmg-g21` is the right target.
|
||||
`setup_intel.py` knows both B60 ids, `e211` and `e221` (both seen on B60 cards). On a `--layer-split` the startup line "N experts are neither in VRAM nor mirrored"
|
||||
counts the other card's layers; the lines `100% of the experts resident` that follow are the ones to read. Set `STRATA_MIRROR_MIB=0` there,
|
||||
or the pinned mirror is allocated and not used.
|
||||
- **`setvars.sh` and `set -u`:** `source /opt/intel/oneapi/setvars.sh` in a shell with `set -u` stops at `OCL_ICD_FILENAMES: unbound variable`
|
||||
(oneAPI 2026.1.1). Source it before `set -u`, or run `set +u` around it.
|
||||
- **Without Docker:** `sycl/serve/strata-sycl.sh` needs Docker. Natively, `source setvars.sh`, export `SYCL_CACHE_PERSISTENT=0`,
|
||||
`ZES_ENABLE_SYSMAN=1` and `STRATA_VERIFY_DEVICE_PLAN=1`, then run `build-sycl-aot/strata` or `sycl/serve/server_intel.py` with an `exe` that does this.
|
||||
A normal user is not in the `render` group on Ubuntu, and then `sycl-ls` shows only the CPU device.
|
||||
- `STRATA_VERIFY_NO_HOST=1` (set by `strata-sycl.sh`) is only valid when every expert is in VRAM. On smaller cards
|
||||
that path is the one that has hung, and #667 found the likely reason: the GPU does not see the CPU's flag
|
||||
stores without a system fence.
|
||||
|
||||
## Windows
|
||||
|
||||
There is no Windows path yet. `setup --backend sycl` on Windows stops and points here. oneAPI exists for Windows,
|
||||
but `sycl/CMakeLists.txt` uses GCC-style flags (`-mavx512f`, `-fp-model=precise`, `-qmkl`) and the runner is a
|
||||
bash/Docker script, so a native Windows build would need work. Nobody has tried it. WSL2 with an Arc has been
|
||||
used by one tester (the B580 row above), but setup cannot detect the card there, because it reads `/sys/class/drm`,
|
||||
which WSL2 does not have.
|
||||
|
||||
## Reporting a problem
|
||||
|
||||
Open an issue with: the card, the driver version, `sycl-ls` output, the oneAPI version, the model and flags, and the
|
||||
engine's stderr. Results from real cards are what move this from experimental to supported.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Overlap streamed KV uploads with prefill computation
|
||||
|
||||
## Why this helps
|
||||
|
||||
When long-context KV lives partly in system RAM, prefill uploads each attention
|
||||
layer's cached prefix. Waiting for every upload on the compute stream leaves
|
||||
the GPU idle.
|
||||
|
||||
This opt-in change starts the next attention layer's upload on a separate stream
|
||||
while the current layer continues computing. It reuses the existing staging
|
||||
pool; it does not allocate another KV buffer.
|
||||
|
||||
## Use and ordering
|
||||
|
||||
Set `STRATA_KV_PREFETCH=1` before starting the engine. It is off by default.
|
||||
A release event protects the staging pool's last reader; a ready event orders
|
||||
the next attention consumer. Early returns drain pending copies before the caller
|
||||
can refill borrowed expert slots. Relayout and destruction also drain the stream.
|
||||
|
||||
## Validation environment
|
||||
|
||||
- Two modified RTX 3080 20 GB cards; CUDA SM86, CUDA 13.1, Linux.
|
||||
- Intel Xeon E5-2686 v4 (18 cores / 36 threads), about 94 GiB usable RAM.
|
||||
- PCIe 3.0, no GPU peer-to-peer access; 17 expert-pool workers.
|
||||
- Qwen3.8-Flash-Next IQ3_S, 131,072-token context, INT8 KV,
|
||||
32,768 resident KV cells.
|
||||
- Medium thinking: 2,048-token reasoning cap, 8,192-token output cap.
|
||||
- Physical GPU power limits stayed at 250 W and 280 W. No power increase.
|
||||
- Historical performance tests used an isolated engine based on official
|
||||
v0.1.38 plus reviewed architectds/Strata changes (best, 05c0f36).
|
||||
This PR contains only our change, ported to official main (99f3dbd).
|
||||
The historical timings are NOT a measured speedup of this pure-upstream port.
|
||||
|
||||
## Historical benefit
|
||||
|
||||
ABBA comparison, two observations per arm, all 20 complete JSON checks passed:
|
||||
|
||||
| Cold input | Serial prefill | Overlapped prefill | Serial request | Overlapped request |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 8K | 5.013 s | 5.025 s | 6.727 s | 6.726 s |
|
||||
| 32K | 16.989 s | 16.082 s | 18.468 s | 17.559 s |
|
||||
| 64K | 32.604 s | 31.259 s | 34.354 s | 32.775 s |
|
||||
|
||||
Prefill time fell by 5.3% at 32K and 4.1% at 64K.
|
||||
There was no useful 8K improvement and no decode speedup.
|
||||
Profiling showed roughly 939 ms / 1,553 ms of serial KV-stage wait moving off
|
||||
the critical path. The copies still execute; they are not eliminated.
|
||||
|
||||
## Validation and remaining work
|
||||
|
||||
- Linux compile/link against pure official main (99f3dbd) passed. No engine
|
||||
was started and no production settings were changed.
|
||||
- No claim of a pure-upstream GPU speedup or bitwise numerical parity yet.
|
||||
- The standalone historical prototype did not run the full vision/120K/cancel
|
||||
safety suite. Later combined builds did, but they do not validate this port.
|
||||
- Keep draft until pure-upstream long-context, cancellation, and image-history
|
||||
tests pass. HIP and other GPUs are unvalidated.
|
||||
+1
-1
@@ -111,7 +111,7 @@ The command is `python <Strata folder>/tools/strata_mcp.py`. The transport is st
|
||||
| --- | --- | --- |
|
||||
| `strata_status` | Whether a model is running: the model, engine version, context, busy or idle, VRAM and RAM in use. What is installed: models, start scripts, engine version, data folder, downloads. An install in progress. This PC's hardware: OS, CPU, RAM, GPUs, page file, free disk. The model and context setup recommends for this PC. | `port`, `hardware` (true/false) |
|
||||
| `strata_models` | Every family and size setup offers, with download size, RAM needed, whether it fits this PC, and whether it is installed. Also the context lengths and the recommendation. | - |
|
||||
| `strata_install` | Runs setup without questions, in the background (see below). Without `confirm: true` it only returns the plan. A second call shows the progress. `cancel: true` stops the install. | `family` (qwen, swift, coder, unsloth), `model` (Q2_0, IQ2_XS, IQ3_XXS, IQ3_S, IQ1_M, UD-Q4_K_XL), `context` (8192 ... 524288), `vision` (no, yes, gpu, cpu), `kv` (int8, q4_0, k8v4), `gpu` (a number) or `gpus` ("0,2", "all"), `backend` (auto, cuda, hip), `low_ram`, `port`, `data_dir`, `confirm`, `cancel` |
|
||||
| `strata_install` | Runs setup without questions, in the background (see below). Without `confirm: true` it only returns the plan. A second call shows the progress. `cancel: true` stops the install. | `family` (qwen, swift, coder, unsloth), `model` (Q2_0, IQ2_XS, IQ3_XXS, IQ3_S, IQ1_M, UD-IQ4_XS, UD-Q4_K_XL), `context` (8192 ... 524288), `vision` (no, yes, gpu, cpu), `kv` (int8, q4_0, k8v4), `gpu` (a number) or `gpus` ("0,2", "all"), `backend` (auto, cuda, hip), `low_ram`, `port`, `data_dir`, `confirm`, `cancel` |
|
||||
| `strata_start` | Starts an installed model in the background and waits until it answers. If it is not ready after `wait_seconds` (default 50), it returns "loading". | `model` (an id from `strata_status`, e.g. `q2_0`, `coder-iq1_m`; default: the most recently used), `port`, `gpu`, `wait_seconds` (0-600) |
|
||||
| `strata_stop` | Unloads the model through the server's own API, then ends the server it started. | `port`, `force` |
|
||||
| `strata_logs` | The last lines of the server log, the engine log or the setup log. | `source` (server, engine, setup), `model`, `lines` (1-500) |
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Reuse long chat history when the last user message is edited
|
||||
|
||||
## Why this helps
|
||||
|
||||
An agent can keep the same long history but replace the final user message.
|
||||
The final cached state no longer matches, and the nearest periodic checkpoint
|
||||
may be far behind. The engine then rereads history it already processed.
|
||||
|
||||
This opt-in change saves one checkpoint at the previous turn boundary, before
|
||||
the final short message. It uses the existing bounded cache and leaves normal
|
||||
token and image-prefix validation in charge of reuse.
|
||||
|
||||
## Use and limits
|
||||
|
||||
Set `STRATA_CACHE_MESSAGE_BOUNDARY=1` before starting the engine.
|
||||
It is off by default, requires prompt caching and a valid turn token, and
|
||||
only selects prefixes of at least 8,192 tokens with a tail of at most 1,024.
|
||||
Layer-split execution is excluded (`multi_gpu`); helper-expert GPUs still work.
|
||||
It can split prefill chunks and add snapshot cost to a fresh request.
|
||||
|
||||
## Validation environment
|
||||
|
||||
- Two modified RTX 3080 20 GB cards; CUDA SM86, CUDA 13.1, Linux.
|
||||
- Intel Xeon E5-2686 v4 (18 cores / 36 threads), about 94 GiB usable RAM.
|
||||
- PCIe 3.0, no GPU peer-to-peer access; 17 expert-pool workers.
|
||||
- Qwen3.8-Flash-Next IQ3_S, 131,072-token context, INT8 KV,
|
||||
32,768 resident KV cells.
|
||||
- Medium thinking: 2,048-token reasoning cap, 8,192-token output cap.
|
||||
- Physical GPU power limits stayed at 250 W and 280 W. No power increase.
|
||||
- Historical performance tests used an isolated engine based on official
|
||||
v0.1.38 plus reviewed architectds/Strata changes (best, 05c0f36).
|
||||
This PR contains only our change, ported to official main (99f3dbd).
|
||||
The historical timings are NOT a measured speedup of this pure-upstream port.
|
||||
|
||||
## Historical benefit
|
||||
|
||||
Same-binary ABCCBA comparison, two repetitions per variant:
|
||||
|
||||
| Metric | On-demand boundary + 16K chunks | Anticipatory boundary + 32K chunks |
|
||||
| --- | ---: | ---: |
|
||||
| First complete 100-record edit | 17.236 s | 11.527 s |
|
||||
| Three complete edits | 41.266 s | 35.802 s |
|
||||
| Cold setup plus three edits | 58.688 s | 53.146 s |
|
||||
| Eleven-case wall-time sum | 133.844 s | 127.983 s |
|
||||
|
||||
First-edit time fell by 33.1%; reused prefix grew from 22,016 to 32,831 tokens.
|
||||
This is a COMBINED boundary-policy and chunk-size comparison, not the isolated
|
||||
benefit of adding this checkpoint to pristine upstream.
|
||||
Hot generation was unchanged (97.55 vs 97.60 tokens/s).
|
||||
The mixed-suite reduction was 4.4%; ordinary cold 8K requests cost about 0.2 s more.
|
||||
|
||||
The historical comparison passed 77 task/cache checks and cancellation/recovery.
|
||||
A separate safety arm for the adopted 32K choice passed 22 task/cache checks,
|
||||
including 120K, image-history isolation, and cancellation/recovery.
|
||||
|
||||
## Validation and related work
|
||||
|
||||
- Standalone selector test passes all 21 cases, without assertions that disappear
|
||||
in Release builds.
|
||||
- Linux compile/link against pure official main (99f3dbd) passed. No engine
|
||||
was started and no production settings were changed.
|
||||
- PR #614 checkpoints an already-reached chunk near the tail without splitting it.
|
||||
This proposal chooses a message boundary, which can split chunks. They solve
|
||||
related cases with different cost and numerical tradeoffs.
|
||||
- Chunk geometry can change floating-point results. No pure-upstream bitwise
|
||||
parity or isolated upstream speedup is claimed. Keep draft until validated.
|
||||
+21
-4
@@ -14,7 +14,7 @@ The installer recommends one for your PC; this page explains the choice. Back to
|
||||
| **32 GB** | **Coder** | it fits 32 GB; best at code, weaker at everything else ([why](#coder)). With a 24 GB card, Q2_0 and IQ2_XS run too ([low-RAM mode](#a-big-graphics-card-and-little-ram)) and are the better pick for general use |
|
||||
| **48 GB** | **IQ2_XS** (or Q2_0, the fastest) | the larger sizes do not fit |
|
||||
| **64 GB** | **IQ2_XS** (recommended), or IQ3_XXS / IQ3_S | every size fits (IQ3_S with little else open) |
|
||||
| **96 GB or more** | **IQ3_S**, or [Unsloth's 4-bit](#unsloth-ud-q4_k_xl-experimental) (experimental) | room for the largest sizes |
|
||||
| **96 GB or more** | **IQ3_S**, or [Unsloth's UD-IQ4_XS](#unsloth-ud-iq4_xs) (~4-bit) | room for the largest sizes |
|
||||
|
||||
Not sure? Take **IQ2_XS**. The **Coder** is the one that fits a 32 GB PC, but it keeps only 256 of the 512 experts,
|
||||
chosen on code data, so it is weaker outside code and in languages other than English. For general use, or whenever
|
||||
@@ -71,8 +71,9 @@ fetches the MTP draft layer (~6 GB, +1 GB with images).
|
||||
|
||||
Shard 1 is the part of the model that gets loaded when it starts: its experts go into your **RAM**, the rest onto
|
||||
your graphics card (the second shard, a 29 GB lookup table, stays on the SSD). So it fits when your **RAM is at least
|
||||
shard 1 + about 10 GB** for Windows and your other programs. With 64 GB of RAM every size fits (IQ3_S with little
|
||||
else open); with 48 GB, Q2_0 and IQ2_XS. A bigger graphics card makes it faster, but it doesn't lower the RAM needed
|
||||
the experts + about 10 GB** for Windows and your other programs - the experts are most of shard 1: 34 GB for Q2_0,
|
||||
35.5 for IQ2_XS, 43 for IQ3_XXS, 50 for IQ3_S, 23 for the Coder (the dense weights in shard 1 go to the graphics
|
||||
card). With 64 GB of RAM every size fits (IQ3_S with little else open); with 48 GB, Q2_0 and IQ2_XS. A bigger graphics card makes it faster, but it doesn't lower the RAM needed
|
||||
- except in the low-RAM mode below.
|
||||
|
||||
### A big graphics card and little RAM
|
||||
@@ -116,9 +117,22 @@ More: [details](DETAILS.md#or-swift-15-a-fine-tune-that-thinks-shorter).
|
||||
START-HERE.bat --setup --family swift --model IQ2_XS
|
||||
```
|
||||
|
||||
### Unsloth UD-IQ4_XS
|
||||
|
||||
**Unsloth's UD-IQ4_XS** (~4-bit) is the fourth version in setup's menu (`--family unsloth`, its first size), a
|
||||
regular choice from 0.1.39: a 94 GB download with 59.5 GB of experts (IQ3_S and IQ4_NL), between IQ3_S and UD-Q4_K_XL
|
||||
in quality. Strata keeps your RAM minus 24 GB of the experts in RAM and reads the rest from the SSD while it answers:
|
||||
on a 64 GB PC part of them come from the SSD (slower; an NVMe SSD helps), from ~80 GB of RAM all of them stay in RAM.
|
||||
It needs 48 GB of RAM or more and engine 0.1.38 or newer; images are an option, as with the other models.
|
||||
Details: [UD-IQ4_XS](UNSLOTH_Q4.md#ud-iq4_xs-setup-from-0139-621).
|
||||
|
||||
```
|
||||
START-HERE.bat --setup --family unsloth --model UD-IQ4_XS
|
||||
```
|
||||
|
||||
### Unsloth UD-Q4_K_XL (experimental)
|
||||
|
||||
**Unsloth's 4-bit UD-Q4_K_XL** (experimental) is the fourth version in setup's menu (`--family unsloth`): the closest
|
||||
**Unsloth's 4-bit UD-Q4_K_XL** (experimental) is the second size of the same family (`--family unsloth`): the closest
|
||||
to the full model, but a 111 GB download whose 77 GB of experts do not fit in RAM. Strata keeps your RAM minus 24 GB
|
||||
of them in RAM and reads the rest from the SSD while it answers: 7-8.5 tokens/s on a 64 GB PC with a 12 GB GPU, several
|
||||
times slower than the sizes above, and long prompts are slow. It needs 48 GB of RAM or more, an NVMe SSD and one
|
||||
@@ -128,6 +142,9 @@ NVIDIA GPU (no images yet). Details and measurements: [UD-Q4_K_XL](UNSLOTH_Q4.md
|
||||
START-HERE.bat --setup --family unsloth --model UD-Q4_K_XL
|
||||
```
|
||||
|
||||
**Unsloth's 6-bit UD-Q6_K_XL** (experimental, not in setup's menu) trades more bytes per expert for less precision
|
||||
loss against the same files: 169.2 GB together, its PLE table Q8_0 (read by the engine since 0.1.38). High RAM
|
||||
machines run it without a RAM budget. Manual workflow: [UD-Q6_K_XL](UNSLOTH_Q6.md).
|
||||
### OrcaRouter Uncensored IQ3_XXS
|
||||
|
||||
For **OrcaRouter's Flash-Next Uncensored IQ3_XXS**, see the [manual compatibility setup](ORCA.md). It needs an
|
||||
|
||||
+99
-1
@@ -56,6 +56,11 @@ Or edit an existing config (`strata-*.json`), then restart:
|
||||
"layer_split": "auto"
|
||||
```
|
||||
|
||||
`"layer_split"` is `"auto"` (placed by each card's free VRAM) or the **first layer of each later card**: one rising
|
||||
number per card after the first, not a count of layers per card. With 4 cards and a 48-layer model, `"24,36,42"`
|
||||
(or `[24, 36, 42]`) puts layers 0-23 on the first card, 24-35 on the second, 36-41 on the third and 42-47 on the last.
|
||||
The server checks it before the start and says what is wrong (0.1.39, #644).
|
||||
|
||||
**Skip the split when the first card holds everything** (opt-in, 0.1.31): `"split_skip_if_fits": true` in the config
|
||||
(engine flag `--split-skip-if-fits`, with `--layer-split auto`) runs on the first card alone when it holds every
|
||||
profiled expert plus the context's KV, the draft layer and the reserve, and says so in the log; otherwise the split
|
||||
@@ -71,11 +76,56 @@ VRAM keep its own prompt buffers - the same output as 0.1.31, measured on an R97
|
||||
differs from the default's (stable and coherent); `STRATA_SPLIT_OWN=auto` does that only where the buffers are at
|
||||
most 12% of each card's VRAM.
|
||||
|
||||
**The idle card can help one-chunk prompts (opt-in, `STRATA_PREFILL_HELP=1`).** A prompt that fits one chunk runs the stages one after the other, so while
|
||||
one card reads its layers the other idles. With it on, each stage hands a share of its streamed experts to the idle card: it
|
||||
streams them over its own PCIe link into its own (lent) prompt buffers, computes their rows on the MMQ path and sends
|
||||
them back - `--peer-device`'s peer streaming, without P2P (the activations and the rows go through mapped host memory,
|
||||
read by copy kernels, so they do not queue behind the expert blobs on either card's copy engine). The share falls with
|
||||
the prompt (0.41 of the streamed experts at 1.5K tokens, 0.32 at 3K) and is off from ~3.3K tokens, where the stages
|
||||
overlap anyway and no share paid. Measured on 2x RTX 3090 (UD-Q4_K_XL, no P2P), prompt tok/s without / with: 1.1K 496 /
|
||||
723, 1.5K 674 / 833, 2K 878 / 1,128, 2.5K 1,017 / 1,296, 3K 1,260 / 1,440; 4K and 8K unchanged, decode unchanged. It
|
||||
costs no VRAM (the idle card's own prompt buffers) and ~110 KB of mapped host memory per token of the largest chunk it
|
||||
helped (~360 MB at 3.3K tokens). The rows it computes round like a different MMQ grouping, so the output is not
|
||||
bit-identical to the default's (it is repeatable: same prompt, same output), which is why it is **opt-in**:
|
||||
`STRATA_PREFILL_HELP=1` turns it on. Native packs on the MMQ prompt path only (not with the fused prompt kernels,
|
||||
`STRATA_PF_FUSED=1`, nor with `--peer-device`). With it on, `STRATA_PREFILL_HELP_FRAC=f` fixes the share.
|
||||
|
||||
The engine flags behind it: `--layer-split K1[,K2..]|auto` and `--split-device D1[,D2..]` (the later stages'
|
||||
devices; default the next visible ones). `--layer-split K --split-device 0` runs both stages on one card sharing
|
||||
everything - the bit-exact check of the hand-off, not a speed mode.
|
||||
|
||||
**auto** tries every placement (all of them for two or three cards; proportional to the free VRAM beyond that) and
|
||||
**Each card loads only its own layers' dense weights** (0.1.39, PR #639) with explicit split points (`--layer-split
|
||||
27`, not `auto`): every card used to keep a full copy (~3.4 GB for the Coder) though its stage reads only its own
|
||||
layers, and the VRAM it frees goes to that card's expert cache (2x MI50 16 GB, Coder: 8,819 -> 10,626 experts in
|
||||
VRAM, decode 39.2 -> 41.7 tok/s). Opt-in for now, on AMD and NVIDIA alike: `STRATA_STAGE_TRIM=1` (please report how
|
||||
it goes).
|
||||
A card holding more experts can change which experts run on the GPU, so the output can differ slightly from a run
|
||||
without it.
|
||||
|
||||
**The resident RAM mode works on a split** (`--resident-experts`, the low-RAM mode). The RAM copy of the experts
|
||||
leaves out the ones every card's cache holds, not only the first card's, and when the rest does not fit whole it keeps
|
||||
the hottest by the expert profile over all the layers. An adaptive swap copies the evicted expert back into RAM from
|
||||
the card that owns its layer. Before, `--resident-experts` with a split ran as `--mmap-experts`, and setup recommended
|
||||
one card in the low-RAM mode; with an engine that has it (`RESIDENT_SPLIT_ENGINE` in setup.py), setup keeps the cards
|
||||
together and the experts no card holds in RAM. Swift 1.5 IQ3_XXS at
|
||||
160K (q4_0 KV, `--prefill 4096`, `--spec 4` with the stock draft layer), RTX 4060 Ti (layers 0-19) + RTX 5080 (20-47),
|
||||
i9-14900KF, 32 GB of RAM, Windows 11, four greedy prompts at a time, decode tok/s:
|
||||
|
||||
| | first four prompts | after three more rounds |
|
||||
|---|---|---|
|
||||
| 5080 alone, `--resident-experts` | 25 | 29 |
|
||||
| split, `--mmap-experts` (what `--resident-experts` became on a split) | 32 | 64 |
|
||||
| split, `--resident-experts` (22 GiB of experts locked in RAM) | 71 | 69 |
|
||||
|
||||
The split with `--mmap-experts` catches up once the OS file cache holds the experts, on a PC with nothing else
|
||||
running; the resident copy is there from the first request and stays locked when other programs need the RAM. With
|
||||
`--pcie-frac 0 --adapt-every 0` the split's greedy output is the same with either mode.
|
||||
|
||||
**A separate VRAM reserve for the later cards:** `--vram-reserve-later-mib N` (default: `--vram-reserve-mib`'s value).
|
||||
The card that drives the monitors needs more headroom than one that drives none; with the display on the last card,
|
||||
`--vram-reserve-mib 300 --vram-reserve-later-mib 1800` gives the first card's cache that VRAM.
|
||||
|
||||
**auto** tries every placement (all of them for two, three or, since 0.1.40, four cards; proportional to the free VRAM beyond that) and
|
||||
keeps the one whose caches would hold the most of the expert profile, hottest pairs weighted most; ties go to the
|
||||
placement that leaves the fullest card the most room. The startup log prints the choice:
|
||||
|
||||
@@ -140,3 +190,51 @@ The Coder on an RTX 5080 + RTX 3090 (Ryzen 9 9950X3D), 32K context; details in
|
||||
- Leave out a much slower card when two already hold the model. An RTX 2080 Ti as a third card made the 5080 +
|
||||
3090 pair slower (68 / 90 tok/s decode): every extra card costs its own round per window.
|
||||
- More cards pay off when the model's routed experts do not fit the faster ones.
|
||||
|
||||
## One conversation with both cards busy (`--pipeline-windows`, opt-in)
|
||||
|
||||
With a split the cards take turns on a verify window: the first card runs its layers and hands off, then waits while
|
||||
the last card runs the rest, the head and the draft. `--pipeline-windows 2` lets the first card start the **next**
|
||||
window while the last card still verifies this one. The next window is a guess: that this window is accepted whole
|
||||
and that its bonus token is the one the draft layer predicts (the draft layer is run on through the drafts of the
|
||||
window in flight). When the guess holds, half of the next window is already done; when it does not, the first card
|
||||
puts its state back (a copy of its recurrent state taken while the window ran) and the next window is built from the
|
||||
real tokens. A guessed window is only started when the draft layer's estimate says it is likely to be kept. Windows
|
||||
copied from earlier context (`--suffix-draft`) are guessed past as well, which is where edits that copy text gain
|
||||
most. `--pipeline-windows 1` overlaps only the short prompt reads that go through the verify windows
|
||||
(`--short-read`).
|
||||
|
||||
Two cards, exactly two stages, `--serve`. In the config:
|
||||
|
||||
```
|
||||
"args": [ ..., "--pipeline-windows", "2" ],
|
||||
"layer_split": "20"
|
||||
```
|
||||
|
||||
- **Cost**: a second verify window on each card, 160 MiB more kept out of each card's expert cache, plus two copies
|
||||
of the first card's recurrent state (about 3 MiB per GDN layer it runs) on the first card with `2`.
|
||||
- **Same text**: the last card only ever runs windows that are verified, and every window row computes what it
|
||||
would in any other window, so the tokens are the serial loop's. With `STRATA_IQ_MT_MIN=1 --pcie-frac 0
|
||||
--adapt-every 0` the greedy output is identical bit for bit to the serial loop's with the same expert caches. The pipeline keeps
|
||||
its VRAM out of the caches, so against a run without the flag a few experts move from a card to the CPU, which
|
||||
rounds them differently, and a near-tie can flip (a serial run given the same caches through `--vram-reserve-mib`
|
||||
matches it exactly).
|
||||
- **Off, with one line in the log saying why**, with `--batch` slots, `--peer-device`, the helper caches
|
||||
(`--expert-cache-device1..3`, `--remote-expert-opt`), a split into three or more stages or onto one GPU
|
||||
(`--split-device 0`), or no draft layer. A request with repetition penalties (`penalty_last_n`) or coupled
|
||||
draft sampling decodes serially.
|
||||
- **Measured** (Swift 1.5 IQ3_XXS, 160K context, q4_0 KV, the stock draft layer, RTX 4060 Ti (layers 0-19) +
|
||||
RTX 5080 (20-47), i9-14900KF, 32 GB of RAM with the resident RAM mode on the split (#848); greedy, 500 tokens, two
|
||||
interleaved pairs of three rounds, decode tok/s): Python code 90.4 -> 103.1, C code 76.6 -> 81.6, English prose
|
||||
63.0 -> 71.1, Italian prose 41.6 -> 46.3, a copy-heavy edit (a 5 KB file back with a rename) 90.5 -> 117.9; mean
|
||||
72.4 -> 84.0 (+16%). It pays when the windows are GPU-bound: with the experts read through the OS file cache
|
||||
(`--mmap-experts` on that 32 GB PC) the file reads dominate and it measured no faster.
|
||||
|
||||
The `STRATA_PIPELINE_*` tuning and test variables (THETA, FORCE_MISS, SWITCH, LOG, TRACE and the like) are read only with
|
||||
`STRATA_PIPELINE_DEBUG=1`. `--pipeline-windows` and `--adapt-async 1` exclude each other (the engine says so and keeps the
|
||||
pipeline).
|
||||
|
||||
## Several conversations at once
|
||||
|
||||
With a layer split, `--batch N --batch-groups G --trim-stage-weights` decodes several conversations together and
|
||||
pipelines them through the cards: see [BATCHING.md](BATCHING.md).
|
||||
@@ -0,0 +1,71 @@
|
||||
# NVIDIA Tesla V100 / Titan V (Volta, sm_70): the community build
|
||||
|
||||
Strata's ready-made engine and the support matrix start at compute capability 7.5 (RTX 20). A V100 (7.0) runs the same
|
||||
engine compiled for it, through the experimental build that issue #236 added. It is **community-tested, not part of the
|
||||
supported cards**: no ready-made engine is published for it and the numbers below come from one machine.
|
||||
|
||||
## Build
|
||||
|
||||
CUDA 13 dropped Volta, so the build needs a **CUDA 12.x** toolkit (12.8 was used) and a host GCC that toolkit accepts.
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -DSTRATA_ENABLE_CUDA=ON -DSTRATA_BUILD_TESTS=OFF \
|
||||
-DCMAKE_CUDA_ARCHITECTURES=70 -DSTRATA_EXPERIMENTAL_SM60=ON
|
||||
cmake --build build --target strata -j
|
||||
```
|
||||
|
||||
Setup does the same by itself for a model on a V100 (the experimental CUDA 12 engine, in `engine-cuda12/`: ready-made
|
||||
on Windows, compiled with a CUDA 12.x toolkit on Linux; [OLDER_GPUS.md](OLDER_GPUS.md)). For a PC with a V100 and a newer card, a build for both architectures (`-DCMAKE_CUDA_ARCHITECTURES="70;86"`) is the
|
||||
natural choice (a community report in issue #509 ran an RTX 3080 beside a V100 that way; it was not repeated here). On the
|
||||
measuring PC a Quadro RTX 4000 (sm_75) sits beside the V100s and was hidden with `CUDA_VISIBLE_DEVICES` because the sm_70-only
|
||||
build has no code for it.
|
||||
|
||||
## What runs on Volta
|
||||
|
||||
The flag only lowers the floor (CMake and the runtime device check) and replaces two intrinsics Volta lacks. It does not
|
||||
select "old" kernels everywhere: which kernel runs is decided per architecture when the engine is built.
|
||||
|
||||
| Part of the prompt path | On sm_70 |
|
||||
| --- | --- |
|
||||
| MoE experts (ggml MMQ) | `dp4a` kernels (Volta has no int8 tensor cores) |
|
||||
| Dense projections (dequantized weights) | FP16 tensor-core GEMMs (cuBLAS / CUTLASS `s884`) |
|
||||
| BF16 projections (hyper-connection, router, indexer, ...) | converted to FP16 and run on the FP16 tensor cores (#655, #540; `STRATA_BF16_TC=0`: cuBLAS BF16, an FP32 SIMT kernel on Volta) |
|
||||
| QSA attention for decode and verify windows (and prompts with `STRATA_PROMPT_ATTN_OLD=1`) | #540's kernel (fewer shuffles, bit-exact; `STRATA_ATTN_PRE75=0`: the one other cards run) |
|
||||
| QSA prompt attention, int8 / FP16 / K8V4 KV | `prompt_attn_v70_kernel` on `mma.m8n8k4` (`STRATA_PROMPT_ATTN_OLD=1`: the decode kernel, one query at a time) |
|
||||
| QSA prompt attention, Q4_0 KV | the decode kernel |
|
||||
| QSA block scores | the warp kernel (the tensor-core scorer needs sm_80) |
|
||||
|
||||
## Measured
|
||||
|
||||
One V100-PCIE-32GB, i9-7960X, Unsloth `UD-IQ4_XS` (not a setup model), int8 KV, MTP `--spec 2`; prompt read speed of random-word prompts, tokens/s,
|
||||
with and without the Volta attention kernel (`STRATA_PROMPT_ATTN_OLD=1`):
|
||||
|
||||
| Prompt tokens | old attention path | `prompt_attn_v70_kernel` |
|
||||
| --- | ---: | ---: |
|
||||
| 7,194 | 1,010 | 1,164 |
|
||||
| 28,650 | 1,063 | 1,251 |
|
||||
| 114,338 | 973 | 1,123 |
|
||||
|
||||
Decode was 38-51 tok/s in every run and is not affected. The profile, the parity numbers, the needle test and the limits are in
|
||||
[bench/results/2026-10-03-v100-prompt-attn](../bench/results/2026-10-03-v100-prompt-attn/README.md).
|
||||
|
||||
What the kernel does and does not guarantee: against an FP64 reference its error is about 5e-6 (output scale 3.6; the FP32 kernel it replaces: 2e-6); 15 of 15
|
||||
needles were found; greedy output was identical on the 28,650- and 114,338-token prompts and differed late in the answer on the 7,194-token one (another
|
||||
summation order).
|
||||
|
||||
## Checking and benchmarking the Volta kernels
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -DSTRATA_PARITY_PROMPT_ATTN=ON ... && cmake --build build --target qsa_prompt_attn_parity
|
||||
CUDA_VISIBLE_DEVICES=0 ./build/qsa_prompt_attn_parity 32768 2048 5 # synthetic, no model; exit 0 = PASS
|
||||
```
|
||||
|
||||
It compares the tensor-core attention with the FP32 kernel and an FP64 host reference and times both. The Q4_0 cases are skipped below
|
||||
sm_80.
|
||||
|
||||
## Limits
|
||||
|
||||
- A layer split over two V100 was not measured with this kernel. The cards work in turn on a single request, so a split adds expert-cache room, not prompt speed.
|
||||
- sm_60 (Pascal) is covered by the same flag but was not measured here.
|
||||
- Volta's tensor cores take FP16 only: a model's BF16 weights are converted, and a weight outside FP16's range would saturate (none did in
|
||||
the check above).
|
||||
@@ -0,0 +1,111 @@
|
||||
# Older GPUs (experimental)
|
||||
|
||||
Strata's supported cards are NVIDIA RTX 20 / 30 / 40 / 50 and the AMD cards in [AMD_HIP.md](AMD_HIP.md). The cards
|
||||
below run through **opt-in, experimental** paths that community members wrote and measured on their own machines. The
|
||||
maintainers have none of these cards: each path is compile-checked and unit-tested here, and the ready-made engines
|
||||
and their output stay exactly as they were. Numbers are the reporters' own, on one machine each.
|
||||
|
||||
## Support matrix
|
||||
|
||||
### NVIDIA
|
||||
|
||||
| Cards | Compute capability | How it runs | What is different on it | Reported |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Tesla P100 | 6.0 | the CUDA 12 engine | `__dp4a` emulated (bit-exact); BF16 projections through fp32 | not measured |
|
||||
| Tesla P40 / P4, GTX 10 series | 6.1 | the CUDA 12 engine | BF16 projections through fp32 (cuBLAS has no BF16 GEMM there, #395) | P40, IQ3_S, engine 0.1.30: prompt 217-374 tok/s, decode 30-33 tok/s (#395) |
|
||||
| Tesla V100, Titan V | 7.0 | the CUDA 12 engine | BF16 projections on the FP16 tensor cores (#655, #540); the prompt attention on `mma.m8n8k4` (#600); a leaner attention kernel (#540) | V100-PCIE-32GB, UD-IQ4_XS: prompt 1,123-1,251 tok/s (#600); V100 32GB, IQ2_XS: prompt +22% from #540 |
|
||||
| RTX 20 (Turing) | 7.5 | **supported**, the ready-made engine | opt-in: `STRATA_BF16_TC=1` runs the BF16 projections on the FP16 tensor cores | RTX 2080 Ti, Q2_0: prompt +15-18% (#655) |
|
||||
|
||||
A card needs enough VRAM to be useful: 12 GB or more is recommended, as for every card (a 2 GB GT 1030 is compute
|
||||
capability 6.1 too, and cannot hold any of the model).
|
||||
|
||||
### AMD
|
||||
|
||||
| Cards | Architecture | How it runs | Reported |
|
||||
| --- | --- | --- | --- |
|
||||
| RX 6800 / 6900 series | gfx1030 | setup (`--backend hip`), unvalidated | [AMD_HIP.md](AMD_HIP.md#rdna2-gfx1030) |
|
||||
| RX 6700 XT | gfx1031 | setup (`--backend hip`), unvalidated (#524) | used daily by its reporter, one card |
|
||||
| RX 5500 XT (RDNA1) | gfx1012 | built by hand: `-DCMAKE_HIP_ARCHITECTURES=gfx1012` (HIP 5.7 or 7) | 8 GB card, IQ3_S, 8K prompt: 15.3 tok/s decode (#442) |
|
||||
| Instinct MI50 / MI60, Radeon VII | gfx906 (wave64) | built by hand: `-DSTRATA_HIP_GFX906=ON` | 2x MI50, Coder IQ1_M, 128K context: decode 50.1 / 47.8 / 45.7 tok/s at 4K / 32K / 128K prompt tokens, prompt ~520 tok/s (#677) |
|
||||
|
||||
## NVIDIA: the CUDA 12 engine
|
||||
|
||||
CUDA 13 dropped Pascal and Volta: it cannot compile for them. Setup therefore keeps a **second engine**, built with
|
||||
CUDA 12.9 and `-DSTRATA_EXPERIMENTAL_SM60=ON`, in its own folder (`engine-cuda12\`, beside `engine\`). One engine runs
|
||||
per model, so the choice is made per model, by the oldest card that model runs on:
|
||||
|
||||
- **Every card the model uses is RTX 20 or newer:** the ready-made CUDA 13 engine, as always.
|
||||
- **A card is Pascal or Volta:** the CUDA 12 engine. Setup says so (`CUDA 12: sm_70 is older than CUDA 13 supports
|
||||
...`). On Windows it downloads `strata-windows-x64-cuda12.zip` with NVIDIA's CUDA 12 libraries (from pip, like the
|
||||
CUDA 13 ones); on Linux, or with `--build`, it compiles the engine with a CUDA 12.x toolkit.
|
||||
|
||||
### Opting in
|
||||
|
||||
An older card is used only when you choose it; a PC with a newer card keeps recommending the newer one.
|
||||
|
||||
| You | Setup |
|
||||
| --- | --- |
|
||||
| have only Pascal / Volta NVIDIA cards (and no AMD card it can use) | uses them, with the CUDA 12 engine |
|
||||
| name the card: `START-HERE.bat --setup --gpu 1`, or `--gpus 0,1` with a newer card | uses it; the model gets the CUDA 12 engine |
|
||||
| `--cuda 12` (or `STRATA_CUDA=12`) | the CUDA 12 engine for this model, on any card |
|
||||
| `--cuda 13` | the CUDA 13 engine even with an older card (a warning: it has no code for that card) |
|
||||
| `STRATA_EXPERIMENTAL_SM60=1` | the older cards are listed as usable (the setting from #295 still works) |
|
||||
|
||||
The choice is kept in the model's config (`"cuda": 12`): its starts and `UPDATE.bat` keep it, and other models keep
|
||||
their own engine. Setting the model up again chooses again (by its cards; add `--cuda 12` to keep a forced choice). A Pascal / 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: CUDA 12 needs 528 or newer on Windows
|
||||
(527.41, NVIDIA's minor-version compatibility) and 525 on Linux; setup's "driver too old" stop says so. Such old
|
||||
drivers were not tested here.
|
||||
|
||||
### Mixed cards
|
||||
|
||||
A model that shares a V100 with an RTX 30 / 40 card runs both on the CUDA 12 engine (it has code for sm_60 to sm_89,
|
||||
plus PTX). An RTX 50 card (sm_120) in a CUDA 12 engine is a warning, not a stop: CUDA 12.8 and newer compile for it,
|
||||
but engines built with 12.8 crashed on long prompts there (#220, #224). Keep the RTX 50 card on its own model (`--gpu
|
||||
N`), where it runs the CUDA 13 engine.
|
||||
|
||||
### Building it yourself
|
||||
|
||||
```sh
|
||||
# Linux (Windows: the same with the toolkit's nvcc.exe)
|
||||
cmake -S . -B build-cuda12 -DSTRATA_ENABLE_CUDA=ON -DSTRATA_BUILD_TESTS=OFF -DSTRATA_EXPERIMENTAL_SM60=ON \
|
||||
-DCMAKE_CUDA_ARCHITECTURES="61;70" -DCMAKE_CUDA_COMPILER=/usr/local/cuda-12.9/bin/nvcc
|
||||
cmake --build build-cuda12 --target strata -j
|
||||
```
|
||||
|
||||
Setup does the same when it compiles: it looks for the newest CUDA 12.x toolkit (`STRATA_NVCC=<path to nvcc>` picks
|
||||
one, #601; on glibc 2.43 use 12.8, see [TROUBLESHOOTING.md](TROUBLESHOOTING.md)).
|
||||
|
||||
### What the flag changes, and what it does not
|
||||
|
||||
`-DSTRATA_EXPERIMENTAL_SM60=ON` lowers the runtime floor to compute capability 6.0 and compiles the older cards' code
|
||||
paths into that build only: the Volta prompt attention (#600), #540's attention kernel (used below sm_75), and the
|
||||
Pascal BF16 path. The ready-made CUDA 13 engine has none of them, so its kernels and its output are unchanged. The
|
||||
FP16 path for BF16 projections is in every build but runs by default only below sm_75; RTX 20 owners can try it with
|
||||
`STRATA_BF16_TC=1` (`=0` turns it off on a V100). It is not bitwise the same as cuBLAS's BF16 kernel (FP16 tensor-core
|
||||
sums round differently): #540 measured a mean KL of 8.4e-3 on the next-token distribution of 24 code prompts on a V100
|
||||
(the same top-1 in 23), the size of other summation-order changes; #655 a worst relative difference of 3.5e-5 per
|
||||
product on an RTX 2080 Ti.
|
||||
|
||||
A/B switches: `STRATA_BF16_TC=0|1`, `STRATA_PROMPT_ATTN_OLD=1` (the decode kernel for prompts), `STRATA_ATTN_PRE75=0`
|
||||
(#540's kernel off). [NVIDIA_V100.md](NVIDIA_V100.md) has the V100 build, its measurements and the parity test.
|
||||
|
||||
## AMD: building gfx906 and gfx1012
|
||||
|
||||
Setup does not build these; build by hand and run `serve/server.py` with a config, as on any other card.
|
||||
|
||||
- **gfx906** (MI50 / MI60 / Radeon VII, wave64): a separate opt-in build, `-DSTRATA_HIP_GFX906=ON
|
||||
-DCMAKE_HIP_ARCHITECTURES=gfx906` (not `STRATA_ENABLE_HIP`). Current ROCm no longer ships gfx906 libraries; the
|
||||
reporter used a community ROCm 7.14 image. Recipe, kernels and measurements: [AMD_HIP.md](AMD_HIP.md#gfx906-instinct-mi50--mi60-radeon-vii-wave64-built-from-source).
|
||||
- **gfx1012** (RX 5500 XT): the wave32 backend, `-DSTRATA_ENABLE_HIP=ON -DCMAKE_HIP_ARCHITECTURES=gfx1012`. HIP 5.7
|
||||
(Ubuntu's packages) works: older hipBLAS (0.x) is used through rocBLAS, the legacy HIP names and the missing
|
||||
`__syncwarp` are version-gated, and RDNA1's missing signed dot4 uses llama.cpp's SDWA sequence
|
||||
(`-DSTRATA_GFX1012_PORTABLE_DOT=ON`: the portable one).
|
||||
|
||||
## Reports welcome
|
||||
|
||||
These paths stay experimental until more people run them. A report with the card, the driver / ROCm version, the
|
||||
model and the engine log (`strata-<model>.log`) in an issue helps; [COMMUNITY_BENCHMARKS.md](COMMUNITY_BENCHMARKS.md)
|
||||
has the format for measurements.
|
||||
+2
-1
@@ -30,7 +30,8 @@ from that dependency (`STRATA_GGUF_PY` can point to its `gguf-py` directory).
|
||||
Use this model's own tokenizer exported into the pack. Do not share another model's `dense.bin`,
|
||||
and do not rename the Orca files to impersonate one of setup's GSQ-RCO models.
|
||||
|
||||
Strata's persistent server also requires the MTP runtime, prepared by the existing tools:
|
||||
Strata's persistent server also uses the MTP runtime (optional: without `--mtp` it drafts by lookup only), prepared by
|
||||
the existing tools:
|
||||
|
||||
```sh
|
||||
.venv/bin/python tools/mtp_fetch.py fetch --out mtp
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Optional checkpoint near the prompt tail
|
||||
|
||||
`--prompt-cache-tail` saves at most one extra mid-prompt checkpoint near the end
|
||||
of a request, at a boundary the batched prefill already reaches. It is off by
|
||||
default and applies to single-GPU serve with positive `--prompt-cache` and
|
||||
`--prompt-cache-every` values.
|
||||
|
||||
A branch can share most of a prompt but diverge before its final cached state.
|
||||
If the periodic checkpoint before that branch is far away, the engine has to
|
||||
read the gap again. The extra checkpoint can reduce that gap. It uses the
|
||||
existing bounded checkpoint cache; it does not add another unbounded history.
|
||||
|
||||
The tail is the first chunk boundary within one chunk of the prompt's end, where the chunk is the size that chunk
|
||||
really had (`--prefill auto` chunks differ from `--prefill N`). The tail checkpoint is evicted first (only one stays
|
||||
alive, the root is never dropped), so it never pushes out a periodic checkpoint when `--prompt-cache` is full.
|
||||
|
||||
The option does not split prefill chunks or alter the periodic checkpoint
|
||||
schedule. It selects the first completed batched chunk within one configured
|
||||
chunk of the prompt's end, excluding the short final suffix controlled by
|
||||
`--short-read`. If a periodic checkpoint already falls there, the same state is
|
||||
saved once. Reread diagnostics and layer splits keep their existing behavior.
|
||||
|
||||
For example, with a 6144-token chunk and a 16384-token checkpoint interval,
|
||||
the first periodic checkpoint is at 18432 and the next would be at 36864.
|
||||
A branch sharing 32769 tokens cannot use the latter. A near-tail checkpoint
|
||||
at 30720 can reuse more of that prefix,
|
||||
without changing how the original prompt was computed. The benefit depends
|
||||
on the request and the branch position; it is not a universal speed increase.
|
||||
|
||||
This is a separate conservative alternative to the Strata-2080Ti Daily's
|
||||
checkpoint-grid patch. The grid changes chunk boundaries to reach exact
|
||||
checkpoint intervals and failed its local paired numerical gate. This option
|
||||
does not claim that the original grid patch has been fixed or validated.
|
||||
@@ -84,6 +84,50 @@ The existing warning about the CUDA0 expert-cache GPU hit path still applies:
|
||||
its outputs diverge from cache-off runs. Treat performance as experimental
|
||||
until the generated tokens have been validated.
|
||||
|
||||
## Optional helper decode optimization
|
||||
|
||||
`--remote-expert-opt` (`--serve` only) optimizes the CUDA1-3 helper caches
|
||||
above. The engine's default is off; since 0.1.39b setup adds it to a config on
|
||||
two or more GPUs (`--gpus`, or "use both" at start). It acts only when a helper
|
||||
cache is configured; a layer split runs exactly as before. To leave it out:
|
||||
setup's `--no-remote-expert-opt`, or `"remote_expert_opt": false` in the
|
||||
model's `strata-*.json` (kept when setup runs again). Measured by the PR's
|
||||
author: dual RTX 4090 +63% mixed / +132% code decode over the plain helper
|
||||
path; RTX 5090 + 4090 +28% / +63%. The primary cache avoids admitting experts already held
|
||||
by a helper, and helpers replace cold experts with frequently routed CPU
|
||||
misses using their existing same-layer slots. Each helper reduces its expert
|
||||
outputs to a weighted partial sum on its GPU before returning one vector per
|
||||
token. Tokens with no CPU expert work skip CPU activation quantization.
|
||||
|
||||
For an existing server configuration with CUDA0 as the primary and CUDA1 as a
|
||||
helper, use these engine arguments alongside the model and profile arguments:
|
||||
|
||||
```text
|
||||
--expert-cache auto --expert-cache-device1 auto --remote-expert-opt
|
||||
```
|
||||
|
||||
`--expert-cache-device1`, `--expert-cache-device2` and
|
||||
`--expert-cache-device3` now also accept `auto`: fill each helper from its
|
||||
assigned ranking using actual aligned expert bytes and free VRAM, retaining
|
||||
the existing 512 MiB allowance. Explicit numeric budgets still work. This is
|
||||
startup capacity sizing, not throughput balancing; with several helpers a
|
||||
card's truncated candidate tail is not redistributed to another card.
|
||||
|
||||
The optimization uses the existing host scheduling and pinned-host transport,
|
||||
not P2P or tensor parallelism. Each layer still waits for its participating
|
||||
helpers. It changes floating-point summation order, so enabled output is not
|
||||
claimed to be bitwise identical. Only two-card CUDA operation has been measured
|
||||
by its author; a community machine measured two cards on HIP (2x RX 6900 XT,
|
||||
PCIe 4.0 x8 each, IQ3_S): there the expert plan's PCIe share had been taking
|
||||
experts the helper already held, which made the primary card wait 43 ms per
|
||||
verify window instead of 19 (decode 40 instead of 66 tok/s). #854 keeps a
|
||||
helper's experts out of that share; before it, `--pcie-frac 0` avoided the
|
||||
cost ([bench/results/2026-10-04-rdna2-helper-pcie-share](../bench/results/2026-10-04-rdna2-helper-pcie-share/README.md)).
|
||||
Three/four cards have not been validated. Without the switch, the
|
||||
existing decode path remains in use. This does not optimize the separate
|
||||
`--peer-device` path below or change its existing incompatibility with helper
|
||||
caches.
|
||||
|
||||
## Peer tier (`--peer-device`)
|
||||
|
||||
`--peer-device N` puts a second adaptive expert cache on CUDA device N. It
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
# Strix Halo (Ryzen AI Max, gfx1151): build from source on Linux
|
||||
|
||||
Strata runs on the Ryzen AI Max "Strix Halo" APUs (Radeon 8060S / 8050S, RDNA3.5, `gfx1151`, one pool of LPDDR5X shared by the
|
||||
CPU and the GPU) through the same HIP backend as the Radeon cards in [AMD_HIP.md](AMD_HIP.md). **Status: experimental.**
|
||||
Everything here was built and measured on one machine, the maintainers' Strix Halo box (Ryzen AI Max+ 395, 128 GB,
|
||||
Ubuntu 26.04, kernel 7.0, ROCm 7.14.1). Setup (`setup.sh`, `START-HERE.bat`) recognizes it from 0.1.40, by its PCI id (`1002:1586`, one id for the 8060S, 8050S and 8040S) and by
|
||||
`gfx1151` exactly: a Strix Point (890M / 880M, gfx1150), Krackan (860M / 840M, gfx1152) or Phoenix / Hawk Point (780M / 760M, gfx1103) Radeon is
|
||||
named as what it is and not supported. On Linux setup compiles the engine for gfx1151 itself (the steps below are the same, by hand). On
|
||||
Windows the ready-made `strata-windows-x64-hip.zip` carries gfx1151 code from 0.1.40 (untested on a Windows Strix Halo so far); a zip
|
||||
without it is refused with a message, never used. Setup counts the GPU's memory as the BIOS carve-out plus the shared memory
|
||||
(the GTT pool; on Windows half the RAM) and recommends UD-IQ4_XS from about 80 GB of memory; it changes no BIOS or kernel setting.
|
||||
|
||||
What the engine does differently on this chip:
|
||||
|
||||
- **Unified memory.** The GPU's "VRAM" is the GTT pool of the same RAM. `device_free_bytes()` (the figure `--expert-cache auto` and
|
||||
the memory plan use) counts what Linux can give back (`MemAvailable` less 6 GiB for the system, `STRATA_UMA_HEADROOM_GIB`
|
||||
changes the 6) instead of only `MemFree`, per device, for any integrated GPU on Linux (this applies to a DGX Spark too).
|
||||
- **gfx11 matrix cores.** The prompt attention, the block scorer, the prompt GEMMs and the prompt experts have WMMA kernels for
|
||||
gfx1100 / 1101 / 1102 / 1150 / 1151. A gfx11 part outside that list (gfx1103, gfx1152) is not given them: it takes the portable path.
|
||||
- **The hipBLASLt table** `tools/hip/gfx1151-hipblaslt-100401.txt` (ROCm 7.14.1's hipBLASLt 1.x `100401`), including the prompt shapes
|
||||
at `--prefill 16384` (T 16383 / 16384).
|
||||
|
||||
## 1. The toolchain (no root needed)
|
||||
|
||||
ROCm 7.14.1 for gfx1151 from AMD's TheRock builds, extracted into a folder of your own:
|
||||
|
||||
```sh
|
||||
mkdir -p ~/rocm && cd ~/rocm
|
||||
curl -LO https://repo.amd.com/rocm/tarball-multi-arch/therock-dist-linux-gfx1151-7.14.1.tar.gz # 1.7 GB
|
||||
sha256sum therock-dist-linux-gfx1151-7.14.1.tar.gz # c40e8f2bd6630a7d11557c762b99c6fa8afb04c9bd0e51ed1675ee1ca24afb00
|
||||
mkdir install && tar -xzf therock-dist-linux-gfx1151-7.14.1.tar.gz -C install
|
||||
export SDK=$HOME/rocm/install
|
||||
export ROCM_PATH=$SDK HIP_PATH=$SDK HIP_PLATFORM=amd
|
||||
export PATH=$SDK/bin:$SDK/lib/llvm/bin:$PATH
|
||||
export LD_LIBRARY_PATH=$SDK/lib:$SDK/lib/rocm_sysdeps/lib:$SDK/lib/llvm/lib:${LD_LIBRARY_PATH:-}
|
||||
```
|
||||
|
||||
You also need CMake 3.24+, Ninja, git and a C++ host toolchain with libstdc++ headers (`sudo apt install build-essential cmake ninja-build git`).
|
||||
The maintainers' box has no system compiler: they point ROCm's clang at a user-space copy of gcc 15 with
|
||||
`--gcc-toolchain=<dir>/usr` (added to `CMAKE_HIP_FLAGS` below; leave it out when the system has one).
|
||||
|
||||
The kernel's amdgpu driver and `/dev/kfd` access (your user in the `render` and `video` groups) are all the GPU side needs.
|
||||
To let the GPU use most of a 128 GB machine the maintainers' box boots with `ttm.pages_limit=29360128 ttm.page_pool_size=29360128`
|
||||
(a 112 GiB GTT; the default is about half of the RAM) and `amd_iommu=off`; both are host settings the engine does not need to run,
|
||||
only to have the room.
|
||||
|
||||
## 2. The engine
|
||||
|
||||
llama.cpp's `ggml` sources are pinned by this repository: CMake fetches the revision it needs. For an offline build, point
|
||||
`STRATA_GGML_DIR` at a checkout of exactly that revision.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/Niko1221/Strata.git && cd Strata
|
||||
cmake -S . -B build-halo -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DSTRATA_ENABLE_HIP=ON -DSTRATA_ENABLE_CUDA=OFF -DSTRATA_PREFILL_MMQ=ON \
|
||||
-DCMAKE_HIP_ARCHITECTURES=gfx1151 \
|
||||
-DCMAKE_HIP_COMPILER=$SDK/lib/llvm/bin/clang++ -DCMAKE_HIP_COMPILER_ROCM_ROOT=$SDK \
|
||||
"-DCMAKE_PREFIX_PATH=$SDK;$SDK/lib/rocm_sysdeps;$SDK/lib/llvm" \
|
||||
"-DCMAKE_HIP_FLAGS=--rocm-path=$SDK --rocm-device-lib-path=$SDK/lib/llvm/amdgcn/bitcode"
|
||||
cmake --build build-halo --target strata -j 8 # about 25 minutes the first time
|
||||
```
|
||||
|
||||
`gfx1151` is "unvalidated" in the build's own wording (it warns). `STRATA_PREFILL_MMQ=ON` builds ggml's MMQ kernels the prompt
|
||||
experts need. The tests: `cmake --build build-halo -j 8` builds them all, `ctest --test-dir build-halo` runs them;
|
||||
`hip_prefill_hcd_exact_parity` and `hip_prefill_hipblaslt_gemm` need the table (next section) and skip without it.
|
||||
|
||||
## 3. The prompt table, and running
|
||||
|
||||
Hand the engine the table (`setup` does it for the supported cards; for gfx1151 you do it): without it the prompt's dense matrix
|
||||
products use plain hipBLAS and are slower.
|
||||
|
||||
```sh
|
||||
export STRATA_HIPBLASLT_TUNING=$PWD/tools/hip/gfx1151-hipblaslt-100401.txt
|
||||
```
|
||||
|
||||
The engine accepts it only for the architecture and the hipBLASLt version named on the table's second line
|
||||
(`STRATA_HIPBLASLT_TUNING_V1 gfx1151 100401`: ROCm 7.14.1) and says so at start-up (`prefill gemm: hipBLASLt tuning enabled (90 rows,
|
||||
gfx1151, version 100401)`). Then run `strata` with the same arguments as on any other card (the model packs, `--serve`, the
|
||||
settings of [DETAILS.md](DETAILS.md)); a pack of the Unsloth UD-IQ4_XS or UD-Q4_K_XL GGUFs is what the numbers below used.
|
||||
|
||||
## 4. What is on by default on gfx1151
|
||||
|
||||
At start the engine looks at GPU 0; on a gfx1151 it sets the switches below unless you set them yourself
|
||||
(`STRATA_GFX1151_DEFAULTS=0` turns the whole table off, and the start-up line names what was set). **Every one of them gave
|
||||
byte-identical output on the maintainers' box** (the same token ids, 4K / 32K / 64K and four models, and a bitwise harness per kernel):
|
||||
they change speed, not answers. Nothing here applies to CUDA or to another AMD architecture.
|
||||
|
||||
| Switch | What it does |
|
||||
|---|---|
|
||||
| `STRATA_GDN_HEAD`, `STRATA_GDN_PP=2`, `STRATA_GDN_CONVL2`, `STRATA_GDN_NOY` | the prompt's gated-delta-net recurrence: four lanes per column, two columns per lane, the convolution and the norms fused |
|
||||
| `STRATA_HCD_EXACT` | the hyper-connection down projection (N 320, K 10240) in a WMMA kernel that reproduces hipBLASLt solution 1176 / 1177's summation order (`hip_prefill_hcd_exact_parity` checks it bit for bit); it runs only where the table makes hipBLASLt pick one of those two, and says once if it cannot |
|
||||
| `STRATA_CVEC_FUSE`, `STRATA_PF_PAD` | a steered layer's write, control vector and next norm in one pass; padded GEMM row strides |
|
||||
| `STRATA_Q8_PACKED`, `STRATA_Q6_PACKED`, `STRATA_MMVF_ROWS`, `STRATA_ATTN_LANECELL` | decode layouts and kernels (packed Q8_0 / Q6_K weights, 4-row BF16 GEMV, one-cell-per-thread attention scores) |
|
||||
| `STRATA_EXPERT_V2`, `STRATA_TSUM`, `STRATA_LFUSE` | the grouped decode experts (IQ3_S / IQ4_NL), single-butterfly warp sums, fewer launches around the shared expert and the KV append |
|
||||
| `STRATA_GDN_SPLIT`, `STRATA_QFUSE`, `STRATA_PLE_BATCH` | the GDN step over four blocks per head, activation images written by their producers, the PLE key / value projections of a verify window at once |
|
||||
| `STRATA_SH_STREAM=1` | the shared expert on a second stream beside the routed experts. It is off by default on other AMD cards (#816), but it pays on gfx1151: decode +1.8% (UD-IQ4_XS) and +6.7% (UD-Q4_K_XL) at 8K |
|
||||
|
||||
## 5. What is not on by default (it changes bits)
|
||||
|
||||
These are faster on gfx1151 and **round differently** from the default path (a different summation order or a half-precision
|
||||
intermediate), so they are opt-in and the maintainers gate them with KL against the default (`STRATA_PF_SWITCH_MIN_T=4096` keeps
|
||||
the short prompts on the default numerics, since these pay on long ones). The maintainers' fast configuration sets all of
|
||||
them:
|
||||
|
||||
```sh
|
||||
export STRATA_PF_FUSED=1 # the prompt experts on the matrix cores (IQ2 / IQ3 / IQ4_XS gate/up, Q2_0 / IQ4_NL / Q8_0 down)
|
||||
export STRATA_PF_GEMM=1 # the prompt's FP16 projections on the 128 x 256 WMMA GEMM
|
||||
export STRATA_HC_UPMIX=1 # the hyper-connection up projection with its epilogue
|
||||
export STRATA_PA_FAST=1 # the prompt attention with single FP16 q and p
|
||||
export STRATA_HIP_WMMA=1 # the prompt attention on matrix cores
|
||||
export STRATA_SELECT_WMMA=1 # the block scorer on matrix cores
|
||||
export STRATA_HC_Q8=1 # the hyper-connection read from the GGUF's own Q8_0 projections
|
||||
export STRATA_PF_SWITCH_MIN_T=4096
|
||||
```
|
||||
|
||||
Two engine flags help on long contexts and are not Strix-specific: `--mtp-window 8192` (the draft layer attends to the last 8,192
|
||||
cells: 64K output +3.5%, 32K -0.4%) and `--spec`, `--lookup-chain`, `--mtp-q4` (see [DETAILS.md](DETAILS.md)).
|
||||
|
||||
## 6. Measured
|
||||
|
||||
Ryzen AI Max+ 395 (128 GB, gfx1151), the maintainers' fast configuration of section 5 on top of the defaults of section 4, `--spec 4`,
|
||||
`--mtp`, `--lookup-chain 3`, `--mtp-q4 all`, `--prefill 16384`, int8 KV, all experts in the unified memory. Prompt tok/s is
|
||||
(prompt tokens - 1) / prompt time, output tok/s is the tokens / decode time; medians of 3 runs, one fresh process per run, the two
|
||||
engines interleaved. "Before" is the maintainers' earlier engine branch (the same switches set by hand), "now" is this release.
|
||||
Every pair printed the same token ids.
|
||||
|
||||
| Model | Context | Prompt before -> now | Output before -> now |
|
||||
|---|---|---|---|
|
||||
| UD-IQ4_XS | 8K | 1,212 -> 1,293 (+6.7%) | 51.7 -> 53.8 (+4.0%) |
|
||||
| UD-IQ4_XS | 128K | 1,241 -> 1,320 (+6.4%) | 49.3 -> 51.4 (+4.2%) |
|
||||
| UD-Q4_K_XL | 8K | 1,136 -> 1,169 (+2.9%) | 50.6 -> 52.6 (+3.9%) |
|
||||
| UD-Q4_K_XL | 128K | 1,220 -> 1,248 (+2.4%) | 41.6 -> 43.3 (+3.9%) |
|
||||
| IQ3_S | 8K | 1,202 -> 1,242 (+3.4%) | 56.2 -> 59.7 (+6.3%) |
|
||||
| IQ3_S | 128K | 1,254 -> 1,312 (+4.6%) | 40.6 -> 42.8 (+5.5%) |
|
||||
| IQ3_XXS | 8K | 1,237 -> 1,238 (+0.1%) | 48.8 -> 51.5 (+5.6%) |
|
||||
| IQ3_XXS | 128K | 1,259 -> 1,252 (-0.6%) | 48.4 -> 50.9 (+5.1%) |
|
||||
|
||||
UD-IQ4_XS at 4K / 32K / 64K (5-6 interleaved pairs): prompt +9.2% / +6.0% / +5.3%, output +5.0% / +3.9% / +4.6%.
|
||||
IQ3_XXS at 8K was measured again with 6 interleaved pairs (an earlier 3-run set had read 5% low while other jobs shared the GPU): prompt equal, output +5.6%. With the defaults off the same build's prompt is 8% lower (1,137), so the table helps there too.
|
||||
|
||||
### Re-measured on the merged 0.1.40 code
|
||||
|
||||
The same box and method, no environment switches at all (the arch defaults only), on the final merged tree. 4 interleaved pairs per cell,
|
||||
medians; "j" is the build of the table above (the Aurora port before the other 0.1.40 work was merged), "merged" is the release
|
||||
code. Every run printed the same token ids, and the ids also match `strata-int29` (the v4.2 config) on UD-IQ4_XS and UD-Q4_K_XL at
|
||||
a8 / 4K / 64K.
|
||||
|
||||
| Model | Context | Prompt j -> merged | Output j -> merged |
|
||||
|---|---|---|---|
|
||||
| UD-IQ4_XS | 8K | 1,260 -> 1,159 (-8.0%, spread 1,070-1,320) | 53.94 -> 53.14 (-1.5%) |
|
||||
| UD-IQ4_XS | 128K | 1,317 -> 1,299 (-1.4%) | 51.38 -> 50.72 (-1.3%) |
|
||||
| UD-Q4_K_XL | 8K | 1,163 -> 1,104 (-5.0%, spread 890-1,240) | 52.66 -> 50.47 (-4.2%) |
|
||||
| UD-Q4_K_XL | 128K | 1,252 -> 1,211 (-3.3%) | 43.28 -> 42.64 (-1.5%) |
|
||||
| IQ3_S | 8K | 1,283 -> 1,265 (-1.4%) | 59.81 -> 59.10 (-1.2%) |
|
||||
| IQ3_S | 128K | 1,311 -> 1,310 (-0.1%) | 42.74 -> 42.09 (-1.5%) |
|
||||
| IQ3_XXS | 8K | 1,127 -> 1,212 (+7.6%) | 51.64 -> 50.95 (-1.3%) |
|
||||
| IQ3_XXS | 128K | 1,252 -> 1,250 (-0.1%) | 50.88 -> 50.22 (-1.3%) |
|
||||
|
||||
The prompt numbers at 8K swing by more than the differences (other jobs share this box), so read them as unchanged. The output
|
||||
column moved by a steady -1.2% to -1.5% (-4% on UD-Q4_K_XL at 8K, where the default arm also had slow runs): since 0.1.39 the
|
||||
shared-expert stream fork is off by default on HIP (#816). It overlaps the shared expert with the routed experts on a second
|
||||
stream and does help on gfx1151, so 0.1.40 turns it on there by default (section 4). It changes no bits (same ids). 4 interleaved pairs at 8K, merged
|
||||
code with the fork off -> on: UD-IQ4_XS output 53.1 -> 54.05 (+1.8%), UD-Q4_K_XL 49.5 -> 52.8 (+6.7%, the default arm ranged 46.0-52.1),
|
||||
prompt unchanged. With the fork on, the merged code's output is at or above the j build's (IQ4_XS 54.05 vs 53.94).
|
||||
+32
-1
@@ -21,13 +21,37 @@ Update it (NVIDIA App or [nvidia.com/drivers](https://www.nvidia.com/drivers)),
|
||||
|
||||
**It says port 8080 is already in use.**
|
||||
Strata is already running. Look for its window. Or another program uses the port: `START-HERE.bat --port 8081`.
|
||||
Only an address that is really taken says "already in use". Any other reason (Windows keeps the port reserved, or
|
||||
the config's `"host"` is not an address of this PC) is printed as what the OS said, with what to change (#769).
|
||||
|
||||
**"Windows blocked the Strata engine (or the image encoder) ... Smart App Control".**
|
||||
On a clean Windows 11, Smart App Control refuses programs that are not signed, and Strata's `strata.exe` and
|
||||
`strata-vision.exe` are not (#735). Turn it off (Windows Security > App & browser control > Smart App Control
|
||||
settings; it cannot be turned back on without a reset of Windows), or, if only the image encoder is blocked, run
|
||||
`START-HERE.bat --setup --vision no`.
|
||||
|
||||
**A new engine misbehaves after an update.**
|
||||
An update keeps the engine it replaced in `engine\.previous` (one generation, about 210 MiB).
|
||||
`python setup.py --rollback-engine` puts it back (and keeps the newer one there: run it again to go forward) (#670).
|
||||
|
||||
**Python or the build tools could not be installed.**
|
||||
Install what it names (links are printed), then run it again. Everything already done is kept.
|
||||
|
||||
**Linux: the engine does not compile (`unsupported GNU version`, or `exception specification is incompatible` for
|
||||
`cospi`/`sinpi`/`rsqrt`).** Two known mismatches between the CUDA toolkit and a new Linux (#601):
|
||||
- gcc newer than 14 (Ubuntu 26.04's default 15): CUDA 12.x and 13.0 refuse it. Install g++-14 and run
|
||||
`CXX=g++-14 CUDAHOSTCXX=g++-14 ./setup.sh`.
|
||||
- glibc 2.43 with CUDA 12.9: the toolkit's math headers clash with glibc's, already in CMake's first test compile.
|
||||
Use CUDA 12.8 or 13.x instead. Setup takes the newest toolkit it finds; `STRATA_NVCC=/usr/local/cuda-12.8/bin/nvcc
|
||||
./setup.sh` makes it use that one (only that one).
|
||||
|
||||
**The first start takes minutes.**
|
||||
It is reading 34-55 GB into RAM; the second start is faster while the files are in the OS cache. Started from Task
|
||||
Scheduler, it can be 24x slower: see [Running it at startup](DETAILS.md#running-it-at-startup-task-scheduler).
|
||||
On Linux the engine now asks the kernel to read the model files ahead (a cold start went from ~920 s to 70 s on one
|
||||
PC); `STRATA_READ_AHEAD=0` turns that off. On Linux with transparent huge pages on `always`, the arena no longer asks
|
||||
for `MADV_HUGEPAGE` on top (a fragmented machine spent minutes compacting memory, #771); `STRATA_NO_ARENA_THP=1` skips
|
||||
that request on any setting.
|
||||
|
||||
## While it answers
|
||||
|
||||
@@ -46,9 +70,16 @@ context.
|
||||
The monitor plugged into the graphics card and other GPU programs take VRAM from the expert cache; RAM running below
|
||||
its rated speed (enable EXPO/XMP in the BIOS) slows the CPU half.
|
||||
|
||||
**An earlier reply that came back empty is gone from the chat history (0.1.40, #843).**
|
||||
The server leaves out finished assistant turns that have no text, so the next reply does not copy the empty ones.
|
||||
`STRATA_KEEP_EMPTY_TURNS=1` in the server's environment renders them as before.
|
||||
|
||||
**A Turing card (RTX 20) reads prompts above ~90K tokens differently (0.1.40, #743).**
|
||||
The prompt's top-k selection takes a wider kernel there, with the same ids. `STRATA_TOPK_STREAM=0` restores the old one.
|
||||
|
||||
**Pictures are refused, or slow.**
|
||||
"this server was started without the vision encoder": the model was set up for text only - run setup again with
|
||||
`--vision gpu` (or `--vision cpu`). Pictures that take 10-30 s are read by the encoder on the CPU; `--vision gpu`
|
||||
`--vision gpu` (or `--vision cpu`). Pictures that take several seconds (about 3 s at 300 image tokens on 8 cores, more with more tokens) are read by the encoder on the CPU; `--vision gpu`
|
||||
(NVIDIA, ~1.4 GB of VRAM) makes it 0.1-0.5 s.
|
||||
|
||||
## AMD cards
|
||||
|
||||
+70
-6
@@ -140,11 +140,17 @@ model's (`data/expert-profile.bin`); it ranks the same 48 x 512 experts and deci
|
||||
budget take first.
|
||||
|
||||
**Choosing the RAM budget N:** your RAM minus 20-24 GB (the OS, the engine itself, the 126 MB router copy, and room for
|
||||
the OS file cache that serves the rest). On 64 GB, 40. The engine clamps a budget larger than the RAM it finds free
|
||||
(minus 4 GB and a 256 MiB margin) and says so; before #403's fix such a clamped budget could then fail the start. Everything above N comes from the SSD for every token, so N is the setting that matters
|
||||
the OS file cache that serves the rest). On 64 GB, 40. The engine checks available RAM and, on Windows, available
|
||||
commit capacity. If N exceeds the smaller value minus 4 GiB headroom, it reduces N to that amount minus a 256 MiB
|
||||
margin and reports which limit applies. Before #403's fix a clamped budget could then fail the safety check.
|
||||
Everything above N comes from the SSD for every token, so N is the setting that matters
|
||||
most; a bigger budget was faster in every measurement (24 / 32 / 40 GiB). When the driver page-locks the whole budget
|
||||
(24 GiB did on this PC, 32 and 40 did not), the GPU also computes a share of the misses over PCIe, as in the resident
|
||||
low-RAM mode; `STRATA_RESIDENT_PIN=0` keeps the budget locked only (the CPU then computes every miss).
|
||||
low-RAM mode; `STRATA_RESIDENT_PIN=0` uses ordinary pageable memory (the CPU then computes every miss).
|
||||
|
||||
On Windows, the allocation log reports available physical RAM and commit capacity before allocation and after a
|
||||
failure. Free RAM alone does not guarantee that Windows can commit the requested memory. If the pageable fallback
|
||||
also fails, the warning preserves both the GPU allocation or mapping error and the `malloc` error.
|
||||
|
||||
**Context:** measured at 4K. The KV cache takes VRAM from the expert cache, so a longer context makes decoding slower;
|
||||
8K is a reasonable start on 12 GB. `--kv int8` halves the KV cache's VRAM.
|
||||
@@ -221,13 +227,71 @@ engine with `--short-read` covering the positions to compare; llama.cpp's side w
|
||||
| `STRATA_PARTIAL_PIN=1` | Registers the hottest part of the RAM budget (up to `STRATA_PARTIAL_PIN_GIB`, default 24) with the GPU driver, so the GPU computes a share of the misses over PCIe (`--pcie-frac`). Measured no faster on this PC, and it changes the numerics of those experts (GPU kernels instead of the CPU's), so off. |
|
||||
| `STRATA_FETCH_THREADS=N` | Threads that read the experts from the GGUF while it answers (default 8; 16 was no faster). The prompt path has its own: `STRATA_STAGER_THREADS` (default 32 here) and `STRATA_STAGER_RING` (128). |
|
||||
|
||||
## UD-IQ4_XS (setup from 0.1.39, #621)
|
||||
|
||||
Unsloth's `UD-IQ4_XS` at the same revision is in setup too, as a regular choice (not experimental; the Unsloth
|
||||
family's first size and its default): `START-HERE.bat --setup --family unsloth --model UD-IQ4_XS` (engine 0.1.38 or
|
||||
newer). It sits between IQ3_S and UD-Q4_K_XL: its routed experts are IQ3_S gate/up (IQ4_XS
|
||||
in one layer) with IQ4_NL downs (Q8_0 in five layers), 59.5 GB of them; the dense side (Q8_0 projections, the IQ4_NL
|
||||
PLE table, a Q6_K head) is UD-Q4_K_XL's. Three shards, 93.7 GB:
|
||||
|
||||
| File | Bytes | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| `Qwen3.8-Flash-Next-UD-IQ4_XS-00001-of-00003.gguf` | 10,946,624 | `5ce89370720f8bf90890f439361282104c1aa1482d4013bb9a50923e758e71a4` |
|
||||
| `Qwen3.8-Flash-Next-UD-IQ4_XS-00002-of-00003.gguf` | 49,835,229,856 | `577a38a2392b40ca2193cea502e1d92f60b8cd370675d308e0ec21885d9daaa7` |
|
||||
| `Qwen3.8-Flash-Next-UD-IQ4_XS-00003-of-00003.gguf` | 43,836,407,744 | `d4634e6d84f0ebb0940be15c90d3790bf6464e3dea3a1cddc567dc0e83ad8833` |
|
||||
|
||||
Setup treats it like UD-Q4_K_XL (the list above): the same RAM budget (your RAM less 24 GB; at most all 55 GiB of its
|
||||
experts, so a PC with ~80 GB of RAM or more holds all of them), the pack with `--compat-bf16`, no `experts.bin`, one
|
||||
GPU by default. It does not ask on AMD: its experts' formats have prompt kernels there too. Images are an option (asked,
|
||||
off by default; NVIDIA): the image path has no restriction for this pack, which uses the original model's image
|
||||
encoder; not yet run with images.
|
||||
|
||||
What is known so far: it packs and runs on a Strix Halo (AMD gfx1151, 128 GB unified memory), where prompts read at
|
||||
~820 tokens/s at 8K and 64K and the answers passed the retrieval checks at 8K and 64K. It has **not** been measured on
|
||||
NVIDIA yet; please report what you see. One fidelity note that applies to every `--compat-bf16` pack (UD-Q4_K_XL too):
|
||||
the pack rounds the file's Q8_0 hyper-connection projections to BF16, and on the Strix Halo that rounding put
|
||||
perplexity 6-9% above llama.cpp's on the same file (teacher-forced, short context). Reading the Q8_0 values instead
|
||||
closes most of that gap; it is measured on AMD only and not in this release.
|
||||
|
||||
## Q4_0 files
|
||||
|
||||
A plain `llama-quantize` Q4_0 file of this model runs the same way: pack shard 1 with `--compat-bf16` as above and
|
||||
point `--native` at it. Tested:
|
||||
[bartowski/Qwen3.8-Flash-Next-GGUF, `Qwen3.8-Flash-Next-Q4_0`, revision `928589f`](https://huggingface.co/bartowski/Qwen3.8-Flash-Next-GGUF/tree/928589fdb66c6ff07f22ac561e3fbce76553548f/Qwen3.8-Flash-Next-Q4_0)
|
||||
(3 shards, 100.6 GB). Its routed experts are Q4_0 for gate/up and for down, except down in layers 0-5, which is Q4_1;
|
||||
the PLE table and the token embedding are Q4_0. The pack serves 302 tensors from the GGUF as stored and converts 362
|
||||
(168 exact, 194 rounded to BF16, largest absolute error 0.0156; 1.23 GiB).
|
||||
|
||||
The Q4_0 expert kernels centre the codes (q - 8) before the integer dot product, as ggml-cpu's Q4_0 x Q8_0 dot does,
|
||||
instead of llama.cpp's CUDA form (uncentred codes, minus 8 x the Q8_1 block sum in float); Q4_1 follows the Q5_1
|
||||
kernel without the fifth bit. `native_expert_parity` on this file (layers 0 and 5: Q4_0/Q4_1, layers 20 and 47:
|
||||
Q4_0/Q4_0): relative error against the float product 1.1-1.3e-2 on the CPU and the GPU alike, CPU vs GPU 1.1e-7 for
|
||||
Q4_0/Q4_0 and 5e-4 to 1.1e-3 with Q4_1 down.
|
||||
|
||||
Measured on one RTX 3060 12 GB (PCIe 3.0 x16), Xeon E5-2696 v4, 121 GiB DDR4-2400, Linux, CUDA 12.4; every expert in
|
||||
RAM, `--max-context 131072 --kv int8 --kv-resident 20480`, expert cache `auto` (1,960 slots, 5.06 GiB), prompt chunk
|
||||
`auto` (6,144 tokens); greedy, `reasoning_effort` medium, one request at a time:
|
||||
|
||||
| Prompt | Prompt read | Output |
|
||||
| --- | --- | --- |
|
||||
| 63 tokens (prose) | - | 25.4 tok/s |
|
||||
| 111 tokens (code) | - | 28.0 tok/s |
|
||||
| 24,461 tokens | 787 tok/s | 24.0 tok/s |
|
||||
| 67,934 tokens (a fact hidden in the middle: found) | 737 tok/s | 26.4 tok/s |
|
||||
|
||||
A coding prompt was answered correctly. Not measured: agreement with llama.cpp on this file (as
|
||||
[above](#quality-against-llamacpp-on-the-same-file) for UD-Q4_K_XL), AMD, Windows, and bartowski's Swift-1.5 Q4_0,
|
||||
whose header shows the same formats.
|
||||
|
||||
## Scope and validation
|
||||
|
||||
- Only UD-Q4_K_XL at revision `38bb39e` is targeted. Other Unsloth quantizations use formats this engine may not
|
||||
have kernels for; the engine checks every layer's formats at start and refuses an unsupported one by name.
|
||||
- UD-Q4_K_XL and UD-IQ4_XS at revision `38bb39e` are targeted, and plain Q4_0 files ([above](#q4_0-files)). Other
|
||||
quantizations use formats this engine may not have kernels for; the engine checks every layer's formats at start
|
||||
and refuses an unsupported one by name.
|
||||
- Tests: the packer's synthetic 4-shard and conversion tests (`.venv/bin/python -m unittest discover -s tools -p
|
||||
test_iq_pack.py`); CTests `gguf_split_test`, `expert_layout_test`, `native_expert_parity_*` (the three real expert
|
||||
format pairs against ggml-cpu, the Q5_1 min term, Q8_0 rows), `prefill_mmq_kquant_test` (with `-DSTRATA_MMQ_KQUANTS=ON`: the
|
||||
format pairs against ggml-cpu, the Q5_1 min term, Q8_0 rows, Q4_0/Q4_0 and Q4_0/Q4_1), `prefill_mmq_kquant_test` (with `-DSTRATA_MMQ_KQUANTS=ON`: the
|
||||
prompt path's MMQ products for Q4_K / Q5_K / Q5_1 / Q8_0 against ggml's dequantized weights); the in-place mode against `experts.bin` on the Coder
|
||||
(identical tokens and logits).
|
||||
- Real runs: greedy answers to a coding prompt (correct) at every budget and setting above, identical across them;
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# Unsloth UD-Q6_K_XL (experimental)
|
||||
|
||||
**Experimental; manual workflow only — setup does not offer this model yet.** This is Unsloth's 6-bit file of the
|
||||
same model the other packs use:
|
||||
[unsloth/Qwen3.8-Flash-Next-GGUF, `UD-Q6_K_XL`, revision `38bb39e`](https://huggingface.co/unsloth/Qwen3.8-Flash-Next-GGUF/tree/38bb39ee97821de2c9009abb7e93950eec396e66/UD-Q6_K_XL).
|
||||
Build it for engine 0.1.38 or newer (earlier engines read `per_layer_token_embd.weight` in IQ4_NL, Q5_0 or FP8 only,
|
||||
below). UD-Q4_K_XL's manual workflow asks nothing of setup, and neither does this one.
|
||||
|
||||
> **0.1.40 status.** The Q6_K gate/up kernels are an opt-in build (`-DSTRATA_Q6K_EXPERTS=ON`, plus `-DSTRATA_MMQ_KQUANTS=ON`
|
||||
> for the prompt kernels): every grouped-kernel instance is loaded at start and would cost other models expert slots. The
|
||||
> Q8_0 PLE table reader is not in this release (it comes with the PLE format series, PR #651); until then this file does
|
||||
> not load. The parity programs below have not been run on a GPU yet.
|
||||
|
||||
## What is in the file
|
||||
|
||||
Six shards, 169,165,382,688 bytes (169.2 GB) together ([sizes and SHA-256 from the HF tree API](https://huggingface.co/api/models/unsloth/Qwen3.8-Flash-Next-GGUF/tree/38bb39ee97821de2c9009abb7e93950eec396e66/UD-Q6_K_XL);
|
||||
shards 1, 2 and 6 were also checked bit for bit against their Hugging Face LFS hashes after download). Shard 1 holds
|
||||
only metadata; shard 2 only the output head (`output.weight`, `output_hc_*`, all Q8_0 but `output_hc_norm`, F32);
|
||||
shard 3 is exactly the PLE table, nothing else.
|
||||
|
||||
| File | Bytes | SHA-256 |
|
||||
| --- | --- | --- |
|
||||
| `Qwen3.8-Flash-Next-UD-Q6_K_XL-00001-of-00006.gguf` | 10,946,624 | `8bdc6bad55b6699f46f60708829932ff4018d7ac9bd309c388534fa31753bdb7` |
|
||||
| `Qwen3.8-Flash-Next-UD-Q6_K_XL-00002-of-00006.gguf` | 682,434,912 | `494ca4ed3dbf97bc28da88af3890b8877b9032f909812d00c0526a9ca5e91d2e` |
|
||||
| `Qwen3.8-Flash-Next-UD-Q6_K_XL-00003-of-00006.gguf` | 54,400,261,312 | `34efd79a80a1ce540a517a5d56171924b66ce1c38b04c904f17ad6d8ef17cf20` |
|
||||
| `Qwen3.8-Flash-Next-UD-Q6_K_XL-00004-of-00006.gguf` | 49,814,108,512 | `0ea4b599880a5a52fcd9c188ba1443f646c69c96571904ddbd6a79246a01a997` |
|
||||
| `Qwen3.8-Flash-Next-UD-Q6_K_XL-00005-of-00006.gguf` | 49,419,317,344 | `9948e81ae8368144b135a7403d8c0f2cf9a6c2f9f692313865855b2832e696c9` |
|
||||
| `Qwen3.8-Flash-Next-UD-Q6_K_XL-00006-of-00006.gguf` | 14,838,313,984 | `e4ae255234f42b18012f6d6eec5bf615b57479b7e0a0233c47436a321ffeec3f` |
|
||||
|
||||
Formats, read from the shard headers with `tools/gguf_reader.py` (all six shards verified: shard 4 = blocks 0-20,
|
||||
520 tensors, shard 5 = blocks 20-41, 530 tensors, and per layer the types above):
|
||||
|
||||
- **Routed experts:** `ffn_gate_exps` and `ffn_up_exps` are Q6_K (seen in every layer of shard 6: blocks 41-47);
|
||||
`ffn_down_exps` is Q8_0 (per layer 891.3 MB at 640×2560×512, against Q6_K's 688.1 MB at 2560×640×512 for gate or
|
||||
up). Gate and up are Q6_K in 47 of 48 layers and Q8_0 in layer 2; down is Q8_0 in every layer (the pack's
|
||||
`native_experts.txt`: 47 rows of ggml types 14/8, and layer 2 is 8/8). Q6_K gate/up is native: the
|
||||
engine multiplies them directly on the GPU in every path check below, and the CPU has the ggml block layout (210
|
||||
bytes per 256 elements); the per-role layout in `native_experts.txt` v4 handles shards whose boundaries cut
|
||||
through a layer like UD-Q4_K_XL's layer 11 case does.
|
||||
- **Everything else:** attention and shared-expert projections, SSM, hyper-connections, the PLE key/value, the token
|
||||
embedding and the output head are Q8_0, or F32/BF16 where the engine reads floats — the same pattern
|
||||
[UD-Q4_K_XL](UNSLOTH_Q4.md) handles with `tools/iq_pack.py --compat-bf16`.
|
||||
- **The PLE table** (`per_layer_token_embd.weight`, [160, 320001536]): **Q8_0**, its own shard 3. A row is
|
||||
5 blocks of 32 elements at 34 bytes = 170 bytes, and the whole table is 54.4 GB, against the 28.8 GB IQ4_NL of
|
||||
UD-Q4_K_XL. Engine 0.1.38 reads Q8_0 rows natively: `PleTable` accepts the type, the row reader is already
|
||||
parameterized by bytes-per-row (its selftest runs 90, 110 and 170 byte rows), and no GPU kernel changes —
|
||||
`gather` outputs floats. (UD-Q4_K_XL's table is IQ4_NL; the FP8 table of [tools/ple_fp8_pack.py](../tools/ple_fp8_pack.py)
|
||||
needs the original checkpoint, which this file is not a substitute for. Converting this Q8_0 table to FP8 would
|
||||
trade table precision away for 6% smaller rows; Q8_0 read directly has none that conversion's error.)
|
||||
|
||||
Q6_K gate/up costs 210 bytes per 256 elements, against Q4_K's 144, so the routed experts' share of the file grows
|
||||
about 45% per element read; expected decode speed accordingly lower than UD-Q4_K_XL's on the same hardware. The
|
||||
table row gather grows 170/90 = 1.89x over IQ4_NL.
|
||||
|
||||
## The pack (by hand)
|
||||
|
||||
Same workflow as [UD-Q4_K_XL](UNSLOTH_Q4.md#the-pack-by-hand), from the repository root:
|
||||
|
||||
```sh
|
||||
.venv/bin/python tools/iq_pack.py \
|
||||
--gguf /path/to/Qwen3.8-Flash-Next-UD-Q6_K_XL-00001-of-00006.gguf \
|
||||
--out packs/ud-q6_k_xl --compat-bf16
|
||||
```
|
||||
|
||||
**`--compat-bf16` is required** for the same reason as there (the 195 small Q8_0 projections the engine reads as
|
||||
BF16). Do not add `--experts-bin`; the experts stay in the GGUF files. Built here end to end on this file
|
||||
(`packs/ud-q6_k_xl`, the packer's own tests cover the shard and conversion logic via
|
||||
`.venv/bin/python -m unittest discover -s tools -p test_iq_pack.py`): 1079 tensors indexed, 303 served natively, 460
|
||||
tensors converted (264 exact, 196 rounded, max abs error 0.0144 - the same profile as UD-Q4_K_XL's), 195
|
||||
compat-BF16 tensors (1.20 GiB), arena 1.38 GiB, and `native_experts.txt` v4 with the per-role shard column on
|
||||
layers 20 and 41.
|
||||
|
||||
## The server
|
||||
|
||||
Like UD-Q4_K_XL's [config](UNSLOTH_Q4.md#the-server), with the Q6 paths and names. `--ple-gguf` is not needed: the
|
||||
engine finds shard 3 by the table's name. On a machine whose RAM holds the ~110 GB of routed experts plus the rest
|
||||
(499 GB total, ~325 GB free, measured on the machine this was written on), there is no RAM budget and no
|
||||
`--expert-cache` gymnastics: drop `--resident-budget-gib` and `--expert-cache-device1` from the Q4 config. The
|
||||
54.4 GB table is mmap'd (the default) — do not switch it to `--ple-io ram` unless the RAM really is there to lock
|
||||
it. `mtp/rt` is the base model's draft layer as always.
|
||||
|
||||
```json
|
||||
{
|
||||
"exe": "build/strata",
|
||||
"args": [
|
||||
"--pack", "packs/ud-q6_k_xl",
|
||||
"--native", "/path/to/Qwen3.8-Flash-Next-UD-Q6_K_XL-00001-of-00006.gguf",
|
||||
"--prefill", "auto", "--spec", "4", "--spec-min-p", "0.5",
|
||||
"--mtp", "mtp/rt", "--max-context", "8192"
|
||||
],
|
||||
"cwd": ".",
|
||||
"tokenizer": "packs/ud-q6_k_xl/tokenizer",
|
||||
"model_name": "qwen3.8-flash-next-ud-q6_k_xl",
|
||||
"log": "strata-ud-q6_k_xl.log",
|
||||
"host": "127.0.0.1",
|
||||
"port": 8080
|
||||
}
|
||||
```
|
||||
|
||||
On 64 GB machines treat the Q4 numbers below as the starting point: the RAM budget matters the same way, and
|
||||
everything it leaves out comes from the SSD, faster now per expert (Q6_K) and per table row (170 bytes).
|
||||
|
||||
## What the engine did not have, and has now
|
||||
|
||||
UD-Q4_K_XL's expert formats (gate/up Q4_K, down Q5_1) were in every path; Q6_K gate/up was not. This branch adds
|
||||
it (the PLE Q8_0 reader is the other half):
|
||||
|
||||
- **Decode windows:** the grouped expert kernels (`native_expert_grouped`) take gate/up Q6_K — a pinned copy of
|
||||
llama.cpp's `vec_dot_q6_K_q8_1` behind `Fmt<14>`, and the dequantizer (`dequantize_q6_K` folded onto the kernel's
|
||||
32 threads). Per-token MMVQ (`native_mmvq`) already had Q6_K.
|
||||
- **The prompt path:** llama.cpp's MMQ prompt kernels now instantiate Q6_K (`STRATA_MMQ_KQUANTS=ON` builds
|
||||
`template-instances/mmq-instance-q6_k.cu` alongside q4_k/q5_k/q5_1; `moe_mmq` accepts and dispatches it).
|
||||
- **The gates:** `native_expert_supported` took Q6_K/Q8_0 pairs (gate/up Q8_0 as in layer 2 was already in the
|
||||
lists) — the engine previously refused the whole pack with "no GPU kernels for".
|
||||
|
||||
Tests: `native_grouped_parity` loops Q6_K/Q8_0 pairs; `prefill_mmq_kquant_test` runs Q6_K gate/up products at
|
||||
1280×2560 against a double-precision reference; `native_expert_parity --synthetic q6_K/q8_0` checks GPU and CPU
|
||||
against ggml's dequantized weights. All three are built (CUDA, `86;120`). They have not been run on a GPU: the
|
||||
card was serving the file below.
|
||||
|
||||
## Status: what is verified and what is not
|
||||
|
||||
Verified here (test clone, Linux, NVIDIA GPU, source build):
|
||||
|
||||
- The six files' sizes and SHA-256 (HF tree API; shards 1, 2, 6 additionally against the downloaded blobs).
|
||||
- The tensor inventory of shards 1, 2 and 6, read from the headers (`tools/gguf_reader.py`); shards 4 and 5's
|
||||
tensor counts and per-layer expert types, from the pack's `conversions.json` and `native_experts.txt` v4.
|
||||
- Q8_0 PLE rows: `ple_reader_test --selftest` runs 90, 110 and 170-byte rows (all OK); `ple_q8_parity` on a
|
||||
synthetic Q8_0 table (`tools/ple_q8_fixture.py`) matches ggml's reference dequantizer bit for bit through both
|
||||
Mmap and Direct (`max_abs 0.000e+00`). Both still pass on this branch with the Q6_K expert work in.
|
||||
- The pack on the finished file (numbers above), and the engine binary builds (CUDA `86;120`, MMQ K-quants on).
|
||||
- Q6_K expert source support: decode-side MMVQ was already there; the grouped verify-window kernels, the MMQ
|
||||
prompt kernels and the `native_expert_supported` gate were not, and are in this branch (section above).
|
||||
|
||||
The server has run this file. On 2026-10-04, Linux, an RTX PRO 4500 Blackwell (compute capability 12.0) plus a
|
||||
second GPU, a Threadripper PRO 5975WX (AVX2, no AVX-512), 30 expert-pool workers and 499 GB of RAM, engine 0.1.39
|
||||
loaded 101.75 GiB of experts (8.43 GiB/s) and served one review. The cold read was 14,915 tokens at 3,176 tok/s.
|
||||
Fourteen turns generated 5,799 tokens in 74.1 s (78.3 tok/s) and finished at 44,160 tokens of context; drafts were
|
||||
accepted 3,485 of 4,406 (79.1%). The same machine on engine 0.1.38, before this branch was rebased onto 0.1.39,
|
||||
read 249,193 tokens cold at 2,802 tok/s and then generated 1,079 tokens at 69.0 tok/s. Quality against llama.cpp
|
||||
on this file has not been measured, so the page stays experimental. The three Q6_K parity programs above are the
|
||||
remaining GPU check.
|
||||
@@ -109,6 +109,22 @@ inline void dequantize_q5_0(const uint8_t* block, float* out) {
|
||||
out[j + 16] = (float)(x1 - 16) * d;
|
||||
}
|
||||
}
|
||||
// ---- Q5_1: 32 elements from a 24-byte block: fp16 d, fp16 m, then Q5_0's qh[4] and qs[16]. The 5-bit codes are
|
||||
// unsigned and the block's minimum m is added instead of Q5_0's fixed -16 offset (dequantize_row_q5_1).
|
||||
inline void dequantize_q5_1(const uint8_t* block, float* out) {
|
||||
const float d = fp16_to_fp32(read_u16(block));
|
||||
const float m = fp16_to_fp32(read_u16(block + 2));
|
||||
const uint8_t* qh = block + 4;
|
||||
const uint8_t* qs = block + 8;
|
||||
const uint32_t h =
|
||||
(uint32_t)qh[0] | ((uint32_t)qh[1] << 8) | ((uint32_t)qh[2] << 16) | ((uint32_t)qh[3] << 24);
|
||||
for (int j = 0; j < 16; ++j) {
|
||||
const int x0 = (int)(qs[j] & 0x0F) | (int)(((h >> j) & 1u) << 4);
|
||||
const int x1 = (int)(qs[j] >> 4) | (int)(((h >> (j + 16)) & 1u) << 4);
|
||||
out[j] = (float)x0 * d + m;
|
||||
out[j + 16] = (float)x1 * d + m;
|
||||
}
|
||||
}
|
||||
// ---- IQ4_NL: 32 elements from an 18-byte block. Non-linear 4-bit codebook, NOT a linear grid, so
|
||||
// the table must be exact. Copied verbatim from ggml-common.h `kvalues_iq4nl` at 3cf03257:
|
||||
// -127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// include/strata/core/arch_defaults.hpp - the per-architecture defaults of switches that are exact everywhere.
|
||||
//
|
||||
// Every gfx1151 (Strix Halo, RDNA3.5, unified memory) switch in the table was measured on Aurora (experiments/s23 .. s26,
|
||||
// the c-best3-v4.2 config) with BYTE-IDENTICAL output against the same engine with the switch off: the same token ids at
|
||||
// 4K / 32K / 64K and on four models, and a bitwise harness per kernel. They are therefore on by default on that
|
||||
// architecture and nowhere else: a CUDA build and every other HIP architecture run the code they always ran.
|
||||
// Switches that change bits (STRATA_PF_GEMM, STRATA_PF_FUSED, STRATA_HC_UPMIX, STRATA_PA_FAST, STRATA_HIP_WMMA,
|
||||
// STRATA_SELECT_WMMA, STRATA_HC_Q8: rounding-level, KL-gated) stay opt-in; docs/STRIX_HALO.md lists them.
|
||||
//
|
||||
// A switch the user set (to anything) is never overridden; STRATA_GFX1151_DEFAULTS=0 turns the whole table off.
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace strata::core {
|
||||
|
||||
/// The environment settings that are the defaults of `gcn_arch` (hipDeviceProp_t::gcnArchName; "gfx1151" or
|
||||
/// "gfx1151:sramecc-:xnack-"): empty for every other architecture.
|
||||
std::vector<std::pair<std::string, std::string>> arch_default_env(const char* gcn_arch);
|
||||
|
||||
/// Sets `arch_default_env(gcn_arch)` 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. Call once, before any engine code reads its
|
||||
/// switches (they are read lazily, at first use).
|
||||
std::vector<std::string> apply_arch_defaults(const char* gcn_arch);
|
||||
|
||||
} // namespace strata::core
|
||||
@@ -53,13 +53,94 @@ struct ConversationKvReuse {
|
||||
std::vector<ConversationKv> kv;
|
||||
// Original image extent for validation, and the earliest subsequent rewrite.
|
||||
int64_t captured_tokens = 0, unchanged_tokens = 0;
|
||||
// With a layer split: the later stages' own retained K/V, one per stage, same extents (empty: none)
|
||||
std::vector<ConversationKvReuse> stages;
|
||||
size_t bytes() const {
|
||||
size_t n = kv.capacity() * sizeof(ConversationKv);
|
||||
size_t n = kv.capacity() * sizeof(ConversationKv) + stages.capacity() * sizeof(ConversationKvReuse);
|
||||
for (const auto& layer : kv) n += layer.bytes();
|
||||
for (const auto& s : stages) n += s.bytes();
|
||||
return n;
|
||||
}
|
||||
};
|
||||
|
||||
/// Layer-split parking: each running checkpoint holds the first stage's state and, in `stage_parts`, one part per
|
||||
/// later stage; a parked conversation keeps them in one image per stage. `split` MOVES the checkpoints apart (no
|
||||
/// running state is copied): the complete ones (a part for each of `stages` stages) into `stage0` and `parts[k]`,
|
||||
/// every part carrying the checkpoint's ids / images / LRU stamp, the others aside; `merge` puts them back in their
|
||||
/// original order. merge(split(x)) == x.
|
||||
struct ConversationCheckpointSplit {
|
||||
std::vector<ConversationCheckpoint> stage0;
|
||||
std::vector<std::vector<ConversationCheckpoint>> parts; ///< [stage][checkpoint]
|
||||
std::vector<ConversationCheckpoint> rest; ///< not split (no complete set of stage parts)
|
||||
std::vector<bool> complete; ///< per original position: in stage0/parts or in rest
|
||||
};
|
||||
|
||||
inline ConversationCheckpointSplit conversation_checkpoints_split(std::vector<ConversationCheckpoint>&& checks,
|
||||
size_t stages) {
|
||||
ConversationCheckpointSplit out;
|
||||
out.parts.resize(stages);
|
||||
out.complete.reserve(checks.size());
|
||||
size_t whole = 0;
|
||||
for (const auto& c : checks) {
|
||||
const bool complete = c.stage_parts.size() == stages;
|
||||
out.complete.push_back(complete);
|
||||
whole += complete ? 1 : 0;
|
||||
}
|
||||
out.stage0.reserve(whole);
|
||||
out.rest.reserve(checks.size() - whole);
|
||||
for (auto& parts : out.parts) parts.reserve(whole);
|
||||
// Allocate and copy identity metadata before moving any running state.
|
||||
// If allocation fails, every input checkpoint remains usable by the caller.
|
||||
for (const auto& c : checks) {
|
||||
if (c.stage_parts.size() != stages) continue;
|
||||
for (size_t k = 0; k < stages; ++k) {
|
||||
ConversationCheckpoint part;
|
||||
part.ids = c.ids; part.imgs = c.imgs; part.used = c.used;
|
||||
out.parts[k].push_back(std::move(part));
|
||||
}
|
||||
}
|
||||
size_t w = 0;
|
||||
for (auto& c : checks) {
|
||||
if (c.stage_parts.size() != stages) { out.rest.push_back(std::move(c)); continue; }
|
||||
for (size_t k = 0; k < stages; ++k) {
|
||||
auto& dst = out.parts[k][w];
|
||||
auto& src = c.stage_parts[k];
|
||||
dst.gdn = std::move(src.gdn); dst.ple = std::move(src.ple);
|
||||
dst.tails = std::move(src.tails); dst.dead = std::move(src.dead);
|
||||
dst.block_pos = std::move(src.block_pos);
|
||||
}
|
||||
c.stage_parts.clear();
|
||||
out.stage0.push_back(std::move(c));
|
||||
++w;
|
||||
}
|
||||
checks.clear();
|
||||
return out;
|
||||
}
|
||||
|
||||
/// false (and `out` empty) when the parts do not line up: a stage with another number of checkpoints, or a part
|
||||
/// whose ids differ from its checkpoint's.
|
||||
inline bool conversation_checkpoints_merge(ConversationCheckpointSplit&& split, std::vector<ConversationCheckpoint>& out) {
|
||||
out.clear();
|
||||
for (const auto& p : split.parts)
|
||||
if (p.size() != split.stage0.size()) return false;
|
||||
size_t whole = 0, other = 0;
|
||||
for (bool c : split.complete) (c ? whole : other) += 1;
|
||||
if (!split.complete.empty() && (whole != split.stage0.size() || other != split.rest.size())) return false;
|
||||
if (split.complete.empty()) split.complete.assign(split.stage0.size(), true); // a restored image: all whole
|
||||
for (size_t j = 0; j < split.stage0.size(); ++j)
|
||||
for (const auto& p : split.parts)
|
||||
if (p[j].ids != split.stage0[j].ids) return false;
|
||||
size_t w = 0, r = 0;
|
||||
for (bool c : split.complete) {
|
||||
if (!c) { out.push_back(std::move(split.rest[r++])); continue; }
|
||||
ConversationCheckpoint cp = std::move(split.stage0[w]);
|
||||
for (auto& p : split.parts) cp.stage_parts.push_back(std::move(p[w]));
|
||||
++w;
|
||||
out.push_back(std::move(cp));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
struct SavedConversation {
|
||||
// Runtime compatibility only; NOT a model/weights identity or disk schema.
|
||||
std::array<int64_t, 18> geometry{};
|
||||
@@ -69,10 +150,13 @@ struct SavedConversation {
|
||||
std::vector<ConversationCheckpoint> checkpoints;
|
||||
std::vector<ConversationKv> kv; // main layers followed by the draft layer
|
||||
bool cvec = true;
|
||||
// with a layer split, the later stages' own images, one per stage, in stage order
|
||||
std::vector<SavedConversation> stage_images;
|
||||
|
||||
size_t bytes() const {
|
||||
size_t n = live.bytes() + checkpoints.capacity() * sizeof(ConversationCheckpoint) +
|
||||
kv.capacity() * sizeof(ConversationKv);
|
||||
for (const auto& s : stage_images) n += s.bytes();
|
||||
for (const auto& c : checkpoints) n += c.bytes();
|
||||
for (const auto& k : kv) n += k.bytes();
|
||||
return n;
|
||||
@@ -110,13 +194,17 @@ public:
|
||||
|
||||
// Retain only the restored K/V buffers, not duplicate running checkpoints.
|
||||
// This optimization never evicts a parked conversation to make itself fit.
|
||||
void retain(std::vector<ConversationKv>&& kv, int64_t tokens) {
|
||||
// `stage_kv`: with a layer split, the later stages' restored K/V (one per stage), retained with the first's.
|
||||
void retain(std::vector<ConversationKv>&& kv, int64_t tokens,
|
||||
std::vector<std::vector<ConversationKv>>&& stage_kv = {}) {
|
||||
reuse_ = {};
|
||||
ConversationKvReuse candidate{std::move(kv), tokens, tokens};
|
||||
ConversationKvReuse candidate{std::move(kv), tokens, tokens, {}};
|
||||
for (auto& k : stage_kv) candidate.stages.push_back(ConversationKvReuse{std::move(k), tokens, tokens, {}});
|
||||
if (enabled() && candidate.bytes() <= budget_ - bytes_) reuse_ = std::move(candidate);
|
||||
}
|
||||
void limit_reuse(int64_t first_dirty) {
|
||||
reuse_.unchanged_tokens = std::min(reuse_.unchanged_tokens, first_dirty);
|
||||
for (auto& s : reuse_.stages) s.unchanged_tokens = std::min(s.unchanged_tokens, first_dirty);
|
||||
if (reuse_.unchanged_tokens <= 0) reuse_ = {};
|
||||
}
|
||||
ConversationKvReuse take_reuse() { return std::exchange(reuse_, {}); }
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
// On-disk persistence of a conversation's state (SavedConversation): one file holding the running state, the
|
||||
// checkpoints the caller passes (the engine passes only the deepest one), every QSA layer's authoritative K/V and
|
||||
// the draft layer's K/V. Pure host code: no CUDA. Format v1 and the fail-closed read order are in
|
||||
// docs/DETAILS.md (Session files).
|
||||
//
|
||||
// Format v1 is little-endian, with fixed-width integers and IEEE-754 floats; a big-endian build is refused at
|
||||
// compile time. The hashes detect accidental corruption; they do not authenticate a file: restore trusted files only.
|
||||
#pragma once
|
||||
|
||||
#include "strata/core/conversation_cache.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace strata::core {
|
||||
|
||||
// What the file is bound to: the model files it was computed with, and the engine settings that change what the
|
||||
// saved bytes mean. A read with any other identity is refused before the payload is parsed.
|
||||
struct SessionFileIdentity {
|
||||
uint64_t model = 0, config = 0;
|
||||
};
|
||||
|
||||
// Non-cryptographic 64-bit hash with xxHash64-style rounds (NOT the standard XXH64 stream): corruption and identity
|
||||
// checks, not adversarial input. Streaming and one-shot agree.
|
||||
class SessionHasher {
|
||||
public:
|
||||
explicit SessionHasher(uint64_t seed = 0);
|
||||
void update(const void* data, size_t n);
|
||||
uint64_t digest() const;
|
||||
private:
|
||||
void block(const uint8_t* p);
|
||||
uint64_t lane_[4];
|
||||
uint8_t pending_[32];
|
||||
size_t npending_ = 0;
|
||||
uint64_t total_ = 0;
|
||||
};
|
||||
uint64_t session_hash64(const void* data, size_t n, uint64_t seed);
|
||||
|
||||
// Typed, length-delimited fields for an identity hash: each field is (tag, type, length, bytes), so two different
|
||||
// field lists never give the same byte stream, and a double is hashed by its exact bits (no decimal rounding).
|
||||
class SessionIdentityBuilder {
|
||||
public:
|
||||
explicit SessionIdentityBuilder(uint64_t seed) : hash_(seed) {}
|
||||
void str(const std::string& tag, const std::string& value);
|
||||
void i64(const std::string& tag, int64_t value);
|
||||
void u64(const std::string& tag, uint64_t value);
|
||||
void f64(const std::string& tag, double value);
|
||||
void bytes(const std::string& tag, const void* data, size_t n);
|
||||
uint64_t digest() const { return hash_.digest(); }
|
||||
private:
|
||||
void field(const std::string& tag, uint8_t type, const void* data, uint64_t n);
|
||||
SessionHasher hash_;
|
||||
};
|
||||
|
||||
// The RESOLVED rope configuration (rope_scaling.hpp, after the CLI flags and the model file's keys met). The cached
|
||||
// K is post-RoPE, so a different rotation makes the saved K mean something else.
|
||||
struct SessionRope {
|
||||
int64_t type = 0;
|
||||
double freq_base = 0, factor = 0, freq_scale = 0, orig_ctx = 0, ext_factor = 0, attn_factor = 0, beta_fast = 0,
|
||||
beta_slow = 0;
|
||||
};
|
||||
|
||||
// Every engine setting that changes what the saved state means. Sampling, seeds, draft tuning, the expert tier and
|
||||
// the prompt-reading chunk are not here: they change what is computed next, not what the saved cells hold.
|
||||
struct SessionConfig {
|
||||
std::string engine_version; // a file is bound to one engine version
|
||||
std::string backend; // "cuda" or "hip": no cross-backend restore
|
||||
std::string kv; // --kv
|
||||
int64_t max_context = 0, kv_resident = 0, mtp_window = 0;
|
||||
bool kv_rot = false; // STRATA_KV_ROT
|
||||
SessionRope rope;
|
||||
// the control vectors as loaded: a digest of the tables the engine uploaded (every file's content x its exact
|
||||
// scale, summed), the mode, the layer range and the direction; 0 when none is loaded
|
||||
uint64_t cvec = 0;
|
||||
// the arithmetic switches that change the computed state (name, value), in a fixed order
|
||||
std::vector<std::pair<std::string, int64_t>> switches;
|
||||
};
|
||||
uint64_t session_config_fingerprint(const SessionConfig& config);
|
||||
|
||||
// One model input the engine loaded and the role it plays ("native shard 1", "expert blk.3.ffn_up", ...). The role,
|
||||
// not the path, enters the fingerprint, so a moved model folder still matches. Every (role, file) pair counts: one
|
||||
// file in two roles is two entries (its bytes are read once). `optional`: a missing file is recorded as absent
|
||||
// instead of failing (a pack without experts.bin).
|
||||
struct SessionModelFile {
|
||||
std::string role, path;
|
||||
bool optional = false;
|
||||
};
|
||||
// Sampled fingerprint of the model inputs: role, size, first and last MiB of each entry, in the given order (a file
|
||||
// listed under several roles is read once). Paths are UTF-8 (wide APIs on Windows; invalid UTF-8 is refused). An
|
||||
// edit in the middle of a file that keeps its size is NOT detected: model files must not change while sessions saved
|
||||
// with them are kept. `beat` (optional) is called after each distinct file is read.
|
||||
bool session_model_fingerprint(const std::vector<SessionModelFile>& files, uint64_t& fingerprint, std::string& error,
|
||||
const std::function<void()>& beat = {});
|
||||
|
||||
// What the engine loaded, as its loaders resolved it (generate.cpp fills it). `experts`: (role, file) of the expert
|
||||
// source - the pack's experts.bin, or every expert tensor read from a GGUF in place, per layer and role, as
|
||||
// native_experts.txt resolved it (ExpertSource::model_inputs).
|
||||
struct SessionInputs {
|
||||
std::vector<std::string> native_shards, native_dense, native_head;
|
||||
std::string embedding, ple, pack, mtp;
|
||||
std::vector<std::pair<std::string, std::string>> experts;
|
||||
};
|
||||
// The fingerprint's list: every role with its file, none merged (a path in two roles is two entries).
|
||||
std::vector<SessionModelFile> session_model_inputs(const SessionInputs& inputs);
|
||||
|
||||
// Why a session file operation failed, for the caller's answer (the server maps it to an HTTP status):
|
||||
// invalid refused: not a session file, corrupt, another model or configuration, over this engine's limits, a
|
||||
// link, nothing to save (400; nothing changed)
|
||||
// storage no room on the disk: the free-space preflight, or ENOSPC / EDQUOT / EFBIG (507)
|
||||
// memory the RAM preflight refused it, or an allocation failed (503)
|
||||
// io any other failure: permissions, read/write/flush/rename errors, a missing folder (500)
|
||||
enum class SessionError { none, invalid, storage, memory, io };
|
||||
const char* session_error_name(SessionError e);
|
||||
struct SessionStatus {
|
||||
SessionError error = SessionError::none;
|
||||
// write: the new file has replaced `path` (the rename happened). Together with a failure: the file's bytes are
|
||||
// complete and flushed, but the folder flush after the rename failed, so the new name may not survive a power loss
|
||||
bool published = false;
|
||||
// write (POSIX): this filesystem cannot flush a folder (fsync EINVAL); the save succeeded without that flush
|
||||
bool dir_flush_unsupported = false;
|
||||
};
|
||||
|
||||
// Progress of a file transfer: (bytes done, bytes in the file). A write reports once when its temporary file is
|
||||
// created (0) and after every block it hands to the OS (16 MiB, or less for the last one: a small file reports too);
|
||||
// a read reports after every block it reads (16 MiB, or what is left; the first one, which holds the header, too).
|
||||
// Each report follows a completed transfer of at most 16 MiB: no step between two reports moves more.
|
||||
using SessionProgress = std::function<void(uint64_t done, uint64_t total)>;
|
||||
// A step that does not stream - one call that blocks until it is done, over far more than 16 MiB or on the OS's
|
||||
// schedule (flushing the file, the rename and folder flush) - is announced before it starts, with the bytes it
|
||||
// concerns. The engine turns this into an explicit, bounded allowance (session_phase_limit_s) for its watchdog and
|
||||
// for the server; when the allowance runs out the step is stuck.
|
||||
using SessionPhase = std::function<void(const char* phase, uint64_t bytes)>;
|
||||
// The longest a non-streaming step over `bytes` may take before both sides call it stuck: 60 s plus one second per
|
||||
// 4 MiB (a slow disk or network folder at 4 MiB/s), at most an hour. 1.2 GB: 345 s.
|
||||
int64_t session_phase_limit_s(uint64_t bytes);
|
||||
|
||||
// Optional knobs of a write.
|
||||
struct SessionWriteOptions {
|
||||
// the free-space PREFLIGHT: refused when the disk has less than the new file plus this many bytes free before the
|
||||
// write starts. Not a reservation: other writers can take the space afterwards.
|
||||
uint64_t min_free_bytes = 0;
|
||||
bool durable = true; // flush the file before the rename and (POSIX) the folder after it
|
||||
SessionProgress progress;
|
||||
// "flush" (the file's flush, `bytes` = the file) and "publish" (rename and folder flush), before each starts
|
||||
SessionPhase phase;
|
||||
// tests: return an errno value to make a step fail ("write", "file_flush", "rename", "dir_flush"), 0 to go on
|
||||
std::function<int(const char* step)> fault;
|
||||
};
|
||||
|
||||
// Writes a temporary file beside `path` (a unique hidden name, created exclusively: never an existing file or link;
|
||||
// mode 0600 on POSIX), flushes it, renames it over `path` and, on POSIX, flushes the folder (Windows has no folder
|
||||
// flush: the rename is MoveFileExW with MOVEFILE_WRITE_THROUGH). A failure BEFORE the rename keeps an existing file
|
||||
// at `path` and removes only this call's own temporary file. A failure of the folder flush AFTER the rename returns
|
||||
// false with `status->published`: the new file is at `path` (the old one is gone), its directory entry may not
|
||||
// survive a power loss. A filesystem that cannot flush a folder (EINVAL) is not a failure; `status` says so.
|
||||
bool session_file_write(const std::string& path, const SavedConversation& image, const SessionFileIdentity& id,
|
||||
size_t& bytes, std::string& error, const SessionWriteOptions& options = {},
|
||||
SessionStatus* status = nullptr);
|
||||
|
||||
// One K/V layer read straight from where it lives (device or pinned host pool) into the file's staging buffer,
|
||||
// without a full host copy first. `read(part, offset, dst, n)` copies bytes [offset, offset+n) of part
|
||||
// 0..4 = k, v, k_scale, v_scale, pooled; after a failed read `error()` (optional) says why. The file bytes are
|
||||
// identical to writing a captured ConversationKv.
|
||||
struct SessionKvSource {
|
||||
int format = 0;
|
||||
int64_t cells = 0, heads = 0, head_dim = 0, page_size = 0, pooled_rows = 0, idx_dim = 0;
|
||||
std::array<size_t, 5> sizes{};
|
||||
std::function<bool(size_t part, size_t offset, void* dst, size_t n)> read;
|
||||
std::function<std::string()> error;
|
||||
};
|
||||
// `meta` carries everything but the K/V (meta.kv must be empty); `kv` supplies the layers in order.
|
||||
bool session_file_write(const std::string& path, const SavedConversation& meta, const std::vector<SessionKvSource>& kv,
|
||||
const SessionFileIdentity& id, size_t& bytes, std::string& error,
|
||||
const SessionWriteOptions& options = {}, SessionStatus* status = nullptr);
|
||||
|
||||
// The checkpoints worth a disk write: the deepest one (the next turn's resume point when the live tail was
|
||||
// rewritten). Earlier checkpoints only serve edits further back and cost ~118 MB each at this geometry.
|
||||
std::vector<ConversationCheckpoint> session_checkpoints_to_save(const std::vector<ConversationCheckpoint>& chain);
|
||||
// The same choice without a copy (nullptr for an empty chain).
|
||||
const ConversationCheckpoint* session_deepest_checkpoint(const std::vector<ConversationCheckpoint>& chain);
|
||||
|
||||
// What a SAVE copies into RAM besides what the engine already holds, for its RAM preflight.
|
||||
struct SessionSaveLive {
|
||||
uint64_t state_bytes = 0; // the live running state (gdn + ple + every owned QSA layer's tail, dead, block_pos)
|
||||
uint64_t tokens = 0, images = 0;
|
||||
uint64_t kv_layers = 0; // K/V sources (owned QSA layers + the draft)
|
||||
};
|
||||
// The peak, with saturating arithmetic: two copies of the deepest checkpoint (the list handed to the capture and the
|
||||
// one inside the captured image), the copied live state with its token and image lists, the K/V source directory,
|
||||
// the 16 MiB write buffer and 1 MiB for the small vectors.
|
||||
uint64_t session_save_peak_bytes(const ConversationCheckpoint* deepest, const SessionSaveLive& live);
|
||||
// The SAVE's RAM PREFLIGHT, BEFORE anything is copied: `admit` is asked for session_save_peak_bytes; only when it
|
||||
// agrees is the deepest checkpoint copied into `out`. On a refusal nothing was allocated, `out` is empty, `chain`
|
||||
// and the live state are untouched and no file was opened. Not a reservation.
|
||||
bool session_save_checkpoints(const std::vector<ConversationCheckpoint>& chain, const SessionSaveLive& live,
|
||||
const std::function<bool(uint64_t need_bytes, std::string& why)>& admit,
|
||||
std::vector<ConversationCheckpoint>& out, std::string& why);
|
||||
|
||||
// What a read checks before it allocates. Every count is checked against the bytes left in the payload and against
|
||||
// these limits BEFORE its array is allocated; the defaults accept whatever the payload can hold. The engine sets all
|
||||
// of them from its runtime (conversation_session_read_limits), and then the file size is bounded too.
|
||||
struct SessionReadLimits {
|
||||
uint64_t max_file_bytes = UINT64_MAX;
|
||||
uint64_t max_tokens = UINT64_MAX; // token and image counts of the live state and of each checkpoint
|
||||
uint64_t max_checkpoints = UINT64_MAX;
|
||||
uint64_t max_kv_layers = UINT64_MAX;
|
||||
// the runtime's geometry and layer range: a file with others is refused before its state arrays are read
|
||||
std::optional<std::array<int64_t, 18>> geometry;
|
||||
std::optional<std::pair<int64_t, int64_t>> layer_range;
|
||||
// the most bytes of each running-state array (gdn, ple, tails, dead, block_pos), live and per checkpoint
|
||||
std::array<uint64_t, 5> max_state_bytes{UINT64_MAX, UINT64_MAX, UINT64_MAX, UINT64_MAX, UINT64_MAX};
|
||||
// per K/V layer (the QSA layers, then the draft): the most bytes of each part (k, v, k_scale, v_scale, pooled).
|
||||
// A layer past the end of the list is bounded by the payload only.
|
||||
std::vector<std::array<uint64_t, 5>> max_kv_bytes;
|
||||
// the RAM PREFLIGHT: asked once the header and identity match and before any payload allocation, with what the
|
||||
// parse holds at its peak (session_read_peak_bytes); false refuses the file. Not a reservation.
|
||||
std::function<bool(uint64_t need_bytes, std::string& why)> admit;
|
||||
SessionProgress progress;
|
||||
};
|
||||
// The largest file these limits admit (UINT64_MAX when one of them is open); session_file_read applies it.
|
||||
uint64_t session_read_max_file_bytes(const SessionReadLimits& limits);
|
||||
// What the RAM preflight is asked for a file of `file_bytes`: the parsed image (about the payload), the 16 MiB read
|
||||
// buffer, and the per-allocation overhead of its arrays (a page per 16 MiB segment plus the small vectors).
|
||||
uint64_t session_read_peak_bytes(uint64_t file_bytes);
|
||||
// Full validation (a regular file with one link, size, header, identity, bounded parse, payload hash) before `image`
|
||||
// is replaced. The file is opened without following a symbolic link (or a reparse point) at its name, and the checks
|
||||
// are made on the opened handle. On failure `image` is unchanged and `status->error` says what kind of failure.
|
||||
bool session_file_read(const std::string& path, const SessionFileIdentity& id, SavedConversation& image,
|
||||
size_t& bytes, std::string& error, const SessionReadLimits& limits = {},
|
||||
SessionStatus* status = nullptr);
|
||||
|
||||
// The folder the free-space preflight asks about for `path`, by the rules of Windows or of POSIX (exposed so both can
|
||||
// be tested anywhere). Windows: backslashes and a trailing backslash, as GetDiskFreeSpaceExW requires for a UNC
|
||||
// folder (\\server\share\dir\) and gives a drive root (C:\); empty for a bare file name (the current folder).
|
||||
std::string session_free_space_dir(const std::string& path, bool windows_rules);
|
||||
|
||||
} // namespace strata::core
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "strata/core/conversation_cache.hpp"
|
||||
#include "strata/core/conversation_file.hpp"
|
||||
#include "strata/core/layer.hpp"
|
||||
#include "strata/core/session.hpp"
|
||||
|
||||
@@ -29,6 +30,19 @@ bool conversation_kv_restore(const ConversationKv& image, const QsaState& state,
|
||||
// fingerprints the authoritative payload only. Never changes model state.
|
||||
bool conversation_kv_verify(const ConversationKv& image, const QsaState& state, const ModelGeometry& g,
|
||||
int64_t upto, bool include_index, uint64_t& fingerprint, std::string& error);
|
||||
// Disk save without a host copy: the layer's authoritative K/V as a streamed source (same bytes and metadata as
|
||||
// conversation_kv_save). Caller synchronizes the device first and keeps the state untouched while it is read.
|
||||
bool conversation_kv_source(SessionKvSource& source, const QsaState& state, const ModelGeometry& g,
|
||||
int64_t upto, bool include_index, std::string& error);
|
||||
// The bytes of each K/V part (k, v, k_scale, v_scale, pooled) a snapshot of `upto` tokens holds: no state read.
|
||||
bool conversation_kv_part_sizes(const QsaState& state, const ModelGeometry& g, int64_t upto, bool include_index,
|
||||
std::array<uint64_t, 5>& sizes, std::string& error);
|
||||
// Disk sessions: the read limits this session can ever restore - its geometry and layer range, at most
|
||||
// min(max_tokens, its cells) tokens, `max_checkpoints` checkpoints, the exact running-state sizes and the K/V part
|
||||
// sizes at that many tokens - so session_file_read refuses an oversized or foreign file before it allocates.
|
||||
bool conversation_session_read_limits(SessionReadLimits& limits, const SessionState& session, const ModelGeometry& g,
|
||||
const QsaState& draft, uint64_t max_tokens, uint64_t max_checkpoints,
|
||||
std::string& error);
|
||||
|
||||
struct ConversationStateSizes {
|
||||
size_t gdn = 0, ple = 0, tail = 0, dead = 0, block_pos = 0;
|
||||
@@ -67,6 +81,12 @@ bool conversation_snapshot_save(SavedConversation& image, const ConversationView
|
||||
const SessionState& session, const ModelGeometry& g,
|
||||
const QsaState& draft, std::string& error,
|
||||
ConversationKvReuse reuse = {}, size_t* reused_bytes = nullptr);
|
||||
// Disk save without capturing the K/V on the host: `meta` gets everything but the K/V (running state copied,
|
||||
// checkpoints as given by the view), `sources` one streamed source per QSA layer then the draft. Caller has
|
||||
// synchronized and must not run the session until the file is written.
|
||||
bool conversation_snapshot_sources(SavedConversation& meta, std::vector<SessionKvSource>& sources,
|
||||
const ConversationView& view, const SessionState& session,
|
||||
const ModelGeometry& g, const QsaState& draft, std::string& error);
|
||||
bool conversation_snapshot_validate(const SavedConversation& image, const SessionState& session,
|
||||
const ModelGeometry& g, const QsaState& draft, std::string& error);
|
||||
enum class ConversationRestore { restored, invalid, transfer_failed };
|
||||
@@ -76,4 +96,21 @@ ConversationRestore conversation_snapshot_restore(const SavedConversation& image
|
||||
const ModelGeometry& g, const QsaState& draft,
|
||||
std::string& error);
|
||||
|
||||
// The same with `draft == nullptr`: an image WITHOUT the draft layer's K/V (kv holds the session's own QSA layers
|
||||
// only). A layer split's later stages park this way; the draft ring is saved once, with the first stage's image.
|
||||
// An image is validated and restored with the same kind of call it was saved with (a K/V layer count mismatch is
|
||||
// rejected as invalid).
|
||||
bool conversation_snapshot_bytes(const ConversationView& view, const SessionState& session, const ModelGeometry& g,
|
||||
const QsaState* draft, size_t& bytes, std::string& error);
|
||||
bool conversation_snapshot_capture_bytes(const ConversationKvReuse& reuse, const ConversationView& view,
|
||||
const SessionState& session, const ModelGeometry& g, const QsaState* draft,
|
||||
size_t& bytes, std::string& error);
|
||||
bool conversation_snapshot_save(SavedConversation& image, const ConversationView& view, const SessionState& session,
|
||||
const ModelGeometry& g, const QsaState* draft, std::string& error,
|
||||
ConversationKvReuse reuse = {}, size_t* reused_bytes = nullptr);
|
||||
bool conversation_snapshot_validate(const SavedConversation& image, const SessionState& session,
|
||||
const ModelGeometry& g, const QsaState* draft, std::string& error);
|
||||
ConversationRestore conversation_snapshot_restore(const SavedConversation& image, SessionState& session,
|
||||
const ModelGeometry& g, const QsaState* draft, std::string& error);
|
||||
|
||||
} // namespace strata::core
|
||||
@@ -0,0 +1,125 @@
|
||||
#pragma once
|
||||
// dma_batch.hpp - independent host-to-device blob copies as one submission: copy_blobs.
|
||||
//
|
||||
// The batched path is midhatn's (PR #807, MIT-compatible contribution): cudaMemcpyBatchAsync for independent
|
||||
// pinned expert uploads on CUDA 13+, with the Windows alias lookup it found necessary. eddoursul's fork (F12) uses
|
||||
// the same call for the adaptive tier's and the prefetch's copies: under WDDM a copy submitted alone raises an
|
||||
// interrupt when it lands, on the processor its GPU's interrupts go to; a batch raises one.
|
||||
//
|
||||
// Modes (STRATA_DMA_BATCH, read once; `dma_batch_mode()`):
|
||||
// 0 one cudaMemcpyAsync per blob: exactly the loop the engine always ran
|
||||
// 1 cudaMemcpyBatchAsync (CUDA 13+, groups of 2..128), the copies in any order, the batch in the stream's
|
||||
// 2 as 1, plus CUDA 13.4's cudaMemcpyFlagPreferOverlapWithCompute (a hint)
|
||||
// Every other build (HIP, CUDA before 13) and every single copy or group over 128 takes the loop. A batch the
|
||||
// runtime refuses is redone as the loop (with one warning), never dropped: readiness is signalled only for
|
||||
// uploads that were submitted.
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace strata::core {
|
||||
|
||||
/// The mode: STRATA_DMA_BATCH (0, 1, 2); unset = the engine's default (`default_mode`, set at the call site's first
|
||||
/// use; 0 until a measured win makes it 1).
|
||||
inline int dma_batch_mode(int default_mode = 0) {
|
||||
static const int mode = [default_mode] {
|
||||
const char* e = std::getenv("STRATA_DMA_BATCH");
|
||||
return e != nullptr && e[0] != '\0' ? std::atoi(e) : default_mode;
|
||||
}();
|
||||
return mode;
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
// A registered Windows host pointer may differ from its device-visible alias; the batch API needs the alias
|
||||
// (cudaMemcpyAsync(kind=HostToDevice) resolves it itself). The experts' blobs are fixed, so each is looked up once.
|
||||
inline cudaError_t host_alias(const void* p, const void** out) {
|
||||
static std::mutex mu;
|
||||
static std::unordered_map<const void*, const void*> cache;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(mu);
|
||||
const auto it = cache.find(p);
|
||||
if (it != cache.end()) { *out = it->second; return cudaSuccess; }
|
||||
}
|
||||
void* alias = nullptr;
|
||||
const cudaError_t e = cudaHostGetDevicePointer(&alias, const_cast<void*>(p), 0);
|
||||
if (e != cudaSuccess) return e;
|
||||
std::lock_guard<std::mutex> lk(mu);
|
||||
cache.emplace(p, alias);
|
||||
*out = alias;
|
||||
return cudaSuccess;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/// `n` independent copies, dst[i] <- src[i] (bytes[i] each), on `stream`. The sources are immutable pinned host
|
||||
/// blobs and the destinations disjoint; the caller keeps them alive until the stream completes. Ordering with
|
||||
/// earlier and later work on the stream is that of the copies one by one. Returns the first error (the loop
|
||||
/// continues past none: the first failing copy ends it).
|
||||
inline cudaError_t copy_blobs(void* const* dst, const void* const* src, const size_t* bytes, size_t n,
|
||||
cudaStream_t stream, int batch_mode) {
|
||||
if (n == 0) return cudaSuccess;
|
||||
#if !defined(STRATA_USE_HIP) && !defined(STRATA_HIP_GFX906) && defined(CUDART_VERSION) && CUDART_VERSION >= 13000
|
||||
constexpr size_t cap = 128;
|
||||
if (batch_mode != 0 && n > 1 && n <= cap) {
|
||||
std::array<const void*, cap> srcs{};
|
||||
bool aliased = true;
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
if (detail::host_alias(src[i], &srcs[i]) != cudaSuccess) { aliased = false; break; }
|
||||
if (aliased) {
|
||||
cudaMemcpyAttributes attr{};
|
||||
attr.srcAccessOrder = cudaMemcpySrcAccessOrderStream;
|
||||
#if CUDART_VERSION >= 13040
|
||||
if (batch_mode == 2) attr.flags = cudaMemcpyFlagPreferOverlapWithCompute;
|
||||
#endif
|
||||
size_t first = 0;
|
||||
std::array<void*, cap> dsts{};
|
||||
std::array<size_t, cap> sizes{};
|
||||
for (size_t i = 0; i < n; ++i) { dsts[i] = dst[i]; sizes[i] = bytes[i]; }
|
||||
const cudaError_t e = cudaMemcpyBatchAsync(dsts.data(), srcs.data(), sizes.data(), n, &attr, &first, 1, stream);
|
||||
if (e == cudaSuccess) return cudaSuccess;
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
warned = true;
|
||||
std::fprintf(stderr, "strata: cudaMemcpyBatchAsync failed (%s); the uploads go one copy at a time\n",
|
||||
cudaGetErrorString(e));
|
||||
}
|
||||
(void) cudaGetLastError();
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void) batch_mode;
|
||||
#endif
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const cudaError_t e = cudaMemcpyAsync(dst[i], src[i], bytes[i], cudaMemcpyHostToDevice, stream);
|
||||
if (e != cudaSuccess) return e;
|
||||
}
|
||||
return cudaSuccess;
|
||||
}
|
||||
|
||||
/// The verifier's form: `n` blobs of `bytes` each into consecutive slots of `dst`.
|
||||
inline cudaError_t copy_expert_blobs(uint8_t* dst, const uint8_t* const* src, int n, size_t bytes,
|
||||
cudaStream_t stream, int batch_mode) {
|
||||
if (n <= 0) return cudaSuccess;
|
||||
constexpr size_t cap = 128;
|
||||
if (batch_mode == 0 || (size_t) n > cap) { // the old loop, with no staging of its own
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const cudaError_t e = cudaMemcpyAsync(dst + (size_t) i * bytes, src[i], bytes, cudaMemcpyHostToDevice, stream);
|
||||
if (e != cudaSuccess) return e;
|
||||
}
|
||||
return cudaSuccess;
|
||||
}
|
||||
void* d[cap] = {}; // plain arrays: nvcc on Linux (13.0, g++) rejects std::array<T, cap> here ("template argument 2 is invalid")
|
||||
const void* s[cap] = {};
|
||||
size_t b[cap] = {};
|
||||
for (int i = 0; i < n; ++i) { d[i] = dst + (size_t) i * bytes; s[i] = src[i]; b[i] = bytes; }
|
||||
return copy_blobs(d, s, b, (size_t) n, stream, batch_mode);
|
||||
}
|
||||
|
||||
} // namespace strata::core
|
||||
@@ -0,0 +1,114 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace strata::core::detail {
|
||||
|
||||
// Non-owning views: the complement and exchange allocations retain their original
|
||||
// owners until FileExpertSource::close(). Ownership changes via slot IDs; the
|
||||
// physical buffer addresses, bytes and CUDA aliases stay in place. This first path requires uniform, fully mapped/pinned expert slots.
|
||||
class ExchangeStorage {
|
||||
public:
|
||||
struct View {
|
||||
uint8_t* host = nullptr;
|
||||
const uint8_t* device = nullptr;
|
||||
};
|
||||
|
||||
bool initialize(const std::vector<uint64_t>& offsets, uint8_t* resident_host,
|
||||
const uint8_t* resident_device, uint64_t resident_bytes,
|
||||
uint8_t* exchange_host, const uint8_t* exchange_device,
|
||||
size_t count, size_t blob_bytes, std::string& error) {
|
||||
error.clear();
|
||||
if (active()) { error = "exchange storage already initialized"; return false; }
|
||||
if (!resident_host || !resident_device || !exchange_host || !exchange_device ||
|
||||
!blob_bytes || !count || !resident_bytes || resident_bytes % blob_bytes ||
|
||||
count > std::numeric_limits<size_t>::max() / blob_bytes) {
|
||||
error = "invalid uniform mapped exchange geometry"; return false;
|
||||
}
|
||||
const size_t resident_slots = (size_t)(resident_bytes / blob_bytes);
|
||||
if (resident_slots > std::numeric_limits<size_t>::max() - count) {
|
||||
error = "exchange slot count overflow"; return false;
|
||||
}
|
||||
auto experts = std::make_unique<std::atomic<size_t>[]>(offsets.size());
|
||||
std::vector<size_t> spares(count);
|
||||
std::vector<View> buffers(resident_slots + count);
|
||||
for (size_t slot = 0; slot < resident_slots; ++slot)
|
||||
buffers[slot] = {resident_host + slot * blob_bytes, resident_device + slot * blob_bytes};
|
||||
std::vector<bool> used(resident_slots, false);
|
||||
for (size_t i = 0; i < offsets.size(); ++i) {
|
||||
const uint64_t at = offsets[i];
|
||||
experts[i].store(kAbsent, std::memory_order_relaxed);
|
||||
if (at == ~uint64_t{0}) continue;
|
||||
if (at >= resident_bytes || at % blob_bytes || used[(size_t)(at / blob_bytes)]) {
|
||||
error = "invalid or duplicate resident slot"; return false;
|
||||
}
|
||||
used[(size_t)(at / blob_bytes)] = true;
|
||||
experts[i].store((size_t)(at / blob_bytes), std::memory_order_relaxed);
|
||||
}
|
||||
for (bool present : used) if (!present) {
|
||||
error = "unassigned resident slot"; return false;
|
||||
}
|
||||
for (size_t q = 0; q < count; ++q) {
|
||||
spares[q] = resident_slots + q;
|
||||
buffers[spares[q]] = {exchange_host + q * blob_bytes, exchange_device + q * blob_bytes};
|
||||
}
|
||||
experts_ = std::move(experts);
|
||||
expert_count_ = offsets.size();
|
||||
spares_.swap(spares);
|
||||
buffers_.swap(buffers);
|
||||
blob_bytes_ = blob_bytes;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool active() const { return blob_bytes_ != 0; }
|
||||
View resident(size_t expert) const {
|
||||
if (expert >= expert_count_) return {};
|
||||
const size_t slot = experts_[expert].load(std::memory_order_acquire);
|
||||
return slot != kAbsent ? buffers_[slot] : View{};
|
||||
}
|
||||
View spare(size_t q) const { return q < spares_.size() ? buffers_[spares_[q]] : View{}; }
|
||||
|
||||
// Only after every CPU reader and H2D copy of `in` has completed, and `out`
|
||||
// has landed in spare(q). No allocation, memcpy, or CUDA operation here.
|
||||
bool commit(size_t in, size_t out, size_t q, const uint8_t* staged, size_t bytes) {
|
||||
if (in >= expert_count_ || out >= expert_count_ || q >= spares_.size() ||
|
||||
!staged || bytes != blob_bytes_) return false;
|
||||
const size_t incoming_slot = experts_[in].load(std::memory_order_acquire);
|
||||
if (incoming_slot == kAbsent || experts_[out].load(std::memory_order_acquire) != kAbsent ||
|
||||
buffers_[spares_[q]].host != staged) return false;
|
||||
// A background router lookahead can query residency to skip redundant
|
||||
// file prefetches. Publish one atomic slot ID; the pointer/alias pair is
|
||||
// immutable. Actual compute readers still obey the completion contract.
|
||||
experts_[out].store(spares_[q], std::memory_order_release);
|
||||
spares_[q] = incoming_slot;
|
||||
experts_[in].store(kAbsent, std::memory_order_release);
|
||||
++exchanges_;
|
||||
avoided_bytes_ += bytes;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t exchanges() const { return exchanges_; }
|
||||
uint64_t avoided_bytes() const { return avoided_bytes_; } // memcpy payload, not read+write total
|
||||
void clear() {
|
||||
experts_.reset(); expert_count_ = 0; buffers_.clear(); spares_.clear();
|
||||
blob_bytes_ = 0; exchanges_ = avoided_bytes_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr size_t kAbsent = std::numeric_limits<size_t>::max();
|
||||
std::unique_ptr<std::atomic<size_t>[]> experts_;
|
||||
size_t expert_count_ = 0;
|
||||
std::vector<View> buffers_; // immutable physical addresses after initialize
|
||||
std::vector<size_t> spares_; // touched only by the serialized cache manager
|
||||
size_t blob_bytes_ = 0;
|
||||
uint64_t exchanges_ = 0, avoided_bytes_ = 0;
|
||||
};
|
||||
|
||||
} // namespace strata::core::detail
|
||||
@@ -16,21 +16,20 @@
|
||||
// * `h_layer` held-out is **0.0456** - only 4.6% of (layer, token) pairs have all ten experts resident - so
|
||||
// this cannot be a per-layer grouped kernel and the three-way split is not an optimisation.
|
||||
//
|
||||
// **WHAT THIS FILE IS AND IS NOT, TODAY.** It is the SLOT STORAGE and the RESIDENCY TABLE: it allocates the
|
||||
// VRAM, fills it from the host arena, and answers `(layer, expert) -> slot or -1`. It does **not** yet compute
|
||||
// anything - `moe_hit_grouped_s2` does not exist and the hit/miss split is not wired into the graph - so with
|
||||
// the cache on and nothing consuming it, **the engine is slower by the fill cost and faster by nothing.**
|
||||
// That is stated here rather than discovered from a benchmark, and it is why `--expert-cache` defaults to 0.
|
||||
//
|
||||
// The order of work is `Memory/R4-design-note.md` §7: slots and residency first, then the split, then the
|
||||
// kernel. This is that first step, and the step it unblocks is the one that can be measured.
|
||||
// **WHAT THIS FILE IS.** The SLOT STORAGE and the RESIDENCY TABLE: it allocates the VRAM, fills it from the host
|
||||
// arena, and answers `(layer, expert) -> slot or -1`. It computes nothing itself: the GPU computes the experts it
|
||||
// holds in the verify window's grouped kernels and the prompt path (the hit/miss split), and the CPU pool the rest.
|
||||
// (Written when nothing consumed the slots yet and `--expert-cache` defaulted to 0; setup's configs use `auto`.)
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "strata/core/vmm.hpp"
|
||||
|
||||
namespace strata::core {
|
||||
|
||||
/// Where one expert's VRAM lives, or -1. `int32_t` and not a bool because the slot index is what the kernel
|
||||
@@ -73,6 +72,12 @@ std::vector<std::pair<int32_t, int32_t>> rank_learned_profile(int64_t n_layers,
|
||||
bool write_expert_profile(const std::string& path, int64_t n_layers, int64_t n_expert,
|
||||
const std::vector<std::pair<int32_t, int32_t>>& ranked, std::string& err);
|
||||
|
||||
/// Free device memory for the expert cache. On a unified memory system - one memory for the CPU and the GPU, such as
|
||||
/// the DGX Spark (GB10); CUDA reports it as cudaDevAttrIntegrated - cudaMemGetInfo counts only MemFree, not the page cache - which, with the experts read from the GGUF in place, is mostly those
|
||||
/// files' clean pages and gives way to a device allocation. There the figure is MemAvailable less 6 GiB for the OS
|
||||
/// and the engine's host side (STRATA_UMA_HEADROOM_GIB). Elsewhere it is cudaMemGetInfo's.
|
||||
size_t device_free_bytes();
|
||||
|
||||
class ExpertCache {
|
||||
public:
|
||||
ExpertCache() = default;
|
||||
@@ -93,13 +98,44 @@ public:
|
||||
void close();
|
||||
|
||||
bool valid() const { return base_ != nullptr; }
|
||||
int64_t slots() const { return slots_; }
|
||||
/// The slots usable now: all of them, unless `shrink` gave the tail's VRAM back (#533) - then the ones wholly
|
||||
/// inside the VRAM still backed, always a prefix. The prompt path's loan (the last slots) and every report
|
||||
/// follow this; `full_slots()` is what `open` allocated.
|
||||
int64_t slots() const { return live_slots_; }
|
||||
int64_t full_slots() const { return slots_; }
|
||||
/// Slots actually claimed. Not the same as `slots()` - the cache does not evict, so a run that routes
|
||||
/// fewer distinct experts than there are slots leaves the rest empty.
|
||||
int64_t resident() const { return per_layer_ ? admitted_ : next_free_; }
|
||||
int64_t bytes() const { return off_.empty() ? slots_ * blob_ : (int64_t) off_.back(); }
|
||||
/// The bytes of `slots()` (all of the arena unless shrunk); `full_bytes()`: of every slot.
|
||||
int64_t bytes() const { return slot_end(live_slots_); }
|
||||
int64_t full_bytes() const { return slot_end(slots_); }
|
||||
double gib() const { return (double) bytes() / 1073741824.0; }
|
||||
|
||||
/// **#533: HOT VRAM RESIZE, OPT-IN (--vram-elastic).** With a segment size set before `open`/`open_sized`, the
|
||||
/// arena is one reserved address range backed by physical segments of that size (CUDA virtual memory
|
||||
/// management) instead of one cudaMalloc, so `shrink` can give the tail's VRAM back to the driver - for another
|
||||
/// program - and `grow` can take it again, while every slot keeps its address (the captured graphs, the verify
|
||||
/// plan's pointers and the prompt path's loan all hold addresses into it). 0 (the default): one cudaMalloc, as
|
||||
/// always. CUDA only: a HIP build refuses it at open. The slots past `slots()` after a shrink must hold no
|
||||
/// resident expert and no loan: the CALLER evicts them first (they become CPU misses).
|
||||
void set_segment_bytes(int64_t seg_bytes) { seg_req_ = seg_bytes > 0 ? seg_bytes : 0; }
|
||||
bool segmented() const { return !segs_.empty(); }
|
||||
int64_t segment_bytes() const { return seg_; }
|
||||
/// Bytes of the arena backed by VRAM now (a whole number of segments; the arena when not segmented).
|
||||
int64_t mapped_bytes() const;
|
||||
/// Unmaps every segment past the first `keep_bytes` (rounded UP to a segment boundary): `slots()` becomes the
|
||||
/// slots wholly inside what stays. Waits for the device first. False with `err` when not segmented or a driver
|
||||
/// call failed.
|
||||
bool shrink(int64_t keep_bytes, std::string& err);
|
||||
/// Maps segments again up to `want_bytes` (rounded DOWN to a segment, at most the arena; the last, shorter
|
||||
/// segment only when `want_bytes` covers the arena). Stops at the first segment the driver cannot back (false,
|
||||
/// `err`: what was mapped by then stays). The new slots are empty until the caller fills them.
|
||||
bool grow(int64_t want_bytes, std::string& err);
|
||||
/// The number of leading slots that fit wholly inside the first `bytes` bytes.
|
||||
int64_t slots_within(int64_t bytes) const;
|
||||
/// The bytes the first `n` slots span.
|
||||
int64_t bytes_of(int64_t n) const { return slot_end(n < 0 ? 0 : n > slots_ ? slots_ : n); }
|
||||
|
||||
/// `(layer, expert)` -> slot index, or `kNotResident`. Bounds-checked: a bad layer or expert returns
|
||||
/// `kNotResident` rather than reading whatever is adjacent in the table.
|
||||
int32_t slot_of(int64_t layer, int64_t expert) const;
|
||||
@@ -108,6 +144,13 @@ public:
|
||||
/// sweep) and a placeholder policy would set the hit rate that everything downstream is then sized against.
|
||||
int32_t admit(int64_t layer, int64_t expert);
|
||||
|
||||
/// Publish a same-layer replacement after its slot copy has completed.
|
||||
void replace(int64_t layer, int32_t old_expert, int32_t new_expert) {
|
||||
auto& old = residency_[(size_t) layer * n_expert_ + old_expert];
|
||||
residency_[(size_t) layer * n_expert_ + new_expert] = old;
|
||||
old = kNotResident;
|
||||
}
|
||||
|
||||
/// **R4.2g: GIVE EACH LAYER ITS OWN SLOTS. ROUND 328 MEASURED WHY THE GLOBAL FORM CANNOT WORK.**
|
||||
///
|
||||
/// `admit`'s original policy hands out slots in ARRIVAL ORDER from one counter shared by every layer
|
||||
@@ -158,13 +201,32 @@ public:
|
||||
/// Slots filled so far, for the startup report.
|
||||
int64_t fills() const { return fills_; }
|
||||
|
||||
/// The elastic K/V (--kv-grow): the arena in a VMM range (vmm.hpp) instead of one cudaMalloc, so the K/V can take
|
||||
/// single chunks of it and give them back. Applies to the next `open`; ignored where VMM is not available.
|
||||
static void set_vmm(bool enabled);
|
||||
/// The arena's range (null: one cudaMalloc).
|
||||
VmmRange* vmm_range() { return vmm_.get(); }
|
||||
/// Byte offset of slot `s` in the arena (s == slots(): the end).
|
||||
uint64_t slot_offset(int64_t s) const { return off_.empty() ? (uint64_t) s * (uint64_t) blob_ : off_[(size_t) s]; }
|
||||
|
||||
private:
|
||||
#if defined(STRATA_USE_HIP)
|
||||
bool ensure_blocking_staging(std::size_t bytes, std::string& err);
|
||||
uint8_t* blocking_staging_ = nullptr;
|
||||
std::size_t blocking_staging_bytes_ = 0;
|
||||
#endif
|
||||
int64_t slot_end(int64_t n) const { return off_.empty() ? n * blob_ : (int64_t) off_[(size_t) n]; }
|
||||
bool open_segmented(uint64_t want, std::string& err);
|
||||
void release_segmented();
|
||||
uint8_t* base_ = nullptr;
|
||||
int64_t live_slots_ = 0; ///< #533: slots() - all of them unless shrunk
|
||||
int64_t seg_req_ = 0; ///< #533: the segment size asked for (0: one cudaMalloc)
|
||||
int64_t seg_ = 0; ///< #533: the segment size used (a multiple of the driver's granularity)
|
||||
uint64_t reserved_ = 0; ///< #533: the reserved address range's size
|
||||
std::vector<unsigned long long> segs_; ///< #533: each segment's physical handle (0: unmapped)
|
||||
std::vector<int64_t> seg_size_; ///< #533: each segment's size (the last one may be shorter)
|
||||
int64_t mapped_segs_ = 0; ///< #533: segments [0, mapped_segs_) are backed
|
||||
std::unique_ptr<VmmRange> vmm_; ///< the arena's range when it is in VMM (set_vmm)
|
||||
std::vector<int32_t> residency_; ///< [n_layers * n_expert] -> slot or kNotResident
|
||||
int64_t slots_ = 0;
|
||||
int64_t n_layers_ = 0;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "strata/core/expert_cache.hpp"
|
||||
#include "strata/core/exchange_storage.hpp"
|
||||
#include "strata/core/hit_hook.hpp"
|
||||
#include "strata/kernels/cpu/pool.hpp"
|
||||
|
||||
@@ -65,6 +66,27 @@ struct CgroupMemoryStat {
|
||||
/// Returns false when the required memory.stat counters were unavailable.
|
||||
bool cgroup_available_bytes(uint64_t limit, const CgroupMemoryStat& stat, uint64_t& bytes);
|
||||
|
||||
/// #633: the RAM this process can get. `available`: MemAvailable (Windows: the available physical memory), lowered
|
||||
/// to the room under the tightest cgroup limit; `cgroup_limit`: that tightest limit itself (v2 memory.max of the group
|
||||
/// and its ancestors, or v1 memory.limit_in_bytes), ~0 when there is none - what a container can never exceed.
|
||||
struct HostMemory {
|
||||
uint64_t available = 0;
|
||||
uint64_t cgroup_limit = ~uint64_t{0};
|
||||
uint64_t commit = ~uint64_t{0}; ///< available Windows commit capacity; ~0 when not reported
|
||||
};
|
||||
|
||||
/// Linux reads `meminfo`, `self_cgroup` and the cgroup tree under `cgroup_root` (the parameters are for tests; the
|
||||
/// defaults are the real files). cgroup v2 as before (an unreadable limit of a group that has one fails); cgroup v1's
|
||||
/// memory controller (`<root>/memory/<path>`: memory.limit_in_bytes - memory.usage_in_bytes); no cgroup line at all is
|
||||
/// MemAvailable alone. False when the RAM cannot be determined.
|
||||
bool host_available_memory(HostMemory& m, const std::string& meminfo = "/proc/meminfo",
|
||||
const std::string& self_cgroup = "/proc/self/cgroup",
|
||||
const std::string& cgroup_root = "/sys/fs/cgroup");
|
||||
|
||||
/// 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);
|
||||
|
||||
/// Build compact offsets for experts absent from both the primary GPU cache and an optional second GPU tier.
|
||||
/// Kept CPU-only so selection and byte accounting can be tested without initializing a GPU.
|
||||
bool make_cache_complement_plan(
|
||||
@@ -122,6 +144,11 @@ public:
|
||||
virtual void begin_layer(int64_t layer, const int32_t* ids, int64_t k) { (void) layer; (void) ids; (void) k; }
|
||||
/// Plan v0.3 P6: the DEVICE address of a pinned, mapped blob (the GPU can read it over PCIe), or null.
|
||||
virtual const uint8_t* device_alias(int64_t layer, int64_t expert) const { (void) layer; (void) expert; return nullptr; }
|
||||
/// A file-backed source: start reading this expert's pages now (it will be needed by the CPU); no-op elsewhere.
|
||||
virtual void prefetch(int64_t layer, int64_t expert) { (void) layer; (void) expert; }
|
||||
/// A file-backed source: this expert lives in VRAM, so its pages need not stay in RAM - hand them back to the
|
||||
/// kernel (a later read re-reads the file; no result depends on it). Returns the bytes released; 0 elsewhere.
|
||||
virtual uint64_t release(int64_t layer, int64_t expert) { (void) layer; (void) expert; return 0; }
|
||||
/// Whether the verify window may give the GPU a PCIe share of this layer's misses at all (each expert is still
|
||||
/// checked with `pinned`). The arena answers per layer through its expert 0; the resident RAM mode's compact
|
||||
/// copy has no expert 0 when the GPU cache holds it, so it answers for the whole copy.
|
||||
@@ -132,9 +159,17 @@ public:
|
||||
/// stays valid for the layer it was asked in and the next one or two; a consumer that keeps a blob longer (the
|
||||
/// prompt path's stager queues a whole chunk) copies it with `copy_blob` instead.
|
||||
virtual bool transient(int64_t layer, int64_t expert) const { (void) layer; (void) expert; return false; }
|
||||
/// Disk sessions: the files this source read its experts from, as (role, path), resolved by the loader itself
|
||||
/// - the pack's experts.bin, or every GGUF tensor native_experts.txt named, per layer and role
|
||||
/// ("expert blk.L.ffn_up"). Filled by open(); empty before.
|
||||
const std::vector<std::pair<std::string, std::string>>& model_inputs() const { return inputs_; }
|
||||
/// The blob's bytes into `dst` (blob_bytes(layer) of them). Safe from several threads for a source whose
|
||||
/// `transient` can be true.
|
||||
virtual bool copy_blob(int64_t layer, int64_t expert, uint8_t* dst);
|
||||
/// Asks the OS for these pairs' bytes ahead of the `blob` calls that read them. False (nothing asked) by default.
|
||||
virtual bool advise_pairs(const std::pair<int32_t, int32_t>* pairs, int64_t n) const {
|
||||
(void) pairs; (void) n; return false;
|
||||
}
|
||||
/// The `n` experts of `layer` the CPU is about to ask `blob` for, all at once: a source that reads a file may
|
||||
/// fetch them in parallel. The bytes `blob` then returns are the same. Default: nothing.
|
||||
virtual void prefetch(int64_t layer, const int64_t* experts, int64_t n) { (void) layer; (void) experts; (void) n; }
|
||||
@@ -143,6 +178,11 @@ public:
|
||||
virtual void warm(int64_t layer, const int64_t* experts, int64_t n) { (void) layer; (void) experts; (void) n; }
|
||||
/// Whether `warm` does anything (the predictor is not run otherwise).
|
||||
virtual bool warms() const { return false; }
|
||||
protected:
|
||||
/// What model_inputs() returns; set by a source's open().
|
||||
void record_inputs(const std::string& pack_experts, const std::string& gguf,
|
||||
const strata::kernels::cpu::ExpertLayout& lay, bool from_gguf);
|
||||
std::vector<std::pair<std::string, std::string>> inputs_;
|
||||
};
|
||||
|
||||
/// CS-T, routing-aware prefetch of the file tier: when the CPU pool starts layer `l`, a worker thread applies layer
|
||||
@@ -331,6 +371,9 @@ struct ExpertDispatch {
|
||||
GpuPlanSink* plan = nullptr;
|
||||
int pcie_num = 0;
|
||||
int64_t pcie_experts = 0; ///< distinct experts the GPU read over PCIe in verify windows
|
||||
/// #588: routed (token, expert) entries the GPU computed from outside its cache in verify windows: read over PCIe
|
||||
/// (--pcie-frac, kind 1) or on another GPU (kind 2). In neither cache_hits nor cache_refused.
|
||||
int64_t offload_entries = 0;
|
||||
double ms_plan = 0, ms_actq = 0, ms_jobs = 0, ms_run = 0; ///< verify-window dispatch sections
|
||||
/// Plan v0.3 P6: decayed routing counts per (layer, expert) during decode (sized by the caller; empty = off),
|
||||
/// which the driver uses to swap the most-routed missing experts into the VRAM tier between rounds.
|
||||
@@ -418,15 +461,16 @@ public:
|
||||
/// working set instead. `pin = false` is ordinary pageable memory (the ROCm arm: large pinned allocations
|
||||
/// can fail there, and it is what the HIP measurements used).
|
||||
/// - `lend_from_slot` >= 0: the GPU-cache slots from there to the end are the prompt path's lend region; their
|
||||
/// experts are kept in RAM too, from the last slot down, as far as `available RAM - headroom_bytes` allows
|
||||
/// experts are kept in RAM too, from the last slot down, as far as available memory minus headroom allows
|
||||
/// (a lent slot's expert is streamed during the prompt and copied back after it).
|
||||
/// - the rest (the experts no slot holds) must fit that budget, or nothing is allocated and this returns false.
|
||||
///
|
||||
/// CS-T, `budget_bytes` > 0 (`--resident-budget-gib`): only as many of those experts as fit `budget_bytes`, taken
|
||||
/// in `rank` order (the expert profile: the hottest after the GPU cache's), are copied; the rest stay on the
|
||||
/// mapped files (the SSD tier). No lend region then (a lent slot's expert is read from the files).
|
||||
/// #467: `budget_bytes` = `kResidentWhatFits` is that path sized by the RAM alone (available minus the headroom
|
||||
/// and the #403 margin) - the soft --resident-experts mode's second try when the whole complement does not fit;
|
||||
/// Available memory is limited by both RAM and commit capacity on Windows.
|
||||
/// #467: `budget_bytes` = `kResidentWhatFits` sizes that path from available memory minus headroom and the #403
|
||||
/// margin - the soft --resident-experts mode's second try when the whole complement does not fit;
|
||||
/// false when not even one expert fits. On Windows the mapped experts leave the working set before any reading.
|
||||
static constexpr uint64_t kResidentWhatFits = ~0ull;
|
||||
bool pin_cache_complement(
|
||||
@@ -451,17 +495,34 @@ public:
|
||||
// buffer and calls `stage_exchange`: `out` is then read from that buffer, and `in` still from here (the CPU
|
||||
// computes both until the swap lands). Once the slot copy has landed, `commit_exchanges` moves `out` into
|
||||
// `in`'s place, so the copy keeps holding exactly the experts the GPU does not - with no read of the file.
|
||||
// STRATA_EXCHANGE_ROTATE=1 can instead transfer buffer ownership (uniform, fully pinned/mapped slots).
|
||||
/// Whether the compact copy holds `(layer, expert)`.
|
||||
bool has_resident(int64_t layer, int64_t expert) const;
|
||||
/// Host room for `n` evicted blobs (page-locked when possible). Idempotent for the same or a smaller `n`.
|
||||
bool reserve_exchanges(int64_t n, std::string& err);
|
||||
int64_t exchange_capacity() const { return xstage_cap_; }
|
||||
/// The exchange buffers are page-locked (cudaHostAlloc): copies to and from them are asynchronous.
|
||||
bool exchange_pinned() const { return xstage_pinned_; }
|
||||
uint8_t* exchange_buffer(int64_t q) const;
|
||||
/// Requires `has_resident(layer, in)`, `!has_resident(layer, out)` and `exchange_buffer(q)` holding out's blob.
|
||||
bool stage_exchange(int64_t layer, int64_t in, int64_t out, int64_t q);
|
||||
/// --pipeline-windows: size the exchange table now, so a `stage_exchange` on the adaptive tier's thread never
|
||||
/// reallocates it under a concurrent `blob` (the pool reads it while windows are in flight).
|
||||
void prepare_overrides() { if (override_.empty()) override_.assign((size_t) blobs_, nullptr); }
|
||||
/// After the GPU copies of every staged swap have landed. Returns how many exchanges were applied.
|
||||
int64_t commit_exchanges();
|
||||
/// `commit_exchanges` in two halves, for the asynchronous adaptive tier (--adapt-async): `commit_copies` moves
|
||||
/// every staged evicted blob into its `in`'s place in the copy (on another thread: safe once nothing computes `in`
|
||||
/// from RAM - it is resident on the GPU - while `out` is still read from its exchange buffer), then `commit_flip`
|
||||
/// (the caller's thread, between windows) points `out` there and drops the staging. Returns how many were applied.
|
||||
void commit_copies();
|
||||
int64_t commit_flip();
|
||||
/// The compact copy's blob of `(layer, expert)`, or null; not counted as a read (any thread).
|
||||
const uint8_t* resident_blob(int64_t layer, int64_t expert) const;
|
||||
int64_t exchanges() const { return exchanges_; }
|
||||
bool exchange_rotation() const { return exchange_storage_.active(); }
|
||||
uint64_t rotated_exchanges() const { return exchange_storage_.exchanges(); }
|
||||
uint64_t avoided_exchange_copy_bytes() const { return exchange_storage_.avoided_bytes(); }
|
||||
/// With the compact copy ready: blobs read from the mapped file since (what the plain mmap mode may read from
|
||||
/// the SSD). 0 in a steady resident mode; lend-region experts that did not fit the RAM count here.
|
||||
int64_t file_reads() const { return file_reads_.load(std::memory_order_relaxed); }
|
||||
@@ -475,18 +536,31 @@ public:
|
||||
double file_ms() const { return (double) file_us_.load(std::memory_order_relaxed) / 1000.0; }
|
||||
/// Threads `prefetch` reads the GGUF with (STRATA_FETCH_THREADS, default 8).
|
||||
void set_fetch_threads(int n) { fetch_threads_ = n < 1 ? 1 : n; }
|
||||
/// #286 (Windows): read the experts straight from the drive (FILE_FLAG_NO_BUFFERING, overlapped) instead of
|
||||
/// #286: read the experts straight from the drive (Windows: FILE_FLAG_NO_BUFFERING, overlapped; Linux: O_DIRECT and
|
||||
/// the kernel's asynchronous reads) instead of
|
||||
/// through the mapped files - the GGUF in place, or a pack's experts.bin - when the file cache could not keep them
|
||||
/// beside `ram_bytes` (the RAM budget), cached now or not (their mapped pages would land in the working set); see
|
||||
/// experts_unbuffered. The mapped reads' page faults are one small request each, and the pages they bring in
|
||||
/// take the RAM the budget was sized for. `why` says what decided.
|
||||
/// #577: `ram_bytes` counts up to every expert, and only the experts outside it are what the cache must keep.
|
||||
bool set_unbuffered(uint64_t ram_bytes, std::string& why);
|
||||
/// #577: the same decision once the RAM copy is built (pin_cache_complement), from the RAM it really holds and the
|
||||
/// expert bytes outside it; switches either way (startup only, nothing reading). Returns whether unbuffered.
|
||||
bool recheck_unbuffered(std::string& why);
|
||||
bool unbuffered() const { return !direct_.empty(); }
|
||||
/// Every expert's bytes (n_layers x n_expert blobs).
|
||||
uint64_t expert_bytes() const;
|
||||
/// #286, unbuffered: assembles the blobs of these pairs ahead of the `blob` calls that will ask for them (the
|
||||
/// GPU cache's fill from the profile) - one batch of reads instead of one blob at a time. At most 64 pairs.
|
||||
void prefetch_pairs(const std::pair<int32_t, int32_t>* pairs, int64_t n);
|
||||
/// Mapped reads: asks the OS for these pairs' blobs ahead (platform::advise_willneed). False when unbuffered,
|
||||
/// when read-ahead is off, or before `open`.
|
||||
bool advise_pairs(const std::pair<int32_t, int32_t>* pairs, int64_t n) const override;
|
||||
|
||||
const uint8_t* blob(int64_t layer, int64_t expert) override;
|
||||
/// Windows, opt-in with STRATA_FILE_RELEASE=1: trim the file mapping's full pages after their expert reaches
|
||||
/// VRAM. Never touches the resident complement or staging buffers. Disabled by default.
|
||||
uint64_t release(int64_t layer, int64_t expert) override;
|
||||
bool pinned(int64_t layer, int64_t expert) const override;
|
||||
const uint8_t* device_alias(int64_t layer, int64_t expert) const override;
|
||||
bool pcie_layer(int64_t layer) const override;
|
||||
@@ -504,12 +578,15 @@ public:
|
||||
/// Of the blobs the file tier read for the decode, how many had been warmed for their layer beforehand.
|
||||
int64_t warmed_hits() const { return warm_hits_.load(std::memory_order_relaxed); }
|
||||
int64_t warmed() const { return warm_count_.load(std::memory_order_relaxed); }
|
||||
/// #286: blobs an unbuffered read could not deliver, read through the mapping instead (0 when all went direct).
|
||||
int64_t direct_fallbacks() const { return direct_fallbacks_.load(std::memory_order_relaxed); }
|
||||
|
||||
/// Blobs touched, for the driver to report. With `h = 0` this is `48 * k` per token and the number is only
|
||||
/// interesting once Phase 3 makes it not so.
|
||||
int64_t reads() const override { return reads_; }
|
||||
|
||||
private:
|
||||
const uint8_t* resident_blob(size_t index) const;
|
||||
const uint8_t* mapped_blob(int64_t layer, int64_t expert) const;
|
||||
/// The blob's bytes from the mapped file(s) - experts.bin, or the three GGUF role slices - into `dst`.
|
||||
bool copy_from_files(int64_t layer, int64_t expert, uint8_t* dst) const;
|
||||
@@ -521,11 +598,12 @@ private:
|
||||
struct Fill { size_t v; int64_t layer, e; uint8_t* dst; };
|
||||
/// The claimed buffers' blobs: one overlapped batch when unbuffered, else the fetch threads' mapped copies.
|
||||
void fill_many(const std::vector<Fill>& todo);
|
||||
/// #286: the blobs from the drive, unbuffered: every role's 4 KiB-aligned window is read at once (overlapped)
|
||||
/// into this thread's aligned buffer, then copied into place. False when a read fails.
|
||||
/// #286: the blobs from the drive, unbuffered: every role's 4 KiB-aligned window is read at once (overlapped on
|
||||
/// Windows, io_submit on Linux) into this thread's aligned buffer, then copied into place. False when a read fails.
|
||||
bool read_direct(const Fill* fills, size_t n) const;
|
||||
bool open_direct(std::string& why);
|
||||
std::vector<std::string> paths_; ///< the mapped files, as maps_
|
||||
std::vector<void*> direct_; ///< #286: per file, an unbuffered overlapped handle (Windows)
|
||||
std::vector<void*> direct_; ///< #286: per file, an unbuffered handle (Windows) or O_DIRECT fd (Linux)
|
||||
std::vector<int> role_file_; ///< 3 x n_layers: index into maps_ / direct_
|
||||
/// blobs assembled in the stage buffers (`blob` hands those out): the GGUF in place, or any unbuffered source
|
||||
bool staged() const { return !role_ptr_.empty() || !direct_.empty(); }
|
||||
@@ -559,6 +637,7 @@ private:
|
||||
std::atomic<uint64_t> file_blob_bytes_{0}, file_us_{0};
|
||||
std::unique_ptr<std::atomic<uint32_t>[]> warm_stamp_; ///< per (layer, expert): epoch_ + 1 when warmed
|
||||
std::atomic<int64_t> warm_hits_{0}, warm_count_{0};
|
||||
mutable std::atomic<int64_t> direct_fallbacks_{0};
|
||||
std::unordered_map<int64_t, size_t> stage_of_;
|
||||
uint64_t stage_blob_ = 0;
|
||||
uint64_t stage_seq_ = 0;
|
||||
@@ -578,6 +657,7 @@ private:
|
||||
const uint8_t* complement_device_ = nullptr;
|
||||
uint64_t complement_bytes_ = 0;
|
||||
std::vector<uint64_t> complement_offsets_;
|
||||
detail::ExchangeStorage exchange_storage_; // authoritative when active; original arenas still own memory
|
||||
bool complement_pinned_ = false;
|
||||
bool complement_partial_ = false; ///< CS-T: only the first complement_pin_limit_ bytes are registered
|
||||
uint64_t complement_pin_limit_ = 0;
|
||||
@@ -641,10 +721,14 @@ public:
|
||||
int64_t reads() const { return reads_; }
|
||||
bool pinned(int64_t layer, int64_t expert) const override;
|
||||
const uint8_t* device_alias(int64_t layer, int64_t expert) const override;
|
||||
void prefetch(int64_t layer, int64_t expert) override;
|
||||
uint64_t release(int64_t layer, int64_t expert) override;
|
||||
|
||||
/// What backing was obtained and why, for the startup print. "The engine adapts to the machine it is on" is
|
||||
/// only true if the engine says what it got.
|
||||
const std::string& note() const { return note_; }
|
||||
/// #633: set by `open` when the RAM available is less than the arena needs (a recommendation; it still loads)
|
||||
const std::string& ram_warning() const { return ram_warning_; }
|
||||
double load_gib_per_second() const { return gib_per_s_; }
|
||||
// Loader fix: the load, split. `load_seconds()` is the wall clock of the load loop; the other two are
|
||||
// sums over the reader threads (see LoadStats), so on their own they say how much of that wall was spent
|
||||
@@ -655,6 +739,8 @@ public:
|
||||
|
||||
private:
|
||||
void* arena_ = nullptr; ///< the PinnedArena, owned
|
||||
void* map_ = nullptr; ///< STRATA_ARENA_MMAP: the arena file, mapped read-only (not the PinnedArena)
|
||||
uint64_t map_bytes_ = 0;
|
||||
std::vector<const uint8_t*> dev_slice_; ///< device alias of each registered slice (or of the whole range)
|
||||
uint64_t slice_bytes_ = 0;
|
||||
const uint8_t* base_ = nullptr;
|
||||
@@ -662,6 +748,7 @@ private:
|
||||
int64_t n_expert_ = 0;
|
||||
int64_t reads_ = 0;
|
||||
std::string note_;
|
||||
std::string ram_warning_;
|
||||
double gib_per_s_ = 0.0;
|
||||
double load_seconds_ = 0.0;
|
||||
double load_read_s_ = 0.0;
|
||||
@@ -675,8 +762,9 @@ private:
|
||||
/// layout's type and dimensions, and inside the file. `native` is the --native shard (see set_gguf).
|
||||
bool check_experts_gguf(const std::string& native, const strata::kernels::cpu::ExpertLayout& lay, std::string& err);
|
||||
/// Fills `dst` (lay.total bytes, the experts.bin layout) from the GGUF files, one role at a time.
|
||||
/// `unbuffered`: each chunk read past the file cache (Windows).
|
||||
/// `unbuffered`: each chunk read past the file cache (Windows); `ready`: layer l is written only once
|
||||
/// *ready > l + 1 (an arena that is still being registered).
|
||||
LoadStats load_experts_gguf(const std::string& native, uint8_t* dst, const strata::kernels::cpu::ExpertLayout& lay,
|
||||
int threads, bool unbuffered = false);
|
||||
int threads, bool unbuffered = false, const std::atomic<int>* ready = nullptr);
|
||||
|
||||
} // namespace strata::core
|
||||
@@ -41,6 +41,9 @@
|
||||
#include "strata/kernels/ple.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
|
||||
#include "strata/core/vmm.hpp"
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
@@ -168,6 +171,9 @@ bool moe_layer(const WeightTable& tables, const ModelGeometry& g, int64_t layer,
|
||||
/// rung, so a host that sees it knows `h_x_f`/`h_ids`/`h_weights` are in place.
|
||||
bool moe_route(const WeightTable& tables, const ModelGeometry& g, int64_t layer, int64_t k, const MoEBuffers& b,
|
||||
const float* x, void* stream, std::string& err, const Doorbell* db = nullptr);
|
||||
/// The verify window's n tokens routed at once (see layer.cpp); bitwise per token what `moe_route` gives.
|
||||
bool moe_route_window(const WeightTable& tables, const ModelGeometry& g, int64_t layer, int64_t k, const MoEBuffers& b,
|
||||
const float* x, float* logits, int32_t* ids, float* weights, int n, void* stream, std::string& err);
|
||||
|
||||
/// The finishing half: the shared expert and the combination.
|
||||
///
|
||||
@@ -230,6 +236,9 @@ struct QsaState {
|
||||
/// pages at `block % n_slots`, with the host copy for a resume. The pool pointers above point at the slots.
|
||||
int kv_mode = 0;
|
||||
int64_t n_slots = 0;
|
||||
/// The elastic K/V (qsa_set_kv_elastic): this state's pools are in a VMM range mapped only as far as the
|
||||
/// context needs - its index in layer.cpp's registry; -1: carved from the arena as before.
|
||||
int32_t kv_elastic = -1;
|
||||
strata::kernels::KvHostPools host;
|
||||
strata::kernels::KvStreamMap map;
|
||||
int64_t idx_pooled_rows = 0; ///< rows of `idx_pooled` (a ring, which has no indexer, keeps 2)
|
||||
@@ -273,6 +282,24 @@ uint64_t qsa_state_bytes(const ModelGeometry& g, int64_t max_cells, bool with_ro
|
||||
/// Also puts the MTP drafter's K/V in a ring of its window (`ring_cells` of qsa_state_bytes/init; -1 forces a fully
|
||||
/// resident state).
|
||||
void qsa_set_kv_resident(int64_t cells);
|
||||
/// THE ELASTIC K/V (--kv-grow; vmm.hpp). A mode-0 state's K/V pools get addresses for every cell of the context but
|
||||
/// physical memory only for the first `init_cells`; `qsa_kv_elastic_grow` maps more as the context grows (from
|
||||
/// chunks the expert cache gives up) and `qsa_kv_elastic_shrink` hands them back. The addresses never move, so the
|
||||
/// captured graphs stay valid. Set before sizing and initializing the session and the drafter.
|
||||
void qsa_set_kv_elastic(bool enabled, int64_t init_cells);
|
||||
bool qsa_kv_elastic();
|
||||
/// Cells every elastic state can hold now (INT64_MAX when none is elastic).
|
||||
int64_t qsa_kv_elastic_cells();
|
||||
/// Chunks still to map for every elastic state to hold `cells` cells.
|
||||
int64_t qsa_kv_elastic_need(int64_t cells);
|
||||
/// Maps them, each from `take()` (0: a new chunk), the new memory zeroed. Synchronous; nothing may be running on
|
||||
/// the device. false: out of memory.
|
||||
bool qsa_kv_elastic_grow(int64_t cells, const std::function<VmmChunk()>& take);
|
||||
/// Unmaps the chunks past `cells` cells, each handed to `give`. Returns how many.
|
||||
int64_t qsa_kv_elastic_shrink(int64_t cells, const std::function<void(VmmChunk)>& give);
|
||||
/// Physical bytes the elastic pools hold, and what all of them would at the full context.
|
||||
uint64_t qsa_kv_elastic_mapped_bytes();
|
||||
uint64_t qsa_kv_elastic_full_bytes();
|
||||
int64_t qsa_kv_resident();
|
||||
/// The fewest resident cells a streamed layer may have: one verify window's selections (8 queries x 2,051 cells
|
||||
/// in whole blocks) must fit at once, with room to spare.
|
||||
@@ -288,17 +315,12 @@ bool qsa_kv_int8();
|
||||
/// PR #21: store K/V as Q4_0 after a Hadamard rotation (`--kv q4_0`): 576 B per cell, vs 1,056 in INT8.
|
||||
void qsa_set_kv_q4(bool enabled);
|
||||
bool qsa_kv_q4();
|
||||
/// Hybrid K8V4 (`--kv k8v4`): K in INT8, V in rotated Q4_0 - 816 B per cell. Not with --kv-resident.
|
||||
/// Hybrid K8V4 (`--kv k8v4`): K in INT8, V in rotated Q4_0 - 816 B per cell; streams with --kv-resident too.
|
||||
void qsa_set_kv_hybrid(bool enabled);
|
||||
bool qsa_kv_hybrid();
|
||||
/// The state's KV format for the block-moving functions of kv_stream.hpp (kKvF16 / kKvInt8 / kKvQ4).
|
||||
/// The state's KV format for the block-moving functions of kv_stream.hpp (kKvF16 / kKvInt8 / kKvQ4 / kKvHybrid).
|
||||
inline int qsa_kv_format(const QsaState& st) {
|
||||
// A hybrid K8V4 state is mode 0 only and never reaches the block movers; refuse rather than let it
|
||||
// fall through to kKvF16 - a wrong layout silently applied is worse than a hard stop (PR review).
|
||||
if (st.kv_hybrid) {
|
||||
std::fprintf(stderr, "strata: qsa_kv_format: a hybrid K8V4 state must never reach the block movers\n");
|
||||
std::exit(1); // the kernels' own "unsupported geometry" convention (kv_q8.cu, qsa_decode_attn.cu)
|
||||
}
|
||||
if (st.kv_hybrid) return strata::kernels::kKvHybrid; // K8V4: its own three runs (kv_stream.cu)
|
||||
return st.kv_q4 ? strata::kernels::kKvQ4 : st.kv_int8 ? strata::kernels::kKvInt8 : strata::kernels::kKvF16;
|
||||
}
|
||||
uint64_t qsa_state_init(const ModelGeometry& g, int64_t max_cells, void* base, QsaState& st,
|
||||
|
||||
@@ -43,11 +43,26 @@ public:
|
||||
/// Loads `rt_dir` (from tools/mtp_rt.py) and allocates the layer's K/V and buffers for up to `max_t` rows.
|
||||
/// Call before the VRAM expert tier is sized: this takes ~0.9 GB.
|
||||
bool load(const std::string& rt_dir, const ModelGeometry& g, SessionState& ss, int max_t, std::string& err,
|
||||
int64_t window = 32768);
|
||||
int64_t window = 32768, const MtpDrafter* shared = nullptr);
|
||||
/// The prompt's length: prefill() skips the cells the attention window can never reach again.
|
||||
void set_prompt_len(int64_t n) { prompt_len_ = n; }
|
||||
/// At most this many drafts per round (below max_t - 1): a window longer than the MTP's comes from elsewhere.
|
||||
void set_max_drafts(int k) { max_drafts_ = k; }
|
||||
/// --mtp-hnorm stream (opt-in; before the first draft or prefill): pre_fc_norm_hidden normalizes each
|
||||
/// hyper-connection stream on its own, as llama.cpp's qwen4exp MTP graph does, instead of one RMS over all four.
|
||||
void set_hnorm_per_stream(bool on) { hnorm_stream_ = on; }
|
||||
bool hnorm_per_stream() const { return hnorm_stream_; }
|
||||
/// --mtp-q4 (opt-in; before load): the draft layer's Q8_0 projections and its draft head run from 4-bit (Q4_0)
|
||||
/// copies made at load/bind - fewer bytes per draft step; drafts only, the verify window decides every token.
|
||||
/// (The prompt path's batched K/V pass keeps the Q8_0 originals.)
|
||||
void set_q4(bool proj, bool head) { q4_ = proj; q4_head_ = head; }
|
||||
/// --mtp-draft-vocab FILE (opt-in; before bind): the draft head's token subset from FILE instead of
|
||||
/// rt/draft_vocab.bin (e.g. data/draft_vocab_en.bin, 40K tokens)
|
||||
void set_draft_vocab(const std::string& path) { dvocab_path_ = path; }
|
||||
/// STRATA_MTP_TOP2=1 (diagnostic): draft j's runner-up token in the last draft() (-1 = unknown)
|
||||
static bool top2_env();
|
||||
int32_t top2(int j) const { return j >= 0 && j < (int) top2_.size() ? top2_[(size_t) j] : -1; }
|
||||
int max_t() const { return max_t_; }
|
||||
uint64_t vram_bytes() const { return vram_; }
|
||||
/// The draft layer's K/V state (read-only: --serve's STRATA_STATE_HASH check hashes it)
|
||||
const QsaState& kv_state() const { return st_; }
|
||||
@@ -58,11 +73,18 @@ public:
|
||||
/// the draft head over rt/draft_vocab.bin's subset. The expert cache is sized before bind(), so it reserves this.
|
||||
uint64_t bind_bytes(uint64_t head_row_bytes, int64_t n_vocab) const;
|
||||
/// The main model's embedding and head, and the verify window's final residuals (T rows, hc*n_embd each).
|
||||
bool bind(const WeightTable& wt, const NativeHead* head, const float* window_R, std::string& err);
|
||||
bool bind(const WeightTable& wt, const NativeHead* head, const float* window_R, std::string& err,
|
||||
const MtpDrafter* shared = nullptr);
|
||||
|
||||
/// Prompt cells [cell0, cell0 + n): residual rows `R_rows` (device, hc*n_embd each) and `next_tokens` (host,
|
||||
/// the token at position cell+1). Runs in batches of up to max_t rows.
|
||||
bool prefill(const float* R_rows, const int32_t* next_tokens, int64_t n, int64_t cell0, std::string& err);
|
||||
/// the token at position cell+1). Runs in batches of up to max_t rows. `sync` false: returns without waiting
|
||||
/// for the drafter's stream (the caller orders on it: `stream()`).
|
||||
bool prefill(const float* R_rows, const int32_t* next_tokens, int64_t n, int64_t cell0, std::string& err,
|
||||
bool sync = true);
|
||||
/// --pipeline-windows: capture the prefill graphs and size its input records for windows of up to max_t rows now,
|
||||
/// so a later prefill(sync = false) of a window neither captures nor allocates (both wait for the device).
|
||||
bool prepare_prefill(std::string& err);
|
||||
cudaStream_t stream() const { return cs_; }
|
||||
|
||||
/// One round: catch-up over T cells from `p` (rows = the window's final residuals, `tokens` = the window's
|
||||
/// argmaxes: row t pairs R_{p+t} with the token at p+t+1), then the draft chain from row `a` (the last
|
||||
@@ -82,8 +104,34 @@ public:
|
||||
/// Coupled mode with penalties: the history the next draft() chain starts from - what the session holds after the
|
||||
/// window's commit (`tail`) and the window's pick at its last accepted row (`next`, the next window's row 0).
|
||||
void set_draft_history(const int32_t* tail, int64_t n_tail, int32_t next);
|
||||
void set_ple_session(SessionState* s) { ple_ss_ = s; }
|
||||
bool coupled() const { return coupled_active_; }
|
||||
|
||||
// ---- --pipeline-windows 2: the chain as one asynchronous launch (the round and its steps back to back, no host
|
||||
// wait), its first `n_force` steps fed the given tokens instead of their own picks (teacher forcing: the drafts of
|
||||
// a window already in flight), each output read once the event after its step has completed.
|
||||
/// Before `load`: the round/step graphs get the forcing kernel (a no-op while its token is -1), and the drafter's
|
||||
/// stream the highest priority (STRATA_MTP_PRIORITY=0: the default priority).
|
||||
void set_force_capture(bool on) { force_on_ = on; }
|
||||
/// The rows the next round reads (null: the bound `window_R`), copied into the bound buffer before the round.
|
||||
void set_source_R(const float* rows) { src_R_ = rows; }
|
||||
/// Capture the round graphs for T = 1..max_t and every step graph now (a capture syncs the drafter's stream).
|
||||
bool prepare_chain(std::string& err);
|
||||
/// The catch-up over T cells from `p` (as draft()), then `n_out` outputs from row `a`: output j is the chain's
|
||||
/// own pick, and step j (1 <= j <= n_force) takes force[j - 1] as its input token instead of output j - 1.
|
||||
/// `n_early`: an event after each of the first `n_early` outputs (chain_outputs_ready).
|
||||
bool chain_launch(int T, const int32_t* tokens, int64_t p, int a, const int32_t* force, int n_force, int n_out,
|
||||
int n_early, std::string& err);
|
||||
/// 1: every output is in chain_tok()/chain_prob(); 0: still running; -1: an error.
|
||||
int chain_poll(std::string& err);
|
||||
/// How many of the chain's first outputs (up to `n_early`; all of them once the chain is done) have landed in
|
||||
/// chain_tok()/chain_prob(); -1: an error. A window's size is often decided by its first outputs (a draft below
|
||||
/// spec_min_p ends it), so the next launch need not wait for all of them.
|
||||
int chain_outputs_ready(std::string& err);
|
||||
bool chain_live() const { return chain_live_; }
|
||||
const int32_t* chain_tok() const { return chain_tok_; }
|
||||
const float* chain_prob() const { return chain_prob_; }
|
||||
|
||||
double ms_draft = 0, ms_prefill = 0;
|
||||
int64_t rounds = 0;
|
||||
|
||||
@@ -102,6 +150,13 @@ public:
|
||||
|
||||
private:
|
||||
bool record_forward(int T, int step_row0, cudaStream_t cs, std::string& err);
|
||||
/// The layer's front for T rows at step rows [row0, +T): the embedding, the fc projections, the attention
|
||||
/// hyper-connection read (R_, inj_, mixed_) and the K/V appended.
|
||||
bool record_front(int T, int row0, cudaStream_t cs, std::string& err);
|
||||
/// The rest for row 0 at step row `step_row`: the attention from the q projection, the MLP, the final mixer,
|
||||
/// the head, the draft and its probability. Needs row 0's R_, inj_ and mixed_ (its q8_1 rows in xq_).
|
||||
bool record_rest(int step_row, cudaStream_t cs, std::string& err);
|
||||
void norm_rope(float* data, const float* gamma, int rows, int cols, const int32_t* p, cudaStream_t cs);
|
||||
bool capture_prefill(int T, std::string& err);
|
||||
bool capture_prefill_dev(int T, std::string& err); ///< E-4: without the mapped staging (inputs copied on device)
|
||||
bool capture_round(int T, bool coupled, std::string& err);
|
||||
@@ -124,12 +179,37 @@ private:
|
||||
|
||||
const ModelGeometry* g_ = nullptr;
|
||||
SessionState* ss_ = nullptr;
|
||||
SessionState* ple_ss_ = nullptr;
|
||||
const WeightTable* wt_ = nullptr;
|
||||
const NativeHead* head_ = nullptr;
|
||||
const float* window_R_ = nullptr;
|
||||
int max_t_ = 0;
|
||||
int device_ = -1; ///< the device `load` ran on: the public calls switch to it (layer split)
|
||||
int max_drafts_ = 1 << 30;
|
||||
bool hnorm_stream_ = false;
|
||||
bool q4_ = false, q4_head_ = false;
|
||||
uint8_t* dense4_ = nullptr; ///< --mtp-q4: Q4_0 copies of the Q8_0 tensors
|
||||
std::vector<std::pair<std::string, uint64_t>> q4_off_;
|
||||
int dhead_type_ = -1; ///< the draft head subset's ggml type (the main head's, or Q4_0)
|
||||
std::string dvocab_path_;
|
||||
std::string vocab_file() const { return dvocab_path_.empty() ? rt_dir_ + "/draft_vocab.bin" : dvocab_path_; }
|
||||
/// a projection's weights for the draft layer's own pass: the Q4_0 copy under --mtp-q4, else the Q8_0 original
|
||||
const void* wq(const char* name, int& type) const;
|
||||
bool make_q4_dense(const std::vector<uint8_t>& blob, std::string& err);
|
||||
bool make_q4_head(std::string& err);
|
||||
void record_top2(int j);
|
||||
std::vector<int32_t> top2_, dvocab_host_;
|
||||
std::vector<float> lg_host_;
|
||||
// --pipeline-windows 2 (chain_launch)
|
||||
bool stage_source_R(int T, std::string& err); ///< set_source_R's rows into the bound buffer
|
||||
bool force_on_ = false, chain_live_ = false;
|
||||
const float* src_R_ = nullptr;
|
||||
int32_t *h_force_ = nullptr, *m_force_ = nullptr; ///< the forced tokens (mapped), -1 = the step's own pick
|
||||
cudaEvent_t ev_chain_ = nullptr;
|
||||
cudaEvent_t ev_step_[8] = {}; ///< after each of the first n_early outputs
|
||||
int steps_seen_ = 0, chain_n_ = 0, chain_early_ = 0;
|
||||
int32_t chain_tok_[8] = {};
|
||||
float chain_prob_[8] = {};
|
||||
int64_t n_vocab_ = 0;
|
||||
uint64_t vram_ = 0;
|
||||
cudaStream_t cs_ = nullptr;
|
||||
@@ -137,12 +217,17 @@ private:
|
||||
cudaGraphExec_t prefill_dev_exec_[9] = {};
|
||||
int32_t* pf_dev_ = nullptr; ///< E-4: a prompt's rows' token / step / position records, uploaded at once
|
||||
int64_t pf_cap_ = 0; ///< its capacity in ints
|
||||
cudaStream_t side_ = nullptr; // the shared expert's branch of the draft graphs (CUDA)
|
||||
cudaEvent_t sh_fork_ = nullptr, sh_join_ = nullptr;
|
||||
cudaGraphExec_t round_exec_[9] = {};
|
||||
|
||||
struct Tensor { std::string name, kind; int64_t rows = 0, cols = 0; uint64_t off = 0, bytes = 0; };
|
||||
std::vector<Tensor> tensors_;
|
||||
uint8_t* dense_ = nullptr;
|
||||
uint8_t* experts_ = nullptr;
|
||||
// Slot drafters borrow immutable weights and head; each still owns its state and scratch.
|
||||
bool owns_weights_ = true;
|
||||
bool owns_draft_head_ = true;
|
||||
void* state_arena_ = nullptr;
|
||||
QsaState st_;
|
||||
void* arena_ = nullptr;
|
||||
@@ -161,6 +246,8 @@ private:
|
||||
int64_t window_ = 0; // attention over the last window_ cells (0 = every cell)
|
||||
int64_t prompt_len_ = 0;
|
||||
float* probs_ = nullptr;
|
||||
uint8_t* arg_scratch_ = nullptr; ///< argmax_rows' and row_top_prob_split's partials and counters
|
||||
uint8_t* top_scratch_ = nullptr;
|
||||
// device
|
||||
int32_t *tok_ = nullptr, *step_ = nullptr, *pos_ = nullptr, *row_ = nullptr, *ident_ = nullptr;
|
||||
float *Rin_ = nullptr, *R_ = nullptr, *emb_ = nullptr, *en_ = nullptr, *e2_ = nullptr, *hn_ = nullptr, *h2_ = nullptr;
|
||||
|
||||
@@ -19,21 +19,27 @@ public:
|
||||
~NativeDense();
|
||||
NativeDense(const NativeDense&) = delete;
|
||||
NativeDense& operator=(const NativeDense&) = delete;
|
||||
/// With layer_hi >= 0, only the `blk.<l>.` matrices with layer_lo <= l < layer_hi are uploaded (a layer
|
||||
/// split's stage holds its own layers' projections, not the whole model's); the others keep data == nullptr.
|
||||
bool load(const std::vector<std::string>& shards, WeightTable& table, std::string& err,
|
||||
bool include_ple_key = false);
|
||||
bool include_ple_key = false, int64_t layer_lo = 0, int64_t layer_hi = -1);
|
||||
/// Plan v0.3 P1: the canonical tensor names `load` would serve natively from these shards (eligible name,
|
||||
/// supported type, 2-D), read from the GGUF headers only - so the canonical arena can skip them.
|
||||
static bool served_names(const std::vector<std::string>& shards, bool include_ple_key,
|
||||
std::set<std::string>& out, std::string& err);
|
||||
/// Layer split: load only blocks [lb, le) (every other `blk.N.` projection belongs to another GPU's stage; the
|
||||
/// PLE tensors are loaded everywhere). Process-wide, read by the next `load`; (-1, -1) = all layers.
|
||||
static void set_layer_range(int lb, int le);
|
||||
/// #326: a native pack whose `blk.1.ple_key.weight` row is unquantized (iq_pack --compat-bf16 of a GGUF key
|
||||
/// the native kernel also reads, e.g. OrcaRouter's IQ3_XXS) serves the PLE from that row, so it is taken out
|
||||
/// of `skip` and `load` does not upload the GGUF key over it. A quantized row leaves `skip` unchanged.
|
||||
static bool keep_unquantized_ple_key(const std::string& pack_dir, std::set<std::string>& skip, std::string& err);
|
||||
uint64_t weight_bytes() const { return bytes_; }
|
||||
size_t tensor_count() const { return weights_.size(); }
|
||||
size_t tensor_count() const { return weights_.size() - packed_keys_.size(); }
|
||||
|
||||
private:
|
||||
std::vector<void*> weights_;
|
||||
std::vector<const void*> packed_keys_; // GGUF-layout pointers registered with STRATA_Q8_PACKED=1
|
||||
void* scratch_ = nullptr;
|
||||
uint64_t bytes_ = 0;
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// fallback is the common case and not an error path.
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -51,6 +52,14 @@ struct PinnedArena {
|
||||
/// existing allocation path. Population/coordination and backing-file lifetime remain the caller's job.
|
||||
PinnedArena(uint64_t bytes, const std::vector<uint64_t>& bounds, uint64_t max_pinned_bytes = 0,
|
||||
const std::string& shared_file = {}, uint64_t shared_pack_hash = 0);
|
||||
/// #285: reserve only; the caller registers the slices with register_slices(), on a thread, while its readers
|
||||
/// fill the ones already registered (one registration of the whole arena cost ~2.5 s at 33 GiB of 4 KB pages).
|
||||
struct Deferred {};
|
||||
PinnedArena(uint64_t bytes, const std::vector<uint64_t>& bounds, Deferred);
|
||||
/// Registers the deferred slices in order, publishing how many are done in `ready`, and INT_MAX once it
|
||||
/// returns (a slice CUDA refuses ends the registration, and the rest is kept resident by the working-set lock).
|
||||
void register_slices(std::atomic<int>& ready);
|
||||
std::vector<uint64_t> bounds_;
|
||||
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
|
||||
@@ -101,14 +110,19 @@ struct LoadStats {
|
||||
LoadStats load_experts(const std::string& path, uint8_t* dst, uint64_t blob_bytes, uint64_t blobs_per_layer,
|
||||
uint64_t layers, int threads, uint64_t chunk);
|
||||
/// Plan v0.3 P6: the same with one byte range per layer (`layer_off[L]`, `layer_bytes[L]`).
|
||||
/// `ready` (optional): layer L is written only once *ready > L + 1 - its slice and the next one registered (a
|
||||
/// PinnedArena registering its slices meanwhile: writing a page while cudaHostRegister runs on it corrupted the
|
||||
/// arena under WDDM, even on large pages; a layer boundary inside a page puts that page in both registrations).
|
||||
LoadStats load_experts_ranges(const std::string& path, uint8_t* dst, const std::vector<uint64_t>& layer_off,
|
||||
const std::vector<uint64_t>& layer_bytes, int threads, uint64_t chunk);
|
||||
const std::vector<uint64_t>& layer_bytes, int threads, uint64_t chunk,
|
||||
const std::atomic<int>* ready = nullptr);
|
||||
|
||||
/// The same ranges read UNBUFFERED straight into `dst` (Windows): no staging buffer and no file-cache copy - the
|
||||
/// drive's DMA lands where the experts live. Every range, `dst` and `chunk` must be 4 KiB aligned; returns ok =
|
||||
/// false with an empty `error` when they are not (or off Windows), and the caller falls back to load_experts_ranges.
|
||||
LoadStats load_experts_direct(const std::string& path, uint8_t* dst, const std::vector<uint64_t>& layer_off,
|
||||
const std::vector<uint64_t>& layer_bytes, int threads, uint64_t chunk);
|
||||
const std::vector<uint64_t>& layer_bytes, int threads, uint64_t chunk,
|
||||
const std::atomic<int>* ready = nullptr);
|
||||
|
||||
/// Whether the expert files are better read unbuffered (Windows; false elsewhere): a timed probe of random 64 KiB
|
||||
/// reads says they are not in the OS file cache (a cached read takes ~10 us, the drive ~80), and the RAM left
|
||||
@@ -117,8 +131,11 @@ LoadStats load_experts_direct(const std::string& path, uint8_t* dst, const std::
|
||||
/// STRATA_UNBUFFERED_LOAD=1 / 0 forces it. `why` says what decided.
|
||||
/// `cache_counts` false (the file tier with a RAM budget): only whether the files could be kept decides - their mapped
|
||||
/// pages land in the process's working set, so a partly cached file would still take the RAM the budget was sized for.
|
||||
/// `read_bytes` (#577): the bytes the files are read for, when that is less than the files (the file tier reads only
|
||||
/// the experts outside its RAM copy); `kAllFileBytes` = every byte of `files`. See platform::file_cache_keeps.
|
||||
constexpr uint64_t kAllFileBytes = ~0ull;
|
||||
bool experts_unbuffered(const std::vector<std::string>& files, uint64_t arena_bytes, std::string& why,
|
||||
bool cache_counts = true);
|
||||
bool cache_counts = true, uint64_t read_bytes = kAllFileBytes);
|
||||
|
||||
// FNV-1a 64. Per layer, so a corrupt or short read names WHICH layer rather than just failing a whole-file
|
||||
// comparison - the same reason the Phase 1 tools report the first differing element.
|
||||
|
||||
@@ -22,6 +22,9 @@ struct Progress {
|
||||
std::atomic<int64_t> chunk{-1}; ///< #251: the prompt chunk (its first position) a batched-read stage is in
|
||||
std::atomic<int64_t> since_ms{0}; ///< when `where` was set (steady clock): how long a stage has lasted
|
||||
std::atomic<uint64_t> ticks{0}; ///< layers served: a window that still moves, slowly, against one that stopped
|
||||
/// a step that blocks in one call (a file flush, a host->device transfer of a whole session) is given until this
|
||||
/// time (steady ms; 0 = none) instead of the watchdog's limit - an explicit, bounded allowance, not a beat
|
||||
std::atomic<int64_t> allow_until_ms{0};
|
||||
};
|
||||
|
||||
inline int64_t progress_now_ms() {
|
||||
@@ -39,6 +42,8 @@ inline std::atomic<DiagFn>& release_gpu_fn() { static std::atomic<DiagFn> f{null
|
||||
inline void release_gpu_waits(std::FILE* f) {
|
||||
if (auto fn = release_gpu_fn().load()) fn(f);
|
||||
}
|
||||
/// --pipeline-windows: the pipelined loop's windows and the drafter's chain (set while that loop runs)
|
||||
inline std::atomic<DiagFn>& diag_pipeline_fn() { static std::atomic<DiagFn> f{nullptr}; return f; }
|
||||
|
||||
inline Progress& progress() {
|
||||
static Progress p;
|
||||
@@ -55,5 +60,11 @@ inline void progress_at(const char* where, int64_t detail = -1, int64_t chunk =
|
||||
|
||||
inline void progress_beat() { progress().beats.fetch_add(1, std::memory_order_relaxed); }
|
||||
inline void progress_tick() { progress().ticks.fetch_add(1, std::memory_order_relaxed); }
|
||||
/// A beat that also sets (seconds > 0) or clears (0) the allowance of a blocking step: the watchdog does not fire
|
||||
/// before the allowance ends, and fires at its end if nothing beat since.
|
||||
inline void progress_allow(int64_t seconds) {
|
||||
progress().allow_until_ms.store(seconds > 0 ? progress_now_ms() + seconds * 1000 : 0, std::memory_order_relaxed);
|
||||
progress_beat();
|
||||
}
|
||||
|
||||
} // namespace strata::core
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace strata::core {
|
||||
class RemoteExperts;
|
||||
class ExpertSource;
|
||||
|
||||
// Remote Expert Decode Optimization v1 on existing helper caches. One instance per serial engine.
|
||||
class RemoteExpertOpt {
|
||||
public:
|
||||
RemoteExpertOpt() = default;
|
||||
~RemoteExpertOpt();
|
||||
RemoteExpertOpt(const RemoteExpertOpt&) = delete;
|
||||
RemoteExpertOpt& operator=(const RemoteExpertOpt&) = delete;
|
||||
|
||||
void attach(RemoteExperts& remote); // before RemoteExperts::open
|
||||
bool init(std::string& err); // after all helper caches are open
|
||||
bool owns(int64_t layer, int32_t expert) const;
|
||||
bool adapt(const std::vector<float>& usage, const std::vector<int32_t>& primary,
|
||||
const std::vector<std::pair<int32_t, int32_t>>& pending, int max_swaps,
|
||||
ExpertSource& source);
|
||||
|
||||
// Bracket the existing pool callback; its signature and dispatch remain unchanged.
|
||||
void begin(const float* weights, int token_begin, int tokens);
|
||||
void end() { weights_ = nullptr; }
|
||||
void copy_rows(float* dst, const float* src, int token_begin, int tokens,
|
||||
const int32_t* hit_rows, const int32_t* count, void* stream) const;
|
||||
void combine(float* dst, float* scratch, int token_begin, int tokens,
|
||||
const uint32_t* skip, uint32_t ring, void* stream) const;
|
||||
|
||||
private:
|
||||
friend class RemoteExperts;
|
||||
static size_t metadata_bytes();
|
||||
bool active() const { return weights_ != nullptr; }
|
||||
void prepare(const RemoteExperts& remote, void* metadata) const;
|
||||
bool reduce(RemoteExperts& remote, const void* metadata, std::string& err);
|
||||
void accumulate(const RemoteExperts& remote);
|
||||
|
||||
struct Peer { RemoteExperts* remote; float* sum = nullptr; };
|
||||
std::vector<Peer> peers_;
|
||||
float* h_sum_ = nullptr;
|
||||
float* m_sum_ = nullptr;
|
||||
int32_t* h_mask_ = nullptr;
|
||||
int32_t* m_mask_ = nullptr;
|
||||
const float* weights_ = nullptr;
|
||||
int token_begin_ = 0, tokens_ = 0;
|
||||
};
|
||||
} // namespace strata::core
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <vector>
|
||||
|
||||
namespace strata::core {
|
||||
class RemoteExpertOpt;
|
||||
|
||||
/// A static, profile-filled expert tier on another CUDA device. CUDA0 keeps all
|
||||
/// dense weights and state; results return through the existing pinned CPU rows.
|
||||
@@ -27,7 +28,7 @@ public:
|
||||
bool open(int device, int slots, int64_t layers, int64_t experts,
|
||||
const std::vector<std::pair<int32_t, int32_t>>& ranked,
|
||||
const ExpertCache& primary, ExpertSource& source,
|
||||
std::vector<uint8_t>& claimed, std::string& err);
|
||||
std::vector<uint8_t>& claimed, std::string& err, bool auto_size = false);
|
||||
void close();
|
||||
|
||||
/// `kind` is the primary verifier's classification (-1 = CPU candidate),
|
||||
@@ -36,6 +37,9 @@ public:
|
||||
int64_t k, const int32_t* kind, const int32_t* primary_res,
|
||||
std::string& err);
|
||||
bool owns(int64_t index) const { return owned_[(size_t) index] != 0; }
|
||||
/// This helper's cache holds (layer, expert): begin() will take its rows unless the plan gave them away.
|
||||
bool holds(int64_t layer, int32_t expert) const { return cache_.slot_of(layer, expert) >= 0; }
|
||||
bool optimized_decode() const { return remote_opt_ != nullptr; }
|
||||
bool finish(float* out, std::string& err);
|
||||
int64_t resident() const { return cache_.resident(); }
|
||||
int64_t computed() const { return computed_; }
|
||||
@@ -48,6 +52,8 @@ public:
|
||||
double ms_wait() const { return ms_wait_; }
|
||||
|
||||
private:
|
||||
friend class RemoteExpertOpt;
|
||||
RemoteExpertOpt* remote_opt_ = nullptr;
|
||||
int device_ = -1;
|
||||
int64_t n_expert_ = 0;
|
||||
int32_t groups_ = 0;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#include "strata/core/hit_hook.hpp"
|
||||
#include "strata/core/layer.hpp"
|
||||
#include "strata/kernels/cpu/pool.hpp"
|
||||
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
@@ -369,7 +370,7 @@ struct SessionLoopScratch {
|
||||
float* y_miss = nullptr; ///< pinned host staging for the pool's answer, `parts_bytes` long
|
||||
size_t parts_bytes = 0;
|
||||
cudaEvent_t probe = nullptr;
|
||||
long long pinned_core = -1; ///< the affinity to restore, or -1 if the host was never pinned
|
||||
strata::kernels::cpu::ThreadAffinity pinned_core{}; ///< affinity to restore, or invalid if the host was not pinned
|
||||
bool pinned = false;
|
||||
|
||||
/// Allocates the buffers and pins the host thread. Call ONCE, at session setup.
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -38,6 +40,7 @@
|
||||
namespace strata::core {
|
||||
|
||||
class NativeHead;
|
||||
class RemoteExpertOpt;
|
||||
|
||||
/// The CPU pool for a window: x_f (n_tok, n_embd), ids (n_tok, k) -> out (n_tok * k, n_embd), hit rows zeroed.
|
||||
using PoolMultiFn = void (*)(void* user, const float* x_f, const int32_t* ids, int64_t n_tok, int64_t k, float* out,
|
||||
@@ -45,6 +48,7 @@ using PoolMultiFn = void (*)(void* user, const float* x_f, const int32_t* ids, i
|
||||
|
||||
struct VerifyHits {
|
||||
const int32_t* d_res = nullptr; ///< device [n_layers * n_expert] slot or -1
|
||||
const int32_t* h_res = nullptr; ///< host [n_layers * n_expert] slot or -1 (when all >= 0 in stage: zero-doorbell)
|
||||
const uint8_t* cache_base = nullptr; ///< slot 0 of the VRAM expert arena
|
||||
const uint64_t* slot_off = nullptr; ///< E-6: host per-slot offsets when slots differ in size (null: slot * blob)
|
||||
int64_t n_slots = 0; ///< E-6: how many (for the device copy)
|
||||
@@ -57,9 +61,13 @@ public:
|
||||
~Verifier();
|
||||
Verifier(const Verifier&) = delete;
|
||||
Verifier& operator=(const Verifier&) = delete;
|
||||
void set_remote_expert_opt(RemoteExpertOpt* opt) { remote_opt_ = opt; } // before init/capture
|
||||
|
||||
/// The watchdog's view of the window in flight (issue #31): the layer, the GPU's sequence, the flags.
|
||||
void diag(std::FILE* f) const;
|
||||
/// #649 (STRATA_VERIFY_TRACE=1, opt-in): the last windows' host-side handshake events and the GPU's breadcrumbs
|
||||
/// (in mapped memory, so they read while the GPU hangs). Nothing when the trace is off.
|
||||
void trace_dump(std::FILE* f) const;
|
||||
|
||||
/// #267: raise every flag the window's spin kernels wait on past any ring (UINT32_MAX), so a window the GPU
|
||||
/// cannot finish drains instead of staying resident, then wait up to `timeout_ms` for its streams. For the
|
||||
@@ -119,6 +127,55 @@ public:
|
||||
|
||||
/// Keep the first `n_keep` (1..T) tokens of the last window; advances `ss.ple_prev` by them.
|
||||
bool commit(int n_keep, std::string& err);
|
||||
|
||||
// ================================ SEVERAL SEQUENCES IN ONE WINDOW ================================
|
||||
//
|
||||
// A batch window holds S INDEPENDENT sequences, one token each: row s is slot s, at slot s's own position,
|
||||
// reading and writing slot s's own state (GDN recurrence and conv history, QSA K/V and indexer, PLE history),
|
||||
// which lives in `slots[s]` - a session carved like this verifier's own (same layer range, same max_cells).
|
||||
// Everything that is per row already (hyper-connections, dense projections, router, shared expert, the
|
||||
// routed experts on the GPU and the CPU, the head) runs once over the S rows, so the weights are read once
|
||||
// per window for all the sequences. Row s's arithmetic is the single-token window's, so a slot's greedy
|
||||
// tokens are its solo greedy tokens (modulo the multi-token CPU kernel choice: STRATA_IQ_MT_MIN=1).
|
||||
//
|
||||
// Greedy only, no drafts (MTP) in a batch window. `init_slots` once after `init` (S <= max_t); a layer
|
||||
// split's stages each get their own sessions, and run_slots/commit_slots continue into the next stage.
|
||||
bool init_slots(const std::vector<SessionState*>& slots, std::string& err);
|
||||
int n_slots() const { return (int) slots_.size(); }
|
||||
/// One batch window over slots [0, S): tokens[s] at positions pos[s]; out[s] = the greedy pick after it.
|
||||
bool run_slots(int S, const int32_t* tokens, const int64_t* pos, PoolMultiFn pool, void* user, int32_t* out,
|
||||
std::string& err);
|
||||
/// The same over the S slots `rows` (row t is slot rows[t], any distinct slots in any order): the slots not
|
||||
/// listed are not touched, so an idle slot keeps its state (a finished conversation it may continue later).
|
||||
bool run_slot_rows(const int* rows, int S, const int32_t* tokens, const int64_t* pos, PoolMultiFn pool, void* user,
|
||||
int32_t* out, std::string& err);
|
||||
/// Keep every row of the last batch window.
|
||||
bool commit_slots(std::string& err);
|
||||
/// Commit an accepted prefix in each contiguous slot group of the last window. `keep` has
|
||||
/// one entry per slot (indexed by slot ID), each in 1..that slot's group length.
|
||||
bool commit_slot_prefixes(const int* keep, std::string& err);
|
||||
/// --batch-mtp: slot rotation makes many row layouts, so bound the captured batch graph pairs (LRU). 0 = unbounded.
|
||||
void set_batch_graph_limit(size_t n) { batch_graph_limit_ = n; }
|
||||
|
||||
// ---- The stages of a layer split as a PIPELINE. A batch window over the slot GROUP
|
||||
// [base, base + S) is launched on ONE stage with its commit right behind it on the stage's stream (a batch window
|
||||
// keeps every row, so the commit needs no host decision), and the host serves the rings of every stage that has
|
||||
// a window in flight from one thread (batch_poll does not block). Stage k can then run group g while stage k+1
|
||||
// runs group g-1. Rows of group `base` use hand-off rows [base, base + S), so groups never share a hand-off row.
|
||||
bool batch_launch(int base, int S, const int32_t* tokens, const int64_t* pos, std::string& err);
|
||||
/// 1 = this stage's window and commit are done (the last stage's picks are in batch_out), 0 = still running,
|
||||
/// -1 = an error (err). Serves every layer that has rung so far.
|
||||
int batch_poll(PoolMultiFn pool, void* user, std::string& err);
|
||||
bool batch_busy() const { return b_running_; }
|
||||
/// A slot's sampling (temperature / top_p / top_k / min_p / seed; penalties are not applied in batch windows):
|
||||
/// its row is drawn again on the last stage with Philox(seed, position), as a solo window draws it. Greedy by
|
||||
/// default. Set on the first stage, it reaches the last.
|
||||
void set_slot_sampling(int slot, const strata::kernels::SamplerParams& sp) {
|
||||
if (slot >= 0 && slot < (int) slot_sp_.size()) slot_sp_[(size_t) slot] = sp;
|
||||
if (next_) next_->set_slot_sampling(slot, sp);
|
||||
}
|
||||
const int32_t* batch_out() const { return b_out_; }
|
||||
bool last_stage() const { return g_ != nullptr && le_ == g_->n_layers; }
|
||||
/// commit() returns without waiting for its graph (a single-GPU session sets it): the next window follows it on
|
||||
/// the same stream and the drafter reads nothing it writes, so it overlaps the draft. Whoever reads or writes
|
||||
/// the session from another stream or the host afterwards (a new request, a checkpoint, a snapshot, the prompt
|
||||
@@ -128,6 +185,45 @@ public:
|
||||
/// false with `err` when it failed. Free when nothing is pending.
|
||||
bool wait_commit(std::string& err);
|
||||
|
||||
// ---- PIPELINED WINDOWS (--pipeline-windows, a layer split on two GPUs). One conversation's windows with the
|
||||
// stages overlapped: stage 0 runs window K+1 while stage 1 still runs window K. The same window as `run`, driven
|
||||
// without blocking the host, so one host thread keeps a window in flight on each stage (the batch pipeline's
|
||||
// pattern, batch_launch / batch_poll, for one sequence with drafts). Two verifiers per stage (one per window
|
||||
// parity) share the stage's stream and each has its own hand-off. `pl_launch` stages and launches (it never
|
||||
// captures: `capture_all` first, with nothing in flight), `service` serves the layers whose doorbells have rung
|
||||
// and returns at once, `done` polls the window's completion, `pl_finish` reads the picks, `pl_commit_async`
|
||||
// queues the commit. Nothing chains to `next_`: the caller drives every stage.
|
||||
/// The compute stream to use instead of a private one (the two verifiers of one stage share it). Before `init`.
|
||||
void set_stream(cudaStream_t s) { ext_stream_ = s; }
|
||||
/// Every layer copies the token rows to the host (doorbell_publish), not only the layers with a routed expert
|
||||
/// outside the VRAM tier by the device's residency table: with windows in flight the adaptive tier marks an
|
||||
/// expert evicted on the host (the pool then computes it on the CPU) before the device table follows. Also turns
|
||||
/// the device-planned layers (E-6) off. Before `init`.
|
||||
void set_always_publish(bool on) { always_publish_ = on; }
|
||||
cudaStream_t stream() const { return cs_; }
|
||||
int device() const { return device_; }
|
||||
/// Capture every window size and the commit graph now (a capture syncs the stream: never with a window in flight).
|
||||
bool capture_all(std::string& err);
|
||||
bool pl_launch(int T, const int32_t* tokens, int64_t pos0, std::string& err);
|
||||
/// Stage a window ahead of its launch (positions, and the PLE rows from `ple_prev` = the two tokens before the
|
||||
/// window as they WILL be, their pages prefetched). A later `pl_launch` of the same window (T, pos0, tokens, and
|
||||
/// `ss.ple_prev` equal to `ple_prev` by then) skips the staging; anything else stages again.
|
||||
bool prestage(int T, const int32_t* tokens, int64_t pos0, const int32_t ple_prev[2], std::string& err);
|
||||
/// 1: every layer served; 0: the GPU has not reached the next layer yet; -1: an error (`err`).
|
||||
int service(PoolMultiFn pool, void* user, std::string& err);
|
||||
bool in_flight() const { return fl_active_; }
|
||||
/// The window's graph (and its profile copy) completed; false while it runs. An error sets `err`.
|
||||
bool done(std::string& err);
|
||||
/// After `done`: the profile, the last stage's host sampling and picks (`out` may be null on an earlier stage).
|
||||
bool pl_finish(int32_t* out, std::string& err);
|
||||
/// The commit without a host sync; `ss.ple_prev` advances now (host side). A second call for the same window
|
||||
/// (after its state was restored) replays it with another count.
|
||||
bool pl_commit_async(int n_keep, std::string& err);
|
||||
/// Fold another verifier's counters and GPU profile into this one's (the two verifiers of one stage report once).
|
||||
void absorb_stats(Verifier& o);
|
||||
/// The watchdog's line for a pipelined verifier: in flight, layers served, the GPU's ring and flags, its events.
|
||||
void diag_pipelined(std::FILE* f, const char* name) const;
|
||||
|
||||
/// Measurement hook (STRATA_LOGPOS): after run(), write one line per row t of the last window's head -
|
||||
/// "pos target logprob top top_logprob hit extra_logprob target_logprob_without_extra" - where row t is the
|
||||
/// distribution at pos0 + t, targets[t] is the token at pos0 + t + 1, extra_logprob is the log-probability of
|
||||
@@ -161,7 +257,60 @@ public:
|
||||
std::string profile_report();
|
||||
|
||||
private:
|
||||
RemoteExpertOpt* remote_opt_ = nullptr;
|
||||
bool capture(int T, std::string& err);
|
||||
// batch windows (see init_slots)
|
||||
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]
|
||||
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, only with a graph limit)
|
||||
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
|
||||
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);
|
||||
return k;
|
||||
}
|
||||
static std::vector<int> batch_key(const int* rows, int S, int hbase) {
|
||||
std::vector<int> k;
|
||||
k.reserve((size_t) S + 1);
|
||||
k.push_back(hbase);
|
||||
for (int t = 0; t < S; ++t) k.push_back(rows[t]);
|
||||
return k;
|
||||
}
|
||||
// batch_launch / batch_poll
|
||||
bool b_running_ = false;
|
||||
int64_t b_k_ = 0, b_steps_ = 0;
|
||||
std::chrono::steady_clock::time_point b_last_;
|
||||
int32_t b_out_[8] = {};
|
||||
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] = {};
|
||||
bool capture_batch(const int* rows, int S, int hbase, std::string& err);
|
||||
void collect_profile(); ///< STRATA_VERIFY_PROFILE: add the last window's stamps to prof_sum_
|
||||
void accumulate_profile(const unsigned long long* stamps); ///< one window's stamps (host copy) into prof_sum_
|
||||
// pipelined windows (pl_launch ...)
|
||||
cudaStream_t ext_stream_ = nullptr; ///< set_stream: the stage's shared stream (not destroyed here)
|
||||
bool always_publish_ = false;
|
||||
void pl_stage(int T, const int32_t* tokens, int64_t pos0, const int32_t ple_prev[2]);
|
||||
cudaEvent_t ev_done_ = nullptr, ev_commit_ = nullptr;
|
||||
unsigned long long* prof_pin_ = nullptr; ///< pinned host copy of the stamps (pipelined windows)
|
||||
bool fl_active_ = false, fl_prof_ = false, fl_ple_ = false, commit_live_ = false, pl_prestaged_ = false;
|
||||
int fl_T_ = 0;
|
||||
int64_t fl_k_ = 0, fl_total_ = 0;
|
||||
double fl_since_ms_ = 0, fl_flush_ms_ = 0, fl_launch_ms_ = 0;
|
||||
int32_t pl_prev_[2] = {-1, -1};
|
||||
std::vector<uint32_t> pl_ple_rows_; ///< the window's PLE rows (T x PLE_N_HEADS), gathered when layer 0 is served
|
||||
bool capture_commit_batch(const int* rows, int S, int hbase, std::string& err);
|
||||
bool stage_batch(const int* rows, int S, int hbase, const int32_t* tokens, const int64_t* pos, std::string& err);
|
||||
strata::kernels::SamplerParams sampling_ = [] {
|
||||
strata::kernels::SamplerParams s;
|
||||
s.greedy = true;
|
||||
@@ -173,8 +322,19 @@ private:
|
||||
bool head_sampling_ = true; ///< set_head_sampling
|
||||
int device_ = -1; ///< the device `init` ran on: run/commit switch to it (layer split)
|
||||
std::atomic<bool> released_{false}; ///< #267: release_gpu_waits ran (maybe on the watchdog thread): no more windows
|
||||
bool all_resident_ = false; ///< 100% of experts in [lb_, le_) resident in VRAM: zero-doorbell graph
|
||||
/// #871: the zero-doorbell graph plans from the device residency table alone, so it is only right while every
|
||||
/// expert of the stage is in VRAM. A prompt loan, a VRAM shrink or an adaptive swap marks some -1 for a while:
|
||||
/// every window then runs the doorbell graph (exec_nr_ / the batch key's top bit), the same as a stage that never
|
||||
/// was all-resident. refresh_ar() looks at the host table before each window.
|
||||
bool ar_off_ = false;
|
||||
bool ar_on() const { return all_resident_ && !ar_off_; }
|
||||
void refresh_ar();
|
||||
uint32_t* h_plan_err_ = nullptr; uint32_t* m_plan_err_ = nullptr; // set by resident_plan on a -1 (all-resident graph)
|
||||
const int32_t* h_res_ = nullptr; ///< the host residency table (VerifyHits::h_res)
|
||||
bool device_plan_ = false; ///< E-6: resident-only layers planned on the device (STRATA_VERIFY_DEVICE_PLAN)
|
||||
uint32_t* skip_ = nullptr; ///< E-6: per group, the ring whose plan the device built (0: the host's)
|
||||
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)
|
||||
const float* hand_in_ = nullptr;
|
||||
@@ -182,8 +342,17 @@ private:
|
||||
Verifier* next_ = nullptr;
|
||||
void* next_user_ = nullptr;
|
||||
bool ple_stage() const { return lb_ <= 1 && 1 < le_; } ///< holds layer 1, where the PLE block runs
|
||||
void stage_inputs(int T, const int32_t* tokens, int64_t pos0);
|
||||
bool staged_ = false;
|
||||
bool copy_used_ = false;
|
||||
bool capture_commit(std::string& err);
|
||||
bool record_window(int T, cudaStream_t cs, std::string& err);
|
||||
// #649: STRATA_VERIFY_TRACE=1 - a host event ring (trace_ev) and GPU breadcrumbs: the profiler's stamp points,
|
||||
// per layer and token group, written to mapped memory (null when the trace is off)
|
||||
void trace_ev(const char* what, int64_t step, int64_t layer, int64_t aux) const;
|
||||
unsigned long long* trace_h_ = nullptr;
|
||||
unsigned long long* trace_m_ = nullptr;
|
||||
size_t trace_n_ = 0;
|
||||
static constexpr int kProfPer = 33; // stamps per layer (32 left the hc-read second
|
||||
// half's up-stamp at slot 32 = the next layer's slot 0: D8)
|
||||
bool prof_on_ = false;
|
||||
@@ -198,12 +367,17 @@ private:
|
||||
VerifyHits hits_;
|
||||
const NativeHead* head_ = nullptr;
|
||||
int max_t_ = 0;
|
||||
float* ple_key_ = nullptr; ///< STRATA_PLE_BATCH: the window rows' PLE key / value projections
|
||||
float* ple_val_ = nullptr;
|
||||
int last_t_ = 0;
|
||||
int64_t last_pos0_ = 0;
|
||||
int32_t last_tokens_[8] = {};
|
||||
int64_t n_vocab_ = 0;
|
||||
cudaStream_t cs_ = nullptr;
|
||||
cudaStream_t sh_cs_ = nullptr;
|
||||
cudaEvent_t ev_fork_ = nullptr, ev_join_ = nullptr;
|
||||
cudaGraphExec_t exec_[9] = {};
|
||||
cudaGraphExec_t exec_nr_[9] = {}; // #871: the doorbell variant of a stage that is all-resident otherwise
|
||||
cudaGraphExec_t commit_exec_ = nullptr;
|
||||
|
||||
// mapped staging (host pointer, device alias)
|
||||
@@ -243,6 +417,7 @@ private:
|
||||
float *ple_ = nullptr, *emb_ = nullptr, *R_ = nullptr, *mixed_ = nullptr, *bo_ = nullptr;
|
||||
float *inj_ = nullptr, *inj2_ = nullptr, *lo_ = nullptr, *rs_ = nullptr, *xn_ = nullptr;
|
||||
uint8_t* xq_ = nullptr; // T columns of q8_1
|
||||
uint8_t* sh_xq_ = nullptr; // T columns of q8_1 for shared expert branch
|
||||
float *qkv_L_ = nullptr, *h_L_ = nullptr, *gate_L_ = nullptr, *beta_L_ = nullptr; // per GDN layer
|
||||
float *z_ = nullptr, *y_ = nullptr, *y_dummy_ = nullptr;
|
||||
float *qfull_ = nullptr, *qcur_ = nullptr, *kcur_ = nullptr, *vcur_ = nullptr, *idx_raw_L_ = nullptr;
|
||||
@@ -261,6 +436,8 @@ private:
|
||||
void* hit_scratch_ = nullptr;
|
||||
float *head_mixed_ = nullptr, *head_inj_ = nullptr, *head_logits_ = nullptr;
|
||||
uint16_t* sh_bf16_ = nullptr;
|
||||
uint8_t* arg_scratch_ = nullptr; ///< argmax_rows' partials and counters
|
||||
int32_t* one_ = nullptr; ///< device {1}: the n_keep of a one-token window, which commits itself
|
||||
float *sh_gate_ = nullptr, *sh_up_ = nullptr, *sh_g_ = nullptr;
|
||||
float* hist_snap_ = nullptr; // T * NG_HIST * NG_HC_DIM
|
||||
int64_t cap_ = 0, max_blocks_ = 0, attn_scratch_floats_ = 0;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
// CUDA virtual memory (the elastic K/V, --kv-grow): an address range reserved once, whose physical memory is mapped and
|
||||
// unmapped in chunks of the device's granularity (2 MiB). The K/V pools grow with the context by taking chunks the
|
||||
// expert cache gives up, and neither moves: every captured graph and stored pointer stays valid. The driver calls
|
||||
// are reached through the runtime's entry-point query, so nothing links the driver library. Not under HIP.
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace strata::core {
|
||||
|
||||
/// A physical chunk: the driver's allocation handle (CUmemGenericAllocationHandle); 0 = none.
|
||||
using VmmChunk = unsigned long long;
|
||||
|
||||
/// The current device supports it and the entry points resolved.
|
||||
bool vmm_available();
|
||||
/// The chunk size (0 when not available).
|
||||
uint64_t vmm_granularity();
|
||||
/// A new physical chunk on the current device (0: out of memory), and its release.
|
||||
VmmChunk vmm_chunk_new();
|
||||
void vmm_chunk_free(VmmChunk h);
|
||||
|
||||
class VmmRange {
|
||||
public:
|
||||
VmmRange() = default;
|
||||
~VmmRange() { release(); }
|
||||
VmmRange(const VmmRange&) = delete;
|
||||
VmmRange& operator=(const VmmRange&) = delete;
|
||||
|
||||
/// Reserves `bytes` (rounded up to whole chunks) of addresses, nothing mapped.
|
||||
bool reserve(uint64_t bytes);
|
||||
/// Unmaps and frees every chunk, then the addresses.
|
||||
void release();
|
||||
uint8_t* base() const { return (uint8_t*) (uintptr_t) base_; }
|
||||
int64_t chunks() const { return (int64_t) h_.size(); }
|
||||
bool mapped(int64_t i) const { return i >= 0 && i < chunks() && h_[(size_t) i] != 0; }
|
||||
int64_t mapped_count() const;
|
||||
/// Maps chunks [lo, hi) that are not mapped yet, each to `take()` or, when that returns 0, to a new chunk.
|
||||
/// Readable and writable when it returns; false: out of memory (what was mapped stays mapped).
|
||||
template <class Take> bool map_range(int64_t lo, int64_t hi, Take take);
|
||||
/// Unmaps chunk i and hands back its physical chunk (0 when it was not mapped).
|
||||
VmmChunk unmap(int64_t i);
|
||||
|
||||
private:
|
||||
bool map_one(int64_t i, VmmChunk h);
|
||||
bool set_access(int64_t lo, int64_t hi);
|
||||
/// Readable and writable [lo, hi) (all newly mapped); when that fails they are unmapped and freed again, so no
|
||||
/// chunk counts as mapped without access.
|
||||
bool commit_run(int64_t lo, int64_t hi);
|
||||
unsigned long long base_ = 0;
|
||||
std::vector<VmmChunk> h_; // per chunk: its physical chunk, 0 = unmapped
|
||||
};
|
||||
|
||||
template <class Take> bool VmmRange::map_range(int64_t lo, int64_t hi, Take take) {
|
||||
lo = lo < 0 ? 0 : lo;
|
||||
hi = hi > chunks() ? chunks() : hi;
|
||||
int64_t run = -1; // access is set once per run of newly mapped chunks (it costs as much as the map)
|
||||
for (int64_t i = lo; i < hi; ++i) {
|
||||
if (mapped(i)) {
|
||||
if (run >= 0 && !commit_run(run, i)) return false;
|
||||
run = -1;
|
||||
continue;
|
||||
}
|
||||
VmmChunk h = take();
|
||||
if (h == 0) h = vmm_chunk_new();
|
||||
if (h == 0 || !map_one(i, h)) {
|
||||
if (h != 0) vmm_chunk_free(h);
|
||||
if (run >= 0) commit_run(run, i);
|
||||
return false;
|
||||
}
|
||||
if (run < 0) run = i;
|
||||
}
|
||||
return run < 0 || commit_run(run, hi);
|
||||
}
|
||||
|
||||
} // namespace strata::core
|
||||
@@ -90,6 +90,9 @@ struct WeightRef {
|
||||
const void* native_data = nullptr;
|
||||
void* native_q8_1 = nullptr;
|
||||
int native_type = -1;
|
||||
/// S23 experiment (STRATA_HC_Q8=1): a hyper-connection projection's Q8_0 bytes as the GGUF stores them, owned by
|
||||
/// NativeDense, for the verify window's read (every other path keeps the pack's BF16 `data`)
|
||||
const void* hc_q8 = nullptr;
|
||||
/// Plan v0.3 P1: false when the loader SKIPPED this tensor's canonical bytes because another form serves it
|
||||
/// (native GGUF projections, the native head). The metadata above stays valid; `data` is null.
|
||||
bool resident = true;
|
||||
|
||||
@@ -1,5 +1,45 @@
|
||||
#pragma once
|
||||
#include <hipblas/hipblas.h>
|
||||
#include <hipblas/hipblas-version.h>
|
||||
|
||||
#if hipblasVersionMajor == 0
|
||||
// Ubuntu's hipBLAS 0.54 has no workspace API or compute-type enum. Use its
|
||||
// underlying rocBLAS API directly, including ownership of the real handle,
|
||||
// so the engine's explicit workspace contract is preserved.
|
||||
#include <rocblas/rocblas.h>
|
||||
#define CUBLAS_COMPUTE_32F rocblas_datatype_f32_r
|
||||
#define CUBLAS_DEFAULT_MATH 0
|
||||
#define CUBLAS_GEMM_DEFAULT rocblas_gemm_algo_standard
|
||||
#define CUBLAS_OP_N rocblas_operation_none
|
||||
#define CUBLAS_OP_T rocblas_operation_transpose
|
||||
#define CUBLAS_STATUS_SUCCESS rocblas_status_success
|
||||
#define CUDA_R_16BF rocblas_datatype_bf16_r
|
||||
#define CUDA_R_16F rocblas_datatype_f16_r
|
||||
#define CUDA_R_32F rocblas_datatype_f32_r
|
||||
#define cublasCreate rocblas_create_handle
|
||||
#define cublasDestroy rocblas_destroy_handle
|
||||
#define cublasHandle_t rocblas_handle
|
||||
#define cublasSetStream rocblas_set_stream
|
||||
#define cublasSetWorkspace rocblas_set_workspace
|
||||
#define cublasStatus_t rocblas_status
|
||||
|
||||
inline rocblas_status cublasSetMathMode(rocblas_handle h, int mode) {
|
||||
// This API has only its default arithmetic; GEMM below explicitly selects
|
||||
// FP32 accumulation. Reject any request for a different math mode.
|
||||
if (!h) return rocblas_status_invalid_handle;
|
||||
return mode == CUBLAS_DEFAULT_MATH ? rocblas_status_success : rocblas_status_invalid_value;
|
||||
}
|
||||
inline rocblas_status cublasGemmEx(rocblas_handle h, rocblas_operation trans_a,
|
||||
rocblas_operation trans_b, int m, int n, int k, const void* alpha,
|
||||
const void* a, rocblas_datatype a_type, int lda,
|
||||
const void* b, rocblas_datatype b_type, int ldb, const void* beta,
|
||||
void* c, rocblas_datatype c_type, int ldc,
|
||||
rocblas_datatype compute_type, rocblas_gemm_algo algo) {
|
||||
return rocblas_gemm_ex(h, trans_a, trans_b, m, n, k, alpha,
|
||||
a, a_type, lda, b, b_type, ldb, beta, c, c_type, ldc,
|
||||
c, c_type, ldc, compute_type, algo, 0, 0);
|
||||
}
|
||||
#else
|
||||
#define CUBLAS_COMPUTE_32F HIPBLAS_COMPUTE_32F
|
||||
#define CUBLAS_DEFAULT_MATH HIPBLAS_DEFAULT_MATH
|
||||
#define CUBLAS_GEMM_DEFAULT HIPBLAS_GEMM_DEFAULT
|
||||
@@ -17,3 +57,4 @@
|
||||
#define cublasSetStream hipblasSetStream
|
||||
#define cublasSetWorkspace hipblasSetWorkspace
|
||||
#define cublasStatus_t hipblasStatus_t
|
||||
#endif
|
||||
@@ -10,6 +10,7 @@
|
||||
#endif
|
||||
#endif
|
||||
#include <hip/hip_runtime.h>
|
||||
#include <hip/hip_version.h>
|
||||
// Do not let HIP's legacy macro corrupt libstdc++ attribute names.
|
||||
#ifdef __noinline__
|
||||
#undef __noinline__
|
||||
@@ -19,6 +20,7 @@
|
||||
#define cudaDevAttrClockRate hipDeviceAttributeClockRate
|
||||
#define cudaDevAttrComputeCapabilityMajor hipDeviceAttributeComputeCapabilityMajor
|
||||
#define cudaDevAttrComputeCapabilityMinor hipDeviceAttributeComputeCapabilityMinor
|
||||
#define cudaDevAttrIntegrated hipDeviceAttributeIntegrated
|
||||
#define cudaDeviceGetAttribute hipDeviceGetAttribute
|
||||
#define cudaDeviceCanAccessPeer hipDeviceCanAccessPeer
|
||||
#define cudaDeviceEnablePeerAccess hipDeviceEnablePeerAccess
|
||||
@@ -39,8 +41,13 @@
|
||||
#define cudaEventSynchronize hipEventSynchronize
|
||||
#define cudaEvent_t hipEvent_t
|
||||
#define cudaFree hipFree
|
||||
#if HIP_VERSION_MAJOR < 7
|
||||
#define cudaFreeHost hipHostFree
|
||||
#else
|
||||
#define cudaFreeHost hipFreeHost
|
||||
#endif
|
||||
#define cudaFuncAttributeMaxDynamicSharedMemorySize hipFuncAttributeMaxDynamicSharedMemorySize
|
||||
#define cudaFuncAttributePreferredSharedMemoryCarveout hipFuncAttributePreferredSharedMemoryCarveout
|
||||
#define cudaGetDevice hipGetDevice
|
||||
#define cudaGetDeviceCount hipGetDeviceCount
|
||||
#define cudaGetDeviceProperties hipGetDeviceProperties
|
||||
@@ -54,13 +61,20 @@
|
||||
#define cudaGraphUpload hipGraphUpload
|
||||
#define cudaGraph_t hipGraph_t
|
||||
#define cudaHostAlloc hipHostMalloc
|
||||
#if HIP_VERSION_MAJOR < 7
|
||||
#define cudaHostAllocDefault hipHostMallocDefault
|
||||
#define cudaHostAllocMapped hipHostMallocMapped
|
||||
#define cudaHostAllocPortable hipHostMallocPortable
|
||||
#else
|
||||
#define cudaHostAllocDefault hipHostAllocDefault
|
||||
#define cudaHostAllocMapped hipHostAllocMapped
|
||||
#define cudaHostAllocPortable hipHostAllocPortable
|
||||
#endif
|
||||
#define cudaHostGetDevicePointer hipHostGetDevicePointer
|
||||
#define cudaHostRegister hipHostRegister
|
||||
#define cudaHostRegisterMapped hipHostRegisterMapped
|
||||
#define cudaHostRegisterPortable hipHostRegisterPortable
|
||||
#define cudaHostRegisterReadOnly hipHostRegisterReadOnly
|
||||
#define cudaHostUnregister hipHostUnregister
|
||||
#define cudaLaunchHostFunc hipLaunchHostFunc
|
||||
#define cudaMalloc hipMalloc
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
// CUDA device intrinsics used by Strata kernels that HIP does not provide on RDNA2 / RDNA3 / RDNA4 (wave32).
|
||||
// CUDA device intrinsics used by Strata kernels on supported wave32 HIP targets.
|
||||
// This header is included only from the HIP cuda_runtime compatibility shim.
|
||||
#if defined(__HIPCC__)
|
||||
#include <hip/hip_version.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -15,7 +16,8 @@ __device__ __forceinline__ int signed_byte(uint32_t word, int lane) {
|
||||
|
||||
// CUDA's signed __dp4a: four signed byte products accumulated modulo 2^32.
|
||||
__device__ __forceinline__ int dp4a(int a, int b, int c) {
|
||||
#if (defined(__gfx1100__) || defined(__gfx1101__) || defined(__gfx1102__) || defined(__gfx1200__) || \
|
||||
#if (defined(__gfx1100__) || defined(__gfx1101__) || defined(__gfx1102__) || defined(__gfx1150__) || \
|
||||
defined(__gfx1151__) || defined(__gfx1200__) || \
|
||||
defined(__gfx1201__)) && __has_builtin(__builtin_amdgcn_sudot4)
|
||||
// RDNA3 and RDNA4 expose the signed/unsigned dot4 form (v_dot4_i32_iu8). Mark
|
||||
// both packed operands signed to preserve CUDA __dp4a semantics; keep the
|
||||
@@ -25,6 +27,21 @@ __device__ __forceinline__ int dp4a(int a, int b, int c) {
|
||||
// RDNA2 has no sudot4 (that is gfx11+), but it has the plain signed v_dot4_i32_i8 (dot1-insts): the same
|
||||
// signed x signed byte products accumulated modulo 2^32, no clamp.
|
||||
return __builtin_amdgcn_sdot4(a, b, c, false);
|
||||
#elif defined(__gfx1012__) && !defined(STRATA_GFX1012_PORTABLE_DOT)
|
||||
// RDNA1 has no native signed dot4. SDWA selects and sign-extends byte
|
||||
// operands directly, avoiding separate shift/mask/sign-extension work.
|
||||
// Adapted from pinned llama.cpp ggml-cuda/common.cuh (MIT; see
|
||||
// third_party/ggml/LICENSE), same modulo-2^32 accumulation contract.
|
||||
int lo, hi;
|
||||
asm("v_mul_i32_i24 %1, sext(%3), sext(%4) dst_sel:DWORD dst_unused:UNUSED_PAD src0_sel:BYTE_0 src1_sel:BYTE_0\n"
|
||||
"v_mul_i32_i24 %2, sext(%3), sext(%4) dst_sel:DWORD dst_unused:UNUSED_PAD src0_sel:BYTE_1 src1_sel:BYTE_1\n"
|
||||
"v_add3_u32 %0, %1, %2, %0\n"
|
||||
"v_mul_i32_i24 %1, sext(%3), sext(%4) dst_sel:DWORD dst_unused:UNUSED_PAD src0_sel:BYTE_2 src1_sel:BYTE_2\n"
|
||||
"v_mul_i32_i24 %2, sext(%3), sext(%4) dst_sel:DWORD dst_unused:UNUSED_PAD src0_sel:BYTE_3 src1_sel:BYTE_3\n"
|
||||
"v_add3_u32 %0, %1, %2, %0\n"
|
||||
// c changes before the high-byte reads; it must not alias a or b.
|
||||
: "+&v"(c), "=&v"(lo), "=&v"(hi) : "v"(a), "v"(b));
|
||||
return c;
|
||||
#else
|
||||
const uint32_t ua = static_cast<uint32_t>(a);
|
||||
const uint32_t ub = static_cast<uint32_t>(b);
|
||||
@@ -84,6 +101,17 @@ __device__ __forceinline__ void require_full_wave_mask(uint32_t mask) {
|
||||
if (mask != 0xffffffffu) __builtin_trap();
|
||||
}
|
||||
|
||||
// Older HIP has no __syncwarp. A wave barrier alone does not order memory.
|
||||
// Release/acquire fences cover the shared-memory exchange used by attention.
|
||||
#if HIP_VERSION_MAJOR < 7
|
||||
__device__ __forceinline__ void syncwarp(uint32_t mask = 0xffffffffu) {
|
||||
require_full_wave_mask(mask);
|
||||
__builtin_amdgcn_fence(__ATOMIC_RELEASE, "workgroup");
|
||||
__builtin_amdgcn_wave_barrier();
|
||||
__builtin_amdgcn_fence(__ATOMIC_ACQUIRE, "workgroup");
|
||||
}
|
||||
#endif
|
||||
|
||||
template <typename T>
|
||||
__device__ __forceinline__ T shfl_xor_sync(uint32_t mask, T value, int lane_mask, int width = 32) {
|
||||
require_full_wave_mask(mask);
|
||||
@@ -125,6 +153,9 @@ __device__ __forceinline__ unsigned ballot_sync(uint32_t mask, int predicate) {
|
||||
#define __shfl_up_sync(...) (::strata::hip_compat::shfl_up_sync(__VA_ARGS__))
|
||||
#define __shfl_sync(...) (::strata::hip_compat::shfl_sync(__VA_ARGS__))
|
||||
#define __ballot_sync(mask, predicate) (::strata::hip_compat::ballot_sync((mask), (predicate)))
|
||||
#if HIP_VERSION_MAJOR < 7
|
||||
#define __syncwarp(...) (::strata::hip_compat::syncwarp(__VA_ARGS__))
|
||||
#endif
|
||||
// AMD's sleep instruction accepts only 0..15; the synchronization loops use it as a
|
||||
// backoff hint, so use its smallest portable delay independently of CUDA cycle counts.
|
||||
#define __nanosleep(cycles) __builtin_amdgcn_s_sleep(1)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
// CUDA spelling used by the attention kernels; included only in HIP builds.
|
||||
#include <hip/hip_math_constants.h>
|
||||
#define CUDART_INF_F HIP_INF_F
|
||||
@@ -44,9 +44,19 @@ void bf16_gemv_split(const uint16_t* x, const uint16_t* w, float* y, int64_t n_i
|
||||
/// No allocations or synchronization, including when stream is null (the CUDA default stream).
|
||||
void bf16_gemv_fp32_mmvf(const float* x, const uint16_t* w, float* y,
|
||||
int64_t n_in, int64_t n_out, void* stream);
|
||||
/// `ncols` columns at once: x is [ncols][n_in], y is [ncols][n_out]; each column bitwise equal to a
|
||||
/// `bf16_gemv_fp32_mmvf` call on it.
|
||||
void bf16_gemv_fp32_mmvf_cols(const float* x, const uint16_t* w, float* y, int64_t n_in, int64_t n_out, int ncols,
|
||||
void* stream);
|
||||
/// bf16_gemv_fp32_mmvf for n_tok (1..8) activation rows x[t * ldx], outputs y[t * ldy + j]; one launch,
|
||||
/// the weight read once, every output bit-identical to its own single-row call.
|
||||
void bf16_gemv_fp32_mmvf_multi(const float* x, int64_t ldx, const uint16_t* w, float* y, int64_t ldy,
|
||||
int64_t n_in, int64_t n_out, int n_tok, void* stream);
|
||||
/// S26 STRATA_LFUSE: bf16_gemv_fp32_mmvf_multi(x, ldx, w, y, ldy, n_in, n_out, n_tok) plus the 1-row
|
||||
/// bf16_gemv_fp32_mmvf_multi(x, ldx, w_aux, y_aux, ldy_aux, n_in, 1, n_tok) in ONE launch, every output bitwise the
|
||||
/// two calls'. Returns false (nothing launched) unless the rows path applies (STRATA_MMVF_ROWS=1, n_out >= 64, 2-8 rows).
|
||||
bool bf16_gemv_fp32_mmvf_multi_aux(const float* x, int64_t ldx, const uint16_t* w, float* y, int64_t ldy,
|
||||
int64_t n_in, int64_t n_out, int n_tok, const uint16_t* w_aux, float* y_aux,
|
||||
int64_t ldy_aux, void* stream);
|
||||
|
||||
} // namespace strata::kernels
|
||||
@@ -67,6 +67,14 @@ struct ActQ {
|
||||
int32_t sum[MAXC];
|
||||
float hx[MAXC];
|
||||
int nchunks;
|
||||
/// The AVX-2 "bit-plane" image of `q` (src/kernels/cpu/q2_avx2.cpp explains it), filled only by
|
||||
/// `act_quant_q8_1_avx2` with STRATA_Q2_BITPLANE=1 and read only by `q2_0_gguf_rows_multi_avx2`. Per pair p of 64-value blocks:
|
||||
/// `qp[128p + 32k + 16h + i] = q[128p + 64h + 4i + k]`, `psum[8p + 4h + m]` = the sum of q[128p + 64h + 16m ..
|
||||
/// + 16), `pscale[8p + 2c + e] = scale[4p + c]`. `bp_pairs` is 0 when the image is absent (n % 128 != 0).
|
||||
alignas(32) int8_t qp[H];
|
||||
alignas(32) int32_t psum[H / 16];
|
||||
alignas(32) float pscale[H / 16];
|
||||
int bp_pairs = 0;
|
||||
};
|
||||
|
||||
/// Per-worker scratch. Owned by the caller and passed in, so the token path performs NO allocations
|
||||
@@ -172,6 +180,14 @@ void q2_0_gguf_rows_multi(const uint8_t* w, size_t row_bytes, int nblocks, const
|
||||
/// The same two for CPUs without AVX-512 (src/kernels/cpu/q2_avx2.cpp, compiled for AVX2 only).
|
||||
void q2_0_gguf_rows_multi_avx2(const uint8_t* w, size_t row_bytes, int nblocks, const ActQ* const* a, int nt,
|
||||
float* const* out, int r0, int r1);
|
||||
/// STRATA_Q2_BITPLANE=1 (read once): the AVX-2 Q2_0 kernel reads the bit-plane image instead of the legacy one.
|
||||
bool q2_bitplane_enabled();
|
||||
/// The pre-bit-plane AVX-2 kernel (the default), kept for A/B and parity.
|
||||
void q2_0_gguf_rows_multi_avx2_legacy(const uint8_t* w, size_t row_bytes, int nblocks, const ActQ* const* a, int nt,
|
||||
float* const* out, int r0, int r1);
|
||||
/// The same rows with AVX-VNNI on or off, not as cpu_avxvnni_ok() says (tests and benches; on only where it holds).
|
||||
void q2_0_gguf_rows_multi_avx2_v(bool vnni, const uint8_t* w, size_t row_bytes, int nblocks, const ActQ* const* a,
|
||||
int nt, float* const* out, int r0, int r1);
|
||||
void act_quant_q8_1_avx2(const float* x, int n, ActQ& a);
|
||||
|
||||
void s2_expert_scalar(const uint8_t* blob, const float* x, float* out, bool quant_acts);
|
||||
|
||||
@@ -44,9 +44,41 @@ struct ExpertLayout {
|
||||
/// Plan v0.3 P6: whether this CPU (and its OS) runs the AVX-512 kernels (F, BW, VL, VNNI, VBMI). Probed in a
|
||||
/// file compiled without AVX-512, so asking is safe everywhere; STRATA_FORCE_AVX2=1 answers no (for tests).
|
||||
bool cpu_avx512_ok();
|
||||
/// Whether this CPU runs AVX-512F + AVX-512BW + AVX-512VL + AVX-512DQ (e.g. Skylake-X i9-9920X) for iq_avx512.cpp.
|
||||
bool cpu_avx512bw_ok();
|
||||
/// Whether this CPU (and its OS) runs the AVX2 kernels (AVX, AVX2, FMA, F16C): the floor of every expert kernel
|
||||
/// (q2_avx2.cpp, iq_avx2.cpp, and ggml-cpu in the portable build). STRATA_FORCE_AVX2 does not change it.
|
||||
bool cpu_avx2_ok();
|
||||
/// Whether this CPU (and its OS) runs AVX (Sandy Bridge and newer): the AVX1 router dot (kq_avx1.cpp) and ggml-cpu in
|
||||
/// a STRATA_ISA_FLOOR=avx build. The other expert rows then run on ggml-cpu (experimental, slow).
|
||||
bool cpu_avx1_ok();
|
||||
/// SSE4.2 + POPCNT: ggml-cpu in a STRATA_ISA_FLOOR=none build.
|
||||
bool cpu_sse42_ok();
|
||||
/// STRATA_FORCE_ISA=avx2|avx|sse caps the probes above (3 = no cap, 2 = AVX2, 1 = AVX, 0 = SSE4.2), so the older-CPU
|
||||
/// paths run on any PC: tests and the experimental builds' checks. ggml-cpu keeps what it was compiled for.
|
||||
int cpu_isa_cap();
|
||||
/// The experimental older-CPU build this engine is ("avx", "sse4.2"), or "" for the normal build (AVX2 floor).
|
||||
const char* isa_floor_build();
|
||||
/// STRATA_IQ256_GATHER, the AVX-2 i-quant kernels' gathered grid decode (iq_avx2.cpp): -1 unset (auto, see
|
||||
/// cpu_gather_fast_here), 0 the scalar decode on every core, 1 the gathered one on every core.
|
||||
int iq256_gather_setting();
|
||||
/// Whether this CPU's performance cores gather the IQ grid entries faster than they assemble them from scalar loads:
|
||||
/// an Intel CPU with AVX-VNNI, i.e. Alder Lake / Sapphire Rapids or newer, and not one of the E-core-only parts.
|
||||
/// The older Intel cores gather slowly (Haswell, Broadwell) or under the Downfall (GDS) microcode (Skylake to Tiger
|
||||
/// Lake), AMD Zen 2/3 gather slowly, and Zen 4/5 run the AVX-512 kernels. STRATA_FORCE_ISA answers no.
|
||||
bool cpu_gather_fast();
|
||||
/// cpu_gather_fast() and the CALLING THREAD runs on a performance core: CPUID 1Ah core type 40h on a hybrid CPU (its
|
||||
/// E-cores, 20h, gather slower than they assemble). Probed once per thread: the pool pins each worker to one core.
|
||||
bool cpu_gather_fast_here();
|
||||
/// Whether the AVX-2 expert kernels take their AVX-VNNI forms (vpdpwssd / vpdpbusd: Alder Lake, Sapphire Rapids and
|
||||
/// later, P- and E-cores alike; the same integer sums): cpu_avx2_ok() and CPUID 7.1:EAX[4], not capped by
|
||||
/// STRATA_FORCE_ISA, and not an older-CPU build (STRATA_ISA_FLOOR, which stays on its floor and AVX2).
|
||||
/// STRATA_NO_AVXVNNI=1 answers no.
|
||||
bool cpu_avxvnni_ok();
|
||||
/// Whether a native pack's layer of this ggml type runs on Strata's own Q2_0 kernels (AVX2 / AVX-512). On a
|
||||
/// CPU without AVX2 (a native, non-portable build: ggml-cpu compiled for this CPU's SSE) a Q2_0 layer takes
|
||||
/// ggml-cpu's own Q2_0 vec_dot like every other native type: slower, but it runs.
|
||||
inline bool q2_native_kernels(int type) { return type == 42 && cpu_avx2_ok(); }
|
||||
/// The CPU's brand string (CPUID 0x80000002..4), for messages; "unknown" when it has none.
|
||||
std::string cpu_name();
|
||||
/// Q2_0 GGUF rows / activation quantizer on the kernels this CPU has.
|
||||
|
||||
@@ -18,9 +18,27 @@ void iq256_gu_rows(int ggml_type, const uint8_t* blob, size_t gu_row, size_t up_
|
||||
void iq256_rows(int ggml_type, const uint8_t* w, size_t row_bytes, int n, const void* const* act, int nt,
|
||||
float* const* out, int r0, int r1);
|
||||
|
||||
/// The kernels' variants, a mask. Every variant computes the same bits; they differ in speed from core to core.
|
||||
/// kIq256Gather: IQ3_XXS, IQ3_S and IQ2_S read their grid entries with one gather (STRATA_IQ256_GATHER).
|
||||
/// kIq256Vnni: AVX-VNNI's vpdpwssd / vpdpbusd, every format and iq4nl256_down_rows (cpu_avxvnni_ok).
|
||||
inline constexpr int kIq256Gather = 1;
|
||||
inline constexpr int kIq256Vnni = 2;
|
||||
/// The variant iq256_gu_rows / iq256_rows take on the calling thread.
|
||||
int iq256_variant() noexcept;
|
||||
/// Every variant bit this build and this CPU can run (tests and benches).
|
||||
int iq256_variants() noexcept;
|
||||
/// iq256_gu_rows / iq256_rows in a given variant (tests and benches; only bits of iq256_variants()).
|
||||
void iq256_gu_rows_v(int variant, int ggml_type, const uint8_t* blob, size_t gu_row, size_t up_off, int n,
|
||||
const void* const* act, int nt, float* const* ff, int r0, int r1);
|
||||
void iq256_rows_v(int variant, int ggml_type, const uint8_t* w, size_t row_bytes, int n, const void* const* act,
|
||||
int nt, float* const* out, int r0, int r1);
|
||||
|
||||
/// out[t][r] = w_r . h[t] for IQ4_NL (type 20) rows against Q8_0 activations (ggml's block_q8_0).
|
||||
/// IQ4_NL is a 32-value-block format, so this does not go through iq256_rows (QK_K blocks, Q8_K acts).
|
||||
void iq4nl256_down_rows(const uint8_t* w, size_t row_bytes, int n, const void* const* hq, int nt,
|
||||
float* const* out, int r0, int r1);
|
||||
/// The same in a given variant (only the kIq256Vnni bit matters here).
|
||||
void iq4nl256_down_rows_v(int variant, const uint8_t* w, size_t row_bytes, int n, const void* const* hq, int nt,
|
||||
float* const* out, int r0, int r1);
|
||||
|
||||
} // namespace strata::kernels::cpu
|
||||
@@ -0,0 +1,18 @@
|
||||
// include/strata/kernels/cpu/kq_avx1.hpp - the AVX1 router dot for older CPUs (from the Strata_Dirigo fork).
|
||||
//
|
||||
// bf16_rows_dot_multi (kq_avx2.cpp) is compiled with -mavx2 and uses instructions an AVX-only CPU does
|
||||
// not have (`vpmovzxwd`, `vfmadd`), so an AVX-only host cannot call it at all. This is the same
|
||||
// arithmetic on AVX1 instructions: 8 float lanes per token, one weight stream per row shared by all
|
||||
// `nt` tokens, one horizontal sum per (row, token). AVX1 has no FMA, so the multiply and the add stay
|
||||
// separate; it has no 256-bit integer ops, so the bf16 -> f32 widening runs in 128-bit halves.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace strata::kernels::cpu {
|
||||
|
||||
/// Mirrors bf16_rows_dot_multi's arithmetic (same accumulation shape, mul+add instead of FMA).
|
||||
/// Requires cols % 8 == 0 and nt <= 8, exactly like the AVX2 kernel it stands in for.
|
||||
void bf16_rows_dot_multi_avx1(const uint16_t* w, int rows, int cols, const float* x, int nt, float* out);
|
||||
|
||||
} // namespace strata::kernels::cpu
|
||||
@@ -39,6 +39,9 @@ void native_quant_act(const NativeFmt& f, const float* x, void* dst);
|
||||
/// h (n_ff floats) -> the down activation (h_bytes).
|
||||
void native_quant_h(const NativeFmt& f, const float* h, void* dst);
|
||||
|
||||
/// From how many tokens native_gu_rows gives this gate/up type to a multi-token kernel (#152; ggml-cpu's per-token dot
|
||||
/// below that). 1: a token's rows are the same alone and in any group.
|
||||
int native_gu_mt_min(int gu_type);
|
||||
/// ff[t][r] = silu(gate_r . a[t]) * (up_r . a[t]) for rows r in [r0, r1), `nt` tokens.
|
||||
void native_gu_rows(const NativeFmt& f, const uint8_t* blob, const void* const* act, int nt, float* const* ff,
|
||||
int r0, int r1);
|
||||
|
||||
@@ -76,12 +76,25 @@ struct CpuTopology {
|
||||
int p_cores = 0; ///< Physical performance cores
|
||||
int p_threads = 0; ///< Total logical threads on performance cores
|
||||
int e_cores = 0; ///< Efficient cores
|
||||
std::vector<int> worker_cores; ///< Ordered logical core IDs for workers (excluding host if skip_first)
|
||||
std::vector<int> worker_cores; ///< Ordered CPU IDs; on Windows, group * 64 + processor within the group
|
||||
int host_core = -1; ///< Logical core reserved for host thread
|
||||
};
|
||||
|
||||
/// Which core the host thread - the layer loop that spins on the GPU's flags - takes when `skip_first` reserves one
|
||||
/// (--host-core first|last, STRATA_HOST_CORE). `First` is the layout the pool has always used. `Last` (eddoursul's
|
||||
/// fork, F12) puts the host on the last physical core and lets the workers have the first: Windows sends a GPU's
|
||||
/// interrupts to one logical processor, usually the first, and every copy that lands raises one (~13 us in the ISR
|
||||
/// and a DPC), which delays a spinning host there. The placement changes no result, only where threads run. A hybrid
|
||||
/// CPU keeps `First` (its first core is the best P-core).
|
||||
enum class HostCore { First, Last };
|
||||
void set_host_core(HostCore where);
|
||||
HostCore host_core_setting();
|
||||
|
||||
CpuTopology detect_cpu_topology(bool skip_first, PoolAffinity affinity = PoolAffinity::All);
|
||||
|
||||
/// The logical processor the host thread is pinned to under the current setting (-1: no topology).
|
||||
int planned_host_core(PoolAffinity affinity = PoolAffinity::All);
|
||||
|
||||
/// One logical processor per PHYSICAL core, so a worker is never scheduled onto an SMT sibling of another
|
||||
/// worker. On the 6-core/12-thread machine this project measures on, `hardware_concurrency()/2` workers on
|
||||
/// logical processors 0..5 would put every worker on a sibling pair and halve the useful bandwidth - which is
|
||||
@@ -100,9 +113,23 @@ std::vector<int> physical_cores(bool skip_first, PoolAffinity affinity = PoolAff
|
||||
/// - exactly 5/6 of L9's 44.14 on 6 - and at **26.9 GB/s inside the host loop**, where the unpinned spinning
|
||||
/// host is free to land on a worker's core or its SMT sibling. That 1.35x is not the kernel.
|
||||
///
|
||||
/// Returns the PREVIOUS affinity mask, or -1 if the platform refused; pass it to `restore_thread_affinity`.
|
||||
long long pin_current_thread(int core);
|
||||
void restore_thread_affinity(long long previous);
|
||||
/// The previous host placement. `valid` is false when querying or setting placement failed.
|
||||
/// Windows uses a reversible CPU Set selection, leaving hard affinity (including Windows 11's implicit
|
||||
/// all-group eligibility) untouched. An empty selection restores inheritance from the process defaults.
|
||||
/// Linux stores the complete, dynamically sized native CPU mask, including CPU IDs above 63.
|
||||
struct ThreadAffinity {
|
||||
#if defined(_WIN32)
|
||||
std::vector<unsigned long> cpu_sets;
|
||||
#else
|
||||
std::vector<unsigned long> mask;
|
||||
#endif
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
/// Selects one encoded processor for the caller; Windows CPU Sets respect existing hard-affinity limits.
|
||||
/// Restore on the same calling thread. Pool-owned workers use hard group affinity instead.
|
||||
ThreadAffinity pin_current_thread(int core);
|
||||
void restore_thread_affinity(const ThreadAffinity& previous);
|
||||
|
||||
class ExpertPool {
|
||||
public:
|
||||
|
||||
@@ -52,6 +52,10 @@ bool cvec_replicate(std::string& err);
|
||||
void cvec_set_enabled(bool on);
|
||||
bool cvec_enabled();
|
||||
|
||||
/// The CURRENT device's tables (dir n_layers x n_embd, s n_layers, the request flag), for a caller that applies the
|
||||
/// vector inside its own kernel (the prompt path's STRATA_CVEC_FUSE). False without a vector on this device.
|
||||
bool cvec_tables(const float** dir, const float** s, const int** on);
|
||||
|
||||
/// Layer `layer`'s vector on T tokens' residual stacks (`R + t * r_ld`, hc streams of n_embd). With `write`, the
|
||||
/// pending FFN write `R += bo * 2 sigmoid(inj / hc)` (the fused read's arithmetic) is applied first, for callers
|
||||
/// whose writes are folded into the next layer's read; without it, R must already hold the layer's output.
|
||||
|
||||
Loaded 100 of 543 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user