Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c8dd32252 | ||
|
|
5964d113f8 | ||
|
|
fea2d02aaa | ||
|
|
81a7260194 | ||
|
|
3b1ab2848d | ||
|
|
80a70b86de | ||
|
|
9222b6a1d3 | ||
|
|
67a48427b8 | ||
|
|
d64ae1b0b1 | ||
|
|
27c0cabd81 | ||
|
|
2f5f7c1135 | ||
|
|
3bf804c9c8 | ||
|
|
0af0f9f125 | ||
|
|
ea67f6999a | ||
|
|
eb818c50ed | ||
|
|
092a62daaa | ||
|
|
1ed28f8863 | ||
|
|
38c7f7017d | ||
|
|
c924d3c9d7 | ||
|
|
6f8800aefe | ||
|
|
814716336a | ||
|
|
0530a1f7c2 | ||
|
|
f6db09bae2 | ||
|
|
217ddda0eb | ||
|
|
a14e85d8e6 | ||
|
|
164dd3bbef | ||
|
|
6577b69d5d | ||
|
|
dca4217501 | ||
|
|
84b1e9815e | ||
|
|
c266cfb3cb | ||
|
|
a8301a2709 | ||
|
|
669023fc08 | ||
|
|
b440640929 | ||
|
|
155995b65c | ||
|
|
e890e30766 | ||
|
|
7b04360f93 | ||
|
|
e22a3c9ba9 | ||
|
|
b7196c23de | ||
|
|
fcb0c2d0ec | ||
|
|
e17efa53ce | ||
|
|
3f81245864 | ||
|
|
9471faa5f0 | ||
|
|
3b51719abc | ||
|
|
2ba95cf61f | ||
|
|
437a60676f | ||
|
|
f1b6a2144c | ||
|
|
c62d980112 | ||
|
|
051db74cdf | ||
|
|
0c467e9d45 | ||
|
|
76ac5842ff | ||
|
|
5de8d26949 | ||
|
|
e4b31d6f69 | ||
|
|
b96f174775 | ||
|
|
f0010a607f | ||
|
|
ed0ed38e2d | ||
|
|
c4aec611b2 | ||
|
|
3ae6b3041b | ||
|
|
ebaeda85f1 | ||
|
|
815256d89b | ||
|
|
b14862351b | ||
|
|
f8f4e8a7df | ||
|
|
acf63bef73 | ||
|
|
098fd545b3 | ||
|
|
a7cd2de97a | ||
|
|
be58604387 | ||
|
|
5701fc65ce | ||
|
|
0351ecb11d | ||
|
|
d9042a16bd | ||
|
|
642f5120a7 | ||
|
|
98a139c3c7 | ||
|
|
828a2559a9 | ||
|
|
bf8285b8c9 | ||
|
|
a71fd34758 | ||
|
|
7d55581527 | ||
|
|
034f2fdebe | ||
|
|
c5b13accfd | ||
|
|
4791d6b676 | ||
|
|
8bf178febf | ||
|
|
1fa22cd29a | ||
|
|
e2858b9855 | ||
|
|
e6dbbc312a | ||
|
|
425bca7199 | ||
|
|
1298a3971c | ||
|
|
c40a03e6dc | ||
|
|
eebb870922 | ||
|
|
dd8201d20d | ||
|
|
a4c503d7ce | ||
|
|
23b3dfc2ca | ||
|
|
8be3f916f2 | ||
|
|
424b364a88 | ||
|
|
0788a91c64 | ||
|
|
ad98bf1d17 | ||
|
|
c67180edf1 | ||
|
|
a539e12c97 | ||
|
|
2a22daa4f0 | ||
|
|
27a5707de5 | ||
|
|
72d0bf667e | ||
|
|
e7ded666be | ||
|
|
3ae2123bce | ||
|
|
6d4874a491 | ||
|
|
b1ebc3d7ca | ||
|
|
ff9ca03f3c | ||
|
|
1ef68f83e7 | ||
|
|
aec327ee51 | ||
|
|
9fe2ad3f55 | ||
|
|
ba6966d83f | ||
|
|
4c953b424d | ||
|
|
6f83fe2c42 | ||
|
|
2ccf57ec46 | ||
|
|
89799596a6 | ||
|
|
48ec4a59ac | ||
|
|
e0d79b02f9 | ||
|
|
d45cd9d3cf | ||
|
|
5bc7f8cab3 | ||
|
|
b7d5ea2f82 | ||
|
|
d232721db6 | ||
|
|
5f49ec4599 | ||
|
|
6878105560 | ||
|
|
7f97c35aca | ||
|
|
651a56806a | ||
|
|
d276adf277 | ||
|
|
d8f58af1b5 | ||
|
|
dcba249d01 | ||
|
|
5f426c38cb | ||
|
|
128223891e | ||
|
|
17c9b92bc9 | ||
|
|
d1dec64362 | ||
|
|
011c9e600a | ||
|
|
76f90e5843 | ||
|
|
328a3f5bd2 | ||
|
|
1df3a3108b | ||
|
|
daa654bd35 | ||
|
|
68864c564e | ||
|
|
2c27afec08 | ||
|
|
1c9bfd2a6e | ||
|
|
be4e24d421 | ||
|
|
3a7a6955cf | ||
|
|
4f864bcd2d | ||
|
|
2c8eb88196 | ||
|
|
e0cfb80f53 | ||
|
|
86d10ed8f9 | ||
|
|
01f8c32559 | ||
|
|
894f133b7d | ||
|
|
8a4106a47e | ||
|
|
4c3edf4765 | ||
|
|
f05725c332 | ||
|
|
8de1ec4165 | ||
|
|
3fbcea2f79 | ||
|
|
dc16e3a7cb | ||
|
|
5cdb59cfa9 | ||
|
|
0cf83f610a | ||
|
|
18b4affb87 | ||
|
|
aae92ceb26 | ||
|
|
926e7a38b8 | ||
|
|
366c8dbaae | ||
|
|
70f4240728 | ||
|
|
30a67051d0 | ||
|
|
b8b381a3b9 | ||
|
|
25d025fef8 | ||
|
|
8895949ef7 | ||
|
|
0da82e740c | ||
|
|
95cfbd4cc4 | ||
|
|
c8b1436022 | ||
|
|
9de04cd44e | ||
|
|
31ca248fbe | ||
|
|
63214a5655 | ||
|
|
86620837b2 | ||
|
|
75d0d0b966 | ||
|
|
6515aebac2 | ||
|
|
f801953992 | ||
|
|
53752401f2 | ||
|
|
ab61f892d8 | ||
|
|
034422c4d3 | ||
|
|
aece46db89 | ||
|
|
1e7bcb8b6e | ||
|
|
c5eb2dd923 | ||
|
|
88942998c5 | ||
|
|
0255581677 | ||
|
|
9b5b758358 | ||
|
|
29c811217f | ||
|
|
5f81c5dc47 | ||
|
|
cbf7f1d9f9 | ||
|
|
4e83ef04d5 | ||
|
|
34d7b2a3ba | ||
|
|
31fa54fb68 | ||
|
|
bc3f293861 | ||
|
|
21b0fdb0d4 | ||
|
|
564d0abebe | ||
|
|
413544b6d9 | ||
|
|
634e5826a5 | ||
|
|
c4f1e05dbd | ||
|
|
e2b4464917 | ||
|
|
10edc50f05 | ||
|
|
f353087104 | ||
|
|
79946b9c4f | ||
|
|
4cb96acd81 | ||
|
|
ca40c88ef4 | ||
|
|
e28889d120 | ||
|
|
890fee81b0 | ||
|
|
5e633b4ff7 | ||
|
|
7c11a196d5 | ||
|
|
8bf77c0755 | ||
|
|
8c41d3611c | ||
|
|
2aa1ec2d56 | ||
|
|
7a55995818 | ||
|
|
82a97251ea | ||
|
|
cbfd5b926b | ||
|
|
2e58e320b5 | ||
|
|
301ab7c7b4 | ||
|
|
0fce78c1c3 | ||
|
|
095e524fca | ||
|
|
88278af79a | ||
|
|
c864d804b6 | ||
|
|
51f418f15a | ||
|
|
9f38ee338c | ||
|
|
b2f2149431 | ||
|
|
ed61ad2a9c | ||
|
|
06e614017d | ||
|
|
2411e7cd3b | ||
|
|
8cfe44b71d | ||
|
|
1b47c1d076 | ||
|
|
b1594fbbe5 | ||
|
|
4dd62738df | ||
|
|
f6b4101127 | ||
|
|
d581286885 | ||
|
|
0b736f7702 | ||
|
|
b6825a493c | ||
|
|
c0b0869b03 | ||
|
|
2219cc99bf | ||
|
|
4681762e3c | ||
|
|
6b93b42877 | ||
|
|
d3cb2f58f1 | ||
|
|
9bb1dd2044 | ||
|
|
c3472caf8e | ||
|
|
f1bfcc2253 | ||
|
|
a64f1dd99b | ||
|
|
02791a6d18 | ||
|
|
672fd99ccc | ||
|
|
5b47f889b0 | ||
|
|
5db632e72a | ||
|
|
f5c4047e78 | ||
|
|
1f727f4b52 | ||
|
|
ebe4db78d4 | ||
|
|
dea6920b81 | ||
|
|
9e24f4ab32 | ||
|
|
dcbaa992d2 | ||
|
|
79cb59c574 | ||
|
|
ae456dfe5c | ||
|
|
176e11a597 | ||
|
|
5dc151d4d3 | ||
|
|
0dbea601a0 | ||
|
|
3d5594d656 | ||
|
|
284552fe26 | ||
|
|
c24f2a139f | ||
|
|
bd4c6bf1af | ||
|
|
fa56c2b387 | ||
|
|
cf34e915c4 | ||
|
|
14be49605f | ||
|
|
6b29adad1a | ||
|
|
e728a132d7 | ||
|
|
32b9f75376 | ||
|
|
1972af6da1 | ||
|
|
7db513297d | ||
|
|
bf2926e05a | ||
|
|
4dee320dd5 | ||
|
|
c316a7c215 | ||
|
|
73b68dfd88 | ||
|
|
ce938fb8c9 | ||
|
|
fb2f387407 | ||
|
|
9d2870d187 | ||
|
|
b611ec5f1e | ||
|
|
ddbbda4824 | ||
|
|
386155fa25 | ||
|
|
ff64e08bba | ||
|
|
a569d80680 | ||
|
|
461d0baee7 | ||
|
|
ef5d8ab946 | ||
|
|
bcda2e4b0d | ||
|
|
34d6c37df3 | ||
|
|
f207907b14 | ||
|
|
7ebdd34356 | ||
|
|
aa44b9ce53 | ||
|
|
af189129a4 | ||
|
|
c6a0e739be | ||
|
|
e52668857c | ||
|
|
c0c3a06ce4 | ||
|
|
f39d951904 | ||
|
|
968fcb70b0 | ||
|
|
b486d6f2b4 | ||
|
|
43127e3982 | ||
|
|
6c688faf22 | ||
|
|
0a35538b74 | ||
|
|
72f66767ca | ||
|
|
031adf99bf | ||
|
|
e3040e9e25 | ||
|
|
6f1eae68ff | ||
|
|
085474b8dc | ||
|
|
1d6a040129 | ||
|
|
a0e09803ac | ||
|
|
2fcc25aab6 | ||
|
|
67bb56bd1d | ||
|
|
d511c5102e | ||
|
|
d408fa2ff1 | ||
|
|
970c75c8f6 | ||
|
|
ee8e6404b0 | ||
|
|
17cb817594 | ||
|
|
594e379291 | ||
|
|
fd4f7d445c | ||
|
|
226b5f5492 | ||
|
|
9dc1c96377 | ||
|
|
4e1e6bad4e | ||
|
|
3de97a8892 | ||
|
|
c23a4a5827 | ||
|
|
4aabf3b8f5 | ||
|
|
6ce6413606 | ||
|
|
4fd5b0646d | ||
|
|
703daada09 | ||
|
|
d69c9c4b4f | ||
|
|
b22206c7bc | ||
|
|
a4f7975324 | ||
|
|
bcf88b7383 | ||
|
|
6a55bcb5e9 | ||
|
|
c7938c2d3e | ||
|
|
69a6db1904 | ||
|
|
1b6f782faf | ||
|
|
215aa14f25 | ||
|
|
4fc2e140bd | ||
|
|
740acc8934 | ||
|
|
72fc52251b | ||
|
|
0c79d7db94 | ||
|
|
d298999c3c | ||
|
|
632b4ed283 | ||
|
|
b111d1e830 | ||
|
|
56dee6b938 | ||
|
|
d101771758 | ||
|
|
a5b1c2cefa | ||
|
|
ca1d469e9a | ||
|
|
47d9d6c75a | ||
|
|
f2709679db | ||
|
|
c330caf4da | ||
|
|
7adced3db5 | ||
|
|
a0b6d1cd11 | ||
|
|
774096d16b | ||
|
|
76186bb595 | ||
|
|
e346198ff3 | ||
|
|
98faf8139e | ||
|
|
17b93dcb7c | ||
|
|
52ecf67ef7 | ||
|
|
f74ba2ea53 | ||
|
|
4fb0dde729 | ||
|
|
f94a34c330 | ||
|
|
c7c0e37bc4 | ||
|
|
2f4e42b9cf | ||
|
|
d61a443eb8 | ||
|
|
52cc6b4af0 | ||
|
|
31361a98bf | ||
|
|
87b7bb2ccb | ||
|
|
48c34f735a | ||
|
|
537f8fef8e | ||
|
|
83ae413176 | ||
|
|
80f7679b61 | ||
|
|
b8488e0d2b | ||
|
|
d0afabb6ab | ||
|
|
656478c68d | ||
|
|
fccfd343d7 | ||
|
|
5a9b4f4fc4 | ||
|
|
4509f2f997 | ||
|
|
f9e1fff983 | ||
|
|
a6ffc04d6d | ||
|
|
a5c7abe074 | ||
|
|
2793e3bd33 | ||
|
|
1b9b768939 | ||
|
|
642425bf2e | ||
|
|
cc500be1c9 | ||
|
|
13806c323e | ||
|
|
2f361a3757 | ||
|
|
2dfb254f7b | ||
|
|
28b150f854 | ||
|
|
676b57bff2 | ||
|
|
9424830516 | ||
|
|
00e8d10994 | ||
|
|
bf69ec2da4 | ||
|
|
7924bf65d2 | ||
|
|
867d7477ff | ||
|
|
f5410a6feb | ||
|
|
bb7e399c30 | ||
|
|
4bd5912695 | ||
|
|
8c07c0c36a | ||
|
|
9ae93f7b1f | ||
|
|
9f13d1e78f | ||
|
|
a0abdf7588 | ||
|
|
149abc896b | ||
|
|
069384b4f6 | ||
|
|
c644312d15 | ||
|
|
ac71d2b32e | ||
|
|
c093ac263f | ||
|
|
436fbd0a6e | ||
|
|
7274c10e9a | ||
|
|
f0afedc50c | ||
|
|
5bda973a43 | ||
|
|
b5d14279aa | ||
|
|
2e12958759 | ||
|
|
e4d22e7ef1 | ||
|
|
e59e64ec5d | ||
|
|
393eec80f9 | ||
|
|
06425cf932 | ||
|
|
afbd64bc4f | ||
|
|
2614b82c9a | ||
|
|
5b8cdaf2d9 | ||
|
|
ea6b385df8 | ||
|
|
8ca9093d7d | ||
|
|
ec841ca1e9 | ||
|
|
c4442fed95 | ||
|
|
2496795bd9 | ||
|
|
8d29804204 | ||
|
|
1523c592a6 | ||
|
|
f8c4410a38 | ||
|
|
df4706577e | ||
|
|
afd1f61cc5 | ||
|
|
31d857f817 | ||
|
|
4126250bf1 | ||
|
|
709f8dc63d | ||
|
|
3985c50777 | ||
|
|
f68ccf30c7 | ||
|
|
90767252f4 | ||
|
|
a7bdbfdabc | ||
|
|
9944d2f7f1 | ||
|
|
b42ec662c7 | ||
|
|
b35a9816c7 | ||
|
|
d33bb832ca | ||
|
|
3e7c5d8e1c | ||
|
|
894d1be1f4 | ||
|
|
93aa62ed64 | ||
|
|
83027df587 | ||
|
|
484e06b961 | ||
|
|
ed4595ad69 | ||
|
|
06d7604af7 | ||
|
|
ecf2bdcb7f | ||
|
|
3f1fa67f14 | ||
|
|
881a42adab | ||
|
|
3616148bbf | ||
|
|
24e56539c5 | ||
|
|
8e957cd53b | ||
|
|
4f6ec303b5 | ||
|
|
1f52a00373 | ||
|
|
e68a032a58 | ||
|
|
10715d856d | ||
|
|
e45a9bfd3a | ||
|
|
34e7c5cb1a | ||
|
|
8aaf188aa9 | ||
|
|
22703894ec | ||
|
|
b1713b3ff2 | ||
|
|
0875029d10 | ||
|
|
952cce7752 | ||
|
|
30c64f09d7 | ||
|
|
cbc0e329c6 | ||
|
|
f680a80d88 | ||
|
|
d8a6a0e012 | ||
|
|
3cc18d7bb7 | ||
|
|
1994f3acb2 | ||
|
|
c911065a14 | ||
|
|
40c605f7bc | ||
|
|
96a9107a27 | ||
|
|
4e76d6c9a6 | ||
|
|
8441cfd0e2 | ||
|
|
9cc2a9b6aa | ||
|
|
9b8436df4e | ||
|
|
c6a20b7e85 | ||
|
|
88b29d481a | ||
|
|
2efa4e067a | ||
|
|
eb7eac250b | ||
|
|
9f5488e3b2 | ||
|
|
cac4816afd | ||
|
|
4ed8eb02fa | ||
|
|
b203b5f71e | ||
|
|
468e3a6252 | ||
|
|
c6aa4223d9 | ||
|
|
c23a78694e | ||
|
|
509466a668 | ||
|
|
a10777a671 | ||
|
|
bcf67d1774 | ||
|
|
c8e5d2f3ce | ||
|
|
6d41b37e76 | ||
|
|
e1ef9df21c | ||
|
|
dab7ee72d0 | ||
|
|
6b28dbbd1e | ||
|
|
e0e0c0545a | ||
|
|
8acee5a055 | ||
|
|
dca9770ef4 | ||
|
|
ff18fbb868 | ||
|
|
cf4749276d | ||
|
|
578bb6aa7a | ||
|
|
1073929c80 | ||
|
|
e5dc00c81e | ||
|
|
1c3c6d3351 | ||
|
|
ce968a2fac | ||
|
|
40d1a3e7d7 | ||
|
|
a269d1b983 | ||
|
|
9d2cb4f6d2 | ||
|
|
74e25e165b | ||
|
|
f5690d51a8 | ||
|
|
e2071c2f8b | ||
|
|
f2345ebd66 | ||
|
|
671f4573da | ||
|
|
3d0a79ca23 | ||
|
|
f523a94227 | ||
|
|
267f35f777 | ||
|
|
71bdfda0b3 | ||
|
|
7662bf85c4 | ||
|
|
44a56865f5 | ||
|
|
7f037f4451 | ||
|
|
5c9884cdee | ||
|
|
5b4db785ca | ||
|
|
321f159032 | ||
|
|
54a02dc32c | ||
|
|
40bf5f5c40 | ||
|
|
bc54d945f6 | ||
|
|
7a2bfc52ae | ||
|
|
e88ee39d1d | ||
|
|
a5ee74ffa3 | ||
|
|
74652692e1 | ||
|
|
85f77658f3 | ||
|
|
f57c0adcb3 | ||
|
|
3fa1b524d7 | ||
|
|
76e51103c7 | ||
|
|
fcca2ccb84 | ||
|
|
eac694ae4c | ||
|
|
4caf857c76 | ||
|
|
e5d987a553 | ||
|
|
dde39e8a8f | ||
|
|
03b75b8da9 | ||
|
|
3d1fb48090 | ||
|
|
042cd201c9 | ||
|
|
b961c7aa8c | ||
|
|
6ec549d2dd | ||
|
|
4ff5972fcb | ||
|
|
04a2e6a3f1 | ||
|
|
c301209636 | ||
|
|
78bba245a5 | ||
|
|
18f7b90ca6 | ||
|
|
1b8e88b9e5 | ||
|
|
ef59c0b555 | ||
|
|
4ea8171636 | ||
|
|
3c3d2881b4 | ||
|
|
604e23cd9a | ||
|
|
41cc4b2e89 | ||
|
|
3d9c21509e | ||
|
|
779bc30501 | ||
|
|
d5bcc3bd8d | ||
|
|
116adbf7d6 | ||
|
|
19d5cd87fa | ||
|
|
2ca90e48ec | ||
|
|
adfb7ecb2a | ||
|
|
a122ba0bdf | ||
|
|
04221c3e2b | ||
|
|
fdb5e48616 | ||
|
|
af78949b17 | ||
|
|
935407d883 | ||
|
|
899d1b1b3a | ||
|
|
02a8193da7 | ||
|
|
c7bfd131b5 | ||
|
|
6f2a3e758d | ||
|
|
e193161de2 | ||
|
|
06b6b23e51 | ||
|
|
553c25ca7c | ||
|
|
8ac47258bb | ||
|
|
dbd1cb9af8 | ||
|
|
94f095c893 | ||
|
|
a4c1644a37 | ||
|
|
aa82a88639 | ||
|
|
9962aae5b7 | ||
|
|
cbbdd16b43 | ||
|
|
25492e4a6b | ||
|
|
7e3a9fec71 | ||
|
|
2a6650a364 | ||
|
|
5e7b9a8301 | ||
|
|
2b209b406d | ||
|
|
6ff75059a9 | ||
|
|
e46be1fe0a | ||
|
|
f86ca9d1c5 | ||
|
|
477fd36bce | ||
|
|
193cbb1fbf | ||
|
|
76e4fef398 | ||
|
|
dac635445b | ||
|
|
046bdc83d9 | ||
|
|
667d3cf7d7 | ||
|
|
4794b13779 | ||
|
|
09b8b54d19 | ||
|
|
98b865c56f | ||
|
|
91baead754 | ||
|
|
80a10f31c3 | ||
|
|
b27c46cbd6 | ||
|
|
1c4970b9ac | ||
|
|
26613a44f6 | ||
|
|
afe92c1727 | ||
|
|
ad262c80aa | ||
|
|
14ec155ec7 | ||
|
|
e9e08adb37 | ||
|
|
fd0e3766a3 | ||
|
|
95f3807699 | ||
|
|
a2367ca55b | ||
|
|
e657d310e7 | ||
|
|
64af23dafc | ||
|
|
bccb0f4280 | ||
|
|
adc4a6822b | ||
|
|
3d2e7f166e | ||
|
|
545fccf007 | ||
|
|
d6aa6504e0 | ||
|
|
d5fda4d2f3 | ||
|
|
8667a3dbf7 | ||
|
|
b7b65edeb1 | ||
|
|
4440a303f8 | ||
|
|
e107297a8f | ||
|
|
6b8cfcd1a3 | ||
|
|
d060783fbb | ||
|
|
a3024ed10a | ||
|
|
c4345a7c03 | ||
|
|
10f3435b00 | ||
|
|
35698ffe70 | ||
|
|
e6ab4c024d | ||
|
|
23b08c7620 | ||
|
|
e23a56f2f8 | ||
|
|
b0b8860d13 | ||
|
|
1486151d4d | ||
|
|
97cb031bf7 | ||
|
|
c59505e638 | ||
|
|
34bd640a19 | ||
|
|
0f5511cfdf | ||
|
|
b3348da493 | ||
|
|
b31c13875e | ||
|
|
b2e3008bfb | ||
|
|
55ca7b955a | ||
|
|
979da9d2bc | ||
|
|
536c7f019e | ||
|
|
de9101935d | ||
|
|
f82fffbe25 | ||
|
|
b808d6e442 | ||
|
|
75127c794e | ||
|
|
96658aebae | ||
|
|
fb14c354b4 | ||
|
|
43ea31544b | ||
|
|
a1788c54be | ||
|
|
7f7bfa962b | ||
|
|
2832586641 | ||
|
|
a04244a4be | ||
|
|
714ca744dc | ||
|
|
e5476b0e12 | ||
|
|
fef5dfec03 | ||
|
|
c157ea373f | ||
|
|
40b41da916 | ||
|
|
31030b170a | ||
|
|
b23dad1d5f | ||
|
|
8837cb6f0f | ||
|
|
707eebd1dd | ||
|
|
f4cb8095c4 | ||
|
|
2325b2ee26 | ||
|
|
7572afc5bf | ||
|
|
4794a8067f | ||
|
|
95ece0bcd2 | ||
|
|
e0aa3b872a | ||
|
|
1237d9a5fa | ||
|
|
337c70d3c2 | ||
|
|
e6aa6f317f | ||
|
|
969d7da7df | ||
|
|
c0abfdc358 | ||
|
|
323a11e721 | ||
|
|
f4078faf4f | ||
|
|
e09eccc839 | ||
|
|
83f2a2c73a | ||
|
|
66ad879d18 | ||
|
|
76ceb365ca | ||
|
|
8f2f34ec4e | ||
|
|
bac1a3e0e9 | ||
|
|
24dc4a1dd6 | ||
|
|
44fbe69c66 | ||
|
|
c31367979d | ||
|
|
240304928b | ||
|
|
82c80ac4d7 | ||
|
|
fb7c62963e | ||
|
|
8b1139d026 | ||
|
|
7b06ea51bd | ||
|
|
2e111557b0 | ||
|
|
c47fbb3f22 | ||
|
|
cd1b3bd31a | ||
|
|
0c7872ddc2 | ||
|
|
ea1e28436f | ||
|
|
261145baaf | ||
|
|
201d674e94 | ||
|
|
4eeccd9c00 | ||
|
|
2dbd302038 | ||
|
|
6149cec03e | ||
|
|
5eea92bb09 | ||
|
|
6ad1ad69ab | ||
|
|
c1440388fb | ||
|
|
e2d03ac4fa | ||
|
|
b533ae5246 | ||
|
|
7a70fcccbc | ||
|
|
796937d485 | ||
|
|
e932ad7eb7 | ||
|
|
3084cd93de | ||
|
|
14691f6e53 | ||
|
|
36e06fcd86 | ||
|
|
65ac8f9106 | ||
|
|
138075961f | ||
|
|
bec4c838ab | ||
|
|
af26053565 | ||
|
|
cfe65f8d1a | ||
|
|
ae40bd7516 | ||
|
|
79e94f37f0 | ||
|
|
bca49fc5f2 | ||
|
|
d631ed2da7 | ||
|
|
036c8f6ec3 | ||
|
|
9319219623 | ||
|
|
31f225e4fb | ||
|
|
eeb05631fe | ||
|
|
8a66da38a5 | ||
|
|
a451dcd913 | ||
|
|
ac658f54a6 | ||
|
|
12fd8cff65 | ||
|
|
fca3c59586 | ||
|
|
35af455aec | ||
|
|
3fe84ec28d | ||
|
|
83cde2ca30 | ||
|
|
5ece068f7e | ||
|
|
196a683d6d | ||
|
|
fd46e34d5c | ||
|
|
c765c5f87b | ||
|
|
983fa18ad3 | ||
|
|
5bcc208a39 | ||
|
|
0e58a652fb | ||
|
|
88c1502dc7 | ||
|
|
97b680e13e | ||
|
|
a77d9fab54 | ||
|
|
58f81f20e3 | ||
|
|
8253477bf9 | ||
|
|
6aa8e25f49 | ||
|
|
af3770c6b0 | ||
|
|
40a2216b42 | ||
|
|
2e1d62961a | ||
|
|
b6a38b3288 | ||
|
|
d9070189c0 | ||
|
|
2725c23f74 | ||
|
|
3980a5ce70 | ||
|
|
2044864dba | ||
|
|
9a01b6e3b8 | ||
|
|
961d37cd55 | ||
|
|
2053388dc9 | ||
|
|
ebdfaacd1c | ||
|
|
9c4516509f | ||
|
|
4908ddaa49 | ||
|
|
d8471050bd | ||
|
|
2856a8045e | ||
|
|
c85cc6dd19 | ||
|
|
aef32e224b | ||
|
|
83aa59fdc5 | ||
|
|
f3be974359 | ||
|
|
9b75c6a127 | ||
|
|
219ae436fa | ||
|
|
79ac3164a0 | ||
|
|
2c6bfef271 | ||
|
|
99f6d47233 | ||
|
|
577d644480 | ||
|
|
b1d4686f4a | ||
|
|
00d2f12a2d | ||
|
|
e77f1dfc3f | ||
|
|
4306182741 | ||
|
|
1b003bd406 | ||
|
|
e4ed5d1a4e | ||
|
|
52f50ba1f1 | ||
|
|
a3cf8a647b | ||
|
|
6c16529a84 | ||
|
|
01c8381ce6 | ||
|
|
efa888b143 | ||
|
|
c37a3c415b | ||
|
|
dd1a55f99a | ||
|
|
51c60366b8 | ||
|
|
1b27554b9c | ||
|
|
c1a5b13fdf | ||
|
|
9d868f38e8 | ||
|
|
3409d9aaec | ||
|
|
20f92128c5 | ||
|
|
b1e0fdaa4b | ||
|
|
a0469e9455 | ||
|
|
e1168345f1 | ||
|
|
5c5fdafb94 | ||
|
|
eaddf15b77 | ||
|
|
a8fdceba5b | ||
|
|
8be9e0976f | ||
|
|
e01f766118 | ||
|
|
401169c02f | ||
|
|
b829a3189d | ||
|
|
b25b309703 | ||
|
|
959e6c34c4 | ||
|
|
75afe3f4a0 | ||
|
|
fa726b4c9c | ||
|
|
1d03976529 | ||
|
|
941041768b | ||
|
|
44997124ff | ||
|
|
3b4c379b68 | ||
|
|
2aeb246cfc | ||
|
|
ae31e3cd36 | ||
|
|
8d838f770f | ||
|
|
7bdc1ffb72 | ||
|
|
a00ed935f1 | ||
|
|
404713f234 | ||
|
|
4895b1cfa0 | ||
|
|
039d2d4609 | ||
|
|
fa6367a98f | ||
|
|
33d37ddff6 | ||
|
|
80e6d91a63 | ||
|
|
ba2c2d5fb7 | ||
|
|
75fabbd941 | ||
|
|
390336f68d | ||
|
|
7e490ef63c | ||
|
|
da34c60ba7 | ||
|
|
3641dafe98 | ||
|
|
c7f803d0db | ||
|
|
5f1bd58e30 | ||
|
|
f533c7faa6 | ||
|
|
31abd50a60 | ||
|
|
91cccc1ae1 | ||
|
|
e77e5550df | ||
|
|
15339326e9 | ||
|
|
1d8d830afa | ||
|
|
c198c25e57 | ||
|
|
ac38793caa | ||
|
|
a6759787c3 | ||
|
|
b2761df626 | ||
|
|
bfed34873a | ||
|
|
b7505b72a4 | ||
|
|
1b856af242 | ||
|
|
051d0fc9c4 | ||
|
|
447b99ba38 | ||
|
|
d85bb21c6b | ||
|
|
d7347cdde6 | ||
|
|
ad3b340d30 | ||
|
|
37f7f03cd2 | ||
|
|
cbb249d3dd | ||
|
|
f95fb3e074 | ||
|
|
6aa502d938 | ||
|
|
67bd2a1609 | ||
|
|
fe6fa9a274 | ||
|
|
db5be2aa55 | ||
|
|
50768496f5 | ||
|
|
ab5adeb79f | ||
|
|
ac5f0d19a3 | ||
|
|
9a22b78887 | ||
|
|
7c5e846121 | ||
|
|
417cbab4ff | ||
|
|
788e0b731b | ||
|
|
debe8ddabb | ||
|
|
7ecfe4a6a6 | ||
|
|
4db866ae00 | ||
|
|
8c676a7ccd | ||
|
|
8b3a96779e | ||
|
|
c61364fc89 | ||
|
|
e29112390f | ||
|
|
8d65058c9f | ||
|
|
e819ba0bd0 | ||
|
|
0b055d8316 | ||
|
|
ee0f328053 | ||
|
|
d4abe4b745 | ||
|
|
78fc321cd3 | ||
|
|
254ee2b9cd | ||
|
|
a93264877c | ||
|
|
e4b885e9fa | ||
|
|
f2fe9a9097 | ||
|
|
d2a38c9e5c | ||
|
|
47e2343b1b | ||
|
|
eeeeddd3f6 | ||
|
|
63ac0838bc | ||
|
|
ccb33b7c7c | ||
|
|
78914f55be | ||
|
|
6c4dbe3362 | ||
|
|
8df9e0afd0 | ||
|
|
395ed1ac54 | ||
|
|
13ded2d662 | ||
|
|
60c4d8de1e | ||
|
|
e288308b46 | ||
|
|
3618b48030 | ||
|
|
59cee65da3 | ||
|
|
9a58a4b73a | ||
|
|
f202d08c5f | ||
|
|
872bcd1974 | ||
|
|
6ad5596b9d | ||
|
|
982f43dee5 | ||
|
|
87fbbdaaa9 | ||
|
|
e3bf0ad04d | ||
|
|
b8c1602715 | ||
|
|
08297223e4 | ||
|
|
64fff9e149 | ||
|
|
b799ef5f6d | ||
|
|
8cc02778e4 | ||
|
|
87efc9ace5 | ||
|
|
c1533d6d5f | ||
|
|
a693660a15 | ||
|
|
436fabc466 | ||
|
|
7c2f878051 | ||
|
|
a2bab5c8c2 | ||
|
|
dc1aa5030a | ||
|
|
463d6780a9 | ||
|
|
9330989efa | ||
|
|
69f9cc7b9e | ||
|
|
b3ff0c2ebf | ||
|
|
4fbd0b3fcb | ||
|
|
d247802094 | ||
|
|
1fe683e951 | ||
|
|
eaa3a58172 | ||
|
|
7ce4b6df37 | ||
|
|
f548cfd843 | ||
|
|
6a29939806 | ||
|
|
ec06637d48 | ||
|
|
4e6a1b46be | ||
|
|
efa38a99ca | ||
|
|
4e788fbf3e | ||
|
|
da4a3a4765 | ||
|
|
4c9423e194 | ||
|
|
4fac421d14 | ||
|
|
7b1405e104 | ||
|
|
97e4dcaa1f | ||
|
|
c473d97109 | ||
|
|
4a786e03d6 | ||
|
|
e0a932a32d | ||
|
|
f31b6efd68 | ||
|
|
f11768e873 | ||
|
|
af34f0a4b7 | ||
|
|
64034f1674 | ||
|
|
8251089c1f | ||
|
|
af4a7e5638 | ||
|
|
5fbd539628 | ||
|
|
e61e90e555 | ||
|
|
17ba2d71ad | ||
|
|
76707b230b | ||
|
|
0a6eced5cc | ||
|
|
490fda2657 | ||
|
|
30a367e5f4 | ||
|
|
c3e9480592 | ||
|
|
d615708a7b | ||
|
|
4d7fede4ed | ||
|
|
9e2d698c9e | ||
|
|
7af6b6c152 | ||
|
|
917d2fc345 | ||
|
|
0ac98f80ee | ||
|
|
8509870849 | ||
|
|
aa936f59f0 | ||
|
|
ef3902477e | ||
|
|
ddfed500a9 | ||
|
|
ade3e6adb5 | ||
|
|
4736639a7e | ||
|
|
dbbe79aae8 | ||
|
|
bdb04dd8d6 | ||
|
|
48b87d42e1 | ||
|
|
e7f7dad3e0 | ||
|
|
0da73d9159 | ||
|
|
5e7a965ac1 | ||
|
|
19d16c7dbe | ||
|
|
d465e37bc1 | ||
|
|
c163678f7d | ||
|
|
0b871e2d79 | ||
|
|
4cca90e757 | ||
|
|
87d70909a8 | ||
|
|
7f9e719c8a | ||
|
|
f37abb1365 | ||
|
|
2b54cf4fbd | ||
|
|
b6b546ae13 | ||
|
|
d42319b6c9 | ||
|
|
c937aeb1df | ||
|
|
9c6255cfce | ||
|
|
998e9df959 | ||
|
|
c6a86ce168 | ||
|
|
743a6b3c61 | ||
|
|
e9a2ca44a7 | ||
|
|
89d0965408 | ||
|
|
8da1d3f8c1 | ||
|
|
eecb29593c | ||
|
|
83c8f0b120 | ||
|
|
c751c23965 | ||
|
|
2e1fcd15d9 | ||
|
|
e849aef2ae | ||
|
|
50f973c9f9 | ||
|
|
60bfe0693a | ||
|
|
9550318e34 | ||
|
|
2812c939fe | ||
|
|
87b90a7882 | ||
|
|
fa25086125 | ||
|
|
7c4b3d743e | ||
|
|
a7cd2b38e6 | ||
|
|
9cede7db08 | ||
|
|
35c72bcd10 | ||
|
|
99cc9829a0 | ||
|
|
837f992bd9 | ||
|
|
19ea30f6ab | ||
|
|
4b5cc08d1a | ||
|
|
ca34357b08 | ||
|
|
4818b1f4a1 | ||
|
|
0c165b48bf | ||
|
|
dab63e9c38 | ||
|
|
a79e653036 | ||
|
|
f9bfe8bc12 | ||
|
|
df52a28548 | ||
|
|
334dde73a2 | ||
|
|
c99896ae66 | ||
|
|
04cca3bca8 | ||
|
|
20b377d30d | ||
|
|
088f8685a7 | ||
|
|
3e6c354504 | ||
|
|
b085d54b94 | ||
|
|
ee8b23bcc7 | ||
|
|
7c4092dabe | ||
|
|
965f22e3db | ||
|
|
44a12d78be | ||
|
|
aaf37247da | ||
|
|
c5755ed837 | ||
|
|
baeb4533be | ||
|
|
ea2d565e60 | ||
|
|
c7b65c7f7b | ||
|
|
d146b0da29 | ||
|
|
9f24934663 | ||
|
|
70f8268dab | ||
|
|
0848864a03 | ||
|
|
8075af7c2a | ||
|
|
533f6b713f | ||
|
|
c20c4aed11 | ||
|
|
24e0157b53 | ||
|
|
f662759046 | ||
|
|
0d4e0dde8c | ||
|
|
db9c322bce | ||
|
|
a655bf22b2 | ||
|
|
df13760cf4 | ||
|
|
e1dca4bff5 | ||
|
|
95e4f27f17 | ||
|
|
1ea66f60af | ||
|
|
394de92915 | ||
|
|
b15b209a03 | ||
|
|
76cdc271c0 | ||
|
|
15937b7e80 | ||
|
|
a161098036 | ||
|
|
0398e09af5 | ||
|
|
004e22c1de | ||
|
|
a4d20310f1 | ||
|
|
0b22b8c736 | ||
|
|
a3dd4b702d | ||
|
|
5fb8fa95b3 | ||
|
|
bcbf2caf3f | ||
|
|
0edf5cb9fc | ||
|
|
14acfc3b3c | ||
|
|
529a7019ae | ||
|
|
fd0d7fd875 | ||
|
|
0bf5fe1ebe | ||
|
|
07fe758ac9 | ||
|
|
90a5d082bc | ||
|
|
fa0d99e5b7 | ||
|
|
8820fa433d | ||
|
|
781690f397 | ||
|
|
20e0bf2e9d | ||
|
|
3571ceb1e4 | ||
|
|
a4a0da0d5d | ||
|
|
22265c6ac7 | ||
|
|
e62e6bf2f5 | ||
|
|
84ca0b6c21 | ||
|
|
daa45b1e37 | ||
|
|
ff62ce3fbe | ||
|
|
7d43470f31 | ||
|
|
14168890c9 | ||
|
|
10ede85dd1 | ||
|
|
5f054d2a2c | ||
|
|
aab3810b54 | ||
|
|
bd773bbd77 | ||
|
|
35cfd187cf | ||
|
|
eaf8745697 | ||
|
|
c0616f06be | ||
|
|
e6b3af5889 | ||
|
|
8e2460d18c | ||
|
|
33120935a3 | ||
|
|
bb33349149 | ||
|
|
f23eda8c95 | ||
|
|
326e391188 | ||
|
|
158a42d8b6 | ||
|
|
be1a871349 | ||
|
|
5e33c4e85d | ||
|
|
b74d6ad3d3 | ||
|
|
375f76fef0 | ||
|
|
02bbf51fb7 | ||
|
|
945971648a | ||
|
|
365c60e7a4 | ||
|
|
34204c4a31 | ||
|
|
805a308d2e | ||
|
|
ff14364de0 | ||
|
|
4e67f1f045 | ||
|
|
e1d11fe727 | ||
|
|
4ff5cd4050 | ||
|
|
4f74de9cdb | ||
|
|
ffbc606682 | ||
|
|
eca3aef3a2 | ||
|
|
ba64156fd4 | ||
|
|
ec53cae11f | ||
|
|
697cbc6376 | ||
|
|
26da97bde0 | ||
|
|
701f31964b | ||
|
|
5ebea67b79 | ||
|
|
d0a02fe426 | ||
|
|
25a1f6e046 | ||
|
|
84507d7179 | ||
|
|
86faf035b6 | ||
|
|
4557f68053 | ||
|
|
8fd8e6da3c | ||
|
|
dd32704d4d | ||
|
|
390da5d905 | ||
|
|
99581ed5e3 | ||
|
|
294c95ebbc | ||
|
|
7709a0e2c8 | ||
|
|
cce9b16a72 | ||
|
|
926171c779 | ||
|
|
f5b7628a7f | ||
|
|
28651a4079 | ||
|
|
72f21aca93 | ||
|
|
b4e477c014 | ||
|
|
81aef750d2 | ||
|
|
75d9b260da | ||
|
|
2fce1e1e61 | ||
|
|
e05565e223 | ||
|
|
21aa723a2a | ||
|
|
ffbb367b8a | ||
|
|
5f48f323da | ||
|
|
0ca3752ea6 | ||
|
|
f4abf43154 | ||
|
|
62bb21a2d7 | ||
|
|
f7f21bc823 | ||
|
|
bda227817e | ||
|
|
fbf045eb6a | ||
|
|
f47b0385f3 | ||
|
|
89febede8b | ||
|
|
31556cd1a0 | ||
|
|
322e3fdfb3 | ||
|
|
8401c1cdb5 | ||
|
|
486555d4f1 | ||
|
|
3152013186 | ||
|
|
1e73f53160 | ||
|
|
b9c6c65482 | ||
|
|
37cab2c76f | ||
|
|
14d0ec7ac8 | ||
|
|
5a00c8fb64 | ||
|
|
1696b5c959 | ||
|
|
0db5d17102 | ||
|
|
85c7bfda29 | ||
|
|
42ffb27951 | ||
|
|
bfe0798660 | ||
|
|
5df75b1d4d | ||
|
|
844f91c606 | ||
|
|
36c6ff200e | ||
|
|
7b2082dbb0 | ||
|
|
8a9d724e6e | ||
|
|
1b6ed0898d | ||
|
|
0c37d0e285 | ||
|
|
2bd0cdc4f9 | ||
|
|
d949d600f8 | ||
|
|
5a8985620c | ||
|
|
10a326a90c | ||
|
|
5322caafe0 | ||
|
|
11bf85d8a8 | ||
|
|
ec42c93947 | ||
|
|
5778a9ef17 | ||
|
|
4868f3234d | ||
|
|
d3c4bba4ca | ||
|
|
8ad2a62730 | ||
|
|
e3de5044b2 | ||
|
|
13cd63567e | ||
|
|
1aa306d7ea | ||
|
|
912d7c1673 | ||
|
|
bc9058a8d4 | ||
|
|
c8698f6257 | ||
|
|
5ec49d7adf | ||
|
|
95654fe073 | ||
|
|
921668da66 | ||
|
|
7d0b3e7090 | ||
|
|
b11004671f | ||
|
|
1d26952f72 | ||
|
|
871a242721 | ||
|
|
e09733aa9d | ||
|
|
d0e9cb3e37 | ||
|
|
896c66180e | ||
|
|
f31f24c8dc | ||
|
|
6b83f43dc9 | ||
|
|
ec73f63f28 | ||
|
|
22fea7b767 | ||
|
|
e437bc472d | ||
|
|
6aa4663bf2 | ||
|
|
651e64ff32 | ||
|
|
322790aff0 | ||
|
|
347d255eaa | ||
|
|
48e6d64c4f | ||
|
|
865b184a0f | ||
|
|
c5388c78cd | ||
|
|
1bd9746f53 | ||
|
|
7e469d797c | ||
|
|
dc1bc972ba | ||
|
|
21da8442f5 | ||
|
|
ac4770f2b1 | ||
|
|
d7895e1690 | ||
|
|
633a87577f | ||
|
|
2d784fa6ff | ||
|
|
3e8b5161be | ||
|
|
480cfc5f3b | ||
|
|
4c968e6199 | ||
|
|
be885026f9 | ||
|
|
dc87d45eb3 | ||
|
|
2496301c60 | ||
|
|
24d28c3f41 | ||
|
|
6aa3b99816 | ||
|
|
e1ee39169d | ||
|
|
9546c3af26 | ||
|
|
64d8488c67 | ||
|
|
aecee58bdd | ||
|
|
4736323d86 | ||
|
|
2622762f89 | ||
|
|
9ddebc1b11 | ||
|
|
dc859b26ff | ||
|
|
a7bb051154 | ||
|
|
a51d311fb9 | ||
|
|
cfd45e0c4d | ||
|
|
3df612f80a | ||
|
|
2425a18705 | ||
|
|
178bde4ce6 | ||
|
|
537333045a | ||
|
|
7f81445c02 | ||
|
|
a24e78f6f1 | ||
|
|
fad1ce5568 | ||
|
|
e4da566272 | ||
|
|
597039b6cb | ||
|
|
3e373187ea | ||
|
|
bf63faab2d | ||
|
|
9627ee00ce | ||
|
|
b73ea23b68 | ||
|
|
236cca71bb | ||
|
|
3e5e0c73b2 | ||
|
|
2e324bbbe8 | ||
|
|
d5be478937 | ||
|
|
52d58ba32e | ||
|
|
66bc889072 | ||
|
|
2e3085a555 | ||
|
|
9587a7a1e0 | ||
|
|
874b8850e8 | ||
|
|
135430b076 | ||
|
|
4d988cb460 | ||
|
|
44d8858747 | ||
|
|
03d9f35ed5 | ||
|
|
c24c426c06 | ||
|
|
341228c718 | ||
|
|
a0bd7b25ea | ||
|
|
18bb5b793b | ||
|
|
b0e3b5add6 | ||
|
|
f7e12d23ce | ||
|
|
62413d835c | ||
|
|
1ee7c7337d | ||
|
|
01f309efef | ||
|
|
decb3f3f8d | ||
|
|
8a11d125db | ||
|
|
f549c31d1d | ||
|
|
5f051b83d0 | ||
|
|
515e045faf | ||
|
|
d442b0307f | ||
|
|
439339ab14 | ||
|
|
5389eb9db8 | ||
|
|
31a3d5553e | ||
|
|
e424cc940d | ||
|
|
cef6027dcb | ||
|
|
48fc04a425 | ||
|
|
486120787d | ||
|
|
090e45adf1 | ||
|
|
48ae834b58 | ||
|
|
0b3299a019 | ||
|
|
a242537838 | ||
|
|
c968d37a48 | ||
|
|
79d8ea066a | ||
|
|
64a91dcdc0 | ||
|
|
85b75dcbea | ||
|
|
456ec79c0a | ||
|
|
4ef3855745 | ||
|
|
dea9b6de5d | ||
|
|
036d473a3c | ||
|
|
c0d02ba96a | ||
|
|
a6bdc3fb5b | ||
|
|
a87fb80e13 | ||
|
|
ac35eebf30 | ||
|
|
080abf86a4 | ||
|
|
6e29a5d314 | ||
|
|
96cb96aced | ||
|
|
1327c0916c | ||
|
|
fec7d1cae2 | ||
|
|
ab7c977216 | ||
|
|
83081577e5 | ||
|
|
ab5ea838de | ||
|
|
dca6a6532f | ||
|
|
4dc3206402 | ||
|
|
caa3c8425a | ||
|
|
2d9b1b348a | ||
|
|
cb73a0538a | ||
|
|
140f011030 | ||
|
|
fb433c3a57 | ||
|
|
cb2740a8c2 | ||
|
|
f1a9eaa47e | ||
|
|
fd13adf248 | ||
|
|
c0a4a9e657 | ||
|
|
7c4ff2de72 | ||
|
|
d284809fb0 | ||
|
|
1e14e6469e | ||
|
|
132faae7cf | ||
|
|
6a5be385c6 |
No files matched your search
@@ -4,9 +4,24 @@ bench/prompts/** -text
|
||||
# launchers keep their platform line endings wherever the repository is cloned
|
||||
*.sh text eol=lf
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
bin/strata text eol=lf
|
||||
|
||||
# copies of real packs' native_experts.txt, parsed byte for byte by tests/core/expert_layout_test.cpp
|
||||
tests/data/native_experts/** -text
|
||||
|
||||
# the quant yardstick's text set: tokenized byte for byte (set.ids.json), never convert its line endings
|
||||
data/quant_quality/** -text
|
||||
|
||||
# the license text is shipped and checked as LF text on every checkout
|
||||
LICENSE text eol=lf
|
||||
|
||||
# container scripts run in Linux: always LF
|
||||
docker/*.sh text eol=lf
|
||||
Dockerfile text eol=lf
|
||||
docker/Dockerfile text eol=lf
|
||||
docker/compose/* text eol=lf
|
||||
|
||||
# Apple tools (codesign, plutil) read these: LF only, wherever the repository is cloned
|
||||
*.plist text eol=lf
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Benchmark
|
||||
description: Share measurements from your machine, or report a problem with the benchmark tools
|
||||
title: "[Benchmark] "
|
||||
labels: ["benchmark"]
|
||||
body:
|
||||
- type: markdown
|
||||
value: |
|
||||
The title must start with **[Benchmark]**. Keep it, and add the GPU and model after it.
|
||||
|
||||
To share a full report, follow the [community benchmark guide](https://github.com/Niko1221/Strata/blob/main/docs/COMMUNITY_BENCHMARKS.md) and open a results-only pull request. The standard protocol is a [draft](https://github.com/Niko1221/Strata/blob/main/docs/BENCHMARK_PROTOCOL.md); existing tools do not implement it yet. Include poor results and failed requests.
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: What did you measure, or what went wrong
|
||||
description: Hardware (GPU and VRAM, CPU, RAM, OS), Strata version, model and quantization, the exact command, and the numbers. Paste the report or link the pull request.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: reproduction-status
|
||||
attributes:
|
||||
label: Reproduction status
|
||||
options:
|
||||
- Measured locally
|
||||
- Unconfirmed (I did not run it)
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,37 +1,101 @@
|
||||
name: Bug report
|
||||
description: Something in Strata doesn't work as documented
|
||||
title: "[Bug]: "
|
||||
title: "[Area] "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
value: |
|
||||
Thanks for the report.
|
||||
Thanks for the report. Please read [CONTRIBUTING.md](https://github.com/Niko1221/Strata/blob/main/CONTRIBUTING.md) first.
|
||||
|
||||
Please search the [open issues](https://github.com/Niko1221/Strata/issues?q=is%3Aissue+state%3Aopen) first — this may already be tracked.
|
||||
Search the [open and closed issues](https://github.com/Niko1221/Strata/issues?q=is%3Aissue) first: this may already be tracked, and new evidence belongs in that thread.
|
||||
|
||||
Write only what you saw. Do not guess at steps, errors, hardware or severity.
|
||||
|
||||
**The title must start with its area:** [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other]. Choose the same one below.
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
description: Start the issue title with the same area in square brackets, for example "[Engine] decode stops after 2 minutes". Pick Other if none fits.
|
||||
options:
|
||||
- "[Engine] (speed, kernels, models, the server and API)"
|
||||
- "[GUI] (the desktop app)"
|
||||
- "[TUI] (the Terminal: strata launch, connect, monitor)"
|
||||
- "[Benchmark] (measurements and reports)"
|
||||
- "[Docs] (documentation)"
|
||||
- "[Other] (anything else, including setup and installation)"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: how
|
||||
attributes:
|
||||
label: Desktop app, Terminal or source?
|
||||
options:
|
||||
- Desktop app (Windows, Linux or Mac)
|
||||
- Terminal (the `strata` command)
|
||||
- Source (START-HERE.bat, setup.sh or a build)
|
||||
- Docker
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: What happened
|
||||
description: What you did, what you got, and what you expected instead. Paste the exact error text if there is one. If there's a set of steps or commands that reproduces it, put them here too.
|
||||
description: What you did, what you got, and what you expected instead. Paste the exact error text if there is one.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Smallest steps or command that reproduce it
|
||||
description: The shortest list of steps that shows the problem. If you cannot make it happen again, say so here.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: reproduction-status
|
||||
attributes:
|
||||
label: Reproduction status
|
||||
options:
|
||||
- Reproduced locally
|
||||
- Unconfirmed static analysis (I read code or ran a scanner; I did not see it fail)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Strata version
|
||||
description: "In the app: Settings > About. In the Terminal: `strata --version`. From source: the About tab of the web page, or `engine` in `GET /v1/status`, or the git commit."
|
||||
placeholder: "Strata 0.2.3 · engine 0.1.48"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: hardware
|
||||
attributes:
|
||||
label: Strata version, GPU, OS
|
||||
description: The engine version is on the About tab, or in `GET /v1/status` under `engine`.
|
||||
placeholder: "0.1.31 · RTX 5070 12GB · Windows 11"
|
||||
label: Graphics card, memory and system
|
||||
description: The card and its VRAM, the RAM of the PC, and the operating system.
|
||||
placeholder: "RTX 5070 12 GB · 64 GB RAM · Windows 11"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: model
|
||||
attributes:
|
||||
label: Model, size and settings
|
||||
description: The model and size you started, the context, and whether pictures were on. Write what is relevant.
|
||||
placeholder: "Qwen3.8-Flash-Next IQ2_XS · 128K context · pictures on"
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Engine log
|
||||
description: Drag in `strata-<model>.log` from your Strata folder. Drag and drop works directly into this box.
|
||||
label: Logs
|
||||
description: |
|
||||
The app: Settings > Engine > Diagnostics > View logs > Save logs, then drag the file in. The Terminal: paste the output of `strata doctor`. From source: drag in `strata-<model>.log` from your Strata folder.
|
||||
Remove API keys, passwords and private text first.
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
value: |
|
||||
A couple of extras that help a lot:
|
||||
|
||||
- Check the [troubleshooting table](https://github.com/Niko1221/Strata/blob/main/docs/DETAILS.md#troubleshooting) first, many reports are already solved there.
|
||||
- Check the [troubleshooting page](https://github.com/Niko1221/Strata/blob/main/docs/TROUBLESHOOTING.md) first, many reports are already solved there.
|
||||
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Troubleshooting and known issues
|
||||
url: https://github.com/Niko1221/Strata/blob/main/docs/DETAILS.md#troubleshooting
|
||||
url: https://github.com/Niko1221/Strata/blob/main/docs/TROUBLESHOOTING.md
|
||||
about: Symptom-to-fix table for crashes, out of memory, hangs, and port problems.
|
||||
@@ -1,18 +1,41 @@
|
||||
name: Feature request
|
||||
description: Suggest something new or an improvement
|
||||
title: "[Feature]: "
|
||||
title: "[Area] "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
value: |
|
||||
Explain what you want in plain language.
|
||||
Explain what you want in plain language. For a large feature, a change to a default or a refactor, please agree the scope here first. Read [CONTRIBUTING.md](https://github.com/Niko1221/Strata/blob/main/CONTRIBUTING.md).
|
||||
|
||||
**The title must start with its area:** [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other]. Choose the same one below.
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
description: Start the issue title with the same area in square brackets, for example "[Engine] decode stops after 2 minutes". Pick Other if none fits.
|
||||
options:
|
||||
- "[Engine] (speed, kernels, models, the server and API)"
|
||||
- "[GUI] (the desktop app)"
|
||||
- "[TUI] (the Terminal: strata launch, connect, monitor)"
|
||||
- "[Benchmark] (measurements and reports)"
|
||||
- "[Docs] (documentation)"
|
||||
- "[Other] (anything else, including setup and installation)"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: idea
|
||||
attributes:
|
||||
label: What do you want to do?
|
||||
description: Describe the goal rather than the implementation. If there's a workaround you use today, mention it.
|
||||
description: Describe the goal rather than the implementation. Say who needs it and why.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: workaround
|
||||
attributes:
|
||||
label: What do you do today instead?
|
||||
description: The workaround you use now, if there is one.
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
value: |
|
||||
All of this counts: model and architecture support, the server and API, the web app, multi-GPU and layer split, images and vision, MCP servers, setup and installation, and performance.
|
||||
All of this counts: model and architecture support, the server and API, the app and the Terminal, multi-GPU and layer split, images and vision, MCP servers, setup and installation, and performance.
|
||||
@@ -1,11 +1,27 @@
|
||||
name: Question
|
||||
description: Ask how to use Strata, or share a result
|
||||
title: "Question: "
|
||||
title: "[Area] "
|
||||
labels: ["question"]
|
||||
body:
|
||||
- type: markdown
|
||||
value: |
|
||||
Not sure how to do something, or want to share numbers from your machine? Ask here.
|
||||
Not sure how to do something, or want to share numbers from your machine? Ask here. To share a full benchmark, see [CONTRIBUTING.md](https://github.com/Niko1221/Strata/blob/main/CONTRIBUTING.md#share-a-benchmark).
|
||||
|
||||
**The title must start with its area:** [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other]. Choose the same one below.
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
description: Start the issue title with the same area in square brackets, for example "[Engine] decode stops after 2 minutes". Pick Other if none fits.
|
||||
options:
|
||||
- "[Engine] (speed, kernels, models, the server and API)"
|
||||
- "[GUI] (the desktop app)"
|
||||
- "[TUI] (the Terminal: strata launch, connect, monitor)"
|
||||
- "[Benchmark] (measurements and reports)"
|
||||
- "[Docs] (documentation)"
|
||||
- "[Other] (anything else, including setup and installation)"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
@@ -13,10 +29,17 @@ body:
|
||||
description: What you're trying to do. Include your version, GPU, and OS if it affects the answer.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: tried
|
||||
attributes:
|
||||
label: What you tried
|
||||
description: What you already did and where you got stuck.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: numbers
|
||||
attributes:
|
||||
label: Results (optional)
|
||||
description: Only if you ran a benchmark or measured speed. Model, quant, context, and the tok/s you get.
|
||||
validations:
|
||||
required: false
|
||||
required: false
|
||||
@@ -1,12 +1,30 @@
|
||||
## Title
|
||||
<!-- If Applicable, reference the GitHub issue -->
|
||||
Issue: Resolves #
|
||||
**The title must start with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other], for example "[Engine] fix decode stop on long prompts" or "[GUI] keep the model list after a tab switch".**
|
||||
|
||||
## Summary
|
||||
<!-- Quick Summary of changes -->
|
||||
<!--
|
||||
Read CONTRIBUTING.md first. Lead with the problem and the result, not a file-by-file list.
|
||||
Placeholders below are not test results: replace them with what you observed, or write "none".
|
||||
-->
|
||||
|
||||
## What changed
|
||||
<!-- Specifics on files changed, and what changes were made there -->
|
||||
## Problem and result
|
||||
<!-- What triggered this change. How it behaves before and after. -->
|
||||
|
||||
## Extra Notes
|
||||
<!-- Any extra notes, delete if there are none -->
|
||||
## Related discussion
|
||||
<!-- The issue URL and the maintainer's agreement on scope, when the change needs one (features, refactors, defaults). Write "none" if not applicable. -->
|
||||
|
||||
## Approach
|
||||
<!-- Why this fix. Compatibility effects. Keep unrelated cleanup out of the PR. -->
|
||||
|
||||
## Validation
|
||||
<!-- Exact commands, the revision and the environment tested, what you saw. Keep the three lists apart. -->
|
||||
|
||||
**Passed:**
|
||||
<!-- command, revision/environment, observed result, log -->
|
||||
|
||||
**Failed:**
|
||||
<!-- command and failure, or "none observed" -->
|
||||
|
||||
**Not run:**
|
||||
<!-- check, why, and what stays unverified (for example CUDA, HIP, SYCL or Metal builds you could not do) -->
|
||||
|
||||
## Limits
|
||||
<!-- Known limitations or remaining work. -->
|
||||
@@ -0,0 +1,218 @@
|
||||
'use strict';
|
||||
// Format check for issues and pull requests (see CONTRIBUTING.md and .github/ISSUE_TEMPLATE/*.yml).
|
||||
// Pure functions only: no network, no filesystem. Used by intake.yml and reopen.yml through reset-bot.js.
|
||||
|
||||
const AREAS = ['Engine', 'GUI', 'TUI', 'Benchmark', 'Docs', 'Other'];
|
||||
const TITLE_RE = new RegExp('^\\[(' + AREAS.join('|') + ')\\] \\S');
|
||||
const NO_ANSWER = /^(_no response_|none|n\/a|na|-|todo|tbd|\.|\?)$/i;
|
||||
const NOT_RUN_LABELS = /^\*\*(passed|failed|not run)\s*:?\*\*\s*:?/gim;
|
||||
|
||||
// Version the reopened work must be tested on.
|
||||
const CURRENT_VERSION_RE = /\b0\.2\.\d+|0\.2\.0-beta/i;
|
||||
// Any version or commit.
|
||||
const ANY_VERSION_RE = /\bv?\d+\.\d+(\.\d+)*(-[a-z0-9.]+)?\b|\b[0-9a-f]{7,40}\b|\bcommit\b/i;
|
||||
const HARDWARE_RE = /\b(rtx|gtx|geforce|quadro|tesla|radeon|rx ?\d|instinct|arc|a\d{3,4}|b\d{2,3}|apple|m[1-5]( pro| max| ultra)?|nvidia|amd|intel|gpu|cpu|vram|\d+ ?gb|ram|windows|linux|ubuntu|debian|fedora|arch|macos|mac os|wsl)\b/i;
|
||||
const REVISION_RE = /0\.2\.\d|beta|\bmain\b|\b[0-9a-f]{7,40}\b/i;
|
||||
|
||||
function stripComments(text) {
|
||||
return String(text || '').replace(/<!--[\s\S]*?-->/g, '');
|
||||
}
|
||||
|
||||
function hasAreaPrefix(title) {
|
||||
return TITLE_RE.test(String(title || ''));
|
||||
}
|
||||
|
||||
// Issue forms render as "### Label\n\nvalue". Returns { label: value } for those headings.
|
||||
function parseSections(body) {
|
||||
const out = {};
|
||||
let cur = null;
|
||||
for (const line of stripComments(body).split(/\r?\n/)) {
|
||||
const m = /^#{1,6}\s+(.+?)\s*#*\s*$/.exec(line);
|
||||
if (m) {
|
||||
cur = m[1].trim();
|
||||
out[cur] = '';
|
||||
} else if (cur !== null) {
|
||||
out[cur] += line + '\n';
|
||||
}
|
||||
}
|
||||
for (const k of Object.keys(out)) out[k] = out[k].trim();
|
||||
return out;
|
||||
}
|
||||
|
||||
function realText(value) {
|
||||
const t = String(value || '').replace(NOT_RUN_LABELS, '').replace(/\s+/g, ' ').trim();
|
||||
return NO_ANSWER.test(t) ? '' : t;
|
||||
}
|
||||
|
||||
function findSection(sections, re) {
|
||||
for (const k of Object.keys(sections)) if (re.test(k)) return { key: k, value: sections[k] };
|
||||
return null;
|
||||
}
|
||||
|
||||
// Required parts of each issue form: [display name, heading regex, minimum characters of real text].
|
||||
const FORMS = {
|
||||
bug: {
|
||||
detect: /what happened/i,
|
||||
parts: [
|
||||
['Area', /^area$/i, 2],
|
||||
['Desktop app, Terminal or source?', /desktop app, terminal or source/i, 2],
|
||||
['What happened', /what happened/i, 20],
|
||||
['Reproduction status', /reproduction status/i, 2],
|
||||
['Strata version', /strata version/i, 3],
|
||||
['Graphics card, memory and system', /graphics card|memory and system/i, 3],
|
||||
],
|
||||
needsCurrentVersion: 'Strata version',
|
||||
},
|
||||
feature: {
|
||||
detect: /what do you want to do/i,
|
||||
parts: [
|
||||
['Area', /^area$/i, 2],
|
||||
['What do you want to do?', /what do you want to do/i, 20],
|
||||
],
|
||||
},
|
||||
question: {
|
||||
detect: /your question/i,
|
||||
parts: [
|
||||
['Area', /^area$/i, 2],
|
||||
['Your question', /your question/i, 15],
|
||||
],
|
||||
},
|
||||
benchmark: {
|
||||
detect: /what did you measure/i,
|
||||
parts: [
|
||||
['What did you measure, or what went wrong', /what did you measure/i, 30],
|
||||
['Reproduction status', /reproduction status/i, 2],
|
||||
],
|
||||
needsCurrentVersionInBody: true,
|
||||
},
|
||||
};
|
||||
|
||||
function checkIssue({ title, body, labels = [], mode = 'intake' }) {
|
||||
const missing = [];
|
||||
if (!hasAreaPrefix(title)) {
|
||||
missing.push('Start the title with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other], then a short description. Example: "[Engine] decode stops after 2 minutes".');
|
||||
}
|
||||
const text = stripComments(body);
|
||||
const sections = parseSections(text);
|
||||
const labelSet = new Set(labels.map((l) => String(l).toLowerCase()));
|
||||
const needCurrent = mode === 'reopen';
|
||||
|
||||
let formKey = null;
|
||||
for (const [key, f] of Object.entries(FORMS)) {
|
||||
if (findSection(sections, f.detect)) { formKey = key; break; }
|
||||
}
|
||||
|
||||
if (formKey) {
|
||||
const form = FORMS[formKey];
|
||||
for (const [name, re, min] of form.parts) {
|
||||
const s = findSection(sections, re);
|
||||
if (!s || realText(s.value).length < min) missing.push('Fill in "' + name + '".');
|
||||
}
|
||||
if (needCurrent && form.needsCurrentVersion) {
|
||||
const s = findSection(sections, /strata version/i);
|
||||
if (s && realText(s.value) && !CURRENT_VERSION_RE.test(s.value)) {
|
||||
missing.push('Re-test on Strata 0.2.0-beta and put that version in "' + form.needsCurrentVersion + '". If it no longer happens, say so in a comment instead.');
|
||||
}
|
||||
}
|
||||
if (needCurrent && form.needsCurrentVersionInBody && !CURRENT_VERSION_RE.test(text)) {
|
||||
missing.push('Say which Strata version you measured on. Re-measure on 0.2.0-beta if you can.');
|
||||
}
|
||||
return { ok: missing.length === 0, missing, kind: formKey };
|
||||
}
|
||||
|
||||
// Free-form text (older issues, blank issues).
|
||||
const plain = realText(text);
|
||||
const lenient = labelSet.has('enhancement') || labelSet.has('question');
|
||||
if (plain.length < (lenient ? 40 : 80)) {
|
||||
missing.push('Describe it in the issue body: what happened, what you expected, and the exact error if there is one. The issue forms (New issue > Bug report) list what to include.');
|
||||
}
|
||||
if (!lenient) {
|
||||
if (!ANY_VERSION_RE.test(text)) missing.push('Add the Strata version (Settings > About, or `strata --version`).');
|
||||
else if (needCurrent && !CURRENT_VERSION_RE.test(text)) missing.push('Re-test on Strata 0.2.0-beta and name that version in the issue. If it no longer happens, say so in a comment instead.');
|
||||
if (!HARDWARE_RE.test(text)) missing.push('Add your graphics card with its VRAM, your RAM and your operating system.');
|
||||
}
|
||||
return { ok: missing.length === 0, missing, kind: 'free-form' };
|
||||
}
|
||||
|
||||
function checkPull({ title, body, mode = 'intake' }) {
|
||||
const missing = [];
|
||||
if (!hasAreaPrefix(title)) {
|
||||
missing.push('Start the title with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other], then a short description. Example: "[Engine] fix decode stop on long prompts".');
|
||||
}
|
||||
const text = stripComments(body);
|
||||
const sections = parseSections(text);
|
||||
const problem = findSection(sections, /problem|result|summary|what.*change/i);
|
||||
const validation = findSection(sections, /validation|testing|tests?\b|how.*tested/i);
|
||||
|
||||
let valText = '';
|
||||
if (problem || validation) {
|
||||
if (!problem || realText(problem.value).length < 20) missing.push('Fill in "Problem and result": what triggered the change, and how it behaves before and after.');
|
||||
if (!validation || realText(validation.value).length < 20) {
|
||||
missing.push('Fill in "Validation": the exact commands, the revision and environment tested, what you saw. Keep Passed, Failed and Not run apart.');
|
||||
} else {
|
||||
valText = validation.value;
|
||||
}
|
||||
} else {
|
||||
const plain = realText(text);
|
||||
if (plain.length < 150) {
|
||||
missing.push('Describe the pull request: the problem and the result. The pull request template shows the parts (Problem and result, Related discussion, Approach, Validation, Limits).');
|
||||
}
|
||||
if (!/(problem|result|before|after|fix|because|bug)/i.test(plain)) missing.push('Add "Problem and result": what triggered the change, and how it behaves before and after.');
|
||||
if (!/(validat|tested|passed|ran |built|build|not run)/i.test(plain)) missing.push('Add "Validation": the exact commands, the revision and environment tested, and what you saw.');
|
||||
valText = plain;
|
||||
}
|
||||
if (mode === 'reopen' && valText && !REVISION_RE.test(valText)) {
|
||||
missing.push('Say in Validation which revision you tested: rebase on main (or 0.2.0-beta) and re-run your checks.');
|
||||
}
|
||||
return { ok: missing.length === 0, missing, kind: 'pull' };
|
||||
}
|
||||
|
||||
function checkItem({ isPull, ...rest }) {
|
||||
return isPull ? checkPull(rest) : checkIssue(rest);
|
||||
}
|
||||
|
||||
function checklist(missing) {
|
||||
return missing.map((m) => '- [ ] ' + m).join('\n');
|
||||
}
|
||||
|
||||
const CONTRIB = 'https://github.com/Niko1221/Strata/blob/main/CONTRIBUTING.md';
|
||||
|
||||
function intakeComment(isPull, missing) {
|
||||
return [
|
||||
'Thanks for the ' + (isPull ? 'pull request' : 'issue') + '. To keep the tracker readable, every item needs an area prefix in the title and the parts of the ' + (isPull ? 'pull request template' : 'issue form') + ' filled in. This one is missing:',
|
||||
'',
|
||||
checklist(missing),
|
||||
'',
|
||||
'Edit the ' + (isPull ? 'pull request' : 'issue') + ' (title and description) and this note goes away on its own. If it is still missing parts in 5 days it will be closed, which is not a rejection: fix it and comment `/reopen`.',
|
||||
'',
|
||||
'Details: [CONTRIBUTING.md](' + CONTRIB + ')',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function reopenMissingComment(isPull, missing) {
|
||||
return [
|
||||
'Not reopened yet. Still missing:',
|
||||
'',
|
||||
checklist(missing),
|
||||
'',
|
||||
'Edit the ' + (isPull ? 'pull request' : 'issue') + ' and comment `/reopen` again. Details: [CONTRIBUTING.md](' + CONTRIB + ')',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function reopenOkComment(isPull) {
|
||||
return 'Reopened, thank you. A maintainer will pick it up from here.' + (isPull ? ' If it conflicts with main, rebase it first; that speeds up review.' : '');
|
||||
}
|
||||
|
||||
function staleComment(isPull) {
|
||||
return [
|
||||
'Closing this ' + (isPull ? 'pull request' : 'issue') + ' because it still did not follow the format after 5 days. That is housekeeping, not a rejection.',
|
||||
'',
|
||||
'To bring it back: fix the title (area prefix) and the missing parts, then comment `/reopen`. A check runs and reopens it, or tells you what is still missing. See [CONTRIBUTING.md](' + CONTRIB + ').',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AREAS, TITLE_RE, hasAreaPrefix, parseSections, stripComments, realText,
|
||||
checkIssue, checkPull, checkItem, checklist,
|
||||
intakeComment, reopenMissingComment, reopenOkComment, staleComment,
|
||||
};
|
||||
@@ -0,0 +1,192 @@
|
||||
'use strict';
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const c = require('./item-check');
|
||||
const bot = require('./reset-bot');
|
||||
|
||||
const bugForm = (over = {}) => {
|
||||
const f = {
|
||||
'Area': '[Engine] (speed, kernels, models, the server and API)',
|
||||
'Desktop app, Terminal or source?': 'Terminal (the `strata` command)',
|
||||
'What happened': 'Decode stops after two minutes with "CUDA error 700". I expected it to continue.',
|
||||
'Smallest steps or command that reproduce it': '_No response_',
|
||||
'Reproduction status': 'Reproduced locally',
|
||||
'Strata version': 'Strata 0.2.0-beta · engine 0.1.45.3',
|
||||
'Graphics card, memory and system': 'RTX 5070 12 GB · 64 GB RAM · Windows 11',
|
||||
...over,
|
||||
};
|
||||
return Object.entries(f).map(([k, v]) => `### ${k}\n\n${v}`).join('\n\n');
|
||||
};
|
||||
|
||||
test('title prefix', () => {
|
||||
assert.ok(c.hasAreaPrefix('[Engine] fix decode'));
|
||||
assert.ok(c.hasAreaPrefix('[Docs] x'));
|
||||
assert.ok(!c.hasAreaPrefix('Engine fix'));
|
||||
assert.ok(!c.hasAreaPrefix('[engine] fix'));
|
||||
assert.ok(!c.hasAreaPrefix('[Engine]'));
|
||||
assert.ok(!c.hasAreaPrefix('[Engine] '));
|
||||
assert.ok(!c.hasAreaPrefix('[Area] foo'));
|
||||
assert.ok(!c.hasAreaPrefix(''));
|
||||
assert.ok(!c.hasAreaPrefix(undefined));
|
||||
});
|
||||
|
||||
test('filled bug form passes in both modes', () => {
|
||||
for (const mode of ['intake', 'reopen']) {
|
||||
const r = c.checkIssue({ title: '[Engine] decode stops', body: bugForm(), labels: ['bug'], mode });
|
||||
assert.deepStrictEqual(r.missing, []);
|
||||
assert.strictEqual(r.kind, 'bug');
|
||||
}
|
||||
});
|
||||
|
||||
test('bug form: missing parts are named', () => {
|
||||
const r = c.checkIssue({ title: 'decode stops', body: bugForm({ 'Strata version': '_No response_', 'What happened': 'bad' }) });
|
||||
assert.ok(!r.ok);
|
||||
assert.ok(r.missing.some((m) => /title/i.test(m)));
|
||||
assert.ok(r.missing.some((m) => m.includes('What happened')));
|
||||
assert.ok(r.missing.some((m) => m.includes('Strata version')));
|
||||
});
|
||||
|
||||
test('reopen needs a 0.2 version, intake does not', () => {
|
||||
const body = bugForm({ 'Strata version': 'Strata 0.1.44' });
|
||||
assert.ok(c.checkIssue({ title: '[Engine] x y', body, mode: 'intake' }).ok);
|
||||
const r = c.checkIssue({ title: '[Engine] x y', body, mode: 'reopen' });
|
||||
assert.ok(!r.ok);
|
||||
assert.ok(r.missing.some((m) => m.includes('0.2.0-beta')));
|
||||
});
|
||||
|
||||
test('html comments and placeholders do not count', () => {
|
||||
const r = c.checkIssue({ title: '[Engine] x y', body: bugForm({ 'What happened': '<!-- describe -->' }) });
|
||||
assert.ok(r.missing.some((m) => m.includes('What happened')));
|
||||
});
|
||||
|
||||
test('feature and question forms', () => {
|
||||
const f = '### Area\n\n[GUI] (the desktop app)\n\n### What do you want to do?\n\nLet me pin a model to the top of the list in the app.';
|
||||
assert.ok(c.checkIssue({ title: '[GUI] pin models', body: f, mode: 'reopen' }).ok);
|
||||
assert.ok(!c.checkIssue({ title: '[GUI] pin models', body: '### Area\n\n[GUI]\n\n### What do you want to do?\n\n_No response_' }).ok);
|
||||
const q = '### Area\n\n[Other]\n\n### Your question\n\nHow do I move the model folder to another drive?';
|
||||
assert.ok(c.checkIssue({ title: '[Other] model folder', body: q }).ok);
|
||||
});
|
||||
|
||||
test('free-form issue', () => {
|
||||
const good = 'Strata 0.2.0-beta on an RTX 3060 12 GB with 32 GB RAM on Linux: the server hangs when I send two requests at once. Expected both to finish.';
|
||||
assert.ok(c.checkIssue({ title: '[Engine] hang on two requests', body: good, mode: 'reopen' }).ok);
|
||||
assert.ok(!c.checkIssue({ title: '[Engine] hang', body: 'it crashes', mode: 'intake' }).ok);
|
||||
const old = good.replace('0.2.0-beta', '0.1.40');
|
||||
assert.ok(c.checkIssue({ title: '[Engine] hang', body: old, mode: 'intake' }).ok);
|
||||
assert.ok(!c.checkIssue({ title: '[Engine] hang', body: old, mode: 'reopen' }).ok);
|
||||
assert.ok(c.checkIssue({ title: '[Other] idea', body: 'It would help if the app remembered my last model between runs, today I pick it again.', labels: ['enhancement'], mode: 'reopen' }).ok);
|
||||
});
|
||||
|
||||
const prBody = (over = {}) => {
|
||||
const p = {
|
||||
problem: 'Decode stopped on prompts above 8k because the stop check read the wrong length. Now it continues.',
|
||||
validation: '**Passed:** `ctest -R decode`, commit abcdef1, RTX 3060, 12 of 12.\n\n**Failed:** none observed\n\n**Not run:** HIP build',
|
||||
...over,
|
||||
};
|
||||
return `## Problem and result\n<!-- hint -->\n${p.problem}\n\n## Related discussion\nnone\n\n## Approach\nFix the length.\n\n## Validation\n${p.validation}\n\n## Limits\nnone`;
|
||||
};
|
||||
|
||||
test('filled PR passes', () => {
|
||||
assert.deepStrictEqual(c.checkPull({ title: '[Engine] fix stop', body: prBody(), mode: 'reopen' }).missing, []);
|
||||
});
|
||||
|
||||
test('untouched PR template fails', () => {
|
||||
const tpl = '## Problem and result\n<!-- What triggered -->\n\n## Validation\n<!-- cmds -->\n\n**Passed:**\n<!-- x -->\n\n**Failed:**\n\n**Not run:**\n';
|
||||
const r = c.checkPull({ title: '[Engine] fix stop', body: tpl });
|
||||
assert.ok(!r.ok);
|
||||
assert.ok(r.missing.some((m) => m.includes('Problem and result')));
|
||||
assert.ok(r.missing.some((m) => m.includes('Validation')));
|
||||
});
|
||||
|
||||
test('PR: empty body and bad title', () => {
|
||||
const r = c.checkPull({ title: 'fix', body: null });
|
||||
assert.ok(!r.ok);
|
||||
assert.ok(r.missing.length >= 2);
|
||||
});
|
||||
|
||||
test('PR reopen needs a revision', () => {
|
||||
const body = prBody({ validation: '**Passed:** I tried it and it worked for me on my machine, looked fine.' });
|
||||
assert.ok(c.checkPull({ title: '[Engine] x y', body, mode: 'intake' }).ok);
|
||||
assert.ok(!c.checkPull({ title: '[Engine] x y', body, mode: 'reopen' }).ok);
|
||||
});
|
||||
|
||||
test('free-form PR', () => {
|
||||
const body = 'This fixes a crash when the context is above 8k, because the length was read from the wrong field. Before it aborted, after it continues. I built it on CUDA and ran the decode test, passed.';
|
||||
assert.ok(c.checkPull({ title: '[Engine] fix crash', body }).ok);
|
||||
assert.ok(!c.checkPull({ title: '[Engine] fix crash', body: 'fixes stuff' }).ok);
|
||||
});
|
||||
|
||||
test('messages', () => {
|
||||
const m = c.intakeComment(false, ['a', 'b']);
|
||||
assert.ok(m.includes('- [ ] a') && m.includes('/reopen') && m.includes('CONTRIBUTING.md'));
|
||||
assert.ok(c.staleComment(true).includes('/reopen'));
|
||||
});
|
||||
|
||||
// handlers with a fake GitHub client
|
||||
function fake(calls, extra = {}) {
|
||||
const rest = {
|
||||
issues: {
|
||||
getLabel: async () => ({}),
|
||||
createLabel: async (a) => calls.push(['createLabel', a.name]),
|
||||
addLabels: async (a) => calls.push(['addLabels', a.labels]),
|
||||
removeLabel: async (a) => calls.push(['removeLabel', a.name]),
|
||||
createComment: async (a) => calls.push(['comment', a.body]),
|
||||
update: async (a) => calls.push(['update', a.state]),
|
||||
get: async () => ({ data: extra.live }),
|
||||
listEvents: async () => {},
|
||||
listForRepo: async () => {},
|
||||
},
|
||||
pulls: { get: async () => ({ data: { mergeable_state: 'clean' } }) },
|
||||
};
|
||||
return { rest, paginate: async (fn) => (fn === rest.issues.listEvents ? (extra.events || []) : (extra.items || [])) };
|
||||
}
|
||||
const core = { info() {} };
|
||||
const repo = { owner: 'o', repo: 'r' };
|
||||
|
||||
test('intake labels and comments once', async () => {
|
||||
const calls = [];
|
||||
const item = { number: 1, title: 'bad', body: '', labels: [], user: { type: 'User' }, author_association: 'NONE' };
|
||||
await bot.intake({ github: fake(calls), context: { repo, payload: { action: 'opened', issue: item } }, core });
|
||||
assert.deepStrictEqual(calls.map((c) => c[0]), ['addLabels', 'comment']);
|
||||
calls.length = 0;
|
||||
item.labels = [{ name: 'needs-format' }];
|
||||
await bot.intake({ github: fake(calls), context: { repo, payload: { action: 'edited', issue: item } }, core });
|
||||
assert.deepStrictEqual(calls, []);
|
||||
});
|
||||
|
||||
test('intake clears the label when edited into shape', async () => {
|
||||
const calls = [];
|
||||
const item = { number: 1, title: '[Engine] x y', body: bugForm(), labels: [{ name: 'needs-format' }], user: { type: 'User' } };
|
||||
await bot.intake({ github: fake(calls), context: { repo, payload: { action: 'edited', issue: item } }, core });
|
||||
assert.deepStrictEqual(calls.map((c) => c[0]), ['removeLabel', 'comment']);
|
||||
});
|
||||
|
||||
test('reopen: ignores strangers, reopens the author when it passes', async () => {
|
||||
const issue = { number: 5, state: 'closed', user: { login: 'amy' }, labels: [] };
|
||||
let calls = [];
|
||||
const comment = { body: '/reopen', user: { login: 'bob', type: 'User' }, author_association: 'NONE' };
|
||||
await bot.reopen({ github: fake(calls), context: { repo, payload: { issue, comment } }, core });
|
||||
assert.deepStrictEqual(calls, []);
|
||||
|
||||
const live = { number: 5, title: '[Engine] x y', body: bugForm(), labels: [] };
|
||||
comment.user.login = 'amy';
|
||||
await bot.reopen({ github: fake(calls, { live }), context: { repo, payload: { issue, comment } }, core });
|
||||
assert.deepStrictEqual(calls.map((c) => c[0]), ['update', 'removeLabel', 'comment']);
|
||||
assert.strictEqual(calls[0][1], 'open');
|
||||
|
||||
calls = [];
|
||||
live.title = 'no prefix';
|
||||
await bot.reopen({ github: fake(calls, { live }), context: { repo, payload: { issue, comment } }, core });
|
||||
assert.deepStrictEqual(calls.map((c) => c[0]), ['comment']);
|
||||
assert.ok(calls[0][1].includes('Not reopened'));
|
||||
});
|
||||
|
||||
test('sweep closes only old flagged items', async () => {
|
||||
const calls = [];
|
||||
const old = new Date(Date.now() - 6 * 86400000).toISOString();
|
||||
const fresh = new Date(Date.now() - 1 * 86400000).toISOString();
|
||||
const mk = (n, created) => ({ number: n, title: 'bad', body: '', labels: [{ name: 'needs-format' }], created_at: created });
|
||||
const gh = fake(calls, { items: [mk(1, old), mk(2, fresh)] });
|
||||
await bot.sweep({ github: gh, context: { repo }, core });
|
||||
assert.deepStrictEqual(calls.map((c) => c[0]), ['comment', 'update']);
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
'use strict';
|
||||
// GitHub-side handlers for intake.yml, reopen.yml and the 5-day sweep.
|
||||
// They take the actions/github-script objects ({ github, context, core }) and never touch PR code:
|
||||
// everything comes from the event payload and the REST API.
|
||||
|
||||
const check = require('./item-check');
|
||||
|
||||
const LABEL = 'needs-format';
|
||||
const STALE_DAYS = 5;
|
||||
const MAINTAINER = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
|
||||
// Items a maintainer closed on purpose stay closed.
|
||||
const BLOCK_LABELS = new Set(['wontfix', 'invalid', 'duplicate', 'declined']);
|
||||
|
||||
function labelNames(item) {
|
||||
return (item.labels || []).map((l) => (typeof l === 'string' ? l : l.name));
|
||||
}
|
||||
|
||||
async function ensureLabel(github, context) {
|
||||
const { owner, repo } = context.repo;
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner, repo, name: LABEL });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
await github.rest.issues.createLabel({
|
||||
owner, repo, name: LABEL, color: 'fbca04',
|
||||
description: 'Title or description does not follow CONTRIBUTING.md yet',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function removeLabel(github, context, number) {
|
||||
const { owner, repo } = context.repo;
|
||||
try {
|
||||
await github.rest.issues.removeLabel({ owner, repo, issue_number: number, name: LABEL });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function itemFromPayload(context) {
|
||||
const p = context.payload;
|
||||
return p.pull_request || p.issue;
|
||||
}
|
||||
|
||||
function isPullItem(item, context) {
|
||||
return Boolean(context.payload.pull_request || item.pull_request);
|
||||
}
|
||||
|
||||
// issues: opened/edited/reopened, pull_request_target: opened/edited/reopened
|
||||
async function intake({ github, context, core }) {
|
||||
const { owner, repo } = context.repo;
|
||||
const item = itemFromPayload(context);
|
||||
if (!item || (item.user && item.user.type === 'Bot')) return;
|
||||
if (MAINTAINER.has(item.author_association) && context.payload.action === 'opened') {
|
||||
// Maintainer items are not nagged on creation.
|
||||
return;
|
||||
}
|
||||
const isPull = isPullItem(item, context);
|
||||
const labels = labelNames(item);
|
||||
const hasLabel = labels.includes(LABEL);
|
||||
const res = check.checkItem({ isPull, title: item.title, body: item.body, labels, mode: 'intake' });
|
||||
core.info('check ' + (isPull ? 'PR' : 'issue') + ' #' + item.number + ': ' + (res.ok ? 'ok' : res.missing.join(' | ')));
|
||||
|
||||
if (res.ok) {
|
||||
if (hasLabel) {
|
||||
await removeLabel(github, context, item.number);
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: item.number, body: 'Thanks, this follows the format now.' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (context.payload.action === 'edited' && !hasLabel) return; // edits only matter for items already flagged
|
||||
if (hasLabel) return; // one note only
|
||||
await ensureLabel(github, context);
|
||||
await github.rest.issues.addLabels({ owner, repo, issue_number: item.number, labels: [LABEL] });
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: item.number, body: check.intakeComment(isPull, res.missing) });
|
||||
}
|
||||
|
||||
// issue_comment created, body starts with /reopen
|
||||
async function reopen({ github, context, core }) {
|
||||
const { owner, repo } = context.repo;
|
||||
const comment = context.payload.comment;
|
||||
const issue = context.payload.issue;
|
||||
if (!/^\s*\/reopen\b/.test(comment.body || '')) return;
|
||||
if (comment.user.type === 'Bot') return;
|
||||
const isAuthor = comment.user.login === issue.user.login;
|
||||
const isMaint = MAINTAINER.has(comment.author_association);
|
||||
if (!isAuthor && !isMaint) {
|
||||
core.info('ignored /reopen from ' + comment.user.login + ' (not the author or a maintainer)');
|
||||
return;
|
||||
}
|
||||
const isPull = Boolean(issue.pull_request);
|
||||
const say = (body) => github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body });
|
||||
const labels = labelNames(issue);
|
||||
const wasOpen = issue.state === 'open';
|
||||
|
||||
if (labels.some((l) => BLOCK_LABELS.has(l.toLowerCase())) && !isMaint) {
|
||||
await say('This one was closed on purpose (' + labels.filter((l) => BLOCK_LABELS.has(l.toLowerCase())).join(', ') + '). Please add your new evidence in a comment and a maintainer will decide.');
|
||||
return;
|
||||
}
|
||||
if (wasOpen && !labels.includes(LABEL)) {
|
||||
core.info('already open, nothing to do');
|
||||
return;
|
||||
}
|
||||
|
||||
// The bot reads the live item, not the possibly stale event copy.
|
||||
const live = (await github.rest.issues.get({ owner, repo, issue_number: issue.number })).data;
|
||||
const res = check.checkItem({ isPull, title: live.title, body: live.body, labels: labelNames(live), mode: 'reopen' });
|
||||
core.info('reopen check #' + live.number + ': ' + (res.ok ? 'ok' : res.missing.join(' | ')));
|
||||
|
||||
if (!res.ok) {
|
||||
await say(check.reopenMissingComment(isPull, res.missing));
|
||||
return;
|
||||
}
|
||||
if (!wasOpen) {
|
||||
try {
|
||||
await github.rest.issues.update({ owner, repo, issue_number: live.number, state: 'open' });
|
||||
} catch (e) {
|
||||
await say('The check passed, but GitHub would not reopen it (' + (e.message || e.status) + '). ' + (isPull ? 'If the branch was deleted, restore it or open a new pull request and link this one.' : 'Please open a new issue and link this one.'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
await removeLabel(github, context, live.number);
|
||||
let extra = '';
|
||||
if (isPull) {
|
||||
try {
|
||||
const pr = (await github.rest.pulls.get({ owner, repo, pull_number: live.number })).data;
|
||||
if (pr.mergeable_state === 'dirty') extra = '\n\nIt has conflicts with main now: please rebase it.';
|
||||
} catch (e) { /* best effort */ }
|
||||
}
|
||||
await say(check.reopenOkComment(isPull) + extra);
|
||||
}
|
||||
|
||||
async function labelTime(github, owner, repo, number, fallback) {
|
||||
const events = await github.paginate(github.rest.issues.listEvents, { owner, repo, issue_number: number, per_page: 100 });
|
||||
let t = null;
|
||||
for (const e of events) {
|
||||
if (e.event === 'labeled' && e.label && e.label.name === LABEL) t = e.created_at;
|
||||
}
|
||||
return t || fallback;
|
||||
}
|
||||
|
||||
// scheduled: close items that kept the needs-format label for more than 5 days
|
||||
async function sweep({ github, context, core }) {
|
||||
const { owner, repo } = context.repo;
|
||||
const cutoff = Date.now() - STALE_DAYS * 86400 * 1000;
|
||||
const items = await github.paginate(github.rest.issues.listForRepo, { owner, repo, state: 'open', labels: LABEL, per_page: 100 });
|
||||
core.info(items.length + ' open items with ' + LABEL);
|
||||
for (const item of items) {
|
||||
const since = new Date(await labelTime(github, owner, repo, item.number, item.created_at)).getTime();
|
||||
if (since > cutoff) continue;
|
||||
const isPull = Boolean(item.pull_request);
|
||||
// Re-check first: an edit that fixed it should have cleared the label already; do not close good items.
|
||||
const res = check.checkItem({ isPull, title: item.title, body: item.body, labels: labelNames(item), mode: 'intake' });
|
||||
if (res.ok) {
|
||||
await removeLabel(github, context, item.number);
|
||||
continue;
|
||||
}
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: item.number, body: check.staleComment(isPull) });
|
||||
await github.rest.issues.update({ owner, repo, issue_number: item.number, state: 'closed', state_reason: 'not_planned' });
|
||||
core.info('closed #' + item.number);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { intake, reopen, sweep, LABEL, STALE_DAYS };
|
||||
@@ -0,0 +1,114 @@
|
||||
name: Docker images
|
||||
|
||||
# Builds the Strata container images of one release from its engine packages and pushes them to ghcr.io.
|
||||
# Run by hand only (Actions tab -> Docker images -> Run workflow). Nothing starts it on its own.
|
||||
#
|
||||
# For each flavor the job downloads the engine package from the release and checks it against the signed update feed
|
||||
# (docker/fetch-engines.py: the feed's Ed25519 signature, the archive's SHA-256, the manifest hash, the package signature,
|
||||
# every file's hash), builds the image with docker/build.sh and, only when "push" is on, pushes
|
||||
# ghcr.io/niko1221/strata:<version>-<flavor> and ghcr.io/niko1221/strata:latest-<flavor>
|
||||
# The first run with "push" off builds everything without publishing it (a dry run).
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: 'Release tag whose engine packages go into the images (for example v0.2.3)'
|
||||
required: true
|
||||
type: string
|
||||
flavors:
|
||||
description: 'Flavors to build, comma separated (empty: every flavor the release has a package for)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
push:
|
||||
description: 'Push the images to ghcr.io (off: build only, nothing is published)'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
latest:
|
||||
description: 'Also move the latest-<flavor> tags (turn off for a pre-release or a test)'
|
||||
required: true
|
||||
default: true
|
||||
type: boolean
|
||||
source_ref:
|
||||
description: 'Git ref of the Dockerfile and the Terminal sources (empty: the release tag)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
IMAGE: ghcr.io/niko1221/strata
|
||||
|
||||
jobs:
|
||||
plan:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
flavors: ${{ steps.list.outputs.flavors }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.source_ref || inputs.release_tag }}
|
||||
- name: Which flavors does the release have a package for
|
||||
id: list
|
||||
env:
|
||||
WANT: ${{ inputs.flavors }}
|
||||
TAG: ${{ inputs.release_tag }}
|
||||
run: |
|
||||
set -eu
|
||||
args="--release-tag $TAG --list --skip-missing"
|
||||
[ -n "$WANT" ] && args="$args --flavors $WANT"
|
||||
python3 docker/fetch-engines.py $args > list.tsv
|
||||
cat list.tsv
|
||||
flavors=$(awk -F'\t' '$3 != "SKIP" {printf "%s\"%s\"", (n++ ? "," : ""), $1}' list.tsv)
|
||||
[ -n "$flavors" ] || { echo "the release has no engine package for any requested flavor"; exit 1; }
|
||||
echo "flavors=[$flavors]" >> "$GITHUB_OUTPUT"
|
||||
|
||||
image:
|
||||
needs: plan
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 3
|
||||
matrix:
|
||||
flavor: ${{ fromJSON(needs.plan.outputs.flavors) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.source_ref || inputs.release_tag }}
|
||||
- name: Free disk space (the runner has about 14 GB; a package plus its image needs about 10 GB)
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL || true
|
||||
docker system prune -af >/dev/null 2>&1 || true
|
||||
df -h /
|
||||
- name: Log in to ghcr.io
|
||||
if: ${{ inputs.push }}
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REGISTRY_USER: ${{ github.actor }}
|
||||
run: printf '%s' "$REGISTRY_TOKEN" | docker login ghcr.io -u "$REGISTRY_USER" --password-stdin
|
||||
- name: Fetch, verify, build (and push when push is on)
|
||||
env:
|
||||
TAG: ${{ inputs.release_tag }}
|
||||
FLAVOR: ${{ matrix.flavor }}
|
||||
PUSH: ${{ inputs.push && '--push' || '' }}
|
||||
LATEST: ${{ !inputs.latest && '--no-latest' || '' }}
|
||||
run: |
|
||||
docker/publish.sh --release-tag "$TAG" --flavors "$FLAVOR" --image "$IMAGE" $PUSH $LATEST
|
||||
- name: Summary
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
{
|
||||
echo "### ${{ matrix.flavor }}"
|
||||
echo "- release: ${{ inputs.release_tag }}, pushed: ${{ inputs.push }}"
|
||||
echo "- image: \`$IMAGE:${INPUT_TAG#v}-${{ matrix.flavor }}\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
env:
|
||||
INPUT_TAG: ${{ inputs.release_tag }}
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Intake
|
||||
|
||||
# New issues and pull requests that do not follow CONTRIBUTING.md get the needs-format label and one friendly note.
|
||||
# Items that keep the label for 5 days are closed by the daily sweep, with a note on how to /reopen.
|
||||
#
|
||||
# pull_request_target is used instead of pull_request on purpose: a pull request from a fork gets a read-only
|
||||
# GITHUB_TOKEN under pull_request, so it could not be labelled or commented on. It is safe here because this
|
||||
# workflow never checks out or runs the pull request's code. It checks out the default branch only (the bot
|
||||
# scripts) and reads the title and description from the event payload and the REST API.
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
pull_request_target:
|
||||
types: [opened, edited]
|
||||
schedule:
|
||||
- cron: '17 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: intake-${{ github.event.issue.number || github.event.pull_request.number || 'sweep' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
check:
|
||||
if: github.event_name == 'issues' || github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
sparse-checkout: .github/scripts
|
||||
persist-credentials: false
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const bot = require('./.github/scripts/reset-bot.js');
|
||||
await bot.intake({ github, context, core });
|
||||
|
||||
sweep:
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
sparse-checkout: .github/scripts
|
||||
persist-credentials: false
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const bot = require('./.github/scripts/reset-bot.js');
|
||||
await bot.sweep({ github, context, core });
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Metal build
|
||||
|
||||
# Compile and link the Metal engine on a macOS runner (no GPU needed: the kernels are compiled at run time). The shared host files
|
||||
# (src/core/verify.cpp, src/prefill, src/program/generate.cpp) call kernel entry points that exist only for CUDA / HIP until Metal gets a
|
||||
# stub or an implementation; a missing one is a link error here, not on a user's Mac (owner round 9, G6).
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'include/**'
|
||||
- 'metal/**'
|
||||
- 'third_party/**'
|
||||
- 'CMakeLists.txt'
|
||||
- '.github/workflows/metal-build.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
metal-link:
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
# setup.py's LLAMA_CPP_COMMIT and metal/setup_mac.py's metal-cpp pin
|
||||
LLAMA_CPP_COMMIT: 3cf03257f219afbe7334045ff7c6a06ac68c627d
|
||||
METAL_CPP_URL: https://developer.apple.com/metal/cpp/files/metal-cpp_26.zip
|
||||
METAL_CPP_SHA256: 4df3c078b9aadcb516212e9cb03004cbc5ce9a3e9c068fa3144d021db585a3a4
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Tools
|
||||
run: brew install ninja
|
||||
- name: metal-cpp
|
||||
run: |
|
||||
curl -fsSL "$METAL_CPP_URL" -o metal-cpp.zip
|
||||
echo "$METAL_CPP_SHA256 metal-cpp.zip" | shasum -a 256 -c -
|
||||
unzip -q metal-cpp.zip -d _mcpp
|
||||
- name: llama.cpp (ggml) at the pinned commit
|
||||
run: |
|
||||
mkdir llama.cpp && cd llama.cpp
|
||||
git init -q
|
||||
git remote add origin https://github.com/ggml-org/llama.cpp.git
|
||||
git fetch -q --depth 1 origin "$LLAMA_CPP_COMMIT"
|
||||
git checkout -q FETCH_HEAD
|
||||
- name: Configure
|
||||
run: |
|
||||
cmake -S metal -B build-metal -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DMETAL_CPP_DIR="$PWD/_mcpp/metal-cpp" -DSTRATA_GGML_DIR="$PWD/llama.cpp"
|
||||
- name: Build and link the engine
|
||||
run: cmake --build build-metal --target strata
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Reopen
|
||||
|
||||
# An author comments /reopen on their closed issue or pull request. The check runs and the item is reopened,
|
||||
# or the bot replies with what is still missing. GitHub does not let authors reopen items a maintainer closed,
|
||||
# so this workflow does it for them.
|
||||
#
|
||||
# issue_comment always runs the workflow file from the default branch with a write-capable token, which is what
|
||||
# we need for fork authors too. It never checks out or executes the pull request's code: it reads the live
|
||||
# title and description through the REST API. Only the author of the item or a maintainer can trigger it.
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: reopen-${{ github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
reopen:
|
||||
if: startsWith(github.event.comment.body, '/reopen') && github.event.comment.user.type != 'Bot'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
sparse-checkout: .github/scripts
|
||||
persist-credentials: false
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const bot = require('./.github/scripts/reset-bot.js');
|
||||
await bot.reopen({ github, context, core });
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Scripts test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/scripts/**'
|
||||
- '.github/workflows/intake.yml'
|
||||
- '.github/workflows/reopen.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
node-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: node --test .github/scripts/item-check.test.js
|
||||
@@ -0,0 +1,65 @@
|
||||
name: Minimal serving runtime
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/terminal-minimal-server.yml
|
||||
- requirements.txt
|
||||
- strata.py
|
||||
- strata_cli/**
|
||||
- strata_control/**
|
||||
- serve/**
|
||||
- tests/terminal/test_runtime.py
|
||||
- tools/minimal_server_smoke.py
|
||||
- tools/strata_tokenizer.py
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/terminal-minimal-server.yml
|
||||
- requirements.txt
|
||||
- strata.py
|
||||
- strata_cli/**
|
||||
- strata_control/**
|
||||
- serve/**
|
||||
- tests/terminal/test_runtime.py
|
||||
- tools/minimal_server_smoke.py
|
||||
- tools/strata_tokenizer.py
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
fake-engine:
|
||||
name: Linux / runtime requirements only
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install only pinned runtime requirements
|
||||
shell: bash
|
||||
run: |
|
||||
python -m venv "$RUNNER_TEMP/strata-runtime-venv"
|
||||
"$RUNNER_TEMP/strata-runtime-venv/bin/python" -m pip install -r requirements.txt
|
||||
- name: Check fake-engine serving without terminal packages
|
||||
shell: bash
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
report = Path(os.environ['RUNNER_TEMP']) / 'minimal-server-smoke.json'
|
||||
python = str(Path(os.environ['RUNNER_TEMP']) / 'strata-runtime-venv' / 'bin' / 'python')
|
||||
subprocess.run([python, 'tools/minimal_server_smoke.py',
|
||||
'--report', str(report)], check=True)
|
||||
print(report.read_text())
|
||||
PY
|
||||
- name: Run full server suite without terminal packages
|
||||
shell: bash
|
||||
run: |
|
||||
"$RUNNER_TEMP/strata-runtime-venv/bin/python" -m unittest discover -s serve -p 'test_*.py'
|
||||
@@ -0,0 +1,115 @@
|
||||
name: Portable monitor client
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/terminal-portable-monitor.yml
|
||||
- packaging/monitor_bundle.py
|
||||
- strata.py
|
||||
- strata_cli/**
|
||||
- strata_control/**
|
||||
- tools/minimal_cli_smoke.py
|
||||
- tools/portable_monitor_smoke.py
|
||||
- tools/portable_monitor_pty_smoke.py
|
||||
- requirements-monitor.txt
|
||||
- LICENSE
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/terminal-portable-monitor.yml
|
||||
- packaging/monitor_bundle.py
|
||||
- strata.py
|
||||
- strata_cli/**
|
||||
- strata_control/**
|
||||
- tools/minimal_cli_smoke.py
|
||||
- tools/portable_monitor_smoke.py
|
||||
- tools/portable_monitor_pty_smoke.py
|
||||
- requirements-monitor.txt
|
||||
- LICENSE
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
client:
|
||||
name: ${{ matrix.os }} / Python ${{ matrix.python }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
python: ['3.10', '3.11']
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
- name: Check source CLI without optional packages
|
||||
shell: bash
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
report = Path(os.environ['RUNNER_TEMP']) / 'minimal-cli-smoke.json'
|
||||
subprocess.run([sys.executable, 'tools/minimal_cli_smoke.py',
|
||||
'--report', str(report)], check=True)
|
||||
print(report.read_text())
|
||||
PY
|
||||
- name: Build and smoke test without site packages
|
||||
shell: bash
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
bundle = Path(os.environ['RUNNER_TEMP']) / 'strata-monitor.pyz'
|
||||
report = bundle.with_name('monitor-stdlib-smoke.json')
|
||||
subprocess.run([sys.executable, 'packaging/monitor_bundle.py',
|
||||
'build', str(bundle)], check=True)
|
||||
subprocess.run([sys.executable, 'tools/portable_monitor_smoke.py',
|
||||
'--bundle', str(bundle), '--report', str(report)], check=True)
|
||||
print(report.read_text())
|
||||
PY
|
||||
- name: Install optional Textual dependency
|
||||
if: matrix.python == '3.11'
|
||||
run: python -m pip install -r requirements-monitor.txt
|
||||
- name: Smoke test packaged interactive Overview
|
||||
if: matrix.python == '3.11'
|
||||
shell: bash
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
bundle = Path(os.environ['RUNNER_TEMP']) / 'strata-monitor.pyz'
|
||||
report = bundle.with_name('monitor-interactive-smoke.json')
|
||||
subprocess.run([sys.executable, 'tools/portable_monitor_smoke.py',
|
||||
'--bundle', str(bundle), '--interactive',
|
||||
'--report', str(report)], check=True)
|
||||
print(report.read_text())
|
||||
PY
|
||||
- name: Check POSIX terminal restoration
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.python == '3.11'
|
||||
shell: bash
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
bundle = Path(os.environ['RUNNER_TEMP']) / 'strata-monitor.pyz'
|
||||
report = bundle.with_name('monitor-pty-smoke.json')
|
||||
subprocess.run([sys.executable, 'tools/portable_monitor_pty_smoke.py',
|
||||
'--bundle', str(bundle), '--report', str(report)], check=True)
|
||||
print(report.read_text())
|
||||
PY
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Title area
|
||||
|
||||
# Checks that a pull request or issue title starts with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other].
|
||||
# It reads the title only: no checkout, no labels, no comments, no write access.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
title:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check the title
|
||||
env:
|
||||
TITLE: ${{ github.event.pull_request.title || github.event.issue.title }}
|
||||
run: |
|
||||
case "$TITLE" in
|
||||
"[Engine] "?*|"[GUI] "?*|"[TUI] "?*|"[Benchmark] "?*|"[Docs] "?*|"[Other] "?*)
|
||||
echo "Title OK: $TITLE" ;;
|
||||
*)
|
||||
echo "::error::The title must start with its area and then a short description. Allowed areas: [Engine], [GUI], [TUI], [Benchmark], [Docs], [Other]. Example: \"[Engine] fix decode stop on long prompts\". Your title: $TITLE"
|
||||
exit 1 ;;
|
||||
esac
|
||||
@@ -48,6 +48,7 @@ build-vision-cuda12.bat
|
||||
|
||||
# setup.py outputs (engine, downloads) and release builds
|
||||
/engine/
|
||||
/engine-sycl/
|
||||
/engine-cuda12/
|
||||
/build-release*/
|
||||
/dist/
|
||||
@@ -64,3 +65,23 @@ build-vision-cuda12.bat
|
||||
|
||||
# local Windows HIP packaging output
|
||||
dist-hip/
|
||||
|
||||
# Terminal / Desktop split: local venvs, build output and archives never enter git
|
||||
.terminal-venv/
|
||||
.terminal-test-venv/
|
||||
node_modules/
|
||||
*.dmg
|
||||
/release/artifacts/
|
||||
/apps/host/dist/
|
||||
/apps/desktop/src-tauri/target/
|
||||
/apps/desktop/src-tauri/resources/node/*
|
||||
!/apps/desktop/src-tauri/resources/node/.gitignore
|
||||
# the Control component (bundled CPython + wheels + strata_control) is built by apps/desktop/scripts/stage-control.py
|
||||
/apps/desktop/src-tauri/resources/control/*
|
||||
!/apps/desktop/src-tauri/resources/control/.gitignore
|
||||
# the pruned DeepSeek Harness (MIT) is built by apps/desktop/scripts/stage-harness.mjs
|
||||
/apps/desktop/src-tauri/resources/harness/*
|
||||
!/apps/desktop/src-tauri/resources/harness/.gitignore
|
||||
# screenshots and checkpoints from the Mac rebuild live outside git (StrataSplit\evidence-archive)
|
||||
/apps/host/docs/evidence/
|
||||
/apps/host/docs/checkpoints/
|
||||
@@ -1,35 +1,84 @@
|
||||
# AGENTS.md
|
||||
|
||||
Strata runs the Qwen3.8-Flash-Next mixture-of-experts model (and its Coder, Swift 1.5 and Unsloth variants) on a
|
||||
normal PC: one NVIDIA or AMD graphics card plus system RAM, on Windows or Linux. It has a C++/CUDA/HIP engine
|
||||
(`src/`, `include/`), a Python server with an OpenAI- and Anthropic-compatible API and a web app (`serve/`), and a
|
||||
one-click installer (`setup.py`, started by `START-HERE.bat` / `setup.sh`).
|
||||
Strata runs the Qwen3.8-Flash-Next mixture-of-experts model (and its Coder, Swift 1.5, Unsloth, Gyro and NVFP4
|
||||
variants, and Qwen3.6-35B-A3B) on a normal PC: one NVIDIA or AMD graphics card plus system RAM on Windows or Linux,
|
||||
or an Apple Silicon Mac. Version 0.2.3, engine 0.1.48. It has three parts:
|
||||
|
||||
- **The app** (`apps/desktop`, the Tauri shell; `apps/host`, the Node host and the web UI). It is for people who do
|
||||
not want a terminal. The installer has the engine inside.
|
||||
- **The Terminal** (`strata.py`, `strata_cli/`, `strata_control/`): the `strata` command. It sets up a model, serves
|
||||
it and watches it. Agents use this part.
|
||||
- **The engine and the server** (`src/`, `include/`, `metal/`, `sycl/` for the C++/CUDA/HIP/SYCL/Metal engine;
|
||||
`serve/` for the Python server with an OpenAI- and Anthropic-compatible API and a web page). `setup.py`, started by
|
||||
`START-HERE.bat` or `setup.sh`, is the older one-click installer from source.
|
||||
|
||||
## Installing Strata for a user
|
||||
|
||||
Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**: check the PC, pick the model by RAM, run setup non-interactively,
|
||||
start and verify the server, and connect the user's apps. Never expose the server beyond `127.0.0.1` without
|
||||
`--api-key`. As an alternative to shell commands, Strata's MCP server ([docs/MCP_SERVER.md](docs/MCP_SERVER.md))
|
||||
offers the same steps as tools.
|
||||
Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**. For an agent that has a shell, the short version is:
|
||||
|
||||
1. Install the Terminal. It needs no administrator rights.
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh # Linux, Mac
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex # Windows PowerShell
|
||||
```
|
||||
|
||||
On Windows, open a new terminal afterwards so that `strata` is on the PATH.
|
||||
2. Check what would happen. Nothing is changed:
|
||||
|
||||
```sh
|
||||
strata launch --plan --model q2_0
|
||||
```
|
||||
|
||||
3. Set up and serve without questions. It downloads the engine for the card and the model (tens of GB), starts the
|
||||
server, prints the addresses and returns. The server keeps running.
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
Useful flags (all of them: `strata launch --help`): `--family`, `--model-path` (files the user already has),
|
||||
`--data-dir`, `--context 32k|64k|128k`, `--vision off|on|gpu|cpu`, `--parallel N`, `--gpu 0,1`,
|
||||
`--access local|lan|cloudflare|tailscale`, `--port`, `--api-key-env NAME`, `--new-key`, `--save-preset NAME`,
|
||||
`--output json`. `strata launch --status` shows what is serving, `strata launch --stop` stops it, and
|
||||
`strata launch --show-key` prints the API key.
|
||||
4. Connect the user's apps: `strata connect` lists them, `strata connect <app> --check` sends one prompt and one tool
|
||||
call the way that app does.
|
||||
|
||||
Keep the server on `127.0.0.1` unless the user asks for more. Beyond it an API key is recommended: `--access lan` makes one for you, and `--no-api-key` serves without one (a warning, never a refusal; on a public tunnel the warning is stronger). The old route (clone
|
||||
the repository and run `START-HERE.bat --yes` or `./setup.sh --yes`) still works and is the fallback when the
|
||||
Terminal cannot run. Strata's MCP server ([docs/MCP_SERVER.md](docs/MCP_SERVER.md)) offers the same steps as tools.
|
||||
|
||||
## Working on the code
|
||||
|
||||
- How the engine works, every measured number, the API and all settings: [docs/DETAILS.md](docs/DETAILS.md) and
|
||||
the [paper](docs/paper/Strata-Paper.pdf).
|
||||
- AMD (HIP) build and validation: [docs/AMD_HIP.md](docs/AMD_HIP.md); multi-GPU: [docs/MULTI_GPU.md](docs/MULTI_GPU.md).
|
||||
- Setup's own tests run without a GPU or downloads: `python tools/test_setup_<name>.py` (for example
|
||||
`tools/test_setup_amd.py`, `tools/test_setup_choices.py`).
|
||||
- Keep the docs' style: plain words, measured numbers with what they were measured on, no claims without a
|
||||
measurement.
|
||||
- The Terminal: [docs/TERMINAL.md](docs/TERMINAL.md) and [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). The app,
|
||||
the packages and the update feed: [packaging/README.md](packaging/README.md) and [packaging/UPDATES.md](packaging/UPDATES.md).
|
||||
- AMD (HIP) build and validation: [docs/AMD_HIP.md](docs/AMD_HIP.md); multi-GPU: [docs/MULTI_GPU.md](docs/MULTI_GPU.md);
|
||||
Mac (Metal): [docs/MAC.md](docs/MAC.md).
|
||||
- Tests: the Terminal and Control tests are in `tests/terminal` (`python -m pytest tests/terminal`). The app's UI
|
||||
tests run with `npm test` in `apps/host`. Setup's own tests run without a GPU or downloads:
|
||||
`python tools/test_setup_<name>.py` (for example `tools/test_setup_amd.py`, `tools/test_setup_choices.py`).
|
||||
- Keep the docs' style: plain words, short sentences, written for non-technical users first. Give measured numbers
|
||||
with what they were measured on. Make no claim without a measurement.
|
||||
|
||||
## Contributing a change or report
|
||||
|
||||
- Search the open issues and pull requests first, and add to a thread that already covers your point.
|
||||
- Open an issue with the form that fits (bug report, feature request or question).
|
||||
- One change per pull request. Say what it changes and what it leaves alone.
|
||||
The rules are in **[CONTRIBUTING.md](CONTRIBUTING.md)**. Read it before you open an issue or a pull request, and
|
||||
follow it whether a person or an agent writes the text. The short version:
|
||||
|
||||
- Start every issue and pull request title with its area: `[Engine]`, `[GUI]`, `[TUI]`, `[Benchmark]`, `[Docs]` or
|
||||
`[Other]`, for example `[Engine] fix …`. A check on GitHub fails a pull request whose title has no area.
|
||||
- Search the open and closed issues and pull requests first, and add to a thread that already covers your point.
|
||||
- Agree the scope in an issue before a large feature, a refactor or a change to a default.
|
||||
- One problem per pull request. Lead with the problem and the result, then the approach, then the validation
|
||||
(passed, failed and not run, kept apart) and the limits.
|
||||
- A new feature is opt-in, and the default path stays byte-identical to the last release. Say how you checked.
|
||||
- Build every backend a file touches (CUDA, HIP, SYCL) before asking for review.
|
||||
- Change a default only where you measured it faster, and show the numbers with what they were measured on.
|
||||
- Never submit measurements, logs or reproduction steps you did not produce. Mark a finding from reading code as
|
||||
unconfirmed static analysis.
|
||||
- A report from hardware the maintainers do not have is welcome. Follow
|
||||
[docs/COMMUNITY_BENCHMARKS.md](docs/COMMUNITY_BENCHMARKS.md), compare against a same-day run of the build you are
|
||||
testing, and say what you did not test.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
rem Benchmark this Strata install the way it is used and write a report you can share (docs/COMMUNITY_BENCHMARKS.md).
|
||||
rem It starts the model with your settings (or uses the one already running), takes about 10 minutes and changes nothing.
|
||||
rem Options: --clients 4 (several requests at once), --long (a 32K prompt), --compare <older results folder>.
|
||||
rem This is the quick speed check. The standard benchmark (docs/BENCHMARK_PROTOCOL.md), whose results other people can compare with, is `strata bench run`.
|
||||
setlocal
|
||||
title Strata - benchmark
|
||||
cd /d "%~dp0"
|
||||
@@ -10,6 +11,8 @@ if not exist ".venv\Scripts\python.exe" (
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo Quick speed check. For results other people can compare with (the standard benchmark, a few minutes), run: .venv\Scripts\python.exe strata.py bench run
|
||||
echo.
|
||||
".venv\Scripts\python.exe" tools\strata_bench.py %*
|
||||
set "RC=%ERRORLEVEL%"
|
||||
pause
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# below refuses anything older: silently building for a pre-Ampere arch would produce a binary that runs
|
||||
# and is wrong about its own performance, which is the worst of both outcomes.
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
project(strata VERSION 0.1.45 LANGUAGES CXX)
|
||||
project(strata VERSION 0.1.48 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)
|
||||
@@ -48,6 +48,17 @@ if(STRATA_PORTABLE AND MSVC)
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded")
|
||||
endif()
|
||||
|
||||
# #3202: Windows hands a program its command line in the system's ANSI code page (cp1251 / cp866 on a Russian Windows, cp1252 on an
|
||||
# English one), but the engine reads every path as UTF-8 (the run configs and the server write UTF-8). A model folder with
|
||||
# Cyrillic letters arrived as other bytes, or as "?" where the page has no such letter, and the model did not open. This manifest
|
||||
# makes the process's ANSI code page UTF-8 (Windows 10 1903 and newer; older Windows ignores it and nothing changes), so the
|
||||
# command line, fopen and std::ifstream all take UTF-8.
|
||||
function(strata_utf8_manifest target)
|
||||
if(MSVC)
|
||||
target_sources(${target} PRIVATE ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/cmake/strata-utf8.manifest)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
@@ -76,6 +87,7 @@ option(STRATA_PREFILL_MMQ "Build the optional GGML MMQ prefill path for HIP" OFF
|
||||
# CUDA_MODULE_LOADING=EAGER, and these took ~3.8 MB of VRAM - 3 IQ3_XXS expert slots on a P100 - from every model); on
|
||||
# HIP a code object loads on first use, so they cost other models nothing there (R9700: the same slots). Used at run
|
||||
# time with STRATA_MMQ_KQ=1 (opt-in: it changes the prompt's rounding against the FP16 path).
|
||||
option(STRATA_MMQ_TQ_OVERLAY "Gyro trellis MMQ prompt kernels on an upstream llama.cpp: an overlay of its ggml-cuda headers written at configure time (cmake/strata_ggml_tq_overlay.cmake)" ON)
|
||||
option(STRATA_MMQ_KQUANTS "Build the MMQ prompt kernels for Q4_K / Q5_K / Q5_1 / Q6_K: the experts of Unsloth's K-quant files, and on HIP the dense GGUF projections (STRATA_DENSE_MMQ)" ON)
|
||||
# Q6_K gate/up in the grouped expert kernels (Unsloth's UD-Q6_K_XL, and layer 2 of UD-Q5_K_XL; docs/QUANTS.md). On by
|
||||
# default since MODEL-UPDATE Phase 1: one more instance per grouped kernel in iq_kernels.cu, +19 KB of sm_60 code,
|
||||
@@ -254,6 +266,9 @@ target_link_libraries(strata-dequant PRIVATE strata_artifact)
|
||||
# this file is now nothing but the CLI entry point, and its inherited guard spells that as a "selftest"
|
||||
target_compile_definitions(strata-dequant PRIVATE STRATA_DEQUANT_SELFTEST)
|
||||
if(STRATA_BUILD_TESTS)
|
||||
add_executable(batch_telemetry_test src/core/batch_telemetry_test.cpp)
|
||||
target_include_directories(batch_telemetry_test PRIVATE include)
|
||||
add_test(NAME batch_telemetry_test COMMAND batch_telemetry_test)
|
||||
add_executable(logit_bias_test src/core/logit_bias_test.cpp)
|
||||
target_include_directories(logit_bias_test PRIVATE include)
|
||||
add_test(NAME logit_bias_test COMMAND logit_bias_test)
|
||||
@@ -289,6 +304,11 @@ if(STRATA_BUILD_TESTS)
|
||||
target_include_directories(split_rules_test PRIVATE include)
|
||||
add_test(NAME split_rules_test COMMAND split_rules_test)
|
||||
|
||||
# the prompt planner's host-memory model (#2824): header-only, no GPU
|
||||
add_executable(prefill_plan_test tests/program/prefill_plan_test.cpp)
|
||||
target_include_directories(prefill_plan_test PRIVATE include)
|
||||
add_test(NAME prefill_plan_test COMMAND prefill_plan_test)
|
||||
|
||||
# the default CPU prefill share is not armed when every expert is resident (#1595): header-only rule, no GPU
|
||||
add_executable(share_rules_test tests/prefill/share_rules_test.cpp)
|
||||
target_include_directories(share_rules_test PRIVATE include)
|
||||
@@ -316,6 +336,8 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
add_library(strata_core STATIC src/core/device.cu src/core/pinned.cu src/platform/memory.cpp src/core/graph.cpp src/core/weights.cpp
|
||||
src/core/layout.cpp)
|
||||
target_include_directories(strata_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
# the DFlash2 drafter (src/core/dflash2.cpp, src/kernels/cuda/dflash2.cu) exists in the CUDA and HIP builds; the Metal and SYCL builds compile without it
|
||||
target_compile_definitions(strata_core PUBLIC STRATA_HAVE_DFLASH2=1)
|
||||
if(STRATA_ENABLE_CUDA AND STRATA_EXPERIMENTAL_SM60)
|
||||
# the runtime device check (src/core/device.cu) admits the cards this build was made for (#236)
|
||||
target_compile_definitions(strata_core PRIVATE STRATA_EXPERIMENTAL_SM60=1)
|
||||
@@ -356,6 +378,7 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
# reference is the whole point - a kernel checked against another copy of itself proves nothing.
|
||||
add_library(strata_kernels STATIC src/kernels/cuda/dequant_s2.cu src/kernels/cuda/s2_gemv.cu
|
||||
src/kernels/cuda/s_gemv.cu
|
||||
src/kernels/cuda/s_gemv_pair.cu
|
||||
src/kernels/cuda/s2_gemv_quads.cu
|
||||
src/kernels/cuda/s2_gemv_fast.cu
|
||||
src/kernels/cuda/router_top10.cu
|
||||
@@ -397,6 +420,7 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
src/kernels/cuda/dequant_bf16.cu
|
||||
src/kernels/cuda/qsa_select.cu
|
||||
src/kernels/cuda/verify_kernels.cu
|
||||
src/kernels/cuda/dflash2.cu
|
||||
src/kernels/cuda/iq_kernels.cu
|
||||
src/kernels/ngram.cpp
|
||||
src/ngram/ple_reader.cpp
|
||||
@@ -424,6 +448,13 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_include_directories(qsa_prompt_attn_parity PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
|
||||
target_link_libraries(qsa_prompt_attn_parity PRIVATE strata_kernels CUDA::cudart)
|
||||
endif()
|
||||
# PR #600: the Volta (sm_70) prompt attention's latency pipeline against the FP32 kernel it replaces. Only in the
|
||||
# experimental sm_60/sm_70 build, where prompt_attn_v70_kernel is compiled in.
|
||||
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA AND STRATA_EXPERIMENTAL_SM60)
|
||||
add_executable(qsa_prompt_attn_sm70_bench bench/qsa_prompt_attn_sm70_bench.cpp)
|
||||
target_include_directories(qsa_prompt_attn_sm70_bench PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
|
||||
target_link_libraries(qsa_prompt_attn_sm70_bench PRIVATE strata_kernels CUDA::cudart)
|
||||
endif()
|
||||
# #512: the top-k's active-block bound (Turing's dispatch, forced on any card by STRATA_TOPK_ACTIVE_ANY) against
|
||||
# the capacity rule, selected ids bitwise
|
||||
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA)
|
||||
@@ -551,9 +582,9 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
# 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/expert_cache.cpp src/core/native_head.cpp src/core/native_dense.cpp src/core/verify.cpp src/core/mtp.cpp src/core/dflash2.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)
|
||||
src/core/arch_defaults.cpp src/core/nv_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 STRATA_ENABLE_CUDA AND NOT STRATA_HIP_GFX906)
|
||||
@@ -683,6 +714,11 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
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_CUDA)
|
||||
# v100/gdn-chunk: the chunked Volta GDN prompt recurrence. The dispatch (src/prefill/kernels.cu) is
|
||||
# Volta-only (cc 7.0) and gated on STRATA_EXPERIMENTAL_SM60; the path itself stays opt-in (STRATA_GDN_CHUNK).
|
||||
target_sources(strata_prefill PRIVATE src/prefill/gdn_chunk.cu)
|
||||
endif()
|
||||
if(STRATA_ENABLE_HIP AND STRATA_HIPBLASLT_AVAILABLE)
|
||||
target_compile_definitions(strata_prefill PRIVATE STRATA_HIPBLASLT_AVAILABLE=1)
|
||||
target_link_libraries(strata_prefill PRIVATE roc::hipblaslt)
|
||||
@@ -699,9 +735,38 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(tune_rocblas PRIVATE hip::host roc::rocblas)
|
||||
endif()
|
||||
|
||||
# DFlash2 drafter tools (not built by default: cmake --build <dir> --target dflash2_parity dflash2_gemm_bench dflash2_attn_bench): the drafter against
|
||||
# the torch port on a synthetic case (needs the pack and a case file, tools/dflash2/mk_parity_case.py), and the timing of its Q8 GEMM and attention
|
||||
add_executable(dflash2_parity EXCLUDE_FROM_ALL src/core/dflash2_parity.cpp)
|
||||
target_include_directories(dflash2_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(dflash2_parity PRIVATE strata_engine strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_executable(dflash2_gemm_bench EXCLUDE_FROM_ALL src/core/dflash2_gemm_bench.cpp)
|
||||
target_include_directories(dflash2_gemm_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(dflash2_gemm_bench PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_executable(dflash2_launch_bench EXCLUDE_FROM_ALL src/core/dflash2_launch_bench.cu)
|
||||
target_include_directories(dflash2_launch_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(dflash2_launch_bench PRIVATE ${_strata_gpu_runtime_target})
|
||||
add_executable(dflash2_bw_bench EXCLUDE_FROM_ALL src/core/dflash2_bw_bench.cu)
|
||||
target_include_directories(dflash2_bw_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(dflash2_bw_bench PRIVATE ${_strata_gpu_runtime_target})
|
||||
add_executable(dflash2_small_bench EXCLUDE_FROM_ALL src/core/dflash2_small_bench.cpp)
|
||||
target_include_directories(dflash2_small_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(dflash2_small_bench PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_executable(dflash2_attn_bench EXCLUDE_FROM_ALL src/core/dflash2_attn_bench.cpp)
|
||||
target_include_directories(dflash2_attn_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(dflash2_attn_bench PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
|
||||
add_executable(strata src/program/generate.cpp)
|
||||
strata_utf8_manifest(strata)
|
||||
target_include_directories(strata PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(strata PRIVATE strata_engine strata_prefill strata_spec ${_strata_gpu_runtime_target})
|
||||
if(WIN32 AND STRATA_ENABLE_HIP)
|
||||
# 0.2.1.2: the ROCm math libraries live in <package>\rocm\bin, which is on PATH only when the launcher starts the engine; load them on first
|
||||
# use (main() adds that folder to the DLL search first) so `strata.exe --version` works from the package folder. amdhip64_7.dll and the
|
||||
# C++ runtime sit beside the exe (tools/hip/package_windows.py) and stay normal imports.
|
||||
target_link_libraries(strata PRIVATE delayimp)
|
||||
target_link_options(strata PRIVATE "LINKER:/DELAYLOAD:hipblas.dll" "LINKER:/DELAYLOAD:libhipblaslt.dll" "LINKER:/DELAYLOAD:rocblas.dll")
|
||||
endif()
|
||||
|
||||
# NEW-MODELS-PLAN.md T1: Qwen3.6-35B-A3B's reference path (one GPU, the model resident, plain kernels), checked
|
||||
# against llama.cpp by tools/qwen35/; the engine itself does not run the family yet
|
||||
@@ -710,7 +775,10 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(strata-q36ref PRIVATE strata_kernels strata_warnings ${_strata_gpu_runtime_target})
|
||||
# the engine path for the same family (serve protocol, prompt chunks, decode graph): src/qwen35/q36_engine.cu, a
|
||||
# library that `strata` switches to for a qwen35moe model (the family switch at the top of its main) and that
|
||||
# strata-q36 wraps on its own
|
||||
# strata-q36 wraps on its own. Not on gfx906 (#3172, #3271): it calls cuBLAS entry points the wave64 compat layer does
|
||||
# not map (cublasSgemm, cublasGemmStridedBatchedEx) and its kernels assume 32-lane warps; main() guards every use with
|
||||
# STRATA_HAVE_QWEN35, so a gfx906 build links and runs the Qwen3.8 family, and `strata --families` says so
|
||||
if(NOT STRATA_HIP_GFX906)
|
||||
add_library(strata_qwen35 STATIC src/qwen35/q36_engine.cu)
|
||||
target_include_directories(strata_qwen35 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(strata_qwen35 PRIVATE strata_prefill strata_kernels strata_warnings ${_strata_gpu_runtime_target})
|
||||
@@ -720,9 +788,11 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_compile_definitions(strata_qwen35 PRIVATE STRATA_Q36_CPU_POOL=1)
|
||||
endif()
|
||||
add_executable(strata-q36 src/qwen35/q36_main.cpp)
|
||||
strata_utf8_manifest(strata-q36)
|
||||
target_link_libraries(strata-q36 PRIVATE strata_qwen35 ${_strata_gpu_runtime_target})
|
||||
target_link_libraries(strata PRIVATE strata_qwen35)
|
||||
target_compile_definitions(strata PRIVATE STRATA_HAVE_QWEN35=1)
|
||||
endif()
|
||||
|
||||
if(STRATA_ENABLE_HIP)
|
||||
enable_testing()
|
||||
@@ -800,6 +870,9 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
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)
|
||||
# the HC write-back kernel timed at the prompt path's shapes with an output checksum (not a ctest; STRATA_GRW_V picks a variant)
|
||||
add_executable(hip_prefill_gr_write_bench tests/hip/prefill_gr_write_bench.cpp)
|
||||
target_link_libraries(hip_prefill_gr_write_bench PRIVATE strata_prefill strata_hip_runtime)
|
||||
# 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)
|
||||
@@ -943,6 +1016,21 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_include_directories(iq_multi_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(iq_multi_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME iq_multi_parity COMMAND iq_multi_parity)
|
||||
# the Gyro trellis decode kernels (TQ2_T / TQK6 / TQK7) on synthetic blocks: decode-once vs per-column bitwise, output
|
||||
# checksums to compare two builds byte for byte, --bench for timings (GPU, no model)
|
||||
add_executable(tq_pascal_bench src/kernels/tq_pascal_bench.cpp)
|
||||
target_include_directories(tq_pascal_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(tq_pascal_bench PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME tq_pascal_bench COMMAND tq_pascal_bench)
|
||||
set_tests_properties(iq_multi_parity PROPERTIES ENVIRONMENT "STRATA_KQ_SPLIT=1") # the Q4_K / Q5_K / Q8_0 Split path against the old kernels
|
||||
# the same Split path on the other routes it can meet: the generic multi kernels (no V2K) and the fused gate/up + SwiGLU kernel, and the sub-16-row
|
||||
# gate/up route switched off (a K-quant row has 160 dots, not the 80 the sub-16 rows assume: STRATA_KQ_SPLIT=1 must stay bitwise on all of them)
|
||||
add_test(NAME iq_multi_parity_kq_nov2k COMMAND iq_multi_parity)
|
||||
set_tests_properties(iq_multi_parity_kq_nov2k PROPERTIES ENVIRONMENT "STRATA_KQ_SPLIT=1;STRATA_EXPERT_V2K=0")
|
||||
add_test(NAME iq_multi_parity_kq_swiglu COMMAND iq_multi_parity)
|
||||
set_tests_properties(iq_multi_parity_kq_swiglu PROPERTIES ENVIRONMENT "STRATA_KQ_SPLIT=1;STRATA_GU_SWIGLU=1")
|
||||
add_test(NAME iq_multi_parity_kq_nosub16 COMMAND iq_multi_parity)
|
||||
set_tests_properties(iq_multi_parity_kq_nosub16 PROPERTIES ENVIRONMENT "STRATA_KQ_SPLIT=1;STRATA_NO_SUB16_GU=1")
|
||||
# the prompt path's DeltaNet recurrence: gdn_rec_kh_kernel against gdn_rec_cols_pipe_kernel, bitwise (GPU,
|
||||
# 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)
|
||||
@@ -998,6 +1086,14 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
add_test(NAME gfx906_gdn_rec_head COMMAND gfx906_gdn_rec_head)
|
||||
set_tests_properties(gfx906_gdn_rec_head PROPERTIES TIMEOUT 120)
|
||||
endif()
|
||||
if(STRATA_ENABLE_CUDA)
|
||||
# the Volta prompt experts (gemm_iq_f16_grouped, STRATA_PF_WMMA) against a double reference; skipped below sm_70
|
||||
add_executable(pf_wmma_parity src/kernels/pf_wmma_parity.cpp)
|
||||
target_include_directories(pf_wmma_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(pf_wmma_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME pf_wmma_parity COMMAND pf_wmma_parity)
|
||||
set_tests_properties(pf_wmma_parity PROPERTIES SKIP_RETURN_CODE 77)
|
||||
endif()
|
||||
add_executable(shared_expert_parity src/kernels/shared_expert_parity.cpp)
|
||||
target_include_directories(shared_expert_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(shared_expert_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
@@ -1009,7 +1105,6 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
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})
|
||||
@@ -1032,6 +1127,22 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(gdn_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME gdn_parity COMMAND gdn_parity --selftest)
|
||||
|
||||
# v100/gdn-chunk: the chunked GDN prompt recurrence vs the token recurrence and a host reference (GPU,
|
||||
# synthetic, no model). The chunked path changes the summation order - FP32-level, not bit-exact - so the
|
||||
# test asserts measured error bounds, and --selftest pins them.
|
||||
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA)
|
||||
add_executable(gdn_chunk_parity src/kernels/gdn_chunk_parity.cpp)
|
||||
target_include_directories(gdn_chunk_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(gdn_chunk_parity PRIVATE strata_prefill ${_strata_gpu_runtime_target})
|
||||
add_test(NAME gdn_chunk_parity COMMAND gdn_chunk_parity --selftest)
|
||||
set_tests_properties(gdn_chunk_parity PROPERTIES SKIP_RETURN_CODE 77)
|
||||
|
||||
# v100/gdn-chunk: the micro benchmark (CUDA events, real GDN shapes): chunked vs recurrent prompt path.
|
||||
add_executable(gdn_chunk_bench bench/gdn_chunk_bench.cpp)
|
||||
target_include_directories(gdn_chunk_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(gdn_chunk_bench PRIVATE strata_prefill ${_strata_gpu_runtime_target})
|
||||
endif()
|
||||
|
||||
add_executable(s2_gemv_q8_parity src/kernels/s2_gemv_q8_parity.cpp)
|
||||
target_include_directories(s2_gemv_q8_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(s2_gemv_q8_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
@@ -1139,6 +1250,16 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(bf16_gemv_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME bf16_gemv_parity COMMAND bf16_gemv_parity --selftest)
|
||||
|
||||
# the V100 fusion work: every fused kernel against its unfused pipeline, bitwise, and the before/after
|
||||
# CUDA-event timings at decode shapes (`--bench`). It is a .cu because it carries the unfused reference
|
||||
# kernels it compares against.
|
||||
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA)
|
||||
add_executable(fusions_parity bench/micro/fusions_parity.cu)
|
||||
target_include_directories(fusions_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(fusions_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME fusions_parity COMMAND fusions_parity)
|
||||
endif()
|
||||
|
||||
add_executable(s_gemv_q8k_parity src/kernels/s_gemv_q8k_parity.cpp)
|
||||
target_include_directories(s_gemv_q8k_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(s_gemv_q8k_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
@@ -1241,6 +1362,11 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(platform_memory_test PRIVATE strata_core ${_strata_gpu_runtime_target})
|
||||
add_test(NAME platform_memory_test COMMAND platform_memory_test)
|
||||
|
||||
# The pinned-memory headroom (pin_budget.hpp) against a simulated ceiling; skips without a GPU.
|
||||
add_executable(pin_budget_test src/core/pin_budget_test.cpp)
|
||||
target_link_libraries(pin_budget_test PRIVATE strata_core ${_strata_gpu_runtime_target})
|
||||
add_test(NAME pin_budget_test COMMAND pin_budget_test)
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
add_executable(pinned_shared_test src/core/pinned_shared_test.cpp)
|
||||
target_link_libraries(pinned_shared_test PRIVATE strata_core ${_strata_gpu_runtime_target})
|
||||
@@ -1310,6 +1436,9 @@ if(STRATA_BUILD_TESTS)
|
||||
add_executable(arch_defaults_test src/core/arch_defaults_test.cpp src/core/arch_defaults.cpp)
|
||||
target_include_directories(arch_defaults_test PRIVATE include)
|
||||
add_test(NAME arch_defaults_test COMMAND arch_defaults_test)
|
||||
add_executable(uma_cache_test src/core/uma_cache_test.cpp)
|
||||
target_include_directories(uma_cache_test PRIVATE include)
|
||||
add_test(NAME uma_cache_test COMMAND uma_cache_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)
|
||||
@@ -1319,6 +1448,9 @@ if(STRATA_BUILD_TESTS)
|
||||
add_executable(draft_policy_test src/spec/draft_policy_test.cpp)
|
||||
target_link_libraries(draft_policy_test PRIVATE strata_spec)
|
||||
add_test(NAME draft_policy_test COMMAND draft_policy_test)
|
||||
add_executable(spec_start_test src/spec/spec_start_test.cpp)
|
||||
target_include_directories(spec_start_test PRIVATE include)
|
||||
add_test(NAME spec_start_test COMMAND spec_start_test)
|
||||
endif()
|
||||
if(STRATA_BUILD_TESTS OR STRATA_BUILD_CONVERSATION_TESTS)
|
||||
add_executable(message_boundary_test src/program/message_boundary_test.cpp)
|
||||
@@ -1344,6 +1476,10 @@ if(STRATA_BUILD_TESTS OR STRATA_BUILD_CONVERSATION_TESTS)
|
||||
add_executable(spec_prob_test src/core/spec_prob_test.cpp)
|
||||
target_include_directories(spec_prob_test PRIVATE include)
|
||||
add_test(NAME spec_prob_test COMMAND spec_prob_test)
|
||||
# the per-card window cap of --draft dflash2 (header-only, CPU)
|
||||
add_executable(draft_cap_test src/core/draft_cap_test.cpp)
|
||||
target_include_directories(draft_cap_test PRIVATE include)
|
||||
add_test(NAME draft_cap_test COMMAND draft_cap_test)
|
||||
# the f32 -> bf16 rounding against ggml's, NaN included, over every f32 bit pattern (header-only, CPU)
|
||||
add_executable(bf16_bits_test src/kernels/bf16_bits_test.cpp)
|
||||
target_include_directories(bf16_bits_test PRIVATE include)
|
||||
@@ -1359,7 +1495,7 @@ if(STRATA_BUILD_TESTS OR STRATA_BUILD_CONVERSATION_TESTS)
|
||||
endif()
|
||||
|
||||
add_library(strata_kernels_cpu STATIC src/kernels/cpu/expert.cpp src/kernels/cpu/pool.cpp
|
||||
src/kernels/cpu/expert_layout.cpp src/kernels/cpu/q2_avx2.cpp)
|
||||
src/kernels/cpu/expert_layout.cpp src/kernels/cpu/q2_avx2.cpp src/kernels/cpu/hadamard.cpp)
|
||||
target_include_directories(strata_kernels_cpu PUBLIC include)
|
||||
# Plan v0.3 P6: only the kernel files get the AVX-512 / AVX2 flags; the pool and the probe run on any x86-64,
|
||||
# and the engine picks the kernels at run time (cpu_avx512_ok).
|
||||
@@ -1467,7 +1603,8 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
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/kq_avx1.cpp src/kernels/cpu/q2_avx1.cpp src/kernels/cpu/iq_avx1.cpp
|
||||
src/kernels/cpu/nvfp4_avx512.cpp src/kernels/cpu/nvfp4_avx2.cpp)
|
||||
src/kernels/cpu/nvfp4_avx512.cpp src/kernels/cpu/nvfp4_avx2.cpp
|
||||
src/kernels/cpu/tq_cpu.cpp src/kernels/cpu/tq_avx2.cpp src/kernels/cpu/tq_avx512.cpp)
|
||||
target_include_directories(strata_kernels_cpu PRIVATE third_party/ggml)
|
||||
if(MSVC)
|
||||
set_source_files_properties(src/kernels/cpu/iq_avx512.cpp src/kernels/cpu/nvfp4_avx512.cpp
|
||||
@@ -1475,6 +1612,8 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
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")
|
||||
set_source_files_properties(src/kernels/cpu/nvfp4_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
|
||||
set_source_files_properties(src/kernels/cpu/tq_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
|
||||
set_source_files_properties(src/kernels/cpu/tq_avx512.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX512")
|
||||
# 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")
|
||||
# the older CPUs' Q2_0 rows and activation quantizer (q2_avx1.cpp, issue #1699): 128-bit integer lanes,
|
||||
@@ -1488,6 +1627,10 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
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/nvfp4_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
|
||||
# the trellis kernels: AVX2 + FMA, and AVX-512 F/BW/VL/VBMI/VNNI (exactly what cpu_avx512_ok() checks)
|
||||
set_source_files_properties(src/kernels/cpu/tq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
|
||||
set_source_files_properties(src/kernels/cpu/tq_avx512.cpp PROPERTIES COMPILE_OPTIONS
|
||||
"-mavx512f;-mavx512bw;-mavx512vl;-mavx512vnni;-mavx512vbmi;-mavx2;-mfma;-mf16c")
|
||||
set_source_files_properties(src/kernels/cpu/kq_avx1.cpp PROPERTIES COMPILE_OPTIONS "-mavx")
|
||||
# the older CPUs' Q2_0 rows and activation quantizer (q2_avx1.cpp, issue #1699): SSE4.2 + AVX with
|
||||
# AVX2/FMA/F16C explicitly off, so a -march=native build host cannot leak AVX2 code into the file that
|
||||
@@ -1551,6 +1694,34 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
add_test(NAME q8k_quant_parity_avx COMMAND ${CMAKE_COMMAND} -E env STRATA_FORCE_ISA=avx $<TARGET_FILE:q8k_quant_parity>)
|
||||
endif()
|
||||
set(_strata_mmq_types q2_0 iq2_xxs iq2_xs iq2_s iq3_xxs iq3_s iq4_nl iq4_xs q8_0)
|
||||
# APR: the agention llama.cpp fork has MMQ instances for its trellis expert types (f62fdead0); any other ggml
|
||||
# tree leaves them to the FP16 prompt path
|
||||
if(STRATA_GGML_DIR AND EXISTS "${STRATA_GGML_DIR}/ggml/src/ggml-cuda/template-instances/mmq-instance-tqk6.cu")
|
||||
list(APPEND _strata_mmq_types tq2_t tqk6 tqk7)
|
||||
set(_strata_mmq_tq ON)
|
||||
elseif(STRATA_MMQ_TQ_OVERLAY AND (STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP))
|
||||
# an upstream llama.cpp: the trellis MMQ instances come from an overlay of the pinned ggml-cuda headers written at
|
||||
# configure time (cmake/strata_ggml_tq_overlay.cmake); off with a message if the llama.cpp tree is not the pinned one
|
||||
if(STRATA_GGML_DIR)
|
||||
set(_strata_tq_llama "${STRATA_GGML_DIR}")
|
||||
else()
|
||||
set(_strata_tq_llama "${strata_llamacpp_SOURCE_DIR}")
|
||||
endif()
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/strata_ggml_tq_overlay.cmake)
|
||||
set(_strata_tq_overlay_dir "${CMAKE_BINARY_DIR}/ggml-tq-overlay")
|
||||
strata_ggml_tq_overlay("${_strata_tq_llama}" "${_strata_tq_overlay_dir}")
|
||||
if(STRATA_TQ_OVERLAY_OK)
|
||||
configure_file(third_party/ggml/ggml-common-tq.h "${_strata_tq_overlay_dir}/ggml-common-tq.h" COPYONLY)
|
||||
configure_file(src/kernels/cuda/tq_lut_i8.cuh "${_strata_tq_overlay_dir}/tq_lut_i8.cuh" COPYONLY)
|
||||
set(_strata_mmq_tq ON)
|
||||
set(_strata_mmq_tq_overlay ON)
|
||||
message(STATUS "trellis MMQ (Gyro prompts): overlay on the pinned ggml-cuda in ${_strata_tq_overlay_dir}")
|
||||
if(STRATA_BUILD_TESTS)
|
||||
add_test(NAME ggml_tq_overlay COMMAND ${CMAKE_COMMAND} -DLLAMA=${_strata_tq_llama}
|
||||
-DOUT=${CMAKE_BINARY_DIR}/ggml-tq-overlay-test -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/ggml_tq_overlay_test.cmake)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
# #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)
|
||||
list(APPEND _strata_mmq_types q5_0)
|
||||
@@ -1565,11 +1736,15 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
set(_strata_gg ${strata_llamacpp_SOURCE_DIR}/ggml)
|
||||
endif()
|
||||
set(_strata_mmq ${_strata_gg}/src/ggml-cuda)
|
||||
set(_strata_quantize_cu ${_strata_mmq}/quantize.cu) # the overlay's copy when the trellis overlay is on: it includes mmq.cuh
|
||||
if(_strata_mmq_tq_overlay)
|
||||
set(_strata_quantize_cu ${_strata_tq_overlay_dir}/quantize.cu)
|
||||
endif()
|
||||
# #136: the Q2_0 pack's prompt experts on Strata's own int8 kernels (moe_fused.cu, opt-in STRATA_PF_FUSED=1; CUDA
|
||||
# only: the HIP branch below keeps MMQ, and an sm_75 device or an sm_75-only build reports them unavailable)
|
||||
add_library(strata_mmq STATIC src/prefill/moe_mmq.cu src/prefill/moe_fused.cu src/prefill/moe_fused_iq.cu
|
||||
src/prefill/mmq_nvfp4_w4a8.cu
|
||||
src/prefill/ggml_cuda_host.cu ${_strata_mmq}/quantize.cu)
|
||||
src/prefill/ggml_cuda_host.cu ${_strata_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 q6_k, the experts of Unsloth's UD-Q4/Q5/Q6_K_XL (docs/QUANTS.md; the dispatch
|
||||
# follows eddoursul/Strata 7706c1b). MODEL-UPDATE Phase 1: on CUDA they are a library of their own, strata_kq
|
||||
@@ -1587,6 +1762,13 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
foreach(_t IN LISTS _strata_mmq_cuda_types)
|
||||
target_sources(strata_mmq PRIVATE ${_strata_mmq}/template-instances/mmq-instance-${_t}.cu)
|
||||
endforeach()
|
||||
if(_strata_mmq_tq_overlay)
|
||||
foreach(_t tq2_t tqk6 tqk7)
|
||||
target_sources(strata_mmq PRIVATE ${_strata_tq_overlay_dir}/template-instances/mmq-instance-${_t}.cu)
|
||||
endforeach()
|
||||
# first on the include path: the patched mmq.cuh / ggml.h shadow the pinned ones for this target only
|
||||
target_include_directories(strata_mmq BEFORE PRIVATE ${_strata_tq_overlay_dir} ${_strata_tq_overlay_dir}/include)
|
||||
endif()
|
||||
# NVFP4: W4A8 (default) in mmq_nvfp4_w4a8.cu with the plain architectures; the opt-in W4A4 needs RTX 50's FP4 MMA,
|
||||
# which exists only in the arch-specific target, so its one unit alone is built with 12x -> 12xa (the default
|
||||
# binary is unchanged and runs on sm_121; a card without that image falls back to W4A8 at run time)
|
||||
@@ -1690,6 +1872,9 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
if(STRATA_ORCA_Q4KS_MMQ)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_ORCA_Q4KS_MMQ=1)
|
||||
endif()
|
||||
if(_strata_mmq_tq)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_TQ=1)
|
||||
endif()
|
||||
target_link_libraries(strata_prefill PUBLIC strata_mmq)
|
||||
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1 STRATA_PREFILL_FUSED=1)
|
||||
if(STRATA_BUILD_TESTS)
|
||||
@@ -1743,6 +1928,10 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
set(_strata_gg "${strata_llamacpp_SOURCE_DIR}/ggml")
|
||||
endif()
|
||||
set(_strata_mmq "${_strata_gg}/src/ggml-cuda")
|
||||
set(_strata_quantize_cu "${_strata_mmq}/quantize.cu") # the overlay's copy when the trellis overlay is on: it includes mmq.cuh
|
||||
if(_strata_mmq_tq_overlay)
|
||||
set(_strata_quantize_cu "${_strata_tq_overlay_dir}/quantize.cu")
|
||||
endif()
|
||||
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()
|
||||
@@ -1753,7 +1942,7 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
src/prefill/moe_fused_iq.cu
|
||||
src/prefill/moe_f16_kq.cu
|
||||
src/prefill/ggml_cuda_host.cu
|
||||
"${_strata_mmq}/quantize.cu")
|
||||
"${_strata_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
|
||||
@@ -1762,6 +1951,11 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
foreach(_t IN LISTS _strata_mmq_types)
|
||||
list(APPEND _strata_mmq_sources "${_strata_mmq}/template-instances/mmq-instance-${_t}.cu")
|
||||
endforeach()
|
||||
if(_strata_mmq_tq_overlay)
|
||||
foreach(_t tq2_t tqk6 tqk7)
|
||||
list(APPEND _strata_mmq_sources "${_strata_tq_overlay_dir}/template-instances/mmq-instance-${_t}.cu")
|
||||
endforeach()
|
||||
endif()
|
||||
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 src/prefill/moe_f16_kq.cu
|
||||
@@ -1770,6 +1964,9 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
target_include_directories(strata_mmq
|
||||
PRIVATE "${_strata_gg}/include" "${_strata_gg}/src" "${_strata_mmq}"
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
if(_strata_mmq_tq_overlay)
|
||||
target_include_directories(strata_mmq BEFORE PRIVATE ${_strata_tq_overlay_dir} ${_strata_tq_overlay_dir}/include)
|
||||
endif()
|
||||
target_compile_definitions(strata_mmq PRIVATE GGML_USE_HIP=1)
|
||||
if(STRATA_MMQ_KQUANTS)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_KQUANTS=1)
|
||||
@@ -1777,6 +1974,9 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
if(STRATA_ORCA_Q4KS_MMQ)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_ORCA_Q4KS_MMQ=1)
|
||||
endif()
|
||||
if(_strata_mmq_tq)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_TQ=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 STRATA_PREFILL_FUSED=1)
|
||||
@@ -1825,7 +2025,8 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
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})
|
||||
# strata_engine: the --load-check mode runs the engine's start-up checks (NativeDense, check_experts_gguf)
|
||||
target_link_libraries(native_expert_parity PRIVATE strata_engine 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})
|
||||
@@ -1864,6 +2065,16 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
set_tests_properties(native_expert_parity_${_name} PROPERTIES TIMEOUT 120)
|
||||
endforeach()
|
||||
add_test(NAME native_expert_parity_q5_1_min COMMAND native_expert_parity --q5_1-min)
|
||||
# APR: the trellis expert types and the Hadamard activation rotation, when ggml comes from the agention fork
|
||||
# (the only tree with these types; marked by its ggml-cuda/tq.cuh)
|
||||
if(STRATA_GGML_DIR AND EXISTS "${STRATA_GGML_DIR}/ggml/src/ggml-cuda/tq.cuh")
|
||||
foreach(_pair tqk6/tqk7 tq2_t/tq2_t)
|
||||
string(REPLACE "/" "_" _name "${_pair}")
|
||||
add_test(NAME native_expert_parity_${_name} COMMAND native_expert_parity --synthetic ${_pair})
|
||||
add_test(NAME native_expert_parity_hadamard_${_name} COMMAND native_expert_parity --synthetic-hadamard ${_pair})
|
||||
set_tests_properties(native_expert_parity_${_name} native_expert_parity_hadamard_${_name} PROPERTIES TIMEOUT 300)
|
||||
endforeach()
|
||||
endif()
|
||||
# #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)
|
||||
endif()
|
||||
@@ -1917,6 +2128,22 @@ if(STRATA_BUILD_TESTS)
|
||||
set_tests_properties(q2_bitplane_parity PROPERTIES ENVIRONMENT "STRATA_Q2_BITPLANE=1")
|
||||
endif()
|
||||
|
||||
if(STRATA_NATIVE_EXPERTS)
|
||||
# APR port: a Hadamard-folded trellis expert (TQ2_T / TQK6 / TQK7) on the CPU path vs llama.cpp's graph for the
|
||||
# same weights (synthetic; needs STRATA_GGML_DIR = a ggml with the trellis types, else it reports and fails)
|
||||
add_executable(native_hadamard_test src/kernels/cpu/native_hadamard_test.cpp)
|
||||
target_link_libraries(native_hadamard_test PRIVATE strata_kernels_cpu strata_artifact)
|
||||
add_test(NAME native_hadamard_test COMMAND native_hadamard_test)
|
||||
set_tests_properties(native_hadamard_test PROPERTIES TIMEOUT 300 SKIP_RETURN_CODE 77)
|
||||
# the CPU trellis kernels (tq_cpu.hpp): AVX2 / AVX-512 vs the scalar kernel bit for bit, vs ggml-cpu's vec_dot
|
||||
# and to_float, plain and with the Hadamard rotation; `tq_cpu_parity --bench` is the micro-benchmark
|
||||
add_executable(tq_cpu_parity src/kernels/cpu/tq_cpu_parity.cpp)
|
||||
target_link_libraries(tq_cpu_parity PRIVATE strata_kernels_cpu)
|
||||
add_test(NAME tq_cpu_parity COMMAND tq_cpu_parity)
|
||||
add_test(NAME tq_cpu_parity_avx2 COMMAND ${CMAKE_COMMAND} -E env STRATA_FORCE_AVX2=1 $<TARGET_FILE:tq_cpu_parity>)
|
||||
set_tests_properties(tq_cpu_parity tq_cpu_parity_avx2 PROPERTIES TIMEOUT 300 SKIP_RETURN_CODE 77)
|
||||
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
|
||||
@@ -1927,6 +2154,23 @@ if(STRATA_BUILD_TESTS)
|
||||
add_executable(pool_test src/kernels/cpu/pool_test.cpp)
|
||||
target_link_libraries(pool_test PRIVATE strata_kernels_cpu)
|
||||
add_test(NAME pool_test COMMAND pool_test --selftest WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
# where the workers are placed (one per physical core by default) and --pool-cores (real pools, no model)
|
||||
add_executable(pool_cores_test src/kernels/cpu/pool_cores_test.cpp)
|
||||
target_link_libraries(pool_cores_test PRIVATE strata_kernels_cpu)
|
||||
add_test(NAME pool_cores_test COMMAND pool_cores_test)
|
||||
# the default CPU expert worker count on mock CPUs (8c/16t, 16c/32t, 64c/128t, 2-socket 128c/256t, ...)
|
||||
add_executable(pool_plan_test src/kernels/cpu/pool_plan_test.cpp)
|
||||
target_include_directories(pool_plan_test PRIVATE include)
|
||||
add_test(NAME pool_plan_test COMMAND pool_plan_test)
|
||||
# 0.2.2: when the pool splits per NUMA node by itself (a fake sysfs node folder, mock cores; STRATA_NUMA_POOL=0, one node = unchanged)
|
||||
add_executable(numa_pool_test src/kernels/cpu/numa_pool_test.cpp)
|
||||
target_include_directories(numa_pool_test PRIVATE include)
|
||||
target_compile_features(numa_pool_test PRIVATE cxx_std_17)
|
||||
add_test(NAME numa_pool_test COMMAND numa_pool_test)
|
||||
# #3204: when the HIP start-up warm-up of hipBLAS may run (pure policy, no GPU)
|
||||
add_executable(warm_policy_test src/prefill/warm_policy_test.cpp)
|
||||
target_include_directories(warm_policy_test PRIVATE include)
|
||||
add_test(NAME warm_policy_test COMMAND warm_policy_test)
|
||||
# issue #29: tiny batches with sleeping workers, as on a big-VRAM card; a hang fails it
|
||||
add_executable(pool_stress src/kernels/cpu/pool_stress.cpp)
|
||||
target_link_libraries(pool_stress PRIVATE strata_kernels_cpu)
|
||||
@@ -1996,6 +2240,23 @@ if(STRATA_HIP_GFX906)
|
||||
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)
|
||||
# #3172: strata_mmq_w4a4 is built from llama.cpp's headers too; without GGML_USE_HIP its common.cuh took the CUDA vendor header
|
||||
# (cuda.h, cuda_bf16.h: not found). Here it only reports that W4A4 (RTX 50's FP4 MMA) is not available.
|
||||
if(TARGET strata_mmq_w4a4)
|
||||
target_compile_definitions(strata_mmq_w4a4 PRIVATE GGML_USE_HIP)
|
||||
endif()
|
||||
# #3271: three more link errors of a 0.1.44+ gfx906 build, all sources that strata_mmq lists but the glob above never saw:
|
||||
# the trellis overlay's quantize.cu and instances (they are generated into the build tree, so the glob missed them: CMake drops a
|
||||
# .cu source whose language is not enabled) and moe_f16_kq.cu, which moe_fused_iq.cu calls (a stub here: no gfx11 device code, so
|
||||
# f16kq::covers() answers false and the int8 kernels run, as on every other card)
|
||||
if(_strata_mmq_tq_overlay)
|
||||
set(_strata_tq_hip ${_strata_tq_overlay_dir}/quantize.cu)
|
||||
foreach(_t tq2_t tqk6 tqk7)
|
||||
list(APPEND _strata_tq_hip ${_strata_tq_overlay_dir}/template-instances/mmq-instance-${_t}.cu)
|
||||
endforeach()
|
||||
list(APPEND _strata_cu ${_strata_tq_hip})
|
||||
endif()
|
||||
target_sources(strata_mmq PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/prefill/moe_f16_kq.cu)
|
||||
endif()
|
||||
set_source_files_properties(${_strata_cu} PROPERTIES LANGUAGE HIP)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# Contributing to Strata
|
||||
|
||||
**AI assistance is welcome. The contributor is responsible for understanding the change and supplying real evidence.**
|
||||
|
||||
These rules apply to issues, pull requests, benchmark reports, and the agents helping prepare them.
|
||||
You should be able to explain every changed line and answer review questions about it.
|
||||
|
||||
## Before starting
|
||||
|
||||
1. Search [issues](https://github.com/Niko1221/Strata/issues) and
|
||||
[pull requests](https://github.com/Niko1221/Strata/pulls), including closed discussions. Add new evidence to an
|
||||
existing thread when it covers your problem.
|
||||
2. Read [AGENTS.md](AGENTS.md) and the documentation for the component you want to change.
|
||||
3. Get maintainer agreement on scope before substantial features, refactors, architecture changes, or changes to
|
||||
defaults. Describe the user problem, proposed behavior, and tradeoffs in an issue first. Agreement on scope
|
||||
does not guarantee a merge. Focused fixes and benchmark reports can go straight to a PR.
|
||||
4. Keep one problem per PR. Update that PR after feedback; do not open competing attempts for the same change.
|
||||
|
||||
Do not resubmit a declined proposal under a different title or through another agent. If new evidence changes
|
||||
the situation, bring it to the original discussion and let a maintainer decide whether to reopen it.
|
||||
|
||||
## Report a problem people can investigate
|
||||
|
||||
Use the matching [issue form](https://github.com/Niko1221/Strata/issues/new/choose). A bug report should include:
|
||||
|
||||
- The smallest steps or command that reproduce the problem, and the expected and actual behavior.
|
||||
- Strata version or commit, OS, GPU and VRAM, and relevant model, quantization, and configuration.
|
||||
- The exact error and relevant logs, with credentials and private content removed.
|
||||
- Whether you reproduced it yourself. Label a finding based only on reading code or a scanner as
|
||||
**unconfirmed static analysis**, and explain the suspected path without presenting it as an observed failure.
|
||||
|
||||
Do not invent reproduction steps, errors, affected hardware, or severity. If you cannot reproduce something,
|
||||
say what you inspected and what would verify the hypothesis. Feature requests should explain a concrete user
|
||||
need and the current workaround. Questions should say what you tried and where you got stuck.
|
||||
|
||||
Example structure, with placeholders to replace with actual observations:
|
||||
|
||||
```text
|
||||
Problem: <one observed behavior>
|
||||
Environment: <commit, OS, GPU/VRAM, model/quant, relevant settings>
|
||||
Reproduce: <exact command or short sequence>
|
||||
Expected: <documented or intended behavior>
|
||||
Actual: <observed behavior and exact error>
|
||||
Evidence: <sanitized log attachment>
|
||||
Reproduction status: <reproduced locally OR unconfirmed static analysis>
|
||||
```
|
||||
|
||||
## Titles
|
||||
|
||||
Every issue and pull request title starts with its area in square brackets, then a short description: `[Engine]`, `[GUI]`, `[TUI]`,
|
||||
`[Benchmark]`, `[Docs]` or `[Other]`. For example `[Engine] fix decode stop on long prompts` or `[GUI] keep the model list after a tab
|
||||
switch`. The issue forms ask for the area and a check on the title fails when the prefix is missing.
|
||||
|
||||
## Prepare a focused pull request
|
||||
|
||||
Lead with the problem and resulting behavior. Explain why the approach is needed, the important limits, and
|
||||
any compatibility effects. Link the issue and scope agreement when applicable. A file-by-file narration or
|
||||
an AI-generated summary of the diff is not a substitute for this explanation.
|
||||
|
||||
- Keep unrelated formatting, cleanup, dependency changes, and speculative abstractions out of the PR.
|
||||
- Add documentation where it changes what a user or developer needs to know. Avoid repeated summaries,
|
||||
speculative roadmaps, and generated documentation with no actionable information.
|
||||
- Preserve existing behavior outside the intended change. New features are opt-in; follow the default-path
|
||||
and performance requirements in [AGENTS.md](AGENTS.md).
|
||||
- Read the complete diff, including generated files, before submitting. Remove debugging output, secrets,
|
||||
private paths, and accidental files.
|
||||
- Prefer a small relevant regression check over tests that only restate the implementation. Documentation-only
|
||||
changes need link, example, and consistency checks; they do not require GPU builds.
|
||||
|
||||
## Show the validation you actually performed
|
||||
|
||||
List exact commands, the revision and environment tested, and observed outcomes. Keep **passed**, **failed**,
|
||||
and **not run** separate. Include relevant output or a log attachment. A command you suggest running is not
|
||||
evidence that it passed; a CI run on a different revision is not validation of the submitted code.
|
||||
|
||||
Build every backend a code change touches, including CUDA, HIP, and SYCL where applicable, before requesting
|
||||
review. For changes that need hardware execution, report that execution separately from compilation. A mock,
|
||||
static check, or successful build does not establish hardware correctness. If you lack a required environment,
|
||||
keep the PR as a draft, identify the missing checks, and ask for targeted help in the same thread. Do not claim
|
||||
compatibility you have not tested.
|
||||
|
||||
Performance claims need before/after runs on the same hardware and workload, with every changed setting
|
||||
identified. Preserve the individual runs, failures, and variability. Follow the
|
||||
[community measurement guide](docs/COMMUNITY_BENCHMARKS.md); standard submissions will use the
|
||||
[benchmark protocol](docs/BENCHMARK_PROTOCOL.md) once it is activated. One fast run does not justify changing a
|
||||
default or claiming a speedup for other machines.
|
||||
|
||||
Example PR body; placeholders are not test results:
|
||||
|
||||
```text
|
||||
Problem and result: <trigger; behavior before and after the change>
|
||||
Related discussion: <issue URL and scope agreement, when required>
|
||||
Approach: <why this fix; relevant compatibility effects>
|
||||
Validation:
|
||||
Passed: <exact command, revision/environment, observed result, log>
|
||||
Failed: <exact command and failure, or none observed>
|
||||
Not run: <check, reason, and what remains unverified>
|
||||
Limits: <known limitations or remaining work>
|
||||
```
|
||||
|
||||
## Rules for AI-assisted work
|
||||
|
||||
Use agents to help investigate, implement, and explain work that you can review. Before submitting, verify
|
||||
their claims against the source, actual command output, or a cited primary reference.
|
||||
|
||||
Never submit fabricated measurements, invented test results, fake logs, or copied claims you have not checked.
|
||||
Do not turn a repository scan into a batch of speculative issues or PRs. Consolidate related findings, establish
|
||||
their effect, and submit the useful evidence. Do not present an agent's confidence or another model's approval
|
||||
as validation. Keep review replies concise and address the requested change or evidence directly.
|
||||
|
||||
Maintainers may request a smaller change, return a PR to draft, or close a submission that lacks evidence,
|
||||
duplicates existing work, or ignores agreed scope. A smaller, understandable contribution with honest limits
|
||||
is easier to review and maintain.
|
||||
|
||||
## Share a benchmark
|
||||
|
||||
A results-only PR should contain the human report, raw measurements, sanitized configuration, and supporting
|
||||
logs. Keep it separate from engine changes and include poor results and failed requests.
|
||||
|
||||
The [current community benchmark guide](docs/COMMUNITY_BENCHMARKS.md) remains available for measurements made
|
||||
with today's tools. The [new protocol](docs/BENCHMARK_PROTOCOL.md) defines the draft standard and future approval
|
||||
workflow. Its draft status must remain visible: existing reports are not automatically standard or approved.
|
||||
@@ -1,119 +1,110 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Strata, built FROM SOURCE (NVIDIA CUDA 13). The engine is compiled here, packaged as an engine component package and then
|
||||
# installed into the same runtime image as the ready-made ones (docker/Dockerfile): same Terminal, same entrypoint, same /data volume.
|
||||
#
|
||||
# Strata: Qwen3.8-Flash-Next on NVIDIA GPUs (RTX 30/40/50, 12+ GB VRAM; two or
|
||||
# three cards can share one model, 8 GB each - docs/MULTI_GPU.md).
|
||||
# Most people want the ready-made images instead (nothing compiled): docs/DOCKER.md.
|
||||
#
|
||||
# The engine is compiled during docker build, so the first container start only
|
||||
# downloads the model (~70 GB) and starts the server. docker build has no GPU,
|
||||
# so the CUDA architectures are fixed here instead of read from nvidia-smi: the
|
||||
# engine is a fat binary with a cubin per listed arch, and the runtime picks the
|
||||
# one matching your card. Narrow CUDA_ARCHITECTURES to your card for a faster
|
||||
# build; a card outside the set needs a rebuild with its own arch.
|
||||
# docker build -t strata:src . # RTX 20/30/40/50 (compiles for each, ~25 min)
|
||||
# docker build -t strata:src --build-arg CUDA_ARCHITECTURES=86 . # one card only (faster); 86 = RTX 30, 89 = RTX 40, 120 = RTX 50
|
||||
# docker build -t strata:src-cuda12 --build-arg CUDA_IMAGE=nvidia/cuda:12.9.1-devel-ubuntu24.04 \
|
||||
# --build-arg CUDA_ARCHITECTURES="60;61;70;75;80;86;89" --build-arg BUILD_VISION=0 . # GTX 10 / P100 / V100 (driver 575+)
|
||||
# docker run --rm --gpus all -p 127.0.0.1:8080:8080 --ulimit memlock=-1 --shm-size 2g --stop-timeout 120 \
|
||||
# -v strata-data:/data -e MODEL=IQ3_XXS -e API_KEY=change-me-please-123 strata:src
|
||||
#
|
||||
# Build:
|
||||
# docker build -t strata .
|
||||
# docker build -t strata --build-arg CUDA_ARCHITECTURES=89 . # RTX 40 only
|
||||
# docker build has no GPU, so the CUDA architectures are given here instead of read from nvidia-smi. The engine is a fat binary with
|
||||
# a machine-code part per listed architecture (plus PTX for the newest, which later cards can JIT), and the runtime picks the one
|
||||
# matching your card. Narrow CUDA_ARCHITECTURES to your card for a faster build. Native code for the CPU that builds the image is NOT
|
||||
# used (STRATA_PORTABLE=ON), so the image runs on any x86-64 CPU with AVX2.
|
||||
#
|
||||
# Run (host needs an NVIDIA driver >= 580 and nvidia-container-toolkit):
|
||||
# docker run --rm --gpus all \
|
||||
# -p 8080:8080 \
|
||||
# --ulimit memlock=-1 \
|
||||
# -v strata-data:/data \
|
||||
# -e MODEL=IQ2_XS \
|
||||
# strata
|
||||
#
|
||||
# Setup choices are env vars, read by docker-entrypoint.sh: FAMILY, MODEL, CONTEXT,
|
||||
# VISION (no | yes | cpu), KV (int8 | q4_0 | k8v4), GPU (one card) or GPUS ("0,2"
|
||||
# or "all", with LAYER_SPLIT), LOW_RAM (auto | on | off), HOST, PORT, API_KEY,
|
||||
# GGUF_DIR (GGUF files you already have), RESIDENT_BUDGET_GIB, KV_STREAMING.
|
||||
#
|
||||
# Only the model files, the prepared pack, the MTP layer and the install config
|
||||
# live in the /data volume; the engine is part of the image. Strata loads 32-62 GB
|
||||
# into RAM, so a capped container needs -e LOW_RAM=on: setup.py reads the RAM from
|
||||
# /proc/meminfo, which here is the host's total, not the container's limit. Add an
|
||||
# API key before exposing the port to a network: -e API_KEY=<secret>. Pass
|
||||
# -e REINSTALL=1 to change the model settings later.
|
||||
#
|
||||
# --gpus all on a host with two usable cards: setup takes both (the layer split is
|
||||
# its recommended default). Pin one card with -e GPU=0, or name them with
|
||||
# -e GPUS=0,2. A volume set up for one card switches to the pair on its first start
|
||||
# on a two-card host unless GPU or GPUS pins it. LOW_RAM=on runs on one card.
|
||||
# Plain legacy-builder features only (no heredocs, no --mount). AMD (HIP) and Intel (SYCL) source builds: docs/AMD_HIP.md, docs/INTEL.md.
|
||||
|
||||
FROM nvidia/cuda:13.0.0-devel-ubuntu24.04
|
||||
|
||||
# STRATA_EXECV=1: setup.py replaces itself with the server, so the server is PID 1
|
||||
# and docker stop's SIGTERM reaches it (see setup.start). Normal Linux starts, which
|
||||
# don't set it, keep spawning the server as a child.
|
||||
ENV DEBIAN_FRONTEND=noninteractive PYTHONUNBUFFERED=1 LANG=C.UTF-8 STRATA_EXECV=1
|
||||
ARG CUDA_IMAGE=nvidia/cuda:13.0.0-devel-ubuntu24.04
|
||||
ARG RUNTIME_BASE=ubuntu:24.04
|
||||
|
||||
# ---- 1. compile the engine and make its component package
|
||||
FROM ${CUDA_IMAGE} AS engine-build
|
||||
ENV DEBIAN_FRONTEND=noninteractive PYTHONUNBUFFERED=1 PATH=/venv/bin:$PATH
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential ca-certificates curl git libatomic1 libgomp1 \
|
||||
python3 python3-pip python3-venv unzip \
|
||||
build-essential ca-certificates git libgomp1 libatomic1 python3 python3-pip python3-venv unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /opt/strata
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
# RTX 20 (75), RTX 30 (86), RTX 40 (89), RTX 50 (120), plus 80 for A-series. CMakeLists
|
||||
# refuses anything below 75. BUILD_VISION=0 skips the image encoder build.
|
||||
ARG CUDA_ARCHITECTURES=75;80;86;89;120
|
||||
ARG CUDA_ARCHITECTURES="75;80;86;89;120"
|
||||
ARG BUILD_VISION=1
|
||||
ARG ENGINE_VERSION=
|
||||
RUN python3 -m venv /venv && /venv/bin/pip install --no-cache-dir -r requirements.txt -r requirements-build.txt
|
||||
# llama.cpp at the pinned commit (ggml for the engine, mtmd for pictures), exactly as setup.py fetches it
|
||||
RUN /venv/bin/python -c "import setup; print(setup.get_llama_cpp())"
|
||||
RUN set -e; \
|
||||
real=""; for a in $(echo "$CUDA_ARCHITECTURES" | tr ';,' ' '); do real="$real;${a}-real"; done; real=${real#;}; \
|
||||
last=$(echo "$CUDA_ARCHITECTURES" | tr ';,' ' ' | awk '{print $NF}'); \
|
||||
sm60=OFF; for a in $(echo "$CUDA_ARCHITECTURES" | tr ';,' ' '); do if [ "$a" -lt 75 ]; then sm60=ON; fi; done; \
|
||||
/venv/bin/cmake -G Ninja -S . -B build -DCMAKE_BUILD_TYPE=Release -DSTRATA_ENABLE_CUDA=ON -DSTRATA_BUILD_TESTS=OFF \
|
||||
-DSTRATA_EXPERIMENTAL_SM60=$sm60 \
|
||||
-DSTRATA_NATIVE_EXPERTS=ON -DSTRATA_PORTABLE=ON -DSTRATA_Q6K_EXPERTS=ON -DSTRATA_MMQ_KQUANTS=ON \
|
||||
"-DCMAKE_CUDA_ARCHITECTURES=${real};${last}-virtual" -DSTRATA_GGML_DIR=/src/third_party/llama.cpp; \
|
||||
/venv/bin/cmake --build build --target strata strata_kq -j "$(nproc)"; \
|
||||
if [ "$BUILD_VISION" = "1" ]; then \
|
||||
/venv/bin/cmake -G Ninja -S tools/vision -B build-vision -DCMAKE_BUILD_TYPE=Release -DLLAMA_DIR=/src/third_party/llama.cpp \
|
||||
-DSTRATA_VISION_CUDA=ON -DSTRATA_PORTABLE=ON -DGGML_CUDA_NCCL=OFF "-DCMAKE_CUDA_ARCHITECTURES=${real};${last}-virtual"; \
|
||||
/venv/bin/cmake --build build-vision --target strata-vision -j "$(nproc)"; \
|
||||
fi
|
||||
RUN set -e; \
|
||||
mkdir -p /stage /engine; \
|
||||
cp build/strata /stage/; \
|
||||
[ -f build/libstrata_kq.so ] && cp build/libstrata_kq.so /stage/ || true; \
|
||||
[ -f build-vision/bin/strata-vision ] && cp build-vision/bin/strata-vision /stage/ || true; \
|
||||
ver="${ENGINE_VERSION:-$(sed -n 's/^[[:space:]]*project([^)]*VERSION[[:space:]]*\([0-9][0-9.]*\).*/\1/p' CMakeLists.txt | head -1)}"; \
|
||||
cudafull=$(nvcc --version | sed -n 's/.*release \([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' | head -1); cudamajor=${cudafull%%.*}; \
|
||||
if [ "$cudamajor" = 13 ]; then mindriver=580.65; else mindriver=575.51; fi; \
|
||||
vision_archs=""; [ -f /stage/strata-vision ] && vision_archs=", \"vision_archs\": \"$CUDA_ARCHITECTURES\""; \
|
||||
echo "{\"version\": \"$ver\", \"source\": \"docker build\", \"archs\": \"$CUDA_ARCHITECTURES\", \"cuda\": \"$cudafull\", \"portable\": true${vision_archs}}" > /stage/BUILD.json; \
|
||||
python3 packaging/collect_libs.py --binary build/strata --search /usr/local/cuda/lib64 --out /stage --also 'libcublasLt.so*'; \
|
||||
archs=""; for a in $(echo "$CUDA_ARCHITECTURES" | tr ';,' ' '); do archs="$archs --arch sm_$a"; done; \
|
||||
last=$(echo "$CUDA_ARCHITECTURES" | tr ';,' ' ' | awk '{print $NF}'); \
|
||||
python3 packaging/package_engine.py --src /stage --backend cuda --version "$ver" --out /engine $archs --arch "compute_$last" \
|
||||
--min-driver nvidia=$mindriver --variant cuda$cudamajor --platform linux-x86_64 --build-id "docker-source-${ver}-cuda${cudafull}" --include SYSTEM-LIBS.txt --include 'libstrata_kq*' \
|
||||
--include strata-vision; \
|
||||
for d in /engine/*/; do python3 packaging/package_engine.py --verify "$d"; python3 packaging/binary_deps.py "$d"; done
|
||||
|
||||
RUN python3 -m venv .venv \
|
||||
&& .venv/bin/pip install --no-cache-dir --upgrade pip \
|
||||
&& .venv/bin/pip install --no-cache-dir -r requirements.txt \
|
||||
&& chmod +x setup.sh docker-entrypoint.sh
|
||||
|
||||
# llama.cpp at the pinned commit, then the engine and the image encoder, built
|
||||
# exactly the way setup.py builds them: native code for the CPU that builds the
|
||||
# image, so build it on the PC it runs on. BUILD.json is what setup.py reads to
|
||||
# decide whether an engine is current: source=local with a matching src hash
|
||||
# means the first start reuses it instead of recompiling.
|
||||
RUN .venv/bin/python - <<'PYEOF'
|
||||
import json, os, pathlib, shutil
|
||||
import setup
|
||||
|
||||
llama = setup.get_llama_cpp()
|
||||
nvcc, _ = setup.find_nvcc()
|
||||
arch = os.environ.get("CUDA_ARCHITECTURES", "75;80;86;89;120").strip().strip('"').replace(",", ";")
|
||||
vision = "gpu" if os.environ.get("BUILD_VISION", "1") == "1" else "none"
|
||||
|
||||
setup.cmake_build(setup.ROOT, setup.ROOT / "build", "strata",
|
||||
["-DSTRATA_ENABLE_CUDA=ON", "-DSTRATA_Q6K_EXPERTS=ON", "-DSTRATA_MMQ_KQUANTS=ON", "-DSTRATA_BUILD_TESTS=OFF",
|
||||
f"-DCMAKE_CUDA_ARCHITECTURES={arch}", f"-DCMAKE_CUDA_COMPILER={nvcc}",
|
||||
f"-DSTRATA_GGML_DIR={llama}"], None, "build-strata.bat")
|
||||
if vision != "none":
|
||||
setup.cmake_build(setup.ROOT / "tools" / "vision", setup.ROOT / "build-vision", "strata-vision",
|
||||
[f"-DLLAMA_DIR={llama}", "-DSTRATA_VISION_CUDA=ON", "-DSTRATA_PORTABLE=OFF",
|
||||
f"-DCMAKE_CUDA_ARCHITECTURES={arch}", f"-DCMAKE_CUDA_COMPILER={nvcc}"], None, "build-vision.bat")
|
||||
|
||||
eng = setup.ROOT / "engine"
|
||||
eng.mkdir(exist_ok=True)
|
||||
shutil.copy2(setup.ROOT / "build" / setup.EXE, eng / setup.EXE)
|
||||
if (setup.ROOT / "build" / setup.KQ_LIB).exists(): # the K-quant MMQ prompt library (UD-Q4/Q5/Q6_K_XL files)
|
||||
shutil.copy2(setup.ROOT / "build" / setup.KQ_LIB, eng / setup.KQ_LIB)
|
||||
if vision != "none":
|
||||
shutil.copy2(setup.ROOT / "build-vision" / "bin" / setup.VEXE, eng / setup.VEXE)
|
||||
bindir = pathlib.Path(nvcc).parent
|
||||
meta = {"source": "local", "version": setup.source_version(),
|
||||
"archs": [int(a.split("-")[0]) for a in arch.split(";") if a.split("-")[0].isdigit()], "vision": vision,
|
||||
"cuda_dirs": [str(d) for d in (bindir, bindir / "x64", bindir.parent / "lib64") if d.is_dir()],
|
||||
"src": setup.source_hash(setup.ENGINE_SOURCES),
|
||||
"vision_src": setup.source_hash(setup.VISION_SOURCES) if vision != "none" else None}
|
||||
(eng / "BUILD.json").write_text(json.dumps(meta, indent=1))
|
||||
PYEOF
|
||||
|
||||
# the cmake trees are build-time only; the engine itself is what the container needs
|
||||
RUN rm -rf build build-vision
|
||||
# ---- 2. the Terminal (the release archive, built here)
|
||||
FROM ${RUNTIME_BASE} AS terminal
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends python3 ca-certificates git && rm -rf /var/lib/apt/lists/*
|
||||
COPY . /src/
|
||||
RUN python3 /src/packaging/package_terminal.py --out /out --cache /cache \
|
||||
&& mkdir -p /opt/strata \
|
||||
&& tar -xzf /out/strata-terminal-*-linux-x86_64.tar.gz -C /opt/strata \
|
||||
&& mv /opt/strata/strata-terminal-* /opt/strata/terminal \
|
||||
&& /opt/strata/terminal/bin/strata --version
|
||||
# llama.cpp at the commit setup.py pins: fetched here, so the first start does not depend on GitHub. Only gguf-py (the model preparation
|
||||
# tools) and the file setup.py looks for (ggml/CMakeLists.txt) are kept: 170 MB become a few
|
||||
RUN cd /opt/strata/terminal && STRATA_CONTROL_INSTALL=1 STRATA_INSTALL_DIR=/opt/strata/seed PYTHONNOUSERSITE=1 \
|
||||
/opt/strata/terminal/python/bin/python3 -c "import setup; print(setup.get_llama_cpp())" \
|
||||
&& rm -rf /opt/strata/seed/third_party/*.zip* \
|
||||
&& cd /opt/strata/seed/third_party/llama.cpp \
|
||||
&& find . -mindepth 1 -maxdepth 1 ! -name gguf-py ! -name ggml ! -name 'LICENSE*' -exec rm -rf {} + \
|
||||
&& find ggml -mindepth 1 -depth ! -type d ! -name CMakeLists.txt -delete && find ggml -mindepth 1 -type d -empty -delete \
|
||||
&& du -sh /opt/strata/seed
|
||||
|
||||
# ---- 3. the runtime image (the same as docker/Dockerfile's, FLAVOR=cuda)
|
||||
FROM ${RUNTIME_BASE}
|
||||
ARG STRATA_VERSION=dev
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends tini ca-certificates libgomp1 libatomic1 libnuma1 zlib1g \
|
||||
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
COPY --from=terminal /opt/strata/terminal /opt/strata/terminal
|
||||
COPY --from=terminal /opt/strata/seed /opt/strata/seed
|
||||
COPY --from=engine-build /engine/ /opt/strata/components/engine/
|
||||
COPY docker/entrypoint.sh docker/healthcheck.sh /opt/strata/
|
||||
COPY docker/strata-wrapper.sh /usr/local/bin/strata
|
||||
RUN chmod 0755 /opt/strata/entrypoint.sh /opt/strata/healthcheck.sh /usr/local/bin/strata
|
||||
ENV STRATA_COMPONENTS_DIR=/opt/strata/components STRATA_INSTALL_DIR=/data/install XDG_STATE_HOME=/data/state \
|
||||
XDG_CONFIG_HOME=/data/config HOME=/data/home PORT=8080 LANG=C.UTF-8 PYTHONUNBUFFERED=1 PYTHONNOUSERSITE=1 STRATA_IN_DOCKER=1 \
|
||||
NVIDIA_VISIBLE_DEVICES=all NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
VOLUME ["/data"]
|
||||
EXPOSE 8080
|
||||
|
||||
# /health is answered before the API key gate, so it works with or without one.
|
||||
# The port only opens after the model loads (1-3 minutes, longer on a first run),
|
||||
# so the start period is generous: a too short one marks a still-loading container
|
||||
# unhealthy and a restart policy would kill it mid-download.
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=600s --retries=3 \
|
||||
CMD curl -fs "http://127.0.0.1:${PORT:-8080}/health" || exit 1
|
||||
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
LABEL org.opencontainers.image.title="Strata" com.strata.flavor="cuda" com.strata.build="from-source" org.opencontainers.image.version="${STRATA_VERSION}"
|
||||
HEALTHCHECK --interval=30s --timeout=8s --start-period=2h --retries=3 CMD ["/opt/strata/healthcheck.sh"]
|
||||
STOPSIGNAL SIGTERM
|
||||
ENTRYPOINT ["/usr/bin/tini", "--", "/opt/strata/entrypoint.sh"]
|
||||
CMD ["run"]
|
||||
@@ -1,239 +1,404 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[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"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <b>Deutsch</b> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<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>
|
||||
<h3 align="center">Ein Spitzenmodell auf deinem eigenen Gaming-PC</h3>
|
||||
|
||||
<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>
|
||||
<p align="center">Unterstützt NVIDIA, AMD, Intel · Windows, Linux, Mac · kostenlos und Open Source</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>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.
|
||||
Strata lässt **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** laufen, ein Modell mit 125
|
||||
Milliarden Parametern, für das man sonst einen Server braucht, auf einem normalen PC. Es chattet, schreibt Code, liest
|
||||
Bilder und arbeitet mit deinen Apps und Coding-Agents zusammen. Deine Chats und Dateien bleiben auf deinem PC.
|
||||
|
||||
## Wie schnell ist es?
|
||||
**Neu hier?** Sieh dir an, [was du brauchst](#was-du-brauchst), und [installiere dann die App](#option-a-die-strata-app).
|
||||
|
||||
Wir haben es auf zwei ganz normalen Gaming-PCs gemessen. Ein Token ist etwa ¾ eines Wortes.
|
||||
- **Läuft auf einem Gaming-PC.** 32 GB Arbeitsspeicher (RAM) und Grafikspeicher zusammen reichen für den Anfang.
|
||||
- **Passt zu deinen Werkzeugen.** Strata spricht die OpenAI- und die Anthropic-API, deshalb verbinden sich Claude Code, Codex CLI, OpenCode und andere Agents damit.
|
||||
- **Liest Bilder.** Das ist standardmäßig eingeschaltet.
|
||||
- **Kostenlos.** MIT-Lizenz, kein Konto, keine Cloud.
|
||||
- **Privat.** Keine Telemetrie. Was Strata herunterlädt, wann es das Internet nutzt und wie es Downloads prüft, steht in [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
- **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>
|
||||
Version 0.2.3 · Engine 0.1.48
|
||||
|
||||
## Was du brauchst
|
||||
|
||||
| | |
|
||||
**Du brauchst mindestens 32 GB Arbeitsspeicher (RAM) und Grafikspeicher (VRAM) zusammen.** Bei einem Mac oder einem Ryzen AI Max
|
||||
ist das der gemeinsame Speicher. Mit mehr Speicher läuft ein größeres oder ein schnelleres Modell.
|
||||
|
||||
- Ein PC mit 32 GB und einer Grafikkarte mit 12-16 GB führt den Coder aus.
|
||||
- Mit 64 GB RAM laufen alle Größen.
|
||||
|
||||
| Dein Computer | Details |
|
||||
| --- | --- |
|
||||
| **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. |
|
||||
| **NVIDIA** GeForce RTX 20, 30, 40, 50 Serie (12 GB oder mehr) | Installiere die App oder das Terminal. |
|
||||
| **AMD** Radeon RX 6800 und neuer, Radeon AI PRO R9700 (12 GB oder mehr) | [AMD-Karten](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [Intel-Engine](docs/INTEL.md) |
|
||||
| **Apple-Silicon-Mac** (M1 oder neuer, macOS 15 oder neuer) | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** und andere Systeme mit gemeinsamem Speicher | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Ältere Karten und Prozessoren | [Ältere GPUs](docs/OLDER_GPUS.md), [Ältere CPUs](docs/INSTALL.md#older-cpus-experimental) |
|
||||
|
||||
Alles andere richtet der Installer ein. Zwei oder drei Karten können sich das Modell teilen ([Multi-GPU](docs/MULTI_GPU.md)).
|
||||
Außerdem nötig:
|
||||
|
||||
Experimentell, von Community-Mitgliedern auf ihren eigenen Rechnern geschrieben und getestet:
|
||||
- etwa 80 GB freier Speicherplatz (mit einer SSD startet es beim ersten Mal viel schneller)
|
||||
- Windows 10 / 11, Linux oder macOS
|
||||
- ein aktueller Grafiktreiber
|
||||
|
||||
- **Ä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).
|
||||
Zwei oder drei Karten desselben Herstellers können sich ein Modell teilen ([Multi-GPU](docs/MULTI_GPU.md)). Strata teilt ein
|
||||
Modell nie auf NVIDIA- und AMD-Karten auf. Die vollständige Liste: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
|
||||
Die vollständige Liste: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
## Installation
|
||||
|
||||
## Installieren
|
||||
Such dir eine Option aus. Alle starten dieselbe Engine und nutzen dieselben Modelle.
|
||||
|
||||
### Lass deine KI es einrichten
|
||||
### Option A: die Strata-App
|
||||
|
||||
Nutzt du einen KI-Coding-Assistenten (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Füge dort diesen Text ein:
|
||||
Der einfachste Weg. Die App bringt die Engine mit: installieren, ein Modell wählen, chatten. Lade die Datei für dein
|
||||
System und deine Grafikkarte von der [Releases-Seite](https://github.com/Niko1221/Strata/releases/latest) herunter. Du bist
|
||||
unsicher, welche du brauchst? Lies den Absatz unter der Tabelle.
|
||||
|
||||
| System | Deine Grafikkarte | Datei zum Herunterladen |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, Treiber 580 oder neuer | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
|
||||
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 Serie oder Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, Treiber 580 oder neuer | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 Serie | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 oder neuer) | eingebaut | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
|
||||
|
||||
**Wenn du unsicher bist:** Bei NVIDIA nimmst du `cuda13`, wenn dein Treiber Version 580 oder neuer ist, sonst `cuda12`.
|
||||
`cuda12` ist für ältere Karten und ältere Treiber. Die Dateien `hip` und `amd` sind für AMD-Karten. Für andere AMD-Karten
|
||||
(RX 6000) gibt es noch keine fertige App: siehe [AMD-Karten](docs/AMD_HIP.md).
|
||||
|
||||
Danach:
|
||||
|
||||
- **Windows:** Starte die `.exe`. Sie wird für deinen Benutzer installiert und braucht keine Administratorrechte. Wenn
|
||||
Windows "Der Computer wurde durch Windows geschützt" meldet, wähle **Weitere Informationen** und dann **Trotzdem
|
||||
ausführen** (der Installer ist noch nicht signiert).
|
||||
- **Linux:** Mach die Datei ausführbar (`chmod +x`) und starte sie. Sie braucht FUSE 2: `sudo apt install libfuse2`
|
||||
(Ubuntu 22.04) oder `libfuse2t64` (Ubuntu 24.04 und neuer).
|
||||
- **Mac:** Öffne die `.dmg` und zieh Strata in den Ordner "Programme".
|
||||
|
||||
Auf dem ersten Bildschirm liest die App deinen PC aus und empfiehlt ein passendes Modell. Drück auf Installieren. Das Modell
|
||||
ist ein großer Download (etwa 40-70 GB) und wird nach einer Unterbrechung dort fortgesetzt, wo er aufgehört hat. Wenn du
|
||||
das Fenster schließt, beenden sich Strata und die Engine.
|
||||
|
||||
### Option B: das Terminal (eine Zeile)
|
||||
|
||||
Das Terminal ist der Befehl `strata`, gedacht für Skripte, Server und KI-Agents.
|
||||
|
||||
Linux und Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
Das Skript prüft deinen PC, lädt das Terminal herunter, kontrolliert die SHA-256-Prüfsumme und entpackt es in deinen
|
||||
Home-Ordner. Es braucht keine Administratorrechte. Öffne unter Windows danach ein neues Terminalfenster. Dann startest du
|
||||
Strata:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Es stellt dir eine Frage nach der anderen: Modell, Kontext, Bilder, wer sich verbinden darf. Danach lädt es die Engine für
|
||||
deine Grafikkarte herunter (signiert und vor der Nutzung probeweise gestartet), dazu das Modell, und beginnt zu laufen.
|
||||
Für Skripte und KI-Agents gibt es Flags:
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
Wenn du das Installationsskript vor dem Ausführen lesen willst, lade es zuerst mit `curl -fsSL <address> -o install.sh`
|
||||
herunter, lies es und führe dann `sh install.sh` aus. Alle Bildschirme und Flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### Option C: deine KI richtet es ein
|
||||
|
||||
Füge das in Claude Code, Cursor, Codex, GitHub Copilot oder einen anderen Coding-Assistenten 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
|
||||
Der Assistent prüft deine Grafikkarte, deinen Arbeitsspeicher und den Speicherplatz, wählt das passende Modell, installiert
|
||||
und startet es und sagt dir, wie du deine Apps verbindest. KI-Werkzeuge können Strata auch über den
|
||||
[MCP-Server](docs/MCP_SERVER.md) installieren, starten und stoppen.
|
||||
|
||||
### Oder mach es selbst
|
||||
Du willst es lieber selbst bauen? Siehe [Aus dem Quellcode bauen](#aus-dem-quellcode-bauen).
|
||||
|
||||
[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.
|
||||
## Modelle
|
||||
|
||||
**Mac (Apple Silicon, M1 bis M5, macOS 15 oder neuer):** Installiere einmal Apples Command Line Tools (`xcode-select --install` im Terminal; Xcode selbst brauchst du nicht). Öffne dann das Terminal im Strata-Ordner und führe **`./setup.sh`** aus. Es baut die Mac-Engine auf deinem Mac (etwa 7 Minuten), stellt dieselben Fragen, lädt das Modell und startet es. Beim nächsten Mal startet `./setup.sh` sofort. Welche Größe zu deinem Mac passt und wie schnell es ist: [docs/MAC.md](docs/MAC.md).
|
||||
Die App und `strata launch` empfehlen ein Modell passend zu deinem Speicher. Es ist ein einziges Modell in mehreren
|
||||
Größen, mehr oder weniger stark komprimiert. Kleinere Größen sind schneller. Größere Größen sind etwas klüger.
|
||||
|
||||
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:
|
||||
### Wenig Speicher? Probier Gyro
|
||||
|
||||
- welches Modell und welche Größe,
|
||||
- wie viel Kontext (wie viel Text sich das Modell merkt),
|
||||
- ob es Bilder lesen soll.
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (von agentionai) ist die kleinste Möglichkeit, dieses Modell auszuführen.
|
||||
|
||||
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`.
|
||||
| Größe | Benötigter Speicher (RAM + VRAM) | Download | Was es ist |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 GB | 58,5 GB | Für PCs, auf denen nichts anderes läuft. Die Qualität ist geringer als bei IQ2_XS. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Ungefähr die Qualität von IQ3_XXS bei etwa 20 % weniger GPU-Speicher. Es schreibt Antworten langsamer als Q2_0: 16 % auf einer RTX 3060 mit 12 GB, 21 % auf einer R9700. |
|
||||
|
||||
> **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.
|
||||
Gyro läuft auf NVIDIA- und AMD-Karten (bei AMD experimentell) auf Intel-Arc-Karten unter Linux (getestet auf einer Arc A750) und ab 0.2.2 auf Macs mit Apple Silicon (Gyro-S auf einem Mac mini mit 32 GB: 9 Tokens/s, die Experten werden von der SSD gelesen). Details und Messwerte:
|
||||
[docs/GYRO.md](docs/GYRO.md). Starte es mit `strata launch --model gyro-s`.
|
||||
|
||||
**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).
|
||||
### Die anderen Größen
|
||||
|
||||
## 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 |
|
||||
| Dein Speicher (RAM + VRAM) | Nimm | Hinweise |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
| **32 GB** | **Gyro-S**, oder den **[Coder](docs/MODELS.md#coder)** für Code | Der Coder braucht 32 GB RAM und behält die Hälfte der Experten, deshalb ist er außerhalb von Code schwächer |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), oder **Gyro-M** (35 GB) | Swift denkt kürzer nach, bevor es antwortet. Gyro-M kommt der Qualität von IQ3_XXS nahe, schreibt Antworten aber 16-21 % langsamer als Q2_0 |
|
||||
| **48 GB** | **IQ3_XXS** (47,0 GB) | Langsamer, besser |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB), das Original oder Swift 1.5 | Am langsamsten, am besten. Es erreicht in den veröffentlichten Tests das volle Modell |
|
||||
| **96 GB oder mehr** | **UD-IQ4_XS** von Unsloth (etwa 72 GB RAM) bis **Q8_0** (etwa 136 GB), 4 bis 8 Bit | Näher am vollen Modell. Bei weniger RAM liest Strata einen Teil von der SSD, das ist viel langsamer |
|
||||
|
||||
- **[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, UD-Q5_K_XL, UD-Q6_K_XL und Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** 4 bis 8 Bit, näher am vollen Modell
|
||||
(93-95 % das gleiche nächste Token wie Q8_0). Schnell, wenn alle Experten in den RAM passen (~88 GB für UD-Q4_K_XL
|
||||
bis ~136 GB für Q8_0); mit weniger RAM liest Strata einen Teil davon während der Antwort von der SSD, was viel
|
||||
langsamer ist (7-8.5 Tokens/s für UD-Q4_K_XL auf einem PC mit 64 GB).
|
||||
- **[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.
|
||||
Auch mit weniger RAM, als eine Zeile nennt, kann es funktionieren: Strata wechselt von selbst in seinen
|
||||
[Modus für wenig RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Rechne mit weniger Tempo, mit einer kleinen
|
||||
Grafikkarte mit deutlich weniger.
|
||||
|
||||
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`).
|
||||
Weitere Auswahl:
|
||||
|
||||
## So benutzt du es
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** ist eine Feinabstimmung, die kürzer nachdenkt, bevor sie antwortet, bei etwa gleicher Qualität und gleichem RAM-Bedarf.
|
||||
- **[NVFP4](docs/NVFP4.md)** ist NVIDIAs 4-Bit-Version für NVIDIA-Karten. Sie braucht etwa 80 GB RAM, um alle Experten im RAM zu halten.
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** ist ein kleineres, anderes Modell, das Bilder liest und mehrere Anfragen gleichzeitig beantworten kann.
|
||||
- **[OrcaRouters Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** richtest du von Hand ein.
|
||||
|
||||
<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>
|
||||
Alle Größen, Downloads und was wo läuft: [docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
- **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.
|
||||
## Wie schnell ist es?
|
||||
|
||||
Mehr: [wo deine Chats gespeichert werden](docs/INSTALL.md#where-things-are-stored), [die API](docs/DETAILS.md#using-it).
|
||||
Gemessen mit einer Grafikkarte mit 12 GB (RTX 5070, Ryzen 5 7600, 64 GB RAM). Ergebnisse anderer Karten findest du hier:
|
||||
[Tempo jeder Größe](docs/MODELS.md#how-fast-is-each-size), [Ergebnisse der Community](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
### Optional: mehr Tempo mit einer Grafikkarte, wenn das ganze Modell in den RAM passt
|
||||
| Modell | Antworten erscheinen mit | Liest deinen Prompt mit |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
|
||||
Zwei Opt-in-Einstellungen (von RubenTorney, #1904 und #1905). Sie helfen, wenn du eine Grafikkarte nutzt und dein RAM alle Experten hält (Stratas Arena-Modus, zum Beispiel Q2_0 oder IQ3_XXS auf einem PC mit 64 GB). Öffne deine Konfiguration `strata-<model>.json` im Strata-Datenordner ([wo](docs/INSTALL.md#where-things-are-stored)) und füge hinzu:
|
||||
Ein Token ist etwa ¾ eines Wortes, 60 Tokens pro Sekunde sind also schneller, als du lesen kannst. "Liest deinen Prompt"
|
||||
beschreibt, wie schnell Strata ein langes Dokument, Code oder einen Chatverlauf aufnimmt.
|
||||
|
||||
- **Schnellere Ausgabe:** `"--adapt-async", "1"` in die Liste `"args"`. Der Experten-Cache wird dann in einem Hilfs-Thread aktualisiert, statt die Antwort anzuhalten. Gemessen auf einer RTX 5070 (Windows, Q2_0): +4-5 % Ausgabe, +7-10 % bei Code und langen Dokumenten. Antworten können leicht vom Standard abweichen (sie bleiben von Lauf zu Lauf gleich).
|
||||
- **Schnelleres Prompt-Lesen unter Windows:** `"STRATA_PIN_BY_NEED": "1"` im Block `"env"`. Windows kann nur etwa den halben RAM für die Grafikkarte sperren; damit werden zuerst die Experten gesperrt, die die Karte wirklich braucht. Gemessen auf der RTX 5070: +4,4 % Prompt-Lesen.
|
||||
Gemessen mit einer AMD-Karte mit 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB RAM):
|
||||
|
||||
| Modell | Antworten erscheinen mit | Liest deinen Prompt mit |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
|
||||
Die Balken haben in beiden Tabellen denselben Maßstab. Eine Karte mit mehr VRAM ist schneller. Eine RTX 3090 (24 GB) sollte
|
||||
etwa 100-140 Tokens pro Sekunde schreiben. Die Engine-Version hinter jeder Zahl steht in [DETAILS.md](docs/DETAILS.md#speed-measured).
|
||||
|
||||
## Benutzung
|
||||
|
||||
### In der App
|
||||
|
||||
Die App hat einen **Chat**, einen **Monitor** für das Modell sowie für GPU, CPU und RAM, eine Seite **Modelle** und die
|
||||
**Einstellungen**. Der Chat kann in einem Ordner auf deinem PC arbeiten. Im Modus **Manuell** fragt er nach, bevor er eine
|
||||
Datei ändert oder einen Befehl ausführt. Mit `/loop 10m <what to do>` wiederholst du eine Nachricht alle 10 Minuten, solange
|
||||
der Chat geöffnet ist.
|
||||
|
||||
Der Chat kann das Web mit DuckDuckGo (oder Bing) durchsuchen, ohne Konto. Unter **Einstellungen, Tools** kannst du einen
|
||||
Suchschlüssel hinzufügen. Die Suche ist die einzige Funktion, die etwas von deinem PC sendet (die Suchbegriffe).
|
||||
|
||||
Um zwei Modelle auszuführen, schalte **Mehrere Modelle ausführen** ein und gib jedem Modell eine eigene Karte desselben
|
||||
Herstellers.
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>Ein Chat in der Strata-App: Der Agent hat einen kleinen Voxel-Pagodengarten als eine HTML-Datei geschrieben, und die Seite öffnet sich in der Vorschau neben dem Gespräch</sub></p>
|
||||
|
||||
### Im Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<sub><code>strata monitor</code>, während das Modell antwortet: Geschwindigkeit, GPU, Speicher und die letzten Anfragen</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided screens: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
|
||||
strata launch --plan --model q2_0 # show what would happen; nothing changes
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
|
||||
strata update --check # is there a new engine? (strata update installs it)
|
||||
strata doctor # check the installed engine files
|
||||
```
|
||||
|
||||
`strata --help` listet alles auf. Starten und Bereitstellen laden nichts herunter. Nur die Installation, `strata launch`
|
||||
(wenn du ein Modell wählst) und `strata update` tun das. Um Strata von deinem Handy oder einem anderen PC aus zu erreichen,
|
||||
nutze `strata launch --access lan`. Der Befehl erzeugt einen API-Schlüssel und startet in einem Netzwerk nicht ohne einen.
|
||||
Ein Browser öffnet sich nur, wenn du `--open` angibst. Anleitung: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Der ganze Client: [docs/TERMINAL.md](docs/TERMINAL.md).
|
||||
|
||||
### Mit KI-Agents
|
||||
|
||||
Ein Agent installiert Strata mit dem Einzeiler aus Option B und führt
|
||||
`strata launch --model NAME --yes --no-tui` aus. Die Schritte, Flags und Exit-Codes stehen in [docs/AI_SETUP.md](docs/AI_SETUP.md),
|
||||
die Regeln für das Repository in [AGENTS.md](AGENTS.md).
|
||||
|
||||
### Andere Apps verbinden
|
||||
|
||||
| App-Typ | Basis-URL |
|
||||
| --- | --- |
|
||||
| OpenAI-kompatibel (die meisten Apps und Agents) | `http://127.0.0.1:8080/v1` (beliebiger API-Schlüssel, beliebiger Modellname) |
|
||||
| Anthropic-API (Claude Code und andere) | `http://127.0.0.1:8080`, ohne `/v1`. Diese Apps hängen `/v1/messages` selbst an. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([Einrichtung](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` gibt die genauen Einstellungen für OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent und OpenClaw aus.
|
||||
`strata connect opencode --write` schreibt die Datei nach einer Rückfrage und legt eine Sicherung an.
|
||||
|
||||
### Bilder
|
||||
|
||||
Bilder sind für Modelle, die sie lesen können, standardmäßig eingeschaltet. Hänge ein Bild im Chat an oder füge es ein, oder
|
||||
sende es aus deiner App. Der Bildleser läuft auf dem Prozessor, wenn deine Grafikkarte weniger als 24 GB hat (einige Sekunden
|
||||
pro Bild, ohne Speicher der Karte zu belegen), und ab 24 GB auf der Grafikkarte. Die Regel gilt für NVIDIA und AMD gleich.
|
||||
Du kannst Bilder pro Modell ausschalten oder den Leser mit `--vision off`, `--vision gpu` oder `--vision cpu` wählen.
|
||||
|
||||
Die Zahlen hinter der Regel: Auf einer Radeon AI PRO R9700 brauchte der Bildleser der Karte 57 ms für ein Bild mit 260 Tokens,
|
||||
der des Prozessors 3,1 s. Auf Karten mit 16 GB (Tesla P100 und RTX A4000, Linux) kostete der Bildleser der Karte 3-5 % des
|
||||
Texttempos, weil er etwa 1,4 GB belegt, die sonst das Modell nutzen würde. Auf dem Mac liest die GPU, egal wie viel Speicher er hat (0,45 s für ein Bild mit 260 Tokens auf einem M4 mit 32 GB, etwa 1,2 GB des gemeinsamen
|
||||
Speichers); der Prozessor braucht dort etwa 50 s pro Bild und liest deshalb nur mit `--vision cpu`.
|
||||
|
||||
### Gut zu wissen
|
||||
|
||||
- Das Denkniveau (**aus, niedrig, mittel oder hoch**) stellst du im Chat-Menü oder unter "reasoning effort" in deiner App ein. Aus ist am schnellsten.
|
||||
- Strata beantwortet standardmäßig eine Anfrage nach der anderen. Für mehrere gleichzeitig nutze `--parallel N` in `strata launch` oder
|
||||
`"parallel": 2` in der Konfiguration ([BATCHING.md](docs/BATCHING.md)). Auf einer Karte mit 12 GB wird dadurch jede Antwort langsamer.
|
||||
- Strata liest die erste Nachricht eines Chats komplett, etwa 1 Minute pro 30.000 Tokens. Folgenachrichten starten in Sekunden.
|
||||
- Wo deine Chats gespeichert werden und die ganze API: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
|
||||
|
||||
## Aus dem Quellcode bauen
|
||||
|
||||
<details>
|
||||
<summary>Windows, Linux und Mac aus dem Repository</summary>
|
||||
|
||||
[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 `./setup.sh` im Strata-Ordner aus.
|
||||
|
||||
**Mac (Apple Silicon, macOS 15 oder neuer):** Installiere einmal die Command Line Tools von Apple mit `xcode-select --install`
|
||||
(Xcode selbst brauchst du nicht). Öffne das Terminal im Strata-Ordner und führe `./setup.sh` aus. Das Skript baut die
|
||||
Mac-Engine auf deinem Mac (etwa 7 Minuten), stellt dieselben Fragen, lädt das Modell herunter und startet es. Beim nächsten
|
||||
Mal startet `./setup.sh` sofort. Welche Größe zu deinem Mac passt: [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
Der Installer erkennt deine Karte und richtet die passende Engine ein. Er fragt, welches Modell und welche Größe du willst,
|
||||
wie viel Kontext (wie viel Text sich das Modell merkt) und ob es Bilder lesen soll. Drück Enter für die empfohlene Antwort.
|
||||
Dann lädt er das Modell herunter (etwa 70 GB) und startet es. Wenn der Download abbricht, führe das Skript erneut aus, dann
|
||||
geht es weiter. Dein Browser öffnet die Strata-Seite 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 sperrt einen Teil davon für die Grafikkarte. Warte ab und schließe das Fenster nicht.
|
||||
|
||||
Starte beim nächsten Mal wieder `START-HERE.bat` (oder `./setup.sh`). Es startet sofort und lädt nichts doppelt herunter.
|
||||
Schließ sein Fenster, um das Modell zu beenden. `UPDATE.bat` (`./update.sh`) aktualisiert Strata, ohne es zu starten.
|
||||
Aktualisieren, Docker, mehrere Karten, wohin die Dateien kommen und alle Optionen: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
Um deinen PC zu messen, führe `BENCH.bat` (`./bench.sh`) aus. Das dauert 5-10 Minuten und schreibt einen Bericht, den du teilen
|
||||
kannst ([so geht's](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
**Optional: mehr Tempo auf einer Karte, wenn das ganze Modell in den RAM passt.** Zwei Einstellungen zum Einschalten von
|
||||
RubenTorney (#1904, #1905). Sie helfen, wenn du eine Grafikkarte nutzt und dein RAM alle Experten aufnimmt, zum Beispiel bei
|
||||
Q2_0 oder IQ3_XXS auf einem PC mit 64 GB. Öffne `strata-<model>.json` im Strata-Datenordner ([wo](docs/INSTALL.md#where-things-are-stored)) und ergänze:
|
||||
|
||||
- `"--adapt-async", "1"` in der Liste `"args"`. Der Expertencache wird dann in einem Hilfsthread aktualisiert, statt die
|
||||
Antwort anzuhalten. Gemessen auf einer RTX 5070 (Windows, Q2_0): +4-5 % beim Decodieren, +7-10 % bei Code und langen
|
||||
Dokumenten. Die Antworten können sich leicht von der Standardeinstellung unterscheiden und bleiben von Lauf zu Lauf gleich.
|
||||
- `"STRATA_PIN_BY_NEED": "1"` im Block `"env"` (Windows). Windows kann nur etwa die Hälfte des RAM für die Grafikkarte sperren,
|
||||
und diese Einstellung sperrt zuerst die Experten, die die Karte am dringendsten braucht. Gemessen auf der RTX 5070: +4,4 % beim Lesen des Prompts.
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
Starte Strata danach neu (`START-HERE.bat` oder `./setup.sh`). Zum Rückgängigmachen entferne die beiden Einträge.
|
||||
Starte Strata nach der Änderung neu. Zum Rückgängigmachen entfernst du die beiden Einträge.
|
||||
|
||||
## Etwas ist schiefgelaufen?
|
||||
</details>
|
||||
|
||||
- **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.
|
||||
## Wie funktioniert das?
|
||||
|
||||
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).
|
||||
Modelle wie dieses laufen normalerweise auf Servern mit Hunderten Gigabyte Grafikspeicher. Deine Grafikkarte hat 12-24 GB.
|
||||
Strata bringt das Modell auf deinen PC, indem es die Arbeit auf den ganzen Rechner verteilt. Stell dir eine Bibliothek vor.
|
||||
|
||||
## Wie funktioniert es?
|
||||
- Das Modell ist ein Team aus 24.576 kleinen Spezialisten ("Experten"), wie 24.576 Nachschlagewerke. Für jedes Wort braucht es nur 10 davon.
|
||||
- **Deine Grafikkarte ist der Schreibtisch.** Er ist klein und schnell. Die etwa 4.000 am häufigsten genutzten Bücher liegen
|
||||
dort aufgeschlagen.
|
||||
- **Dein RAM sind die Regale im Lesesaal.** Alle 24.576 Bücher stehen dort. Braucht ein Wort ein Buch, das nicht auf dem
|
||||
Schreibtisch liegt, holt dein Prozessor es sofort aus dem Regal, während die Karte weiterarbeitet.
|
||||
- **Deine SSD ist das Archiv.** Sie enthält eine Nachschlagetabelle von 29 GB. Das Modell liest pro Wort nur wenige Zeilen
|
||||
daraus, deshalb muss sie nicht im RAM liegen.
|
||||
|
||||
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="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
|
||||
|
||||
<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>
|
||||
Ein kleiner Helfer rät außerdem die nächsten Wörter. Das große Modell prüft sie alle auf einmal und behält die richtigen. Du
|
||||
bekommst dieselbe Antwort, 1,6-1,8-mal früher. Lange Texte werden in großen Stücken gelesen, bis zu 8.192 Tokens auf einmal,
|
||||
mit über 1.000 Tokens pro Sekunde.
|
||||
|
||||
- **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="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
|
||||
|
||||
<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 ausführliche Erklärung: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Jeder Teil mit seinen Zahlen:
|
||||
[die Details](docs/DETAILS.md#how-it-works) und das [Paper](docs/paper/Strata-Paper.pdf).
|
||||
|
||||
## Etwas ist schiefgegangen?
|
||||
|
||||
- **Mein PC ist beim ersten Start von Strata eingefroren.** Strata lädt das Modell in den RAM. Warte ab und schließe das
|
||||
Fenster nicht. Nach 10 Minuten immer noch eingefroren? Starte den PC neu, schließe andere Programme und versuch es noch
|
||||
einmal, oder wähle eine kleinere Größe.
|
||||
- **Es ist beim Herunterladen oder Installieren stehen geblieben.** Starte die App oder `strata launch` noch einmal, oder
|
||||
führe `START-HERE.bat` (oder `./setup.sh`) erneut aus. Es macht dort weiter, wo es aufgehört hat.
|
||||
- **Es ist sehr langsam, die Festplattenanzeige blinkt ständig, oder es meldet "the engine stopped unexpectedly".** Dein PC
|
||||
hat nicht genug freien RAM. Schließe andere Programme (Browser brauchen viel) oder wähle eine kleinere Größe wie Q2_0 oder IQ2_XS.
|
||||
- **Es meldet, dass es keine Engine für diesen PC gibt.** Öffne in der App **Einstellungen, Engine** und drücke **Engine
|
||||
aktualisieren**. Führe im Terminal `strata update` aus. `strata doctor` zeigt, welche Engine-Dateien installiert sind.
|
||||
- **Es meldet, dass Port 8080 schon belegt ist.** Strata läuft bereits. Such das Fenster.
|
||||
|
||||
Weitere Probleme und Lösungen: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Du kommst nicht weiter? Öffne ein
|
||||
[Issue](https://github.com/Niko1221/Strata/issues). Hänge in der App die Datei aus **Einstellungen, Engine, Diagnose, Protokolle anzeigen, Protokolle speichern** an. Füge im Terminal die Ausgabe von `strata doctor` ein. Hänge beim Start aus dem Quellcode die
|
||||
Datei `strata-<model>.log` aus dem Strata-Ordner an. Ein Sicherheitsproblem meldest du bitte privat: [SECURITY.md](SECURITY.md).
|
||||
|
||||
## 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)
|
||||
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), agentionai (Gyro)
|
||||
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)).
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata ist Open Source unter der [MIT-Lizenz](LICENSE). Einige Teile und
|
||||
alle Modelle haben eigene Lizenzen ([welche](docs/HOW_IT_WORKS.md#license)).
|
||||
|
||||
## Mitmachen
|
||||
|
||||
Du hast einen Fehler gefunden oder willst eine Änderung oder einen Benchmark einsenden? Lies zuerst [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
Dort steht, was dazugehört, damit andere deine Arbeit nachprüfen können.
|
||||
|
||||
## Strata unterstützen
|
||||
|
||||
Strata ist kostenlos und Open Source. Wenn es dir nützt, kannst du die Entwicklung unterstützen:
|
||||
Strata ist kostenlos und steht unter der MIT-Lizenz. Ich baue es und teste es auf jeder Hardware, die ich bekommen kann. Wenn es auf deinem PC gut läuft, hilft mir ein Kaffee,
|
||||
weiter neue Karten zu testen und das zu beheben, was ihr meldet. Es besteht kein Druck. Strata zu nutzen und zu melden, was dir auffällt, hilft genauso viel.
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
Du möchtest Strata sponsern? Schreib mir über [GitHub](https://github.com/Niko1221).
|
||||
@@ -1,233 +1,403 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[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"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <b>Español</b> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<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>
|
||||
<h3 align="center">Ejecuta un modelo de última generación en tu propio PC gaming</h3>
|
||||
|
||||
<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>
|
||||
<p align="center">Compatible con NVIDIA, AMD, Intel · Windows, Linux, Mac · libre y de código abierto</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>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.
|
||||
Strata ejecuta **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, un modelo de 125 mil millones
|
||||
de parámetros que normalmente necesita un servidor, en un PC normal. Conversa, escribe código, lee imágenes y funciona
|
||||
con tus aplicaciones y tus agentes de programación. Tus chats y tus archivos se quedan en tu PC.
|
||||
|
||||
## ¿Qué velocidad tiene?
|
||||
**¿Eres nuevo?** Mira [lo que necesitas](#lo-que-necesitas) y luego [instala la aplicación](#opción-a-la-aplicación-strata).
|
||||
|
||||
Lo medimos en dos PC gaming normales. Un token equivale más o menos a ¾ de una palabra.
|
||||
- **Funciona en un PC gaming.** Con 32 GB entre RAM y memoria gráfica ya puedes empezar.
|
||||
- **Funciona con tus herramientas.** Habla las API de OpenAI y de Anthropic, así que Claude Code, Codex CLI, OpenCode y otros agentes se conectan a él.
|
||||
- **Lee imágenes.** Viene activado por defecto.
|
||||
- **Gratis.** Licencia MIT, sin cuenta, sin nube.
|
||||
- **Privado.** Sin telemetría. Lo que Strata descarga, cuándo usa internet y cómo comprueba las descargas está en [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
- **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).
|
||||
Versión 0.2.3 · motor 0.1.48
|
||||
|
||||
<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>
|
||||
## Lo que necesitas
|
||||
|
||||
| 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 |
|
||||
**Necesitas al menos 32 GB entre RAM y memoria gráfica (VRAM).** En un Mac o un Ryzen AI Max, es la memoria unificada.
|
||||
Con más memoria puedes usar un modelo más grande o más rápido.
|
||||
|
||||
</td><td>
|
||||
- Un PC de 32 GB con una tarjeta de 12 a 16 GB ejecuta el Coder.
|
||||
- Con 64 GB de RAM funcionan todos los tamaños.
|
||||
|
||||
| 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
|
||||
|
||||
| | |
|
||||
| Tu ordenador | Detalles |
|
||||
| --- | --- |
|
||||
| **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. |
|
||||
| **NVIDIA** GeForce RTX serie 20, 30, 40, 50 (12 GB o más) | Instala la aplicación o la Terminal. |
|
||||
| **AMD** Radeon RX 6800 y posteriores, Radeon AI PRO R9700 (12 GB o más) | [Tarjetas AMD](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [motor Intel](docs/INTEL.md) |
|
||||
| **Mac con Apple Silicon** (M1 o posterior, macOS 15 o posterior) | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** y otros sistemas con memoria unificada | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Tarjetas y procesadores antiguos | [GPU antiguas](docs/OLDER_GPUS.md), [CPU antiguas](docs/INSTALL.md#older-cpus-experimental) |
|
||||
|
||||
El instalador prepara todo lo demás. Dos o tres tarjetas pueden repartirse el modelo ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
También hace falta:
|
||||
|
||||
Experimental, escrito y probado por miembros de la comunidad en sus propios equipos:
|
||||
- unos 80 GB de disco libre (con un SSD el primer arranque es mucho más rápido)
|
||||
- Windows 10 / 11, Linux o macOS
|
||||
- un controlador gráfico actual
|
||||
|
||||
- **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).
|
||||
Dos o tres tarjetas de la misma marca pueden compartir un modelo ([multi-GPU](docs/MULTI_GPU.md)). Strata nunca reparte un
|
||||
modelo entre tarjetas NVIDIA y AMD. Lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
|
||||
## Instalación
|
||||
|
||||
### Deja que tu IA lo instale
|
||||
Elige una opción. Todas arrancan el mismo motor y usan los mismos modelos.
|
||||
|
||||
¿Usas un asistente de programación con IA (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Pega esto en él:
|
||||
### Opción A: la aplicación Strata
|
||||
|
||||
La forma más fácil. La aplicación lleva el motor dentro: la instalas, eliges un modelo y chateas. Descarga el archivo para
|
||||
tu sistema y tu tarjeta gráfica desde la [página de versiones](https://github.com/Niko1221/Strata/releases/latest). ¿No
|
||||
sabes cuál elegir? Mira el párrafo que hay debajo de la tabla.
|
||||
|
||||
| Sistema | Tu tarjeta gráfica | Archivo que descargar |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, controlador 580 o posterior | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
|
||||
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
|
||||
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
|
||||
| | AMD Radeon serie RX 7000 o Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, controlador 580 o posterior | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon serie RX 7000 | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 o posterior) | integrada | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
|
||||
|
||||
**Si no estás seguro:** con NVIDIA, elige `cuda13` si la versión de tu controlador es la 580 o posterior, y `cuda12` en caso
|
||||
contrario. `cuda12` es para tarjetas y controladores más antiguos. Los archivos `hip` y `amd` son para tarjetas AMD. Las
|
||||
demás tarjetas AMD (RX 6000) todavía no tienen una aplicación ya preparada: mira
|
||||
[Tarjetas AMD](docs/AMD_HIP.md).
|
||||
|
||||
Después:
|
||||
|
||||
- **Windows:** ejecuta el `.exe`. Se instala para tu usuario y no necesita permisos de administrador. Si Windows muestra
|
||||
"Windows protegió tu PC", elige **Más información** y luego **Ejecutar de todas formas** (el instalador todavía no está
|
||||
firmado).
|
||||
- **Linux:** haz el archivo ejecutable (`chmod +x`) y lánzalo. Necesita FUSE 2: `sudo apt install libfuse2` (Ubuntu 22.04)
|
||||
o `libfuse2t64` (Ubuntu 24.04 y posteriores).
|
||||
- **Mac:** abre el `.dmg` y arrastra Strata a Aplicaciones.
|
||||
|
||||
En la primera pantalla, la aplicación analiza tu PC y recomienda un modelo adecuado. Pulsa Instalar. El modelo es una
|
||||
descarga grande (unos 40 a 70 GB) y, si se interrumpe, continúa donde se quedó. Al cerrar la ventana, Strata y el motor se
|
||||
detienen.
|
||||
|
||||
### Opción B: la Terminal (una línea)
|
||||
|
||||
La Terminal es el comando `strata`, pensado para scripts, servidores y agentes de IA.
|
||||
|
||||
Linux y Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
El script analiza tu PC, descarga la Terminal, verifica su SHA-256 y la descomprime en tu carpeta personal. No necesita
|
||||
permisos de administrador. En Windows, abre una ventana de terminal nueva después. Luego arranca Strata:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Te hace una pregunta cada vez: modelo, contexto, imágenes, quién puede conectarse. Después descarga el motor para tu tarjeta
|
||||
gráfica (firmado y arrancado como prueba antes de usarlo) y el modelo, y empieza a servir. Para scripts y agentes de IA, usa
|
||||
opciones:
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
Si quieres leer el script de instalación antes de ejecutarlo, descárgalo primero con `curl -fsSL <address> -o install.sh`,
|
||||
léelo y luego ejecuta `sh install.sh`. Todas las pantallas y opciones: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### Opción C: que lo instale tu IA
|
||||
|
||||
Pega esto en Claude Code, Cursor, Codex, GitHub Copilot u otro asistente de programación:
|
||||
|
||||
```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
|
||||
El asistente revisa tu tarjeta gráfica, tu RAM y tu disco, elige el modelo adecuado, lo instala y lo arranca, y te explica
|
||||
cómo conectar tus aplicaciones. Las herramientas de IA también pueden instalar, arrancar y detener Strata mediante su
|
||||
[servidor MCP](docs/MCP_SERVER.md).
|
||||
|
||||
### O hazlo tú mismo
|
||||
¿Prefieres compilarlo tú? Mira [Compilar desde el código fuente](#compilar-desde-el-código-fuente).
|
||||
|
||||
[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.
|
||||
## Modelos
|
||||
|
||||
**Mac (Apple Silicon, M1 a M5, macOS 15 o posterior):** instala una vez las Command Line Tools de Apple (`xcode-select --install` en Terminal; no hace falta Xcode). Luego abre Terminal en la carpeta de Strata y ejecuta **`./setup.sh`**. Compila el motor de Mac en tu Mac (unos 7 minutos), hace las mismas preguntas, descarga el modelo y lo arranca. La próxima vez, `./setup.sh` arranca enseguida. Qué tamaño cabe en tu Mac y qué velocidad tiene: [docs/MAC.md](docs/MAC.md).
|
||||
La aplicación y `strata launch` recomiendan un modelo según tu memoria. Es un solo modelo en varios tamaños, más o menos
|
||||
comprimido. Los tamaños pequeños son más rápidos. Los grandes son algo más inteligentes.
|
||||
|
||||
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:
|
||||
### ¿Poca memoria? Prueba Gyro
|
||||
|
||||
- qué modelo y qué tamaño,
|
||||
- cuánto contexto (cuánto texto tiene presente el modelo),
|
||||
- si debe entender imágenes.
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (de agentionai) es la forma más pequeña de ejecutar este modelo.
|
||||
|
||||
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`.
|
||||
| Tamaño | Memoria que necesita (RAM + VRAM) | Descarga | Qué es |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 GB | 58,5 GB | Para PC donde no cabe nada más. La calidad es menor que la de IQ2_XS. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Calidad parecida a IQ3_XXS con un 20 % menos de memoria de GPU. Escribe las respuestas un 16 % más despacio que Q2_0 en una RTX 3060 de 12 GB y un 21 % más despacio en una R9700. |
|
||||
|
||||
> **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.
|
||||
Gyro funciona en tarjetas NVIDIA y AMD (en AMD es experimental) en tarjetas Intel Arc con Linux (probado en una Arc A750) y, desde 0.2.2, en Mac con Apple Silicon (Gyro-S en un Mac mini de 32 GB: 9 tokens/s, con los expertos leídos del SSD). Detalles y mediciones:
|
||||
[docs/GYRO.md](docs/GYRO.md). Arráncalo con `strata launch --model gyro-s`.
|
||||
|
||||
**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).
|
||||
### Los demás tamaños
|
||||
|
||||
## ¿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é |
|
||||
| Tu memoria (RAM + VRAM) | Elige | Notas |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
| **32 GB** | **Gyro-S**, o el **[Coder](docs/MODELS.md#coder)** para código | El Coder necesita 32 GB de RAM y conserva la mitad de los expertos, así que es más flojo fuera del código |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), o **Gyro-M** (35 GB) | Swift piensa menos tiempo antes de responder. Gyro-M se acerca a la calidad de IQ3_XXS, pero escribe las respuestas un 16-21 % más despacio que Q2_0 |
|
||||
| **48 GB** | **IQ3_XXS** (47,0 GB) | Más lento, mejor |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB), el original o Swift 1.5 | El más lento, el mejor. Iguala al modelo completo en las pruebas publicadas |
|
||||
| **96 GB o más** | **UD-IQ4_XS** de Unsloth (unos 72 GB de RAM) hasta **Q8_0** (unos 136 GB), de 4 a 8 bits | Más cerca del modelo completo. Con menos RAM, Strata lee una parte desde el SSD, que es mucho más lento |
|
||||
|
||||
- **[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, UD-Q5_K_XL, UD-Q6_K_XL y Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** de 4 a 8 bits, más cerca del modelo completo
|
||||
(93-95 % el mismo token siguiente que Q8_0). Rápidos cuando todos los expertos caben en la RAM (~88 GB para
|
||||
UD-Q4_K_XL hasta ~136 GB para Q8_0); con menos RAM, Strata lee una parte desde el SSD mientras responde, lo que es
|
||||
mucho más lento (7-8.5 tokens/s para UD-Q4_K_XL 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.
|
||||
Con menos RAM de la que indica una fila también puede funcionar: Strata pasa solo a su
|
||||
[modo de poca RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Espera que sea más lento, y mucho más lento con una
|
||||
tarjeta gráfica pequeña.
|
||||
|
||||
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`).
|
||||
Más opciones:
|
||||
|
||||
## Cómo usarlo
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** es un ajuste fino que piensa menos tiempo antes de responder, con una calidad y un uso de RAM casi iguales.
|
||||
- **[NVFP4](docs/NVFP4.md)** es la versión de 4 bits de NVIDIA para tarjetas NVIDIA. Necesita unos 80 GB de RAM para tener todos los expertos en RAM.
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** es un modelo más pequeño y distinto que lee imágenes y puede atender varias peticiones a la vez.
|
||||
- **[El Uncensored IQ3_XXS de OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** se configura a mano.
|
||||
|
||||
<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>
|
||||
Todos los tamaños, descargas y lo que cabe en cada equipo: [docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
- **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.
|
||||
## ¿Qué velocidad tiene?
|
||||
|
||||
Más: [dónde se guardan tus chats](docs/INSTALL.md#where-things-are-stored), [la API](docs/DETAILS.md#using-it).
|
||||
Medido con una tarjeta gráfica de 12 GB (RTX 5070, Ryzen 5 7600, 64 GB de RAM). Resultados con otras tarjetas:
|
||||
[velocidad de cada tamaño](docs/MODELS.md#how-fast-is-each-size), [resultados de la comunidad](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
### Opcional: más velocidad con una sola tarjeta cuando todo el modelo cabe en la RAM
|
||||
| Modelo | Las respuestas aparecen a | Lee tu prompt a |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
|
||||
Dos ajustes opcionales (de RubenTorney, #1904 y #1905). Ayudan si usas una tarjeta gráfica y tu RAM guarda todos los expertos (el modo arena de Strata, por ejemplo Q2_0 o IQ3_XXS en un PC con 64 GB). Abre tu configuración `strata-<model>.json` en la carpeta de datos de Strata ([dónde](docs/INSTALL.md#where-things-are-stored)) y añade:
|
||||
Un token equivale a unos ¾ de palabra, así que 60 tokens por segundo es más rápido de lo que puedes leer. "Lee tu prompt"
|
||||
es lo rápido que Strata procesa un documento largo, código o un historial de chat.
|
||||
|
||||
- **Generación más rápida:** `"--adapt-async", "1"` en la lista `"args"`. La caché de expertos se actualiza entonces en un hilo auxiliar en vez de pausar la respuesta. Medido en una RTX 5070 (Windows, Q2_0): +4-5 % de generación, +7-10 % en código y documentos largos. Las respuestas pueden diferir un poco de las por defecto (son iguales de una ejecución a otra).
|
||||
- **Lectura del prompt más rápida en Windows:** `"STRATA_PIN_BY_NEED": "1"` en el bloque `"env"`. Windows solo puede bloquear cerca de la mitad de la RAM para la tarjeta; esto bloquea primero los expertos que la tarjeta necesita de verdad. Medido en la RTX 5070: +4,4 % en la lectura del prompt.
|
||||
Medido con una tarjeta AMD de 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB de RAM):
|
||||
|
||||
| Modelo | Las respuestas aparecen a | Lee tu prompt a |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
|
||||
Las barras usan la misma escala en las dos tablas. 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. La versión del motor detrás de cada cifra está en [DETAILS.md](docs/DETAILS.md#speed-measured).
|
||||
|
||||
## Uso
|
||||
|
||||
### En la aplicación
|
||||
|
||||
La aplicación tiene un **Chat**, un **Monitor** del modelo y de tu GPU, CPU y RAM, una página de **Modelos** y los
|
||||
**Ajustes**. El chat puede trabajar en una carpeta de tu PC. En modo **Manual** pregunta antes de cambiar un archivo o
|
||||
ejecutar un comando. Escribe `/loop 10m <what to do>` para repetir un mensaje cada 10 minutos mientras el chat esté abierto.
|
||||
|
||||
El chat puede buscar en la web con DuckDuckGo (o Bing), sin cuenta. Puedes añadir una clave de búsqueda en **Ajustes,
|
||||
Herramientas**. La búsqueda es la única función que envía algo fuera de tu PC (las palabras que buscas).
|
||||
|
||||
Para ejecutar dos modelos, activa **Ejecutar varios modelos** y dale a cada modelo su propia tarjeta de la misma marca.
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>Un chat en la app de Strata: el agente escribió un pequeño jardín de pagoda de vóxeles en un solo archivo HTML, y la página se abre en la vista previa junto a la conversación</sub></p>
|
||||
|
||||
### En la Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<sub><code>strata monitor</code> mientras el modelo responde: velocidad, GPU, memoria y las últimas solicitudes</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided screens: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
|
||||
strata launch --plan --model q2_0 # show what would happen; nothing changes
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
|
||||
strata update --check # is there a new engine? (strata update installs it)
|
||||
strata doctor # check the installed engine files
|
||||
```
|
||||
|
||||
`strata --help` lo lista todo. Arrancar y servir no descargan nada. Solo lo hacen la instalación, `strata launch` (cuando
|
||||
eliges un modelo) y `strata update`. Para llegar a Strata desde tu móvil u otro PC, usa `strata launch --access lan`. Crea
|
||||
una clave de API y no arranca en una red sin ella. No se abre ningún navegador salvo que añadas `--open`. Guía:
|
||||
[docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). El cliente completo: [docs/TERMINAL.md](docs/TERMINAL.md).
|
||||
|
||||
### Con agentes de IA
|
||||
|
||||
Un agente instala Strata con el comando de una línea de la opción B y ejecuta
|
||||
`strata launch --model NAME --yes --no-tui`. Los pasos, las opciones y los códigos de salida están en [docs/AI_SETUP.md](docs/AI_SETUP.md),
|
||||
y las reglas del repositorio en [AGENTS.md](AGENTS.md).
|
||||
|
||||
### Conecta tus otras aplicaciones
|
||||
|
||||
| Tipo de aplicación | URL base |
|
||||
| --- | --- |
|
||||
| Compatible con OpenAI (la mayoría de aplicaciones y agentes) | `http://127.0.0.1:8080/v1` (cualquier clave de API, cualquier nombre de modelo) |
|
||||
| API de Anthropic (Claude Code y otros) | `http://127.0.0.1:8080`, sin `/v1`. Estas aplicaciones añaden `/v1/messages` por su cuenta. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| API OpenAI Responses (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuración](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` muestra los ajustes exactos para OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent y OpenClaw.
|
||||
`strata connect opencode --write` escribe el archivo después de preguntar y guarda una copia de seguridad.
|
||||
|
||||
### Imágenes
|
||||
|
||||
Las imágenes vienen activadas por defecto en los modelos que pueden leerlas. Adjunta o pega una imagen en el chat, o envíala
|
||||
desde tu aplicación. El lector de imágenes funciona en el procesador cuando tu tarjeta gráfica tiene menos de 24 GB (unos
|
||||
segundos por imagen, sin quitar memoria a la tarjeta) y en la tarjeta gráfica a partir de 24 GB. La regla es la misma para
|
||||
NVIDIA y AMD. Puedes desactivar las imágenes por modelo, o elegir el lector con `--vision off`, `--vision gpu` o
|
||||
`--vision cpu`.
|
||||
|
||||
Las cifras detrás de la regla: en una Radeon AI PRO R9700, el lector de la tarjeta tardó 57 ms en una imagen de 260 tokens y
|
||||
el del procesador 3,1 s. En tarjetas de 16 GB (Tesla P100 y RTX A4000, Linux), el lector de la tarjeta costó un 3-5 % de
|
||||
velocidad en texto, porque ocupa unos 1,4 GB que de otro modo usaría el modelo. En un Mac lee la GPU, tenga la memoria que tenga (0,45 s para una imagen de 260 tokens en un M4 de 32 GB, unos 1,2 GB de la memoria compartida);
|
||||
el procesador tarda unos 50 s por imagen, así que solo lee con `--vision cpu`.
|
||||
|
||||
### Conviene saber
|
||||
|
||||
- Elige el nivel de razonamiento (**desactivado, bajo, medio o alto**) en el menú del chat o en "reasoning effort" de tu aplicación. Desactivado es lo más rápido.
|
||||
- Strata responde a una petición a la vez por defecto. Para varias a la vez, usa `--parallel N` en `strata launch` o
|
||||
`"parallel": 2` en la configuración ([BATCHING.md](docs/BATCHING.md)). En una tarjeta de 12 GB, esto hace más lenta cada respuesta.
|
||||
- Strata lee entero el primer mensaje de un chat, unos 1 minuto por cada 30.000 tokens. Los mensajes siguientes empiezan en segundos.
|
||||
- Dónde se guardan tus chats y la API completa: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
|
||||
|
||||
## Compilar desde el código fuente
|
||||
|
||||
<details>
|
||||
<summary>Windows, Linux y Mac desde el repositorio</summary>
|
||||
|
||||
[Descarga Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) y descomprímelo, o haz `git clone`.
|
||||
**Windows:** haz doble clic en `START-HERE.bat`. **Linux:** ejecuta `./setup.sh` en la carpeta de Strata.
|
||||
|
||||
**Mac (Apple Silicon, macOS 15 o posterior):** instala una vez las Command Line Tools de Apple con `xcode-select --install`
|
||||
(no hace falta Xcode). Abre la Terminal en la carpeta de Strata y ejecuta `./setup.sh`. El script compila el motor para Mac
|
||||
en tu Mac (unos 7 minutos), hace las mismas preguntas, descarga el modelo y lo arranca. La próxima vez, `./setup.sh` lo
|
||||
arranca enseguida. Qué tamaño cabe en tu Mac: [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
El instalador detecta tu tarjeta y prepara el motor adecuado. Pregunta qué modelo y qué tamaño quieres, cuánto contexto
|
||||
(cuánto texto mantiene el modelo en mente) y si debe leer imágenes. Pulsa Enter para aceptar la respuesta recomendada.
|
||||
Luego descarga el modelo (unos 70 GB) y lo arranca. Si la descarga se detiene, vuelve a ejecutar el script y continúa. Tu
|
||||
navegador abre la página 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 tiempo la primera vez).
|
||||
> Strata carga 35-55 GB en tu RAM y bloquea una parte para la tarjeta gráfica. Espera y no cierres la ventana.
|
||||
|
||||
La próxima vez, ejecuta de nuevo `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. Actualización, Docker,
|
||||
varias tarjetas, dónde van los archivos y todas las opciones: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
Para medir tu PC, ejecuta `BENCH.bat` (`./bench.sh`). Tarda 5-10 minutos y escribe un informe que puedes compartir
|
||||
([cómo](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
**Opcional: más velocidad en una tarjeta cuando todo el modelo cabe en la RAM.** Dos ajustes que se activan a mano, de
|
||||
RubenTorney (#1904, #1905). Ayudan cuando usas una sola tarjeta gráfica y tu RAM guarda todos los expertos, por ejemplo con
|
||||
Q2_0 o IQ3_XXS en un PC de 64 GB. Abre `strata-<model>.json` en la carpeta de datos de Strata ([dónde](docs/INSTALL.md#where-things-are-stored)) y añade:
|
||||
|
||||
- `"--adapt-async", "1"` a la lista `"args"`. Así la caché de expertos se actualiza en un hilo auxiliar en vez de detener la
|
||||
respuesta. Medido en una RTX 5070 (Windows, Q2_0): +4-5 % en decodificación, +7-10 % en código y documentos largos. Las
|
||||
respuestas pueden diferir un poco de las del ajuste por defecto y son iguales de una ejecución a otra.
|
||||
- `"STRATA_PIN_BY_NEED": "1"` en el bloque `"env"` (Windows). Windows solo puede bloquear aproximadamente la mitad de la RAM
|
||||
para la tarjeta gráfica, y este ajuste bloquea primero los expertos que la tarjeta necesita antes. Medido en la RTX 5070: +4,4 % en lectura del prompt.
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
Reinicia Strata (`START-HERE.bat` o `./setup.sh`) después del cambio. Para deshacerlo, quita las dos entradas.
|
||||
Reinicia Strata después del cambio. Para deshacerlo, quita las dos entradas.
|
||||
|
||||
## ¿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).
|
||||
</details>
|
||||
|
||||
## ¿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.
|
||||
Los modelos como este suelen ejecutarse 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. Imagina una biblioteca.
|
||||
|
||||
<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"), como 24.576 libros de consulta. Cada palabra necesita solo 10 de ellos.
|
||||
- **Tu tarjeta gráfica es el escritorio.** Es pequeño y rápido. Los unos 4.000 libros que más se usan están abiertos encima.
|
||||
- **Tu RAM son las estanterías de la sala de lectura.** Los 24.576 libros están ahí. Cuando una palabra necesita un libro que
|
||||
no está en el escritorio, tu procesador lo coge enseguida de la estantería mientras la tarjeta sigue trabajando.
|
||||
- **Tu SSD es el archivo.** Guarda una tabla de consulta de 29 GB. El modelo lee unas pocas filas por palabra, así que no
|
||||
hace falta que esté en la RAM.
|
||||
|
||||
- **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/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>
|
||||
|
||||
<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>
|
||||
Un pequeño ayudante también adivina las siguientes palabras. El modelo grande las comprueba todas a la vez y se queda con
|
||||
las correctas. Obtienes la misma respuesta, entre 1,6 y 1,8 veces antes. Los textos largos se leen en trozos grandes, de
|
||||
hasta 8.192 tokens a la vez, a más de 1.000 tokens por segundo.
|
||||
|
||||
- **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.
|
||||
<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>
|
||||
|
||||
La explicación larga: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte y sus números:
|
||||
La explicación más larga: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte con sus cifras:
|
||||
[los detalles](docs/DETAILS.md#how-it-works) y el [artículo](docs/paper/Strata-Paper.pdf).
|
||||
|
||||
## ¿Algo ha ido mal?
|
||||
|
||||
- **Mi PC se congeló la primera vez que arrancó Strata.** Carga el modelo en la RAM. Espera y no cierres la ventana. ¿Sigue
|
||||
congelado después de 10 minutos? Reinicia el PC, cierra otros programas e inténtalo de nuevo, o elige un tamaño más pequeño.
|
||||
- **Se detuvo durante la descarga o la instalación.** Abre de nuevo la aplicación o `strata launch`, o vuelve a ejecutar
|
||||
`START-HERE.bat` (o `./setup.sh`). Continúa donde se quedó.
|
||||
- **Va muy lento, la luz del disco parpadea sin parar, o dice "the engine stopped unexpectedly".** Tu PC no tiene bastante
|
||||
RAM libre. Cierra otros programas (los navegadores gastan mucha) o elige un tamaño más pequeño, como Q2_0 o IQ2_XS.
|
||||
- **Dice que no hay motor para este PC.** En la aplicación, abre **Ajustes, Motor** y pulsa **Actualizar motor**. En la
|
||||
Terminal, ejecuta `strata update`. `strata doctor` muestra qué archivos del motor están instalados.
|
||||
- **Dice que el puerto 8080 ya está en uso.** Strata ya se está ejecutando. Busca su ventana.
|
||||
|
||||
Más problemas y soluciones: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). ¿Sigues atascado? Abre una
|
||||
[incidencia](https://github.com/Niko1221/Strata/issues). En la aplicación, adjunta el archivo de **Ajustes, Motor,
|
||||
Diagnóstico, Ver registros, Guardar registros**. En la Terminal, pega la salida de `strata doctor`. Si lo ejecutas desde el
|
||||
código fuente, adjunta `strata-<model>.log` de la carpeta de Strata. Para un problema de seguridad, avísanos en privado:
|
||||
[SECURITY.md](SECURITY.md).
|
||||
|
||||
## 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)
|
||||
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), agentionai (Gyro)
|
||||
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)).
|
||||
[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 todos los modelos tienen sus propias licencias ([cuáles](docs/HOW_IT_WORKS.md#license)).
|
||||
|
||||
## Apoya a Strata
|
||||
## Contribuir
|
||||
|
||||
Strata es libre y de código abierto. Si te resulta útil, puedes apoyar su desarrollo:
|
||||
¿Has encontrado un error, o quieres enviar un cambio o un benchmark? Lee primero [CONTRIBUTING.md](CONTRIBUTING.md). Explica
|
||||
qué incluir para que otros puedan comprobar tu trabajo.
|
||||
|
||||
<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>
|
||||
## Apoya Strata
|
||||
|
||||
Strata es gratis y tiene licencia MIT. Lo construyo y lo pruebo en el hardware que consigo. Si te funciona bien en tu PC, un café me ayuda
|
||||
a seguir probando tarjetas nuevas y a arreglar lo que nos cuentas. Sin presión. Usarlo y contar lo que encuentras ayuda igual de mucho.
|
||||
|
||||
<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>
|
||||
|
||||
¿Te interesa patrocinar Strata? Escríbeme a través de [GitHub](https://github.com/Niko1221).
|
||||
@@ -1,238 +1,405 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[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"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <b>Français</b> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<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>
|
||||
<h3 align="center">Faites tourner un modèle de pointe sur votre propre PC de jeu</h3>
|
||||
|
||||
<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>
|
||||
<p align="center">Compatible avec NVIDIA, AMD, Intel · Windows, Linux, Mac · gratuit et open source</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>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.
|
||||
Strata fait tourner **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, un modèle de 125 milliards
|
||||
de paramètres qui demande normalement un serveur, sur un PC ordinaire. Il discute, écrit du code, lit des images et
|
||||
fonctionne avec vos applications et vos agents de code. Vos conversations et vos fichiers restent sur votre PC.
|
||||
|
||||
## Quelle est sa vitesse ?
|
||||
**Nouveau ici ?** Regardez [ce qu'il vous faut](#ce-quil-vous-faut), puis [installez l'application](#option-a--lapplication-strata).
|
||||
|
||||
Nous l'avons mesurée sur deux PC de jeu ordinaires. Un token correspond à environ ¾ d'un mot.
|
||||
- **Tourne sur un PC de jeu.** 32 Go de RAM et de mémoire graphique réunis suffisent pour commencer.
|
||||
- **Fonctionne avec vos outils.** Il parle les API d'OpenAI et d'Anthropic, donc Claude Code, Codex CLI, OpenCode et d'autres agents peuvent s'y connecter.
|
||||
- **Lit les images.** C'est activé par défaut.
|
||||
- **Gratuit.** Licence MIT, pas de compte, pas de cloud.
|
||||
- **Privé.** Aucune télémétrie. Ce que Strata télécharge, quand il utilise internet et comment il vérifie les téléchargements est expliqué dans [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
- **É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>
|
||||
Version 0.2.3 · moteur 0.1.48
|
||||
|
||||
## Ce qu'il vous faut
|
||||
|
||||
| | |
|
||||
**Il vous faut au moins 32 Go de RAM et de mémoire graphique (VRAM) réunis.** Sur un Mac ou un Ryzen AI Max, c'est la
|
||||
mémoire unifiée. Plus de mémoire permet un modèle plus gros ou plus rapide.
|
||||
|
||||
- Un PC de 32 Go avec une carte de 12 à 16 Go fait tourner le Coder.
|
||||
- Avec 64 Go de RAM, toutes les tailles fonctionnent.
|
||||
|
||||
| Votre ordinateur | Détails |
|
||||
| --- | --- |
|
||||
| **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. |
|
||||
| **NVIDIA** GeForce RTX série 20, 30, 40, 50 (12 Go ou plus) | Installez l'application ou le Terminal. |
|
||||
| **AMD** Radeon RX 6800 et plus récent, Radeon AI PRO R9700 (12 Go ou plus) | [Cartes AMD](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [moteur Intel](docs/INTEL.md) |
|
||||
| **Mac Apple Silicon** (M1 ou plus récent, macOS 15 ou plus récent) | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** et autres systèmes à mémoire unifiée | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Cartes et processeurs plus anciens | [GPU anciens](docs/OLDER_GPUS.md), [CPU anciens](docs/INSTALL.md#older-cpus-experimental) |
|
||||
|
||||
L'installateur s'occupe de tout le reste. Deux ou trois cartes peuvent se partager le modèle ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
Il faut aussi :
|
||||
|
||||
Expérimental, écrit et testé par des membres de la communauté sur leurs propres machines :
|
||||
- environ 80 Go d'espace disque libre (un SSD accélère beaucoup le premier démarrage)
|
||||
- Windows 10 / 11, Linux ou macOS
|
||||
- un pilote graphique récent
|
||||
|
||||
- **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).
|
||||
Deux ou trois cartes de la même marque peuvent se partager un modèle ([multi-GPU](docs/MULTI_GPU.md)). Strata ne répartit
|
||||
jamais un modèle entre des cartes NVIDIA et AMD. Liste complète : [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
|
||||
## Installation
|
||||
|
||||
### Laissez votre IA l'installer
|
||||
Choisissez une option. Elles lancent toutes le même moteur et utilisent les mêmes modèles.
|
||||
|
||||
Vous utilisez un assistant de code IA (Claude Code, Cursor, Codex, GitHub Copilot, ...) ? Collez-lui ceci :
|
||||
### Option A : l'application Strata
|
||||
|
||||
Le plus simple. L'application contient le moteur : installez-la, choisissez un modèle, discutez. Téléchargez le fichier
|
||||
correspondant à votre système et à votre carte graphique depuis la [page des versions](https://github.com/Niko1221/Strata/releases/latest).
|
||||
Vous ne savez pas lequel prendre ? Lisez le paragraphe sous le tableau.
|
||||
|
||||
| Système | Votre carte graphique | Fichier à télécharger |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
|
||||
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
|
||||
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon série RX 7000 | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 ou plus récent) | intégrée | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
|
||||
|
||||
**Si vous hésitez :** pour NVIDIA, prenez `cuda13` si la version de votre pilote est 580 ou plus récente, sinon `cuda12`.
|
||||
`cuda12` convient aux cartes plus anciennes et aux pilotes plus anciens. Les fichiers `hip` et `amd` sont pour les cartes
|
||||
AMD. Les autres cartes AMD (RX 6000) n'ont pas encore d'application prête à l'emploi : voir
|
||||
[Cartes AMD](docs/AMD_HIP.md).
|
||||
|
||||
Ensuite :
|
||||
|
||||
- **Windows :** lancez le `.exe`. Il s'installe pour votre utilisateur et ne demande pas de droits d'administrateur. Si
|
||||
Windows affiche « Windows a protégé votre ordinateur », choisissez **Informations complémentaires**, puis **Exécuter
|
||||
quand même** (l'installateur n'est pas encore signé).
|
||||
- **Linux :** rendez le fichier exécutable (`chmod +x`) et lancez-le. Il a besoin de FUSE 2 : `sudo apt install libfuse2`
|
||||
(Ubuntu 22.04) ou `libfuse2t64` (Ubuntu 24.04 et plus récent).
|
||||
- **Mac :** ouvrez le `.dmg` et faites glisser Strata dans Applications.
|
||||
|
||||
Sur le premier écran, l'application analyse votre PC et recommande un modèle adapté. Appuyez sur Installer. Le modèle est un
|
||||
gros téléchargement (environ 40 à 70 Go) qui reprend là où il s'est arrêté en cas d'interruption. Quand vous fermez la
|
||||
fenêtre, Strata et le moteur s'arrêtent.
|
||||
|
||||
### Option B : le Terminal (une ligne)
|
||||
|
||||
Le Terminal est la commande `strata`, pensée pour les scripts, les serveurs et les agents d'IA.
|
||||
|
||||
Linux et Mac :
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
```
|
||||
|
||||
Windows (PowerShell) :
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
Le script analyse votre PC, télécharge le Terminal, vérifie son empreinte SHA-256 et le décompresse dans votre dossier
|
||||
personnel. Il ne demande pas de droits d'administrateur. Sous Windows, ouvrez ensuite une nouvelle fenêtre de terminal.
|
||||
Puis lancez Strata :
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Il pose une question à la fois : modèle, contexte, images, qui peut se connecter. Ensuite il télécharge le moteur pour votre
|
||||
carte graphique (signé, et lancé à titre d'essai avant usage) ainsi que le modèle, puis commence à servir. Pour les scripts
|
||||
et les agents d'IA, utilisez des options :
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
Pour lire le script d'installation avant de l'exécuter, téléchargez-le d'abord avec `curl -fsSL <address> -o install.sh`,
|
||||
lisez-le, puis lancez `sh install.sh`. Tous les écrans et toutes les options : [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### Option C : laissez votre IA l'installer
|
||||
|
||||
Collez ceci dans Claude Code, Cursor, Codex, GitHub Copilot ou un autre assistant de programmation :
|
||||
|
||||
```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).
|
||||
L'assistant vérifie votre carte graphique, votre RAM et votre disque, choisit le modèle adapté, 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 via son
|
||||
[serveur MCP](docs/MCP_SERVER.md).
|
||||
|
||||
### Ou faites-le vous-même
|
||||
Vous préférez le compiler vous-même ? Voir [Compiler depuis les sources](#compiler-depuis-les-sources).
|
||||
|
||||
[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.
|
||||
## Modèles
|
||||
|
||||
**Mac (Apple Silicon, M1 à M5, macOS 15 ou plus récent) :** installez une fois les Command Line Tools d'Apple (`xcode-select --install` dans le Terminal ; Xcode lui-même n'est pas nécessaire). Ouvrez ensuite le Terminal dans le dossier Strata et lancez **`./setup.sh`**. Il compile le moteur Mac sur votre Mac (environ 7 minutes), pose les mêmes questions, télécharge le modèle et le démarre. La fois suivante, `./setup.sh` démarre tout de suite. Quelle taille convient à votre Mac et sa vitesse : [docs/MAC.md](docs/MAC.md).
|
||||
L'application et `strata launch` recommandent un modèle selon votre mémoire. C'est un seul modèle en plusieurs tailles,
|
||||
plus ou moins compressé. Les petites tailles sont plus rapides. Les grandes sont un peu plus intelligentes.
|
||||
|
||||
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 :
|
||||
### Peu de mémoire ? Essayez Gyro
|
||||
|
||||
- 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.
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (par agentionai) est la façon la plus légère de faire tourner ce modèle.
|
||||
|
||||
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`.
|
||||
| Taille | Mémoire nécessaire (RAM + VRAM) | Téléchargement | Ce que c'est |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 Go | 58,5 Go | Pour les PC où rien d'autre ne tient. La qualité est inférieure à celle d'IQ2_XS. |
|
||||
| **Gyro-M** | 35 Go | 92,0 Go | À peu près la qualité d'IQ3_XXS avec environ 20 % de mémoire GPU en moins. Il écrit les réponses 16 % plus lentement que Q2_0 sur une RTX 3060 de 12 Go et 21 % plus lentement sur une R9700. |
|
||||
|
||||
> **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.
|
||||
Gyro fonctionne sur les cartes NVIDIA et AMD (expérimental sur AMD) sur les cartes Intel Arc sous Linux (testé sur une Arc A750) et, depuis la 0.2.2, sur les Mac Apple Silicon (Gyro-S sur un Mac mini de 32 Go : 9 tokens/s, experts lus sur le SSD). Détails et mesures :
|
||||
[docs/GYRO.md](docs/GYRO.md). Lancez-le avec `strata launch --model gyro-s`.
|
||||
|
||||
**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).
|
||||
### Les autres tailles
|
||||
|
||||
## 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 |
|
||||
| Votre mémoire (RAM + VRAM) | Prenez | Remarques |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
| **32 Go** | **Gyro-S**, ou le **[Coder](docs/MODELS.md#coder)** pour le code | Le Coder demande 32 Go de RAM et garde la moitié des experts, il est donc moins bon hors du code |
|
||||
| **40 Go** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 Go), ou **Gyro-M** (35 Go) | Swift réfléchit moins longtemps avant de répondre. Gyro-M approche la qualité d'IQ3_XXS, mais il écrit les réponses 16 à 21 % plus lentement que Q2_0 |
|
||||
| **48 Go** | **IQ3_XXS** (47,0 Go) | Plus lent, meilleur |
|
||||
| **64 Go** | **IQ3_S** (54,8 Go), l'original ou Swift 1.5 | Le plus lent, le meilleur. Il égale le modèle complet dans les tests publiés |
|
||||
| **96 Go ou plus** | **UD-IQ4_XS** d'Unsloth (environ 72 Go de RAM) jusqu'à **Q8_0** (environ 136 Go), 4 à 8 bits | Plus proche du modèle complet. Avec moins de RAM, Strata lit une partie depuis le SSD, ce qui est beaucoup plus lent |
|
||||
|
||||
- **[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, UD-Q5_K_XL, UD-Q6_K_XL et Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0) :** de 4 à 8 bits, plus proches du modèle
|
||||
complet (93-95 % le même token suivant que Q8_0). Rapides quand tous les experts tiennent dans la RAM (~88 Go pour
|
||||
UD-Q4_K_XL jusqu'à ~136 Go pour Q8_0) ; avec moins de RAM, Strata en lit une partie depuis le SSD pendant qu'il
|
||||
répond, ce qui est bien plus lent (7-8.5 tokens/s pour UD-Q4_K_XL 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.
|
||||
Avoir moins de RAM que ce qu'indique une ligne peut encore fonctionner : Strata passe tout seul en
|
||||
[mode peu de RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Attendez-vous à plus de lenteur, et à bien plus avec
|
||||
une petite carte graphique.
|
||||
|
||||
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`).
|
||||
Autres choix :
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** est une version affinée qui réfléchit moins longtemps avant de répondre, pour une qualité et un besoin en RAM à peu près identiques.
|
||||
- **[NVFP4](docs/NVFP4.md)** est la version 4 bits de NVIDIA pour les cartes NVIDIA. Elle demande environ 80 Go de RAM pour garder tous les experts en RAM.
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** est un modèle plus petit et différent, qui lit les images et peut répondre à plusieurs requêtes à la fois.
|
||||
- **[L'Uncensored IQ3_XXS d'OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** se configure à la main.
|
||||
|
||||
Toutes les tailles, les téléchargements et ce qui tient où : [docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
## Quelle est sa vitesse ?
|
||||
|
||||
Mesuré avec une carte graphique de 12 Go (RTX 5070, Ryzen 5 7600, 64 Go de RAM). Voir les résultats d'autres cartes :
|
||||
[vitesse de chaque taille](docs/MODELS.md#how-fast-is-each-size), [résultats de la communauté](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Modèle | Les réponses s'affichent à | 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 |
|
||||
|
||||
Un token vaut environ ¾ de mot, donc 60 tokens par seconde, c'est plus rapide que votre lecture. « Lit votre prompt »
|
||||
indique la vitesse à laquelle Strata absorbe un long document, du code ou un historique de discussion.
|
||||
|
||||
Mesuré avec une carte AMD de 16 Go (RX 9070 XT, Ryzen 9 3900X, 47 Go de RAM) :
|
||||
|
||||
| Modèle | Les réponses s'affichent à | 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 |
|
||||
|
||||
Les barres utilisent la même échelle dans les deux tableaux. Une carte avec plus de VRAM est plus rapide. Une RTX 3090
|
||||
(24 Go) devrait écrire environ 100 à 140 tokens par seconde. La version du moteur derrière chaque chiffre est indiquée dans
|
||||
[DETAILS.md](docs/DETAILS.md#speed-measured).
|
||||
|
||||
## 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 l'application
|
||||
|
||||
- **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.
|
||||
L'application comprend un **Chat**, un **Monitor** du modèle, de votre GPU, de votre CPU et de votre RAM, une page **Models** et les **Settings**. Le chat peut travailler dans un dossier de votre PC. En mode **Manual**, il demande
|
||||
avant de modifier un fichier ou d'exécuter une commande. Tapez `/loop 10m <what to do>` pour répéter un message toutes les
|
||||
10 minutes tant que le chat est ouvert.
|
||||
|
||||
Plus d'infos : [où sont stockées vos discussions](docs/INSTALL.md#where-things-are-stored), [l'API](docs/DETAILS.md#using-it).
|
||||
Le chat peut chercher sur le web avec DuckDuckGo (ou Bing), sans compte. Vous pouvez ajouter une clé de recherche sous
|
||||
**Settings, Tools**. La recherche est la seule fonction qui envoie quelque chose hors de votre PC (les mots recherchés).
|
||||
|
||||
### Optionnel : plus de vitesse sur une seule carte quand tout le modèle tient en RAM
|
||||
Pour faire tourner deux modèles, activez **Run multiple models** (exécuter plusieurs modèles) et donnez à chacun sa propre carte de la même marque.
|
||||
|
||||
Deux réglages optionnels (de RubenTorney, #1904 et #1905). Ils aident si vous utilisez une seule carte graphique et que votre RAM contient tous les experts (le mode arena de Strata, par exemple Q2_0 ou IQ3_XXS sur un PC de 64 Go). Ouvrez votre configuration `strata-<model>.json` dans le dossier de données de Strata ([où](docs/INSTALL.md#where-things-are-stored)) et ajoutez :
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>Une discussion dans l'app Strata : l'agent a écrit un petit jardin de pagode en voxels dans un seul fichier HTML, et la page s'ouvre dans l'aperçu à côté de la conversation</sub></p>
|
||||
|
||||
- **Génération plus rapide :** `"--adapt-async", "1"` dans la liste `"args"`. Le cache d'experts est alors mis à jour dans un thread auxiliaire au lieu de mettre la réponse en pause. Mesuré sur une RTX 5070 (Windows, Q2_0) : +4-5 % de génération, +7-10 % sur le code et les longs documents. Les réponses peuvent légèrement différer de celles par défaut (elles restent identiques d'une exécution à l'autre).
|
||||
- **Lecture du prompt plus rapide sous Windows :** `"STRATA_PIN_BY_NEED": "1"` dans le bloc `"env"`. Windows ne peut verrouiller qu'environ la moitié de la RAM pour la carte ; ce réglage verrouille d'abord les experts dont la carte a vraiment besoin. Mesuré sur la RTX 5070 : +4,4 % en lecture du prompt.
|
||||
### Dans le Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<sub><code>strata monitor</code> pendant que le modèle répond : vitesse, GPU, mémoire et les dernières requêtes</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided screens: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
|
||||
strata launch --plan --model q2_0 # show what would happen; nothing changes
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
|
||||
strata update --check # is there a new engine? (strata update installs it)
|
||||
strata doctor # check the installed engine files
|
||||
```
|
||||
|
||||
`strata --help` liste tout. Démarrer et servir ne téléchargent rien. Seuls l'installation, `strata launch` (quand vous
|
||||
choisissez un modèle) et `strata update` le font. Pour joindre Strata depuis votre téléphone ou un autre PC, utilisez
|
||||
`strata launch --access lan`. La commande crée une clé d'API et refuse de démarrer sur un réseau sans elle. Aucun
|
||||
navigateur ne s'ouvre, sauf si vous ajoutez `--open`. Guide : [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Le client complet : [docs/TERMINAL.md](docs/TERMINAL.md).
|
||||
|
||||
### Avec des agents d'IA
|
||||
|
||||
Un agent installe Strata avec la commande en une ligne de l'option B et lance
|
||||
`strata launch --model NAME --yes --no-tui`. Les étapes, les options et les codes de sortie sont dans [docs/AI_SETUP.md](docs/AI_SETUP.md),
|
||||
et les règles du dépôt dans [AGENTS.md](AGENTS.md).
|
||||
|
||||
### Connecter vos autres applications
|
||||
|
||||
| Type d'application | URL de base |
|
||||
| --- | --- |
|
||||
| Compatible OpenAI (la plupart des applications et des agents) | `http://127.0.0.1:8080/v1` (n'importe quelle clé d'API, n'importe quel nom de modèle) |
|
||||
| API Anthropic (Claude Code et d'autres) | `http://127.0.0.1:8080`, sans `/v1`. Ces applications ajoutent `/v1/messages` elles-mêmes. Claude Code : `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| API OpenAI Responses (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuration](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` affiche les réglages exacts pour OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent et OpenClaw.
|
||||
`strata connect opencode --write` écrit le fichier après confirmation et garde une sauvegarde.
|
||||
|
||||
### Images
|
||||
|
||||
Les images sont activées par défaut pour les modèles qui savent les lire. Joignez ou collez une image dans le chat, ou
|
||||
envoyez-la depuis votre application. Le lecteur d'images tourne sur le processeur quand votre carte graphique a moins de
|
||||
24 Go (quelques secondes par image, sans prendre de mémoire à la carte) et sur la carte graphique à partir de 24 Go. La
|
||||
règle est la même pour NVIDIA et AMD. Vous pouvez désactiver les images par modèle, ou choisir le lecteur avec
|
||||
`--vision off`, `--vision gpu` ou `--vision cpu`.
|
||||
|
||||
Les chiffres derrière la règle : sur une Radeon AI PRO R9700, le lecteur de la carte a pris 57 ms pour une image de
|
||||
260 tokens et celui du processeur 3,1 s. Sur des cartes de 16 Go (Tesla P100 et RTX A4000, Linux), le lecteur de la carte
|
||||
a coûté 3 à 5 % de vitesse sur le texte, parce qu'il occupe environ 1,4 Go que le modèle utiliserait sinon. Sur un Mac, le lecteur tourne sur le GPU quelle que soit la mémoire (0,45 s pour une image de 260 tokens sur un M4 de 32 Go, environ 1,2 Go de la
|
||||
mémoire partagée) ; le processeur met environ 50 s par image, il ne lit donc qu'avec `--vision cpu`.
|
||||
|
||||
### Bon à savoir
|
||||
|
||||
- Choisissez le niveau de réflexion (**désactivé, faible, moyen ou élevé**) dans le menu du chat ou dans le réglage « reasoning effort » de votre application. Désactivé est le plus rapide.
|
||||
- Strata répond à une requête à la fois par défaut. Pour en traiter plusieurs en même temps, utilisez `--parallel N` dans `strata launch` ou
|
||||
`"parallel": 2` dans la configuration ([BATCHING.md](docs/BATCHING.md)). Sur une carte de 12 Go, chaque réponse devient alors plus lente.
|
||||
- Strata lit en entier le premier message d'une discussion, soit environ 1 minute pour 30 000 tokens. Les messages suivants démarrent en quelques secondes.
|
||||
- Où sont stockées vos discussions et l'API complète : [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
|
||||
|
||||
## Compiler depuis les sources
|
||||
|
||||
<details>
|
||||
<summary>Windows, Linux et Mac depuis le dépôt</summary>
|
||||
|
||||
[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.
|
||||
|
||||
**Mac (Apple Silicon, macOS 15 ou plus récent) :** installez une fois les Command Line Tools d'Apple avec `xcode-select --install`
|
||||
(Xcode lui-même n'est pas nécessaire). Ouvrez le Terminal dans le dossier Strata et lancez `./setup.sh`. Le script compile
|
||||
le moteur Mac sur votre Mac (environ 7 minutes), pose les mêmes questions, télécharge le modèle et le démarre. La fois
|
||||
suivante, `./setup.sh` le démarre tout de suite. Quelle taille convient à votre Mac : [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
L'installateur détecte votre carte et prépare le bon moteur. Il demande quel modèle et quelle taille, combien de contexte
|
||||
(la quantité de texte que le modèle garde en tête) et s'il doit lire les images. Appuyez sur Entrée pour la réponse
|
||||
recommandée. Puis il télécharge le modèle (environ 70 Go) et le démarre. Si le téléchargement s'arrête, relancez le script
|
||||
et il reprend. Votre navigateur ouvre la page de Strata à l'adresse `http://127.0.0.1:8080`.
|
||||
|
||||
> Pendant le démarrage du modèle, votre PC peut être lent ou ne plus répondre pendant 1 à 3 minutes (le plus longtemps la
|
||||
> première fois). Strata charge 35 à 55 Go dans votre RAM et en verrouille une partie pour la carte graphique. Patientez et
|
||||
> ne fermez pas la fenêtre.
|
||||
|
||||
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. Mise à jour,
|
||||
Docker, plusieurs cartes, où vont les fichiers et toutes les options : [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
Pour mesurer votre PC, lancez `BENCH.bat` (`./bench.sh`). Cela prend 5 à 10 minutes et produit un rapport que vous pouvez
|
||||
partager ([comment](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
**Facultatif : plus de vitesse sur une carte quand tout le modèle tient en RAM.** Deux réglages à activer soi-même, par
|
||||
RubenTorney (#1904, #1905). Ils aident quand vous utilisez une seule carte graphique et que votre RAM contient tous les
|
||||
experts, par exemple Q2_0 ou IQ3_XXS sur un PC de 64 Go. Ouvrez `strata-<model>.json` dans le dossier de données de Strata ([où](docs/INSTALL.md#where-things-are-stored)) et ajoutez :
|
||||
|
||||
- `"--adapt-async", "1"` à la liste `"args"`. Le cache des experts se met alors à jour sur un thread auxiliaire au lieu de
|
||||
mettre la réponse en pause. Mesuré sur une RTX 5070 (Windows, Q2_0) : +4 à 5 % en décodage, +7 à 10 % sur le code et les
|
||||
longs documents. Les réponses peuvent différer légèrement de celles du réglage par défaut, et restent identiques d'une exécution à l'autre.
|
||||
- `"STRATA_PIN_BY_NEED": "1"` dans le bloc `"env"` (Windows). Windows ne peut verrouiller qu'environ la moitié de la RAM
|
||||
pour la carte graphique, et ce réglage verrouille d'abord les experts dont la carte a le plus besoin. Mesuré sur la RTX 5070 : +4,4 % en lecture du prompt.
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
Redémarrez Strata (`START-HERE.bat` ou `./setup.sh`) après la modification. Pour annuler, retirez les deux entrées.
|
||||
Redémarrez Strata après la modification. Pour annuler, supprimez les deux entrées.
|
||||
|
||||
## 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).
|
||||
</details>
|
||||
|
||||
## 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.
|
||||
Les modèles de ce genre tournent d'habitude sur des serveurs qui disposent de centaines de gigaoctets de mémoire graphique.
|
||||
Votre carte graphique en a 12 à 24. Strata fait tenir le modèle en répartissant le travail sur tout votre PC. Imaginez une
|
||||
bibliothèque.
|
||||
|
||||
<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 »), comme 24 576 ouvrages de référence. Chaque mot n'en demande que 10.
|
||||
- **Votre carte graphique est le bureau.** Il est petit et rapide. Les quelque 4 000 livres les plus utilisés y sont ouverts.
|
||||
- **Votre RAM, ce sont les étagères de la salle de lecture.** Les 24 576 livres s'y trouvent. Quand un mot demande un livre
|
||||
qui n'est pas sur le bureau, votre processeur va le chercher aussitôt sur l'étagère, pendant que la carte continue de travailler.
|
||||
- **Votre SSD, ce sont les archives.** Il contient une table de consultation de 29 Go. Le modèle n'en lit que quelques lignes par
|
||||
mot, elle n'a donc pas besoin de tenir en RAM.
|
||||
|
||||
- **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/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>
|
||||
|
||||
<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>
|
||||
Un petit assistant devine aussi les mots suivants. Le grand modèle les vérifie tous d'un coup et garde les bons. Vous
|
||||
obtenez la même réponse, 1,6 à 1,8 fois plus tôt. Les textes longs sont lus par gros morceaux, jusqu'à 8 192 tokens à la
|
||||
fois, à plus de 1 000 tokens par seconde.
|
||||
|
||||
- **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.
|
||||
<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>
|
||||
|
||||
L'explication plus longue : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Chaque partie et ses chiffres :
|
||||
L'explication plus longue : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Chaque élément avec ses chiffres :
|
||||
[les détails](docs/DETAILS.md#how-it-works) et l'[article](docs/paper/Strata-Paper.pdf).
|
||||
|
||||
## Crédits et licence
|
||||
## Un problème ?
|
||||
|
||||
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)
|
||||
- **Mon PC s'est figé au premier démarrage de Strata.** Il charge le modèle en RAM. Patientez 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.
|
||||
- **Ça s'est arrêté pendant le téléchargement ou l'installation.** Relancez l'application ou `strata launch`, ou relancez
|
||||
`START-HERE.bat` (ou `./setup.sh`). Cela reprend là où ça s'était arrêté.
|
||||
- **C'est très lent, 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 d'autres programmes (les navigateurs en consomment beaucoup) ou choisissez une taille plus petite, comme Q2_0 ou IQ2_XS.
|
||||
- **Il indique qu'il n'y a pas de moteur pour ce PC.** Dans l'application, ouvrez **Settings, Engine** et appuyez sur **Update engine** (mettre à jour le moteur). Dans le Terminal, lancez `strata update`. `strata doctor` montre quels fichiers du moteur sont installés.
|
||||
- **Il indique que le port 8080 est déjà utilisé.** Strata tourne déjà. Cherchez sa fenêtre.
|
||||
|
||||
Autres problèmes et solutions : [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Toujours bloqué ? Ouvrez une
|
||||
[issue](https://github.com/Niko1221/Strata/issues). Dans l'application, joignez le fichier obtenu par **Settings, Engine, Diagnostics, View logs, Save logs**. Dans le Terminal, collez la sortie de `strata doctor`. Si
|
||||
vous avez lancé depuis les sources, joignez `strata-<model>.log` du dossier Strata. Pour un problème de sécurité,
|
||||
signalez-le en privé : [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Remerciements 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), agentionai (Gyro)
|
||||
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)).
|
||||
remerciements : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata est open source sous [licence MIT](LICENSE).
|
||||
Quelques parties et tous les modèles ont leurs propres licences ([lesquelles](docs/HOW_IT_WORKS.md#license)).
|
||||
|
||||
## Contribuer
|
||||
|
||||
Vous avez trouvé un bug, ou vous voulez proposer une modification ou un benchmark ? Lisez d'abord [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
Il indique ce qu'il faut inclure pour que d'autres puissent vérifier votre travail.
|
||||
|
||||
## Soutenir Strata
|
||||
|
||||
Strata est gratuit et open source. S'il vous est utile, vous pouvez soutenir son développement :
|
||||
Strata est gratuit et sous licence MIT. Je le construis et je le teste sur le matériel que je peux obtenir. S'il tourne bien sur votre PC, un café m'aide
|
||||
à continuer de tester de nouvelles cartes et à corriger ce que vous signalez. Aucune obligation. L'utiliser et signaler ce que vous constatez aide tout autant.
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
Vous souhaitez sponsoriser Strata ? Contactez-moi via [GitHub](https://github.com/Niko1221).
|
||||
@@ -1,229 +1,385 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[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"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <b>日本語</b> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<p align="center"><b>1,250 億パラメータの AI モデルを、手元のゲーミング PC で動かす</b><br>
|
||||
NVIDIA または AMD のグラフィックカード(12 GB 以上) · Windows または Linux · 無料・オープンソース</p>
|
||||
<h3 align="center">最先端のモデルを、手元のゲーミングPCで動かす</h3>
|
||||
|
||||
<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)に、1 回のプロンプトで作成を依頼 ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">動画を見る(全編 49 秒)</a></sub></p>
|
||||
<p align="center">NVIDIA、AMD、Intel に対応 · Windows、Linux、Mac · 無料のオープンソース</p>
|
||||
|
||||
Strata を使うと、**[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** を一般的な PC で動かせます。
|
||||
通常はサーバーを必要とする大規模で高性能な AI モデルで、チャットやコードの生成、画像の読み取りができます。
|
||||
アプリやコーディングエージェントとも連携でき、データは PC の外に送信されません。
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
|
||||
</p>
|
||||
|
||||
## どのくらい速い?
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>ボクセルの塔の庭園。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>
|
||||
|
||||
一般的なゲーミング PC 2 台で速度を測定しました。英語の場合、1 トークンはおよそ ¾ 語に相当します。
|
||||
Strata は **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** を動かします。1250億パラメータのモデルで、
|
||||
ふつうはサーバーが必要ですが、Strata なら一般的なPCで使えます。チャット、コード作成、画像の読み取りができ、
|
||||
お使いのアプリやコーディングエージェントとも連携します。チャットとファイルはPCの外に出ません。
|
||||
|
||||
- **生成速度:** 短いチャットで回答を生成する速度です。毎秒 60 トークンなら、人が読むよりも速く文章が表示されます。
|
||||
- **プロンプトの読み込み:** 入力した内容を読み込む速度です。ここでは、文書やコード、チャット履歴など 32K トークン分を読み込ませています。
|
||||
**はじめての方へ。** まず[必要な環境](#必要な環境)を確認して、[アプリをインストール](#方法a-strata-アプリ)してください。
|
||||
|
||||
<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>
|
||||
- **ゲーミングPCで動きます。** RAMとグラフィックスメモリの合計が32 GBあれば始められます。
|
||||
- **お使いのツールと連携します。** OpenAI と Anthropic の API に対応しているので、Claude Code、Codex CLI、OpenCode などのエージェントから接続できます。
|
||||
- **画像を読めます。** 初期設定でオンです。
|
||||
- **無料です。** MIT ライセンス。アカウントもクラウドも不要です。
|
||||
- **プライバシーを守ります。** テレメトリはありません。Strata が何をダウンロードするか、いつインターネットを使うか、ダウンロードをどう検証するかは [docs/SAFETY.md](docs/SAFETY.md) に書いてあります。
|
||||
|
||||
| モデルサイズ | 生成速度 | プロンプトの読み込み |
|
||||
| --- | ---: | ---: |
|
||||
| **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 |
|
||||
バージョン 0.2.3 · エンジン 0.1.48
|
||||
|
||||
</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 |
|
||||
**RAMとグラフィックスメモリ(VRAM)の合計が、少なくとも32 GB必要です。** MacやRyzen AI Maxでは、ユニファイドメモリがその量にあたります。
|
||||
メモリが多いほど、大きなモデルを使えたり、速く動いたりします。
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
- RAM 32 GBのPCに12〜16 GBのグラフィックスカードがあれば、Coder が動きます。
|
||||
- RAM 64 GBなら、どのサイズも動きます。
|
||||
|
||||
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>
|
||||
|
||||
## 必要なもの
|
||||
|
||||
| | |
|
||||
| お使いのPC | 詳細 |
|
||||
| --- | --- |
|
||||
| **グラフィックカード** | **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 の最新のグラフィックドライバーも必要です。 |
|
||||
| **NVIDIA** GeForce RTX 20、30、40、50 シリーズ(12 GB以上) | アプリまたはターミナル版をインストールしてください。 |
|
||||
| **AMD** Radeon RX 6800 以降、Radeon AI PRO R9700(12 GB以上) | [AMD のカード](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md)、[Intel エンジン](docs/INTEL.md) |
|
||||
| **Apple Silicon の Mac**(M1 以降、macOS 15 以降) | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max(Strix Halo)** などのユニファイドメモリ搭載機 | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| 古いカードやプロセッサ | [古いGPU](docs/OLDER_GPUS.md)、[古いCPU](docs/INSTALL.md#older-cpus-experimental) |
|
||||
|
||||
その他の必要なソフトウェアは、インストーラーが用意します。2 枚または 3 枚のカードにモデルを分散させることもできます([マルチ GPU](docs/MULTI_GPU.md))。
|
||||
そのほかに必要なもの:
|
||||
|
||||
以下のハードウェアにも試験的に対応しています。いずれもコミュニティのメンバーが手元のマシンで実装・テストしたものです。
|
||||
- 約80 GBの空きディスク容量(SSDなら初回の起動がずっと速くなります)
|
||||
- Windows 10 / 11、Linux、または macOS
|
||||
- 最新のグラフィックスドライバー
|
||||
|
||||
- **旧世代のグラフィックカード**(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**:動作しますが、速度は遅くなります。[旧世代 CPU の対応状況](docs/INSTALL.md#older-cpus-experimental)を参照してください。
|
||||
|
||||
必要な環境の一覧は、[docs/INSTALL.md](docs/INSTALL.md#what-you-need)をご覧ください。
|
||||
同じメーカーのカードを2〜3枚使って、1つのモデルを動かせます([マルチGPU](docs/MULTI_GPU.md))。NVIDIA と AMD のカードにまたがってモデルを分けることはありません。
|
||||
一覧は [docs/INSTALL.md](docs/INSTALL.md#what-you-need) にあります。
|
||||
|
||||
## インストール
|
||||
|
||||
### AI にセットアップしてもらう
|
||||
3つの方法から1つ選んでください。どれも同じエンジンを起動し、同じモデルを使います。
|
||||
|
||||
Claude Code、Cursor、Codex、GitHub Copilot などの AI コーディングアシスタントを使っている場合は、次の文を貼り付けてください。
|
||||
### 方法A: Strata アプリ
|
||||
|
||||
いちばん簡単な方法です。エンジンはアプリに入っているので、インストールしてモデルを選べばチャットを始められます。お使いのOSとグラフィックスカードに合うファイルを
|
||||
[リリースページ](https://github.com/Niko1221/Strata/releases/latest)からダウンロードしてください。どれか分からない場合は、表の下の説明を見てください。
|
||||
|
||||
| OS | グラフィックスカード | ダウンロードするファイル |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX、ドライバー 580 以降 | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
|
||||
| | NVIDIA、古いカード、またはドライバーが 580 より古い場合 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 シリーズまたは Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 シリーズまたは Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX、ドライバー 580 以降 | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA、古いカード、またはドライバーが 580 より古い場合 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 シリーズまたは Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 シリーズ | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac**(Apple Silicon、macOS 15 以降) | 内蔵 | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
|
||||
|
||||
**迷ったときは:** NVIDIA の場合、ドライバーがバージョン 580 以降なら `cuda13`、それより古ければ `cuda12` を選びます。`cuda12` は古いカードと古いドライバー向けです。
|
||||
`hip` と `amd` のファイルは AMD のカード用です。それ以外の AMD カード(RX 6000)には、まだ用意されたアプリがありません。
|
||||
[AMD のカード](docs/AMD_HIP.md)を見てください。
|
||||
|
||||
そのあとの手順:
|
||||
|
||||
- **Windows:** `.exe` を実行します。ユーザー単位でインストールされ、管理者権限は不要です。「Windows によってPCが保護されました」と表示されたら、
|
||||
**詳細情報**を選び、**実行**を選んでください(インストーラーにはまだコード署名がありません)。
|
||||
- **Linux:** ファイルを実行可能にして(`chmod +x`)起動します。FUSE 2 が必要です。`sudo apt install libfuse2`(Ubuntu 22.04)
|
||||
または `libfuse2t64`(Ubuntu 24.04 以降)を実行してください。
|
||||
- **Mac:** `.dmg` を開き、Strata を「アプリケーション」にドラッグします。
|
||||
|
||||
最初の画面で、アプリがPCを調べて合うモデルをすすめます。インストールを押してください。モデルは大きく(約40〜70 GB)、中断しても続きからダウンロードできます。
|
||||
ウィンドウを閉じると、Strata とエンジンが止まります。
|
||||
|
||||
### 方法B: ターミナル(1行)
|
||||
|
||||
ターミナル版は `strata` コマンドです。スクリプトやサーバー、AI エージェント向けです。
|
||||
|
||||
Linux と Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
```
|
||||
|
||||
Windows(PowerShell):
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
スクリプトはPCを調べ、ターミナル版をダウンロードし、SHA-256 を検証して、ホームフォルダに展開します。管理者権限は不要です。Windows では、終わったら
|
||||
新しいターミナルを開いてください。そのあと Strata を起動します。
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
質問は1つずつ表示されます。モデル、コンテキスト、画像、接続を許可する相手の順です。そのあと、お使いのグラフィックスカード用のエンジン
|
||||
(署名付きで、使う前にテスト起動します)とモデルをダウンロードして、サーバーを開始します。スクリプトや AI エージェントからは、フラグを使います。
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
実行前にインストールスクリプトを読みたい場合は、先に `curl -fsSL <address> -o install.sh` でダウンロードし、内容を確認してから
|
||||
`sh install.sh` を実行してください。すべての画面とフラグは [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md) にあります。
|
||||
|
||||
### 方法C: 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、ディスクを確認して、PC に合ったモデルを選びます。インストールと起動を行い、
|
||||
アプリとの接続方法も案内します。また、Strata の [MCP サーバー](docs/MCP_SERVER.md) を使えば、
|
||||
AI ツールから Strata のインストール・起動・停止を行えます。
|
||||
AI はグラフィックスカード、RAM、ディスクを調べ、合うモデルを選んで、インストールと起動をし、アプリの接続方法を教えてくれます。
|
||||
AI ツールは [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`** を実行します。
|
||||
## モデル
|
||||
|
||||
**Mac(Apple シリコン、M1〜M5、macOS 15 以降):** まず一度だけ Apple の Command Line Tools をインストールします(ターミナルで `xcode-select --install`。Xcode 本体は不要です)。次に Strata フォルダーでターミナルを開き、**`./setup.sh`** を実行します。Mac 用エンジンをお使いの Mac 上でビルドし(約 7 分)、同じ質問をしてからモデルをダウンロードして起動します。次回からは `./setup.sh` ですぐに起動します。どのサイズが Mac に合うかと速度:[docs/MAC.md](docs/MAC.md)。
|
||||
アプリと `strata launch` は、メモリ量に合うモデルをすすめます。モデルは1つで、圧縮の度合いが違う複数のサイズがあります。
|
||||
小さいサイズは速く、大きいサイズは少し賢くなります。
|
||||
|
||||
NVIDIA でも AMD でも手順は同じです。インストーラーがカードを検出し、対応するエンジンを用意します。
|
||||
続いて、次の項目を設定します。
|
||||
### メモリが少ないなら Gyro
|
||||
|
||||
- 使用するモデルとサイズ
|
||||
- コンテキストの長さ(モデルが覚えておけるテキストの量)
|
||||
- 画像を読み取る機能を使うかどうか
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)(agentionai 製)は、このモデルをいちばん小さく動かせる方法です。
|
||||
|
||||
各項目で Enter を押すと、推奨設定が選ばれます。設定が終わるとモデル(約 70 GB)をダウンロードし、起動します。
|
||||
ダウンロードが途中で止まった場合は、もう一度実行すると続きから再開できます。起動が完了すると、ブラウザーで
|
||||
`http://127.0.0.1:8080` が開き、Strata の画面が表示されます。
|
||||
| サイズ | 必要なメモリ(RAM + VRAM) | ダウンロード | 内容 |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27.6 GB | 58.5 GB | ほかのサイズが入らないPC向け。品質は IQ2_XS より下がります。 |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | 品質は IQ3_XXS 程度で、GPUメモリの使用量は約20%少なくなります。回答の書き出しは Q2_0 より、12 GBの RTX 3060 で16%、R9700 で21%遅くなります。 |
|
||||
|
||||
> **モデルの起動中は、1-3 分ほど PC の動作が遅くなったり、応答しなくなったりすることがあります**(初回は特に時間がかかります)。
|
||||
> Strata が 35-55 GB のデータを RAM に読み込み、その一部をグラフィックカード用に固定(ピン留め)しているためです。
|
||||
> 正常な動作なので、ウィンドウに表示される処理状況を確認しながら、閉じずにお待ちください。
|
||||
Gyro は NVIDIA と AMD のカード(AMD は実験的)、および Linux 上の Intel Arc カード(Arc A750 で確認)で動きます。0.2.2 からは Apple Silicon の Mac でも動きます(32 GB の Mac mini で Gyro-S は 9 トークン/秒、エキスパートは SSD から読み込み)。詳細と測定結果は
|
||||
[docs/GYRO.md](docs/GYRO.md) にあります。`strata launch --model gyro-s` で起動します。
|
||||
|
||||
**次回からは** `START-HERE.bat`(または `./setup.sh`)を実行するだけですぐに起動できます。2 回目以降は、ダウンロード済みのファイルを再度取得する必要はありません。
|
||||
モデルを停止するにはウィンドウを閉じてください。`UPDATE.bat`(`./update.sh`)を使うと、Strata を起動せずに更新できます。
|
||||
更新方法や Docker、複数のカードの使い方、ファイルの保存先、各種オプションについては、[docs/INSTALL.md](docs/INSTALL.md)をご覧ください。
|
||||
### ほかのサイズ
|
||||
|
||||
## どのモデルを選べばいい
|
||||
|
||||
インストーラーが RAM の容量に合ったモデルをおすすめします。同じモデルでも、圧縮率によって複数のサイズが用意されています。
|
||||
サイズが小さいほど高速に動作し、大きいほど回答の品質が少し高くなります。
|
||||
|
||||
| RAM | おすすめのモデル | 選ぶ理由 |
|
||||
| メモリ(RAM + VRAM) | 選ぶモデル | 備考 |
|
||||
| --- | --- | --- |
|
||||
| **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) | 他のアプリを開いたままでも、最大サイズのモデルを動かせるだけの余裕がある |
|
||||
| **32 GB** | **Gyro-S**、またはコード向けの **[Coder](docs/MODELS.md#coder)** | Coder はRAM 32 GBが必要で、エキスパートを半分だけ保持するため、コード以外は弱くなります |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB)、または **Gyro-M**(35 GB) | Swift は答える前に考える時間が短くなります。Gyro-M は品質が IQ3_XXS に近いものの、回答の書き出しは Q2_0 より16〜21%遅くなります |
|
||||
| **48 GB** | **IQ3_XXS**(47.0 GB) | 遅くなりますが、品質は上がります |
|
||||
| **64 GB** | **IQ3_S**(54.8 GB)、オリジナルまたは Swift 1.5 | いちばん遅く、いちばん高品質です。公開されているテストでは完全版と同等です |
|
||||
| **96 GB 以上** | Unsloth の **UD-IQ4_XS**(RAM 約72 GB)から **Q8_0**(約136 GB)まで、4〜8ビット | 完全版に近づきます。RAM が足りないと、Strata は一部を SSD から読むため、ずっと遅くなります |
|
||||
|
||||
- **[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 未満の場合、回答の生成中にモデルの一部を SSD から読み込むため、速度が落ちます(NVMe SSD を使うと改善します)。
|
||||
- **[Unsloth UD-Q4_K_XL / UD-Q5_K_XL / UD-Q6_K_XL / Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** 4〜8 ビットで、フルモデルにより近いモデルです
|
||||
(次のトークンが Q8_0 と一致する割合は 93〜95%)。すべてのエキスパートが RAM に収まる場合(UD-Q4_K_XL で約 88 GB、
|
||||
Q8_0 で約 136 GB)は高速です。RAM が足りない場合は回答の生成中に一部を SSD から読み込むため、大幅に遅くなります
|
||||
(RAM が 64 GB の PC で UD-Q4_K_XL は毎秒 7-8.5 トークン程度)。
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** 手動でのセットアップが必要です。
|
||||
インストーラーのメニューからは選べません。
|
||||
表の行より RAM が少なくても動くことがあります。Strata は自動で[低RAMモード](docs/MODELS.md#a-big-graphics-card-and-little-ram)に切り替わります。
|
||||
速度は落ち、グラフィックスカードが小さいとさらに大きく落ちます。
|
||||
|
||||
各サイズのダウンロード情報や必要なメモリ容量は、[docs/MODELS.md](docs/MODELS.md)をご覧ください。後から別のモデルを追加する場合は、
|
||||
`SETUP.bat`(Linux:`./setup.sh --setup`)を実行してください。
|
||||
そのほかの選択肢:
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** はファインチューン版で、答える前に考える時間が短くなります。品質と RAM の必要量はほぼ同じです。
|
||||
- **[NVFP4](docs/NVFP4.md)** は NVIDIA カード向けの NVIDIA 製4ビット版です。すべてのエキスパートを RAM に置くには、約80 GBの RAM が必要です。
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** は、より小さい別のモデルです。画像を読め、複数のリクエストに同時に答えられます。
|
||||
- **[OrcaRouter の Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** は手動で設定します。
|
||||
|
||||
すべてのサイズ、ダウンロード、どの構成で何が動くかは [docs/MODELS.md](docs/MODELS.md) にあります。
|
||||
|
||||
## どのくらい速い?
|
||||
|
||||
12 GBのグラフィックスカード(RTX 5070、Ryzen 5 7600、RAM 64 GB)での数値です。ほかのカードの結果は
|
||||
[各モデルの速度](docs/MODELS.md#how-fast-is-each-size)、[コミュニティの結果](docs/COMMUNITY_BENCHMARKS.md)を見てください。
|
||||
|
||||
| モデル | 回答が出る速さ | プロンプトを読む速さ |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
|
||||
1トークンは英単語の約¾にあたるので、毎秒60トークンは読む速さより速くなります。「プロンプトを読む速さ」は、長い文書やコード、チャット履歴を Strata が取り込む速さです。
|
||||
|
||||
16 GBの AMD カード(RX 9070 XT、Ryzen 9 3900X、RAM 47 GB)での測定値:
|
||||
|
||||
| モデル | 回答が出る速さ | プロンプトを読む速さ |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
|
||||
バーは2つの表で同じ目盛りです。VRAM が多いカードほど速くなり、RTX 3090(24 GB)なら毎秒100〜140トークンほどになるはずです。各数値のエンジンのバージョンは
|
||||
[DETAILS.md](docs/DETAILS.md#speed-measured) にあります。
|
||||
|
||||
## 使い方
|
||||
|
||||
<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 では CPU で画像を読み取ります。Windows ではまだ画像の読み取りに対応していません。
|
||||
- **スマートフォンや別の PC から使う:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>` で起動します。API キーは必ず設定してください。
|
||||
- **リクエストは 1 つずつ:** 標準設定では 1 つずつ処理し、それ以外のリクエストは順番を待ちます。同時に処理するには、
|
||||
`"parallel": 2` を設定してください([BATCHING.md](docs/BATCHING.md))。12 GB のカードでは、リクエストごとの回答速度が落ちます。
|
||||
- **長いプロンプトを送る:** チャットの最初のメッセージは全文を読み込むため、30,000 トークンあたり約 1 分かかります。
|
||||
2 通目以降は数秒で応答が始まります。
|
||||
アプリには、**チャット**、モデルとGPU・CPU・RAMの状態を見る**モニター**、**モデル**ページ、**設定**があります。チャットはPC上のフォルダで作業できます。
|
||||
**手動**モードでは、ファイルを変更したりコマンドを実行したりする前に確認します。`/loop 10m <やること>` と入力すると、チャットを開いている間、メッセージを10分ごとに繰り返し送ります。
|
||||
|
||||
[チャットの保存場所](docs/INSTALL.md#where-things-are-stored)や[API の詳細](docs/DETAILS.md#using-it)も参照してください。
|
||||
チャットは DuckDuckGo(または Bing)でウェブ検索ができ、アカウントは不要です。**設定、ツール**で検索キーを追加することもできます。
|
||||
検索は、PCの外に何かを送る唯一の機能で、送られるのは検索語です。
|
||||
|
||||
### オプション:モデル全体が RAM に収まる場合、GPU 1 枚でさらに高速化
|
||||
2つのモデルを動かすには、**複数のモデルを実行**をオンにして、それぞれのモデルに同じメーカーのカードを1枚ずつ割り当てます。
|
||||
|
||||
2 つのオプトイン設定です(RubenTorney 氏による #1904 と #1905)。GPU を 1 枚使い、RAM にすべてのエキスパートが収まる場合(Strata の arena モード。例:64 GB の PC で Q2_0 や IQ3_XXS)に効果があります。Strata のデータフォルダーにある設定ファイル `strata-<model>.json`([場所](docs/INSTALL.md#where-things-are-stored))を開き、次を追加します:
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>Strata アプリでのチャット。エージェントが小さなボクセルの塔の庭を 1 つの HTML ファイルとして書き、そのページが会話の横のプレビューで開きます</sub></p>
|
||||
|
||||
- **生成の高速化:** `"args"` リストに `"--adapt-async", "1"`。エキスパートキャッシュの更新が回答を止めずに補助スレッドで行われます。RTX 5070(Windows、Q2_0)での実測:生成 +4〜5%、コードや長文では +7〜10%。回答はデフォルトとわずかに異なる場合があります(実行ごとには同じです)。
|
||||
- **Windows でのプロンプト読み込みの高速化:** `"env"` ブロックに `"STRATA_PIN_BY_NEED": "1"`。Windows はGPU 用に RAM の約半分しか固定できないため、この設定で GPU が本当に必要とするエキスパートから先に固定します。RTX 5070 での実測:プロンプト読み込み +4.4%。
|
||||
### ターミナルで
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<sub>モデルが返信を書いている間の <code>strata monitor</code>。速度、GPU、メモリ、最近のリクエストが表示されます</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # ガイド付きの画面: モデル、コンテキスト、画像、アクセス、そして開始
|
||||
strata launch --model q2_0 --yes --no-tui # 質問なし: 開始し、アドレスを表示して戻る
|
||||
strata launch --plan --model q2_0 # 実行内容を表示するだけで、何も変更しない
|
||||
strata launch --status # 今、何が動いているか
|
||||
strata launch --stop # 停止する
|
||||
strata monitor --url http://127.0.0.1:8080 # 速度、GPU、メモリ、リクエストを見る
|
||||
strata update --check # 新しいエンジンがあるか確認(strata update でインストール)
|
||||
strata doctor # インストール済みのエンジンファイルを調べる
|
||||
```
|
||||
|
||||
`strata --help` ですべてのコマンドを確認できます。起動とサーバーの稼働では、何もダウンロードしません。ダウンロードするのは、インストール、
|
||||
`strata launch`(モデルを選んだとき)、`strata update` だけです。スマートフォンや別のPCから Strata に接続するには、
|
||||
`strata launch --access lan` を使います。API キーが作られ、キーがない状態ではネットワーク上で起動しません。`--open` を付けない限り、ブラウザは開きません。
|
||||
ガイド: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。クライアント全体: [docs/TERMINAL.md](docs/TERMINAL.md)。
|
||||
|
||||
### AI エージェントと使う
|
||||
|
||||
エージェントは、方法Bの1行コマンドで Strata をインストールし、
|
||||
`strata launch --model NAME --yes --no-tui` を実行します。手順、フラグ、終了コードは [docs/AI_SETUP.md](docs/AI_SETUP.md) に、
|
||||
リポジトリのルールは [AGENTS.md](AGENTS.md) にあります。
|
||||
|
||||
### ほかのアプリをつなぐ
|
||||
|
||||
| アプリの種類 | ベース URL |
|
||||
| --- | --- |
|
||||
| OpenAI 互換(ほとんどのアプリとエージェント) | `http://127.0.0.1:8080/v1`(API キーもモデル名も何でも可) |
|
||||
| Anthropic API(Claude Code など) | `http://127.0.0.1:8080`、`/v1` は付けません。これらのアプリは `/v1/messages` を自分で付けます。Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API(Codex CLI) | `http://127.0.0.1:8080/v1/responses`([設定方法](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` は、OpenCode、Codex CLI、Claude Code、oh-my-pi、pi、Hermes Agent、OpenClaw の正確な設定を表示します。
|
||||
`strata connect opencode --write` は、確認したうえでファイルを書き込み、バックアップを残します。
|
||||
|
||||
### 画像
|
||||
|
||||
画像を読めるモデルでは、画像は初期設定でオンです。チャットに画像を添付または貼り付けるか、アプリから送ってください。
|
||||
グラフィックスカードが24 GB未満のときは、画像リーダーがプロセッサで動きます(画像1枚につき数秒、カードのメモリは使いません)。24 GB以上のときはグラフィックスカードで動きます。
|
||||
このルールは NVIDIA と AMD で同じです。画像はモデルごとにオフにでき、`--vision off`、`--vision gpu`、`--vision cpu` でリーダーを選べます。
|
||||
|
||||
このルールの根拠となる数値です。Radeon AI PRO R9700 では、カードのリーダーは260トークンの画像に57 ms、プロセッサのリーダーは3.1秒かかりました。
|
||||
16 GBのカード(Tesla P100 と RTX A4000、Linux)では、カードのリーダーを使うとテキストの速度が3〜5%下がりました。
|
||||
本来モデルが使う約1.4 GBをリーダーが占めるためです。Mac ではメモリ量にかかわらず GPU が読み取ります(32 GB の M4 で260トークンの画像に0.45秒、共有メモリを約1.2 GB使います)。プロセッサでは1枚あたり約50秒かかるため、`--vision cpu` を指定したときだけ使います。
|
||||
|
||||
### 知っておくと便利なこと
|
||||
|
||||
- 思考のレベル(**off、low、medium、high**)は、チャットのメニューか、お使いのアプリの「reasoning effort」で選びます。off がいちばん速くなります。
|
||||
- Strata は初期設定では、一度に1つのリクエストに答えます。同時に複数処理するには、`strata launch` で `--parallel N` を使うか、設定に
|
||||
`"parallel": 2` を書きます([BATCHING.md](docs/BATCHING.md))。12 GBのカードでは、1つずつの回答が遅くなります。
|
||||
- Strata はチャットの最初のメッセージを全文読みます。30,000トークンあたり約1分かかります。続きのメッセージは数秒で始まります。
|
||||
- チャットの保存場所と API 全体: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored)、[docs/DETAILS.md](docs/DETAILS.md#using-it)。
|
||||
|
||||
## ソースからビルド
|
||||
|
||||
<details>
|
||||
<summary>リポジトリから Windows、Linux、Mac 用にビルドする</summary>
|
||||
|
||||
[Strata をダウンロード](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip)して展開するか、`git clone` します。
|
||||
**Windows:** `START-HERE.bat` をダブルクリックします。**Linux:** Strata フォルダで `./setup.sh` を実行します。
|
||||
|
||||
**Mac(Apple Silicon、macOS 15 以降):** 最初に一度、`xcode-select --install` で Apple のコマンドラインツールをインストールします
|
||||
(Xcode 本体は不要です)。Strata フォルダでターミナルを開き、`./setup.sh` を実行します。Mac 用エンジンをお使いの Mac 上でビルドし
|
||||
(約7分)、同じ質問をして、モデルをダウンロードし、起動します。2回目以降は `./setup.sh` ですぐに起動します。
|
||||
お使いの Mac に合うサイズは [docs/MAC.md](docs/MAC.md) を見てください。
|
||||
|
||||
インストーラーはカードを見つけて、合うエンジンを設定します。どのモデルとサイズにするか、コンテキストをどれだけにするか(モデルが一度に覚えておけるテキストの量)、
|
||||
画像を読ませるかを聞かれます。おすすめの答えでよければ Enter を押します。そのあとモデル(約70 GB)をダウンロードして起動します。
|
||||
ダウンロードが止まったら、もう一度実行すると続きから再開します。ブラウザで `http://127.0.0.1:8080` の Strata ページが開きます。
|
||||
|
||||
> モデルの起動中は、PCが1〜3分ほど(初回がいちばん長い)遅くなったり、応答しなくなったりすることがあります。Strata は35〜55 GBをRAMに読み込み、
|
||||
> その一部をグラフィックスカード用に固定するためです。ウィンドウを閉じずに待ってください。
|
||||
|
||||
次回からは `START-HERE.bat`(または `./setup.sh`)をもう一度実行するだけです。すぐに起動し、同じものを2回ダウンロードすることはありません。ウィンドウを閉じるとモデルが止まります。
|
||||
`UPDATE.bat`(`./update.sh`)は、起動せずに Strata を更新します。更新、Docker、複数のカード、ファイルの保存先、すべてのオプションは
|
||||
[docs/INSTALL.md](docs/INSTALL.md) にあります。
|
||||
|
||||
PCの性能を測るには、`BENCH.bat`(`./bench.sh`)を実行します。5〜10分かかり、共有できるレポートが作られます
|
||||
([方法](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark))。
|
||||
|
||||
**オプション: モデル全体がRAMに入るとき、1枚のカードをさらに速くする。** RubenTorney による、自分でオンにする2つの設定です(#1904、#1905)。
|
||||
グラフィックスカードが1枚で、RAM がすべてのエキスパートを保持できるときに効果があります。たとえば64 GBのPCでの Q2_0 や IQ3_XXS です。
|
||||
Strata のデータフォルダにある `strata-<model>.json`([場所](docs/INSTALL.md#where-things-are-stored))を開いて、次を追加します。
|
||||
|
||||
- `"args"` のリストに `"--adapt-async", "1"` を追加します。エキスパートキャッシュが、回答を止めずに補助スレッドで更新されます。
|
||||
RTX 5070(Windows、Q2_0)での測定: デコードが4〜5%、コードや長い文書では7〜10%速くなりました。回答は初期設定と少し違うことがありますが、実行ごとの結果は変わりません。
|
||||
- `"env"` ブロックに `"STRATA_PIN_BY_NEED": "1"` を追加します(Windows)。Windows ではグラフィックスカード用に固定できる RAM が半分ほどしかありません。
|
||||
この設定は、カードが最初に必要とするエキスパートを優先して固定します。RTX 5070 での測定: プロンプトの読み取りが4.4%速くなりました。
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
変更後は Strata を再起動してください(`START-HERE.bat` または `./setup.sh`)。元に戻すには、2 つの項目を削除します。
|
||||
変更したら Strata を再起動してください。元に戻すには、2つの項目を削除します。
|
||||
|
||||
</details>
|
||||
|
||||
## 仕組み
|
||||
|
||||
このようなモデルは、ふつう数百GBのグラフィックスメモリを持つサーバーで動きます。お使いのグラフィックスカードは12〜24 GBです。Strata は、PC全体で作業を分担して、モデルを収めます。
|
||||
図書館にたとえてみます。
|
||||
|
||||
- モデルは、24,576人の小さな専門家(「エキスパート」)からなるチームです。24,576冊の参考書のようなものです。1つの単語に必要なのは、そのうち10冊だけです。
|
||||
- **グラフィックスカードは机です。** 小さくて速く、最もよく使う約4,000冊が開いて置いてあります。
|
||||
- **RAM は閲覧室の書棚です。** 24,576冊すべてがここにあります。単語に必要な本が机になければ、プロセッサがすぐに書棚から読み、その間もカードは作業を続けます。
|
||||
- **SSD は書庫です。** 29 GBの参照テーブルがしまってあります。モデルは単語ごとにその数行だけを読むので、RAM に置いておく必要はありません。
|
||||
|
||||
<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>
|
||||
|
||||
小さな助手が、次の数単語も予想します。大きなモデルがそれをまとめて確認し、正しいものだけを残します。答えは同じで、1.6〜1.8倍早く返ります。
|
||||
長い文章は、一度に最大8,192トークンの大きな塊で、毎秒1,000トークンを超える速さで読みます。
|
||||
|
||||
<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>
|
||||
|
||||
詳しい説明は [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md) にあります。各部分と数値は
|
||||
[詳細](docs/DETAILS.md#how-it-works)と[論文](docs/paper/Strata-Paper.pdf)を見てください。
|
||||
|
||||
## うまくいかないとき
|
||||
|
||||
- **初回起動時に PC が応答しなくなる。** モデルの読み込み中は、一時的に応答しなくなることがあります。ウィンドウを閉じずにお待ちください。
|
||||
10 分たっても応答しない場合は、PC を再起動して他のアプリを閉じてからやり直すか、モデルのサイズを小さくしてください。
|
||||
- **ダウンロードやインストールが途中で止まる。** `START-HERE.bat`(または `./setup.sh`)をもう一度実行してください。
|
||||
中断したところから再開できます。
|
||||
- **動作が極端に遅く、ディスクのアクセスランプが点滅し続ける。または「the engine stopped unexpectedly」と表示される。** RAM の空き容量が不足しています。
|
||||
ブラウザーなど、RAM を多く使う他のアプリを閉じるか、小さいモデル(Q2_0 または IQ2_XS)を選んでください。
|
||||
- **ポート 8080 が使用中と表示される。** Strata がすでに起動しています。起動済みのウィンドウを探してください。
|
||||
- **Strata を初めて起動したときに、PCが固まりました。** モデルを RAM に読み込んでいます。ウィンドウを閉じずに待ってください。10分たっても固まったままなら、
|
||||
PCを再起動し、ほかのプログラムを閉じてからもう一度試すか、小さいサイズを選んでください。
|
||||
- **ダウンロードやインストールの途中で止まりました。** アプリか `strata launch` をもう一度起動するか、`START-HERE.bat`(または
|
||||
`./setup.sh`)をもう一度実行してください。止まったところから再開します。
|
||||
- **とても遅い、ディスクのランプが点滅し続ける、または「エンジンが予期せず停止しました」と表示される。** PCの空き RAM が足りません。
|
||||
ほかのプログラムを閉じるか(ブラウザはメモリを多く使います)、Q2_0 や IQ2_XS のような小さいサイズを選んでください。
|
||||
- **このPCに合うエンジンがないと表示される。** アプリでは、**設定、エンジン**を開いて**エンジンを更新**を押します。ターミナルでは
|
||||
`strata update` を実行します。`strata doctor` で、どのエンジンファイルがインストールされているか分かります。
|
||||
- **ポート 8080 はすでに使われていると表示される。** Strata がすでに動いています。ウィンドウを探してください。
|
||||
|
||||
その他の問題と対処方法は、[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)をご覧ください。解決しない場合は
|
||||
[Issue](https://github.com/Niko1221/Strata/issues) を作成し、Strata フォルダー内の `strata-<model>.log` を添付してください。
|
||||
セキュリティ上の問題は、[SECURITY.md](SECURITY.md)の手順に従って非公開で報告してください。
|
||||
|
||||
## しくみ
|
||||
|
||||
この規模のモデルは通常、数百ギガバイトの GPU メモリを搭載したサーバーで動かします。一方、手元のグラフィックカードのメモリは
|
||||
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** にはすべてのエキスパートを保持し、
|
||||
**CPU** が残りの処理を並行して担当します。**SSD** には大きなルックアップテーブルを保存します。
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="小さな補助モデルが次の数語を予測し、大きなモデルがまとめて検証。正しい予測だけを採用する"></p>
|
||||
|
||||
- **予測してから検証:** 小さな補助モデルが次の数語を予測し、大きなモデルがまとめて検証します。
|
||||
回答を変えずに、生成速度を 1.6-1.8 倍に高められます。
|
||||
- **長いテキストはまとめて読み込みます**(一度に最大 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)をご覧ください。
|
||||
そのほかの問題と対処法は [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) にあります。それでも解決しない場合は、
|
||||
[issue](https://github.com/Niko1221/Strata/issues) を作成してください。アプリでは、**設定、エンジン、診断、ログを見る、ログを保存**で作られるファイルを添付します。
|
||||
ターミナルでは、`strata doctor` の出力を貼り付けます。ソースから動かしている場合は、Strata フォルダの `strata-<model>.log` を添付してください。
|
||||
セキュリティ上の問題は、非公開で報告してください: [SECURITY.md](SECURITY.md)。
|
||||
|
||||
## クレジットとライセンス
|
||||
|
||||
使用しているモデルは、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))。
|
||||
モデルは 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)、agentionai(Gyro)、
|
||||
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 は無料で使えるオープンソースソフトウェアです。役に立ったら、開発へのご支援をお願いします。
|
||||
バグを見つけた、変更やベンチマークを送りたい、という方は、先に [CONTRIBUTING.md](CONTRIBUTING.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></p>
|
||||
## Strata を応援する
|
||||
|
||||
Strata は無料で、MIT ライセンスです。私が入手できるハードウェアで開発とテストをしています。お使いのPCで快適に動いたなら、コーヒー1杯分の支援は、
|
||||
新しいカードのテストと、報告いただいた不具合の修正を続ける助けになります。無理のない範囲で大丈夫です。使って、気づいたことを報告してもらえるだけでも同じように助かります。
|
||||
|
||||
<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>
|
||||
|
||||
Strata のスポンサーに関心がある方は、[GitHub](https://github.com/Niko1221) から連絡してください。
|
||||
@@ -1,243 +1,399 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
**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>English</b> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<p align="center"><b>Run a 125-billion-parameter AI model on your own gaming PC</b><br>
|
||||
NVIDIA or AMD graphics card (12 GB or more) · Windows or Linux · free and open source</p>
|
||||
<h3 align="center">Run a state-of-the-art model on your own gaming PC</h3>
|
||||
|
||||
<p align="center">Support for NVIDIA, AMD, Intel · Windows, Linux, Mac · free and open source</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>A voxel pagoda garden, 1 shot prompt running on an RTX 5070 with Strata (IQ3_S, 128K context) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">full video (49 s)</a></sub></p>
|
||||
|
||||
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** 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.
|
||||
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, a 125-billion-parameter model that
|
||||
normally needs a server, on a normal PC. It chats, writes code, reads pictures and works with your apps and coding
|
||||
agents. Your chats and files stay on your PC.
|
||||
|
||||
## How fast is it?
|
||||
**New here?** Check [what you need](#what-you-need), then [install the app](#option-a-the-strata-app).
|
||||
|
||||
We measured it on two ordinary gaming PCs. A token is about ¾ of a word.
|
||||
- **Runs on a gaming PC.** 32 GB of RAM and graphics memory together is enough to start.
|
||||
- **Works with your tools.** It speaks the OpenAI and Anthropic APIs, so Claude Code, Codex CLI, OpenCode and other agents connect to it.
|
||||
- **Reads pictures.** They are on by default.
|
||||
- **Free.** MIT license, no account, no cloud.
|
||||
- **Private.** No telemetry. What Strata downloads, when it uses the internet and how it checks downloads is in [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
- **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>
|
||||
<tr><td>
|
||||
|
||||
| Size | Writes answers | Reads your 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>
|
||||
|
||||
| Size | Writes answers | Reads your prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
|
||||
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
|
||||
| **Coder** | 44 tokens/s | 1,420 tokens/s |
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
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>
|
||||
Version 0.2.3 · engine 0.1.48
|
||||
|
||||
## What you need
|
||||
|
||||
| | |
|
||||
**You need at least 32 GB of RAM and graphics memory (VRAM) together.** On a Mac or a Ryzen AI Max, that is the unified
|
||||
memory. More memory gives a bigger model or a faster one.
|
||||
|
||||
- A 32 GB PC with a 12-16 GB card runs the Coder.
|
||||
- 64 GB of RAM runs every size.
|
||||
|
||||
| Your computer | Details |
|
||||
| --- | --- |
|
||||
| **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. |
|
||||
| **NVIDIA** GeForce RTX 20, 30, 40, 50 series (12 GB or more) | Install the app or the Terminal. |
|
||||
| **AMD** Radeon RX 6800 and newer, Radeon AI PRO R9700 (12 GB or more) | [AMD cards](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [Intel engine](docs/INTEL.md) |
|
||||
| **Apple Silicon Mac** (M1 or newer, macOS 15 or newer) | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** and other unified-memory systems | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Older cards and processors | [Older GPUs](docs/OLDER_GPUS.md), [Older CPUs](docs/INSTALL.md#older-cpus-experimental) |
|
||||
|
||||
The installer sets up everything else. Two or three cards can share the model ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
Also needed:
|
||||
|
||||
Experimental, written and tested by community members on their own machines:
|
||||
- about 80 GB of free disk (an SSD makes the first start much faster)
|
||||
- Windows 10 / 11, Linux or macOS
|
||||
- a current graphics driver
|
||||
|
||||
- **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).
|
||||
- **Apple Silicon Macs (M1 to M5)**, built from source by `./setup.sh` (the Metal engine): [Mac](docs/MAC.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).
|
||||
|
||||
The full list: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
Two or three cards of the same make can share a model ([multi-GPU](docs/MULTI_GPU.md)). Strata never splits a model across
|
||||
NVIDIA and AMD cards. Full list: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
|
||||
## Install
|
||||
|
||||
### Let your AI set it up
|
||||
Pick one option. They all start the same engine and use the same models.
|
||||
|
||||
Do you use an AI coding assistant (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Paste this into it:
|
||||
### Option A: the Strata app
|
||||
|
||||
The easiest way. The app has the engine inside: install it, pick a model, chat. Download the file for your system and
|
||||
graphics card from the [releases page](https://github.com/Niko1221/Strata/releases/latest). Not sure which one? See the
|
||||
line under the table.
|
||||
|
||||
| System | Your graphics card | File to download |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, driver 580 or newer | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
|
||||
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 series or Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, driver 580 or newer | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 series | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 or newer) | built in | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
|
||||
|
||||
**If you are unsure:** for NVIDIA, take `cuda13` if your driver is version 580 or newer, otherwise `cuda12`. `cuda12` is
|
||||
for older cards and older drivers. The `hip` and `amd` files are for AMD cards. RX 6000 cards have no installer of
|
||||
their own: install `hip-rdna3` (Windows) or `amd-rx7900` (Linux), and on a PC with an RX 6000 card it fetches the RX 6000 engine by itself
|
||||
(also `strata update`). That engine is built but not yet tested on a real RX 6000 card: see [AMD cards](docs/AMD_HIP.md).
|
||||
|
||||
Then:
|
||||
|
||||
- **Windows:** run the `.exe`. It installs for your user and needs no administrator rights. If Windows says "Windows
|
||||
protected your PC", choose **More info**, then **Run anyway** (the installer is not code-signed yet).
|
||||
- **Linux:** make the file runnable (`chmod +x`) and start it. It needs FUSE 2: `sudo apt install libfuse2` (Ubuntu 22.04)
|
||||
or `libfuse2t64` (Ubuntu 24.04 and newer).
|
||||
- **Mac:** open the `.dmg` and drag Strata onto Applications.
|
||||
|
||||
On the first screen the app reads your PC and recommends a model that fits. Press install. The model is a big download
|
||||
(about 40-70 GB) and continues where it stopped if it is interrupted. Closing the window stops Strata and the engine.
|
||||
|
||||
### Option B: the Terminal (one line)
|
||||
|
||||
The Terminal is the `strata` command, for scripts, servers and AI agents.
|
||||
|
||||
Linux and Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
The script checks your PC, downloads the Terminal, verifies its SHA-256 and unpacks it into your home folder. It needs no
|
||||
administrator rights. On Windows, open a new terminal window afterwards. Then start Strata:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
It asks one question at a time: model, context, pictures, who may connect. Then it downloads the engine for your graphics
|
||||
card (signed, and test-started before use) and the model, and starts serving. For scripts and AI agents, use flags:
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
To read the install script before it runs, download it first with `curl -fsSL <address> -o install.sh`, read it, then run
|
||||
`sh install.sh`. All screens and flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### Option C: let your AI set it up
|
||||
|
||||
Paste this into Claude Code, Cursor, Codex, GitHub Copilot or another coding assistant:
|
||||
|
||||
```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 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).
|
||||
It checks your graphics card, RAM and disk, picks the model that fits, 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
|
||||
Prefer to build it yourself? See [Build from source](#build-from-source).
|
||||
|
||||
[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.
|
||||
## Models
|
||||
|
||||
**Mac (Apple Silicon, M1 to M5, macOS 15 or newer):** install Apple's Command Line Tools once
|
||||
(`xcode-select --install` in Terminal; Xcode itself is not needed). Then open Terminal in the Strata folder and run
|
||||
**`./setup.sh`**. It builds the Mac engine on your Mac (about 7 minutes), asks the same questions, downloads the model
|
||||
and starts it. Next time, `./setup.sh` starts it right away. Which size fits your Mac and how fast it is:
|
||||
[docs/MAC.md](docs/MAC.md).
|
||||
The app and `strata launch` recommend a model for your memory. It is one model in several sizes, compressed more or less.
|
||||
Smaller sizes are faster. Larger sizes are a bit smarter.
|
||||
|
||||
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:
|
||||
### Low on memory? Try Gyro
|
||||
|
||||
- which model and which size,
|
||||
- how much context (how much text the model keeps in mind),
|
||||
- whether it should read pictures.
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (by agentionai) is the smallest way to run this model.
|
||||
|
||||
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`.
|
||||
| Size | Memory it needs (RAM + VRAM) | Download | What it is |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27.6 GB | 58.5 GB | For PCs that fit nothing else. Quality is lower than IQ2_XS. |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | About IQ3_XXS quality with about 20% less GPU memory. It writes answers 16% (12 GB RTX 3060) to 21% (R9700) slower than Q2_0. |
|
||||
|
||||
> **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.
|
||||
Gyro runs on NVIDIA and AMD cards (AMD is experimental) on Intel Arc cards on Linux (tested on an Arc A750) and, from 0.2.2, on Apple Silicon Macs (Gyro-S on a 32 GB Mac mini: 9 tok/s, streaming from the SSD; [docs/MAC.md](docs/MAC.md#gyro-s-and-gyro-m-agentionais-trellis-experts)). Details and measurements:
|
||||
[docs/GYRO.md](docs/GYRO.md). Start it with `strata launch --model gyro-s`.
|
||||
|
||||
**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).
|
||||
### The other sizes
|
||||
|
||||
**Is it fast on your PC?** Run `BENCH.bat` (`./bench.sh`): about 5-10 minutes, it measures chat, 4K/16K prompts and an agent-style session with your own settings and writes a report to share ([how](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
The tiers count your RAM plus your VRAM, less 5 GB that Windows and the system keep for themselves. Every other size stays a choice.
|
||||
|
||||
## Which model should I pick?
|
||||
|
||||
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 |
|
||||
| Your memory (RAM + VRAM) | Take | Notes |
|
||||
| --- | --- | --- |
|
||||
| **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 UD-IQ4_XS (~4-bit) | room for the largest sizes with everything else open |
|
||||
| **32 GB** | **Gyro-S**, or the **[Coder](docs/MODELS.md#coder)** for code | The Coder needs 32 GB of RAM and keeps half of the experts, so it is weaker outside code |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39.2 GB), or **Gyro-M** (35 GB) | Swift thinks for a shorter time before it answers. Gyro-M is close to IQ3_XXS in quality, but it writes answers 16-21% slower than Q2_0 |
|
||||
| **48 GB** | **IQ3_XXS** (47.0 GB) | Slower, better |
|
||||
| **64 GB** | **IQ3_S** (54.8 GB), the original or Swift 1.5 | The slowest, the best: it matches the full model on the published tests |
|
||||
| **96 GB or more** | Unsloth's **UD-IQ4_XS** (about 72 GB of RAM) to **Q8_0** (about 136 GB), 4 to 8 bit | Closer to the full model. With less RAM, Strata reads part of them from the SSD, which is much slower |
|
||||
|
||||
- **[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, UD-Q5_K_XL, UD-Q6_K_XL and Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):**
|
||||
4- to 8-bit, closer to the full model (93-95% the same next token as Q8_0). Fast where every expert fits in RAM
|
||||
(~88 GB for UD-Q4_K_XL up to ~136 GB for Q8_0); with less RAM Strata reads part of them from the SSD while it
|
||||
answers, which is much slower (7-8.5 tokens/s for UD-Q4_K_XL 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.
|
||||
Less RAM than a row says can still work: Strata switches to its
|
||||
[low-RAM mode](docs/MODELS.md#a-big-graphics-card-and-little-ram) by itself. Expect it to be slower, and much slower
|
||||
with a small graphics card.
|
||||
|
||||
Sizes, downloads and what fits where: [docs/MODELS.md](docs/MODELS.md). To add another model later, run
|
||||
`SETUP.bat` (Linux: `./setup.sh --setup`).
|
||||
More choices:
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** is a fine-tune that thinks for a shorter time before it answers, at about the same quality and RAM.
|
||||
- **[NVFP4](docs/NVFP4.md)** is NVIDIA's 4-bit version for NVIDIA cards. It needs about 80 GB of RAM to keep every expert in RAM.
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** is a smaller, different model that reads pictures and can answer several requests at once.
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** is set up by hand.
|
||||
|
||||
Every size, download and what fits where: [docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
## How fast is it?
|
||||
|
||||
Based on a 12 GB graphics card (RTX 5070, Ryzen 5 7600, 64 GB RAM). See results from other cards:
|
||||
[speed of each model](docs/MODELS.md#how-fast-is-each-size), [community results](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Model | Answers appear at | Reads your prompt at |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
|
||||
A token is about ¾ of a word, so 60 tokens per second is faster than you can read. "Reads your prompt" is how fast Strata
|
||||
takes in a long document, code or chat history.
|
||||
|
||||
Measured on a 16 GB AMD card (RX 9070 XT, Ryzen 9 3900X, 47 GB RAM):
|
||||
|
||||
| Model | Answers appear at | Reads your prompt at |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
|
||||
The bars use one scale for both tables. A card with more VRAM is faster: an RTX 3090 (24 GB) should write about 100-140
|
||||
tokens per second. The engine version behind each number is in [DETAILS.md](docs/DETAILS.md#speed-measured).
|
||||
|
||||
## Using it
|
||||
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="The Strata app's Monitor tab next to a coding agent"><br>
|
||||
<sub>The Strata app's <b>Monitor</b> (left) while a coding agent writes the pagoda garden from the video (right)</sub></p>
|
||||
### In the app
|
||||
|
||||
- **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.
|
||||
The app has a **Chat**, a **Monitor** of the model and your GPU, CPU and RAM, a **Models** page and **Settings**. The chat
|
||||
can work in a folder on your PC. In **Manual** mode it asks before it changes a file or runs a command. Type
|
||||
`/loop 10m <what to do>` to repeat a message every 10 minutes while the chat is open.
|
||||
|
||||
More: [where your chats are stored](docs/INSTALL.md#where-things-are-stored), [the API](docs/DETAILS.md#using-it).
|
||||
The chat can search the web with DuckDuckGo (or Bing), with no account. You can add a search key under **Settings, Tools**.
|
||||
Search is the one feature that sends something off your PC, the search words.
|
||||
|
||||
### Optional: more speed on one card when the whole model fits in RAM
|
||||
To run two models, turn on **Run multiple models** and give each model its own card of the same make.
|
||||
|
||||
Two opt-in settings (by RubenTorney, #1904 and #1905). They help when you run one graphics card and your RAM holds
|
||||
every expert (Strata's arena mode, for example Q2_0 or IQ3_XXS on a 64 GB PC). Open your config,
|
||||
`strata-<model>.json` in the Strata data folder ([where](docs/INSTALL.md#where-things-are-stored)), and add:
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>A chat in the Strata app: the agent wrote a small voxel pagoda garden as one HTML file, and the page opens in the preview next to the conversation</sub></p>
|
||||
|
||||
- **Faster decode:** `"--adapt-async", "1"` to the `"args"` list. The expert cache then updates on a helper thread
|
||||
instead of pausing the answer. Measured on an RTX 5070 (Windows, Q2_0): +4-5% decode, +7-10% on code and long
|
||||
documents. Answers can differ slightly from the default (they stay the same from run to run).
|
||||
- **Faster prompt reading on Windows:** `"STRATA_PIN_BY_NEED": "1"` in the `"env"` block. Windows can lock only about
|
||||
half the RAM for the graphics card; this locks the experts the card actually needs first. Measured on the RTX 5070:
|
||||
+4.4% prompt reading.
|
||||
### In the Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<sub><code>strata monitor</code> while the model writes a reply: speed, GPU, memory and the recent requests</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided screens: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
|
||||
strata launch --plan --model q2_0 # show what would happen; nothing changes
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
|
||||
strata update --check # is there a new engine? (strata update installs it)
|
||||
strata doctor # check the installed engine files
|
||||
```
|
||||
|
||||
`strata --help` lists everything. Starting and serving never download anything. Only the install, `strata launch` (when
|
||||
you choose a model) and `strata update` do. To reach Strata from your phone or another PC, use
|
||||
`strata launch --access lan`. It makes an API key (recommended; add `--no-api-key` to go without, with a warning). No browser opens unless
|
||||
you add `--open`. Guide: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Whole client: [docs/TERMINAL.md](docs/TERMINAL.md).
|
||||
|
||||
### With AI agents
|
||||
|
||||
An agent installs Strata with the one-line command in Option B and runs
|
||||
`strata launch --model NAME --yes --no-tui`. The steps, flags and exit codes are in [docs/AI_SETUP.md](docs/AI_SETUP.md),
|
||||
and the repository rules are in [AGENTS.md](AGENTS.md).
|
||||
|
||||
### Connect your other apps
|
||||
|
||||
| App type | Base URL |
|
||||
| --- | --- |
|
||||
| OpenAI-compatible (most apps and agents) | `http://127.0.0.1:8080/v1` (any API key, any model name) |
|
||||
| Anthropic API (Claude Code and others) | `http://127.0.0.1:8080`, without `/v1`. These apps add `/v1/messages` themselves. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([setup](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` prints the exact settings for OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent and OpenClaw.
|
||||
`strata connect opencode --write` writes the file after asking and keeps a backup.
|
||||
|
||||
### Pictures
|
||||
|
||||
Pictures are on by default for models that can read them. Attach or paste a picture in the chat, or send it from your app.
|
||||
The picture reader runs on the processor when your graphics card has less than 24 GB (a few seconds per picture, nothing
|
||||
taken from the card's memory) and on the graphics card from 24 GB. The rule is the same for NVIDIA and AMD. Turn pictures
|
||||
off per model, or choose the reader with `--vision off`, `--vision gpu` or `--vision cpu`.
|
||||
|
||||
The numbers behind the rule: on a Radeon AI PRO R9700 the card's reader took 57 ms for a 260-token picture and the
|
||||
processor's took 3.1 s. On 16 GB cards (Tesla P100 and RTX A4000, Linux) the card's reader cost 3-5% of text speed,
|
||||
because it holds about 1.4 GB that the model would use otherwise. On a Mac the reader runs on the Mac's GPU whatever its memory (0.45 s for a 260-token picture on an M4 with 32 GB, about 1.2 GB of the shared
|
||||
memory); the processor reader needs about 50 s per picture there, so it runs only when you ask for it with `--vision cpu`.
|
||||
|
||||
### Good to know
|
||||
|
||||
- Choose the thinking level (**off, low, medium or high**) in the chat menu or in your app's "reasoning effort". Off is fastest.
|
||||
- Strata answers one request at a time by default. For several at once, use `--parallel N` in `strata launch` or
|
||||
`"parallel": 2` in the config ([BATCHING.md](docs/BATCHING.md)). On a 12 GB card this makes each answer slower.
|
||||
- Strata reads the first message of a chat in full, about 1 minute per 30,000 tokens. Follow-up messages start in seconds.
|
||||
- Where your chats are stored and the whole API: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
|
||||
|
||||
## Build from source
|
||||
|
||||
<details>
|
||||
<summary>Windows, Linux and Mac from the repository</summary>
|
||||
|
||||
[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.
|
||||
|
||||
**Mac (Apple Silicon, macOS 15 or newer):** install Apple's Command Line Tools once with `xcode-select --install`
|
||||
(Xcode itself is not needed). Open Terminal in the Strata folder and run `./setup.sh`. It builds the Mac engine on your
|
||||
Mac (about 7 minutes), asks the same questions, downloads the model and starts it. Next time, `./setup.sh` starts it
|
||||
right away. Which size fits your Mac: [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
The installer finds your card and sets up the right engine. It asks which model and size, how much context (how much text
|
||||
the model keeps in mind) and whether it should read pictures. Press Enter for the recommended answer. Then it downloads the
|
||||
model (about 70 GB) and starts it. If the download stops, run it again and it continues. Your browser opens the Strata
|
||||
page 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. Wait, and don't close the window.
|
||||
|
||||
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).
|
||||
|
||||
To measure your PC, run `BENCH.bat` (`./bench.sh`). It takes 5-10 minutes and writes a report you can share
|
||||
([how](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
**Optional: more speed on one card when the whole model fits in RAM.** Two opt-in settings by RubenTorney (#1904, #1905).
|
||||
They help when you run one graphics card and your RAM holds every expert, for example Q2_0 or IQ3_XXS on a 64 GB PC. Open
|
||||
`strata-<model>.json` in the Strata data folder ([where](docs/INSTALL.md#where-things-are-stored)) and add:
|
||||
|
||||
- `"--adapt-async", "1"` to the `"args"` list. The expert cache then updates on a helper thread instead of pausing the
|
||||
answer. Measured on an RTX 5070 (Windows, Q2_0): +4-5% decode, +7-10% on code and long documents. Answers can differ
|
||||
slightly from the default and stay the same from run to run.
|
||||
- `"STRATA_PIN_BY_NEED": "1"` in the `"env"` block (Windows). Windows can lock only about half the RAM for the graphics
|
||||
card, and this locks the experts the card needs first. Measured on the RTX 5070: +4.4% prompt reading.
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
Restart Strata (`START-HERE.bat` or `./setup.sh`) after the change. To undo, remove the two entries.
|
||||
Restart Strata after the change. To undo, remove the two entries.
|
||||
|
||||
## Something went wrong?
|
||||
|
||||
- **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 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. Found a
|
||||
security problem? Report it privately: [SECURITY.md](SECURITY.md).
|
||||
</details>
|
||||
|
||||
## How does it work?
|
||||
|
||||
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.
|
||||
12-24 GB. Strata makes the model fit by sharing the work across your whole PC. Picture a library.
|
||||
|
||||
- The model is a team of 24,576 small specialists ("experts"), like 24,576 reference books. Each word needs only 10 of them.
|
||||
- **Your graphics card is the desk.** It is small and fast. The roughly 4,000 books used most often lie open on it.
|
||||
- **Your RAM is the shelves in the reading room.** All 24,576 books are there. When a word needs a book that is not on the
|
||||
desk, your processor reads it from the shelf at once, while the card keeps working.
|
||||
- **Your SSD is the archive.** It holds a 29 GB lookup table. The model reads a few rows of it per word, so it does not
|
||||
need to sit in RAM.
|
||||
|
||||
<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").** 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.
|
||||
A small helper also guesses the next few words. The big model checks all of them at once and keeps the right ones. 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.
|
||||
|
||||
<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. 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).
|
||||
|
||||
## Something went wrong?
|
||||
|
||||
- **My PC froze the first time Strata started.** It loads the model into RAM. 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.** Start the app or `strata launch` again, or run `START-HERE.bat` (or
|
||||
`./setup.sh`) again. It continues where it stopped.
|
||||
- **It is very slow, 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 such as Q2_0 or IQ2_XS.
|
||||
- **It says there is no engine for this PC.** In the app, open **Settings, Engine** and press **Update engine**. In the
|
||||
Terminal, run `strata update`. `strata doctor` shows which engine files are installed.
|
||||
- **It says port 8080 is already in use.** Strata is already running. Look for its window.
|
||||
|
||||
More problems and fixes: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Still stuck? Open an
|
||||
[issue](https://github.com/Niko1221/Strata/issues). In the app, attach the file from **Settings, Engine, Diagnostics, View
|
||||
logs, Save logs**. In the Terminal, paste the output of `strata doctor`. From source, attach `strata-<model>.log` from the
|
||||
Strata folder. For a security problem, report it privately: [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Credits and 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)
|
||||
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), agentionai (Gyro)
|
||||
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)).
|
||||
[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)).
|
||||
|
||||
## Contributing
|
||||
|
||||
Found a bug, or want to send a change or a benchmark? Read [CONTRIBUTING.md](CONTRIBUTING.md) first. It says what to
|
||||
include so that others can check your work.
|
||||
|
||||
## Support Strata
|
||||
|
||||
Strata is free and open source. If it is useful to you, you can support its development:
|
||||
Strata is free and MIT-licensed. I build it and test it on whatever hardware I can get. If it runs well on your PC, a coffee helps
|
||||
me keep testing new cards and fixing what you report. No pressure. Using it and reporting what you find helps just as much.
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
Interested in sponsoring Strata? Contact me via [GitHub](https://github.com/Niko1221).
|
||||
@@ -1,234 +1,394 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[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"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <b>Português</b></p>
|
||||
|
||||
<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>
|
||||
<h3 align="center">Rode um modelo de ponta no seu próprio PC gamer</h3>
|
||||
|
||||
<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) ·
|
||||
<p align="center">Suporte a NVIDIA, AMD, Intel · Windows, Linux, Mac · gratuito e de código aberto</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>Um jardim de pagodes em voxels, gerado com um único prompt, rodando 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.
|
||||
O Strata roda o **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, um modelo de 125 bilhões de parâmetros que
|
||||
normalmente precisa de um servidor, num PC comum. Ele conversa, escreve código, lê imagens e funciona com os seus aplicativos e
|
||||
agentes de programação. Suas conversas e arquivos ficam no seu PC.
|
||||
|
||||
## Qual é a velocidade?
|
||||
**Primeira vez aqui?** Veja [o que você precisa](#o-que-você-precisa) e depois [instale o app](#opção-a-o-app-strata).
|
||||
|
||||
Medimos em dois PCs gamer comuns. Um token equivale a cerca de ¾ de uma palavra.
|
||||
- **Roda num PC gamer.** 32 GB somando memória RAM e memória da placa de vídeo já bastam para começar.
|
||||
- **Funciona com as suas ferramentas.** Ele fala as APIs da OpenAI e da Anthropic, então Claude Code, Codex CLI, OpenCode e outros agentes se conectam a ele.
|
||||
- **Lê imagens.** Já vêm ativadas.
|
||||
- **Gratuito.** Licença MIT, sem conta, sem nuvem.
|
||||
- **Privado.** Sem telemetria. O que o Strata baixa, quando usa a internet e como confere os downloads está em [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
- **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).
|
||||
Versão 0.2.3 · mecanismo 0.1.48
|
||||
|
||||
<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>
|
||||
## O que você precisa
|
||||
|
||||
| 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 |
|
||||
**Você precisa de pelo menos 32 GB somando a memória RAM e a memória da placa de vídeo (VRAM).** Num Mac ou num Ryzen AI Max, isso é a
|
||||
memória unificada. Mais memória permite um modelo maior ou mais rápido.
|
||||
|
||||
</td><td>
|
||||
- Um PC de 32 GB com uma placa de 12 a 16 GB roda o Coder.
|
||||
- 64 GB de RAM rodam todos os tamanhos.
|
||||
|
||||
| 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
|
||||
|
||||
| | |
|
||||
| Seu computador | Detalhes |
|
||||
| --- | --- |
|
||||
| **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. |
|
||||
| **NVIDIA** GeForce RTX série 20, 30, 40, 50 (12 GB ou mais) | Instale o app ou o Terminal. |
|
||||
| **AMD** Radeon RX 6800 e mais novas, Radeon AI PRO R9700 (12 GB ou mais) | [Placas AMD](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [mecanismo Intel](docs/INTEL.md) |
|
||||
| **Mac com Apple Silicon** (M1 ou mais novo, macOS 15 ou mais novo) | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** e outros sistemas de memória unificada | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Placas e processadores mais antigos | [GPUs antigas](docs/OLDER_GPUS.md), [CPUs antigas](docs/INSTALL.md#older-cpus-experimental) |
|
||||
|
||||
O instalador cuida de todo o resto. Duas ou três placas podem dividir o modelo ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
Você também precisa de:
|
||||
|
||||
Experimental, escrito e testado por membros da comunidade nas próprias máquinas:
|
||||
- cerca de 80 GB de espaço livre no disco (um SSD deixa a primeira inicialização bem mais rápida)
|
||||
- Windows 10 / 11, Linux ou macOS
|
||||
- um driver de vídeo atualizado
|
||||
|
||||
- **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).
|
||||
Duas ou três placas da mesma marca podem dividir um modelo ([multi-GPU](docs/MULTI_GPU.md)). O Strata nunca divide um modelo entre
|
||||
placas NVIDIA e AMD. Lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
|
||||
## Instalação
|
||||
|
||||
### Deixe sua IA instalar
|
||||
Escolha uma opção. Todas iniciam o mesmo mecanismo e usam os mesmos modelos.
|
||||
|
||||
Você usa um assistente de programação com IA (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Cole isto nele:
|
||||
### Opção A: o app Strata
|
||||
|
||||
O jeito mais fácil. O app já traz o mecanismo: instale, escolha um modelo e converse. Baixe o arquivo para o seu sistema e a sua
|
||||
placa de vídeo na [página de releases](https://github.com/Niko1221/Strata/releases/latest). Não sabe qual? Veja a explicação
|
||||
logo abaixo da tabela.
|
||||
|
||||
| Sistema | Sua placa de vídeo | Arquivo para baixar |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
|
||||
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
|
||||
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon série RX 7000 | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 ou mais novo) | integrada | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
|
||||
|
||||
**Se você estiver em dúvida:** para NVIDIA, use `cuda13` se o seu driver for a versão 580 ou mais nova, e `cuda12` nos outros casos. O `cuda12`
|
||||
serve para placas e drivers mais antigos. Os arquivos `hip` e `amd` são para placas AMD. Outras placas AMD (RX 6000) ainda não têm app pronto: veja [Placas AMD](docs/AMD_HIP.md).
|
||||
|
||||
Depois:
|
||||
|
||||
- **Windows:** execute o `.exe`. Ele instala só para o seu usuário e não pede direitos de administrador. Se o Windows disser "O Windows
|
||||
protegeu o computador", escolha **Mais informações** e depois **Executar assim mesmo** (o instalador ainda não tem assinatura de código).
|
||||
- **Linux:** torne o arquivo executável (`chmod +x`) e inicie. É preciso ter o FUSE 2: `sudo apt install libfuse2` (Ubuntu 22.04)
|
||||
ou `libfuse2t64` (Ubuntu 24.04 e mais novos).
|
||||
- **Mac:** abra o `.dmg` e arraste o Strata para Aplicativos.
|
||||
|
||||
Na primeira tela, o app lê o seu PC e recomenda um modelo que caiba nele. Clique em instalar. O modelo é um download grande
|
||||
(cerca de 40 a 70 GB) e continua de onde parou se for interrompido. Fechar a janela encerra o Strata e o mecanismo.
|
||||
|
||||
### Opção B: o Terminal (uma linha)
|
||||
|
||||
O Terminal é o comando `strata`, para scripts, servidores e agentes de IA.
|
||||
|
||||
Linux e Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
O script confere o seu PC, baixa o Terminal, verifica o SHA-256 e descompacta na sua pasta pessoal. Não precisa de direitos de
|
||||
administrador. No Windows, abra uma nova janela do terminal depois. Então inicie o Strata:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Ele faz uma pergunta por vez: modelo, contexto, imagens, quem pode se conectar. Depois baixa o mecanismo para a sua placa de vídeo
|
||||
(assinada e testada antes do uso) e o modelo, e começa a servir. Em scripts e agentes de IA, use flags:
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
Para ler o script de instalação antes de executá-lo, baixe-o primeiro com `curl -fsSL <address> -o install.sh`, leia e então rode
|
||||
`sh install.sh`. Todas as telas e flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### Opção C: deixe a sua IA configurar
|
||||
|
||||
Cole isto no Claude Code, Cursor, Codex, GitHub Copilot ou outro assistente de programação:
|
||||
|
||||
```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.
|
||||
Ele confere a sua placa de vídeo, a RAM e o disco, escolhe o modelo que cabe, instala e inicia, e diz como conectar os seus
|
||||
aplicativos. 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
|
||||
Prefere compilar por conta própria? Veja [Compilar a partir do código-fonte](#compilar-a-partir-do-código-fonte).
|
||||
|
||||
[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.
|
||||
## Modelos
|
||||
|
||||
**Mac (Apple Silicon, M1 a M5, macOS 15 ou mais recente):** instale uma vez as Command Line Tools da Apple (`xcode-select --install` no Terminal; o Xcode em si não é necessário). Depois abra o Terminal na pasta do Strata e rode **`./setup.sh`**. Ele compila o motor do Mac no seu Mac (uns 7 minutos), faz as mesmas perguntas, baixa o modelo e o inicia. Da próxima vez, `./setup.sh` inicia na hora. Qual tamanho cabe no seu Mac e qual a velocidade: [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
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.
|
||||
O app e o `strata launch` recomendam um modelo para a sua memória. É um único modelo 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ê |
|
||||
### Pouca memória? Experimente o Gyro
|
||||
|
||||
O [Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (da agentionai) é a menor forma de rodar este modelo.
|
||||
|
||||
| Tamanho | Memória necessária (RAM + VRAM) | Download | O que é |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 GB | 58,5 GB | Para PCs onde nenhum outro tamanho cabe. A qualidade é menor que a do IQ2_XS. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Qualidade próxima à do IQ3_XXS, com cerca de 20% menos memória da GPU. Ele escreve as respostas 16% mais devagar que o Q2_0 numa RTX 3060 de 12 GB e 21% mais devagar numa R9700. |
|
||||
|
||||
O Gyro roda em placas NVIDIA e AMD (AMD é experimental) em placas Intel Arc no Linux (testado numa Arc A750) e, desde a 0.2.2, em Macs com Apple Silicon (Gyro-S num Mac mini de 32 GB: 9 tokens/s, com os especialistas lidos do SSD). Detalhes e medições:
|
||||
[docs/GYRO.md](docs/GYRO.md). Inicie com `strata launch --model gyro-s`.
|
||||
|
||||
### Os outros tamanhos
|
||||
|
||||
| Sua memória (RAM + VRAM) | Escolha | Observações |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
| **32 GB** | **Gyro-S**, ou o **[Coder](docs/MODELS.md#coder)** para código | O Coder precisa de 32 GB de RAM e mantém metade dos experts, então é mais fraco fora de código |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), ou **Gyro-M** (35 GB) | O Swift pensa por menos tempo antes de responder. O Gyro-M chega perto do IQ3_XXS em qualidade, mas escreve as respostas 16 a 21% mais devagar que o Q2_0 |
|
||||
| **48 GB** | **IQ3_XXS** (47,0 GB) | Mais lento, melhor |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB), o original ou o Swift 1.5 | O mais lento, o melhor: empata com o modelo completo nos testes publicados |
|
||||
| **96 GB ou mais** | **UD-IQ4_XS** da Unsloth (cerca de 72 GB de RAM) até **Q8_0** (cerca de 136 GB), de 4 a 8 bits | Mais perto do modelo completo. Com menos RAM, o Strata lê parte deles do SSD, o que é bem mais lento |
|
||||
|
||||
- **[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, UD-Q5_K_XL, UD-Q6_K_XL e Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** de 4 a 8 bits, mais próximos do modelo
|
||||
completo (93-95% o mesmo próximo token que o Q8_0). Rápidos quando todos os especialistas cabem na RAM (~88 GB para
|
||||
o UD-Q4_K_XL até ~136 GB para o Q8_0); com menos RAM, o Strata lê uma parte do SSD enquanto responde, o que é bem
|
||||
mais lento (7-8.5 tokens/s para o UD-Q4_K_XL 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.
|
||||
Com menos RAM do que a linha indica, ainda pode funcionar: o Strata muda sozinho para o
|
||||
[modo de pouca RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Espere mais lentidão, e muito mais com uma placa de vídeo pequena.
|
||||
|
||||
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`).
|
||||
Mais opções:
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** é um ajuste fino que pensa por menos tempo antes de responder, com qualidade e RAM parecidas.
|
||||
- **[NVFP4](docs/NVFP4.md)** é a versão de 4 bits da NVIDIA para placas NVIDIA. Precisa de cerca de 80 GB de RAM para manter todos os experts na RAM.
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** é outro modelo, menor, que lê imagens e pode atender vários pedidos ao mesmo tempo.
|
||||
- **[Uncensored IQ3_XXS da OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** é configurado manualmente.
|
||||
|
||||
Todos os tamanhos, downloads e o que cabe em cada configuração: [docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
## Qual é a velocidade?
|
||||
|
||||
Com base numa placa de vídeo de 12 GB (RTX 5070, Ryzen 5 7600, 64 GB de RAM). Veja resultados de outras placas:
|
||||
[velocidade de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados da comunidade](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Modelo | As respostas aparecem a | Lê o seu prompt a |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
|
||||
Um token equivale a cerca de ¾ de palavra, então 60 tokens por segundo é mais rápido do que você lê. "Lê o seu prompt" é a velocidade com que o Strata
|
||||
absorve um documento longo, um código ou um histórico de conversa.
|
||||
|
||||
Medido numa placa AMD de 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB de RAM):
|
||||
|
||||
| Modelo | As respostas aparecem a | Lê o seu prompt a |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
|
||||
As barras usam a mesma escala nas duas tabelas. Uma placa com mais VRAM é mais rápida: uma RTX 3090 (24 GB) deve escrever cerca de 100 a 140
|
||||
tokens por segundo. A versão do mecanismo por trás de cada número está em [DETAILS.md](docs/DETAILS.md#speed-measured).
|
||||
|
||||
## 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 app
|
||||
|
||||
- **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.
|
||||
O app tem um **Chat**, um **Monitor** do modelo e da sua GPU, CPU e RAM, uma página de **Modelos** e as **Configurações**. O chat
|
||||
pode trabalhar numa pasta do seu PC. No modo **Manual**, ele pergunta antes de alterar um arquivo ou executar um comando. Digite
|
||||
`/loop 10m <o que fazer>` para repetir uma mensagem a cada 10 minutos enquanto o chat estiver aberto.
|
||||
|
||||
Mais: [onde suas conversas ficam salvas](docs/INSTALL.md#where-things-are-stored), [a API](docs/DETAILS.md#using-it).
|
||||
O chat pode pesquisar na web com o DuckDuckGo (ou o Bing), sem conta. Você pode adicionar uma chave de busca em **Configurações, Ferramentas**.
|
||||
A busca é o único recurso que envia algo para fora do seu PC: os termos da pesquisa.
|
||||
|
||||
### Opcional: mais velocidade com uma placa quando o modelo inteiro cabe na RAM
|
||||
Para rodar dois modelos, ative **Executar vários modelos** e dê a cada modelo a sua própria placa da mesma marca.
|
||||
|
||||
Duas configurações opcionais (de RubenTorney, #1904 e #1905). Elas ajudam quando você usa uma placa de vídeo e sua RAM guarda todos os experts (o modo arena do Strata, por exemplo Q2_0 ou IQ3_XXS num PC com 64 GB). Abra sua configuração `strata-<model>.json` na pasta de dados do Strata ([onde](docs/INSTALL.md#where-things-are-stored)) e adicione:
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>Um chat no app Strata: o agente escreveu um pequeno jardim de pagode em voxels em um único arquivo HTML, e a página abre na pré-visualização ao lado da conversa</sub></p>
|
||||
|
||||
- **Geração mais rápida:** `"--adapt-async", "1"` na lista `"args"`. O cache de experts passa a ser atualizado numa thread auxiliar em vez de pausar a resposta. Medido numa RTX 5070 (Windows, Q2_0): +4-5% de geração, +7-10% em código e documentos longos. As respostas podem diferir um pouco do padrão (ficam iguais de uma execução para outra).
|
||||
- **Leitura do prompt mais rápida no Windows:** `"STRATA_PIN_BY_NEED": "1"` no bloco `"env"`. O Windows só consegue travar cerca de metade da RAM para a placa; isto trava primeiro os experts de que a placa realmente precisa. Medido na RTX 5070: +4,4% na leitura do prompt.
|
||||
### No Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<sub><code>strata monitor</code> enquanto o modelo responde: velocidade, GPU, memória e as solicitações recentes</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # telas guiadas: modelo, contexto, imagens, acesso e depois servir
|
||||
strata launch --model q2_0 --yes --no-tui # sem perguntas: serve, mostra o endereço e volta
|
||||
strata launch --plan --model q2_0 # mostra o que aconteceria; nada muda
|
||||
strata launch --status # o que está sendo servido agora
|
||||
strata launch --stop # para o serviço
|
||||
strata monitor --url http://127.0.0.1:8080 # acompanha velocidade, GPU, memória e requisições
|
||||
strata update --check # há um mecanismo novo? (strata update instala)
|
||||
strata doctor # confere os arquivos do mecanismo instalada
|
||||
```
|
||||
|
||||
`strata --help` lista tudo. Iniciar e servir nunca baixam nada. Só a instalação, o `strata launch` (quando você
|
||||
escolhe um modelo) e o `strata update` baixam. Para acessar o Strata do celular ou de outro PC, use
|
||||
`strata launch --access lan`. Ele cria uma chave de API e não inicia numa rede sem uma. Nenhum navegador abre, a menos que
|
||||
você adicione `--open`. Guia: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Cliente completo: [docs/TERMINAL.md](docs/TERMINAL.md).
|
||||
|
||||
### Com agentes de IA
|
||||
|
||||
Um agente instala o Strata com o comando de uma linha da Opção B e executa
|
||||
`strata launch --model NAME --yes --no-tui`. Os passos, as flags e os códigos de saída estão em [docs/AI_SETUP.md](docs/AI_SETUP.md),
|
||||
e as regras do repositório estão em [AGENTS.md](AGENTS.md).
|
||||
|
||||
### Conecte os seus outros aplicativos
|
||||
|
||||
| Tipo de aplicativo | URL base |
|
||||
| --- | --- |
|
||||
| Compatível com OpenAI (a maioria dos apps e agentes) | `http://127.0.0.1:8080/v1` (qualquer chave de API, qualquer nome de modelo) |
|
||||
| API da Anthropic (Claude Code e outros) | `http://127.0.0.1:8080`, sem `/v1`. Esses aplicativos adicionam `/v1/messages` sozinhos. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuração](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` mostra as configurações exatas para OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent e OpenClaw.
|
||||
`strata connect opencode --write` grava o arquivo depois de perguntar e mantém um backup.
|
||||
|
||||
### Imagens
|
||||
|
||||
As imagens vêm ativadas nos modelos que sabem lê-las. Anexe ou cole uma imagem no chat, ou envie pelo seu aplicativo.
|
||||
O leitor de imagens roda no processador quando a sua placa de vídeo tem menos de 24 GB (alguns segundos por imagem, sem tirar nada
|
||||
da memória da placa) e roda na placa de vídeo a partir de 24 GB. A regra é a mesma para NVIDIA e AMD. Desative as imagens
|
||||
por modelo, ou escolha o leitor com `--vision off`, `--vision gpu` ou `--vision cpu`.
|
||||
|
||||
Os números por trás da regra: numa Radeon AI PRO R9700, o leitor da placa levou 57 ms para uma imagem de 260 tokens e o do
|
||||
processador levou 3,1 s. Em placas de 16 GB (Tesla P100 e RTX A4000, Linux), o leitor da placa custou de 3 a 5% da velocidade do texto,
|
||||
porque ocupa cerca de 1,4 GB que o modelo usaria de outra forma. No Mac, a GPU faz a leitura, seja qual for a memória (0,45 s para uma imagem de 260 tokens em um M4 de 32 GB, cerca de 1,2 GB da memória compartilhada);
|
||||
o processador leva cerca de 50 s por imagem, então só lê com `--vision cpu`.
|
||||
|
||||
### Bom saber
|
||||
|
||||
- Escolha o nível de raciocínio (**off, low, medium ou high**) no menu do chat ou no "reasoning effort" do seu aplicativo. Off é o mais rápido.
|
||||
- Por padrão, o Strata responde a um pedido por vez. Para vários ao mesmo tempo, use `--parallel N` no `strata launch` ou
|
||||
`"parallel": 2` na configuração ([BATCHING.md](docs/BATCHING.md)). Numa placa de 12 GB, isso deixa cada resposta mais lenta.
|
||||
- O Strata lê a primeira mensagem de uma conversa por inteiro, cerca de 1 minuto a cada 30.000 tokens. As mensagens seguintes começam em segundos.
|
||||
- Onde ficam as suas conversas e a API completa: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
|
||||
|
||||
## Compilar a partir do código-fonte
|
||||
|
||||
<details>
|
||||
<summary>Windows, Linux e Mac a partir do repositório</summary>
|
||||
|
||||
[Baixe o Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) e descompacte, ou use `git clone`.
|
||||
**Windows:** dê dois cliques em `START-HERE.bat`. **Linux:** execute `./setup.sh` na pasta do Strata.
|
||||
|
||||
**Mac (Apple Silicon, macOS 15 ou mais novo):** instale uma vez as Command Line Tools da Apple com `xcode-select --install`
|
||||
(o Xcode em si não é necessário). Abra o Terminal na pasta do Strata e execute `./setup.sh`. Ele compila o mecanismo do Mac no seu
|
||||
Mac (cerca de 7 minutos), faz as mesmas perguntas, baixa o modelo e inicia. Da próxima vez, `./setup.sh` inicia
|
||||
na hora. Qual tamanho cabe no seu Mac: [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
O instalador encontra a sua placa e configura o mecanismo certo. Ele pergunta qual modelo e tamanho, quanto contexto (quanto texto
|
||||
o modelo mantém em mente) e se deve ler imagens. Pressione Enter para aceitar a resposta recomendada. Depois ele baixa o
|
||||
modelo (cerca de 70 GB) e inicia. Se o download parar, execute de novo e ele continua. O navegador abre a página do Strata em
|
||||
`http://127.0.0.1:8080`.
|
||||
|
||||
> Enquanto o modelo inicia, o seu PC pode ficar lento ou parar de responder por 1 a 3 minutos (mais tempo na primeira vez). O Strata carrega
|
||||
> de 35 a 55 GB na sua RAM e trava parte dela para a placa de vídeo. Espere e não feche a janela.
|
||||
|
||||
Da próxima vez, execute `START-HERE.bat` (ou `./setup.sh`) de novo. Ele inicia na hora e não baixa nada duas vezes. Feche a janela
|
||||
dele 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).
|
||||
|
||||
Para medir o seu PC, execute `BENCH.bat` (`./bench.sh`). Leva de 5 a 10 minutos e gera um relatório que você pode compartilhar
|
||||
([como](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
**Opcional: mais velocidade numa placa quando o modelo inteiro cabe na RAM.** Duas configurações opcionais do RubenTorney (#1904, #1905).
|
||||
Elas ajudam quando você usa uma placa de vídeo e a sua RAM comporta todos os experts, por exemplo Q2_0 ou IQ3_XXS num PC de 64 GB. Abra
|
||||
`strata-<model>.json` na pasta de dados do Strata ([onde fica](docs/INSTALL.md#where-things-are-stored)) e adicione:
|
||||
|
||||
- `"--adapt-async", "1"` à lista `"args"`. O cache de experts passa a ser atualizado numa thread auxiliar, em vez de pausar a
|
||||
resposta. Medido numa RTX 5070 (Windows, Q2_0): +4 a 5% na geração, +7 a 10% em código e documentos longos. As respostas podem diferir
|
||||
um pouco do padrão e continuam iguais de uma execução para outra.
|
||||
- `"STRATA_PIN_BY_NEED": "1"` no bloco `"env"` (Windows). O Windows só consegue travar cerca de metade da RAM para a placa
|
||||
de vídeo, e isso trava primeiro os experts de que a placa precisa. Medido na RTX 5070: +4,4% na leitura do prompt.
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
Reinicie o Strata (`START-HERE.bat` ou `./setup.sh`) depois da mudança. Para desfazer, remova as duas entradas.
|
||||
Reinicie o Strata depois da alteração. Para desfazer, remova as duas entradas.
|
||||
|
||||
## 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).
|
||||
</details>
|
||||
|
||||
## 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.
|
||||
Modelos como este costumam rodar em servidores com centenas de gigabytes de memória de vídeo. A sua placa de vídeo tem
|
||||
12 a 24 GB. O Strata faz o modelo caber dividindo o trabalho entre o PC inteiro. Imagine uma biblioteca.
|
||||
|
||||
<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"), como 24.576 livros de consulta. Cada palavra precisa de apenas 10 deles.
|
||||
- **A sua placa de vídeo é a mesa.** Ela é pequena e rápida. Os cerca de 4.000 livros mais usados ficam abertos nela.
|
||||
- **A sua RAM são as estantes da sala de leitura.** Os 24.576 livros estão lá. Quando uma palavra precisa de um livro que não está na
|
||||
mesa, o seu processador lê da estante na hora, enquanto a placa continua trabalhando.
|
||||
- **O seu SSD é o arquivo.** Ele guarda uma tabela de consulta de 29 GB. O modelo lê algumas linhas dela por palavra, então ela não
|
||||
precisa ficar na RAM.
|
||||
|
||||
- **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/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>
|
||||
|
||||
<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>
|
||||
Um pequeno ajudante também adivinha as próximas palavras. O modelo grande confere todas de uma vez e fica com as certas. Você recebe
|
||||
a mesma resposta, de 1,6 a 1,8 vez mais rápido. Textos longos são lidos em blocos grandes, de até 8.192 tokens por vez, a mais de 1.000 tokens
|
||||
por segundo.
|
||||
|
||||
- **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.
|
||||
<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>
|
||||
|
||||
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).
|
||||
|
||||
## Algo deu errado?
|
||||
|
||||
- **Meu PC travou na primeira vez que o Strata iniciou.** Ele carrega o modelo na RAM. 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.** Abra o app ou o `strata launch` de novo, ou execute `START-HERE.bat` (ou
|
||||
`./setup.sh`) outra vez. Ele continua de onde parou.
|
||||
- **Está muito lento, a luz do disco não para de piscar, ou aparece "o mecanismo parou inesperadamente".** O seu PC não tem
|
||||
RAM livre suficiente. Feche outros programas (os navegadores usam muita), ou escolha um tamanho menor, como Q2_0 ou IQ2_XS.
|
||||
- **Diz que não há mecanismo para este PC.** No app, abra **Configurações, Mecanismo** e clique em **Atualizar mecanismo**. No
|
||||
Terminal, execute `strata update`. O `strata doctor` mostra quais arquivos do mecanismo estão instalados.
|
||||
- **Diz que a porta 8080 já está em uso.** O Strata já está rodando. Procure a janela dele.
|
||||
|
||||
Mais problemas e soluções: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Ainda sem resolver? Abra uma
|
||||
[issue](https://github.com/Niko1221/Strata/issues). No app, anexe o arquivo de **Configurações, Mecanismo, Diagnóstico, Ver
|
||||
registros, Salvar logs**. No Terminal, cole a saída de `strata doctor`. Se estiver rodando pelo código-fonte, anexe o `strata-<model>.log` da pasta do
|
||||
Strata. Para um problema de segurança, informe em particular: [SECURITY.md](SECURITY.md).
|
||||
|
||||
## 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)
|
||||
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), agentionai (Gyro)
|
||||
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)).
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). O Strata é de código aberto sob a [Licença MIT](LICENSE). Algumas partes e
|
||||
todos os modelos têm licenças próprias ([quais](docs/HOW_IT_WORKS.md#license)).
|
||||
|
||||
## Contribuindo
|
||||
|
||||
Encontrou um bug, ou quer enviar uma alteração ou um benchmark? Leia primeiro o [CONTRIBUTING.md](CONTRIBUTING.md). Ele diz o que
|
||||
incluir para que outras pessoas possam conferir o seu trabalho.
|
||||
|
||||
## Apoie o Strata
|
||||
|
||||
O Strata é gratuito e de código aberto. Se ele é útil para você, você pode apoiar o desenvolvimento:
|
||||
O Strata é gratuito e tem licença MIT. Eu o desenvolvo e testo em todo hardware que consigo arranjar. Se ele roda bem no seu PC, um café me ajuda
|
||||
a continuar testando placas novas e corrigindo o que você relatar. Sem pressão. Usar e relatar o que encontrar ajuda tanto quanto.
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
Quer patrocinar o Strata? Fale comigo pelo [GitHub](https://github.com/Niko1221).
|
||||
@@ -1,217 +1,381 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[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"><a href="README.md">English</a> · <b>简体中文</b> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<p align="center"><b>在你自己的游戏电脑上运行 1250 亿参数的 AI 模型</b><br>
|
||||
NVIDIA 或 AMD 显卡(12 GB 及以上)· Windows 或 Linux · 免费开源</p>
|
||||
<h3 align="center">在你自己的游戏电脑上运行最先进的模型</h3>
|
||||
|
||||
<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 上下文)·
|
||||
<p align="center">支持 NVIDIA、AMD、Intel · Windows、Linux、Mac · 免费开源</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>一座体素宝塔花园,由一条提示词一次生成,在装有 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 模型,通常要用服务器才能跑。它能聊天、写代码、看图片,还能配合你的应用和编程智能体一起工作。所有数据都留在你的电脑上。
|
||||
Strata 运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**。这是一个 1250 亿参数的模型,
|
||||
通常需要服务器才能跑,现在用普通电脑就行。它能聊天、写代码、看图片,也能配合你的应用和编程智能体使用。
|
||||
你的聊天记录和文件都留在你自己的电脑上。
|
||||
|
||||
## 速度有多快?
|
||||
**第一次用?** 先看[硬件要求](#硬件要求),再[安装应用](#方式-astrata-应用)。
|
||||
|
||||
我们在两台普通的游戏电脑上做了测试。一个 token 大约相当于 ¾ 个英文单词。
|
||||
- **游戏电脑就能跑。** 内存和显存加起来有 32 GB 就可以开始。
|
||||
- **能接入你的工具。** 支持 OpenAI 和 Anthropic 的 API,Claude Code、Codex CLI、OpenCode 等智能体都能连上。
|
||||
- **能看图片。** 默认开启。
|
||||
- **免费。** MIT 许可证,不用注册账号,不走云端。
|
||||
- **隐私。** 没有遥测。Strata 下载什么、什么时候联网、如何校验下载内容,都写在 [docs/SAFETY.md](docs/SAFETY.md)。
|
||||
|
||||
- **写回答:** 短对话中回复出现的速度。每秒 60 个 token 已经比你的阅读速度还快。
|
||||
- **读提示:** 读入你发送内容的速度(这里是一份 32K token 的文档、代码或聊天记录)。
|
||||
版本 0.2.3 · 引擎 0.1.48
|
||||
|
||||
<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 |
|
||||
**内存和显存(VRAM)加起来至少要有 32 GB。** Mac 和 Ryzen AI Max 上指的是统一内存。内存越大,能跑的模型越大,
|
||||
或者速度越快。
|
||||
|
||||
</td><td>
|
||||
- 32 GB 内存加一张 12-16 GB 显存的显卡,可以跑 Coder。
|
||||
- 64 GB 内存可以跑所有尺寸。
|
||||
|
||||
| 规格 | 写回答 | 读提示 |
|
||||
| --- | ---: | ---: |
|
||||
| **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 的最新显卡驱动。 |
|
||||
| **NVIDIA** GeForce RTX 20、30、40、50 系列(12 GB 及以上) | 安装应用或终端版即可。 |
|
||||
| **AMD** Radeon RX 6800 及更新型号、Radeon AI PRO R9700(12 GB 及以上) | [AMD 显卡](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md)、[Intel 引擎](docs/INTEL.md) |
|
||||
| **Apple 芯片 Mac**(M1 及更新,macOS 15 及以上) | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max(Strix Halo)** 及其他统一内存系统 | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| 较旧的显卡和处理器 | [旧显卡](docs/OLDER_GPUS.md)、[旧处理器](docs/INSTALL.md#older-cpus-experimental) |
|
||||
|
||||
其他的都由安装程序搞定。两到三张显卡可以分担同一个模型([多 GPU](docs/MULTI_GPU.md))。
|
||||
另外还需要:
|
||||
|
||||
实验性支持,由社区成员在自己的机器上编写和测试:
|
||||
- 约 80 GB 的可用磁盘空间(用固态硬盘,首次启动会快很多)
|
||||
- Windows 10 / 11、Linux 或 macOS
|
||||
- 较新的显卡驱动
|
||||
|
||||
- **较老的显卡**(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)。
|
||||
同一品牌的两三张显卡可以共同运行一个模型([多显卡](docs/MULTI_GPU.md))。Strata 不会把一个模型拆到 NVIDIA 和 AMD
|
||||
显卡上。完整列表见 [docs/INSTALL.md](docs/INSTALL.md#what-you-need)。
|
||||
|
||||
## 安装
|
||||
|
||||
### 让你的 AI 帮你装
|
||||
三种方式任选一种。它们启动的是同一个引擎,用的是同样的模型。
|
||||
|
||||
你在用 AI 编程助手吗(Claude Code、Cursor、Codex、GitHub Copilot 等)?把下面这段粘贴给它:
|
||||
### 方式 A:Strata 应用
|
||||
|
||||
最简单的方式。应用内置引擎:安装、选模型、开始聊天。请根据你的系统和显卡,从
|
||||
[发布页](https://github.com/Niko1221/Strata/releases/latest)下载对应的文件。不确定选哪个?看表格下面那一段。
|
||||
|
||||
| 系统 | 你的显卡 | 要下载的文件 |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX,驱动 580 或更新 | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
|
||||
| | NVIDIA,较旧的显卡,或驱动低于 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 系列或 Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 系列或 Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX,驱动 580 或更新 | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA,较旧的显卡,或驱动低于 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 系列或 Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 系列 | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac**(Apple 芯片,macOS 15 及以上) | 内置 | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
|
||||
|
||||
**不确定时:** NVIDIA 显卡的驱动版本在 580 及以上就选 `cuda13`,否则选 `cuda12`。`cuda12` 适用于较旧的显卡和驱动。
|
||||
`hip` 和 `amd` 的文件是给 AMD 显卡用的。其他 AMD 显卡(RX 6000)目前还没有现成的应用,
|
||||
请看 [AMD 显卡](docs/AMD_HIP.md)。
|
||||
|
||||
然后:
|
||||
|
||||
- **Windows:** 运行 `.exe`。它只为当前用户安装,不需要管理员权限。如果 Windows 提示“Windows 已保护你的电脑”,
|
||||
点击**更多信息**,再点**仍要运行**(安装程序还没有代码签名)。
|
||||
- **Linux:** 给文件加上可执行权限(`chmod +x`),然后启动。需要 FUSE 2:`sudo apt install libfuse2`(Ubuntu 22.04),
|
||||
或 `libfuse2t64`(Ubuntu 24.04 及更新)。
|
||||
- **Mac:** 打开 `.dmg`,把 Strata 拖到“应用程序”。
|
||||
|
||||
第一个界面里,应用会检测你的电脑,并推荐合适的模型。点击安装即可。模型很大,要下载约 40-70 GB,中断后会从断点继续。
|
||||
关闭窗口会同时停止 Strata 和引擎。
|
||||
|
||||
### 方式 B:终端(一行命令)
|
||||
|
||||
终端版就是 `strata` 命令,适合脚本、服务器和 AI 智能体。
|
||||
|
||||
Linux 和 Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
```
|
||||
|
||||
Windows(PowerShell):
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
脚本会检查你的电脑,下载终端版,校验 SHA-256,然后解压到你的用户目录。不需要管理员权限。在 Windows 上,装完后请
|
||||
打开一个新的终端窗口。然后启动 Strata:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
它会一次问一个问题:模型、上下文、图片、允许谁连接。接着为你的显卡下载引擎(已签名,使用前会先试启动),再下载
|
||||
模型,并开始提供服务。在脚本和 AI 智能体里,请使用参数:
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
如果想在运行前先看看安装脚本,先用 `curl -fsSL <address> -o install.sh` 下载,读完后再用 `sh install.sh` 运行。
|
||||
所有界面和参数见 [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。
|
||||
|
||||
### 方式 C:让你的 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。
|
||||
它会检查你的显卡、内存和磁盘,选出合适的模型,完成安装和启动,并告诉你怎么连接你的应用。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`**。
|
||||
## 模型
|
||||
|
||||
**Mac(Apple 芯片,M1 到 M5,macOS 15 或更新版本):** 先安装一次 Apple 的 Command Line Tools(在终端里运行 `xcode-select --install`;不需要 Xcode 本身)。然后在 Strata 文件夹里打开终端,运行 **`./setup.sh`**。它会在你的 Mac 上编译 Mac 版引擎(大约 7 分钟),问同样的问题,下载模型并启动。下次运行 `./setup.sh` 会马上启动。哪个大小适合你的 Mac、速度如何:[docs/MAC.md](docs/MAC.md)。
|
||||
应用和 `strata launch` 会按你的内存推荐模型。它是同一个模型的几种尺寸,压缩程度不同。尺寸小的更快,尺寸大的稍微更聪明一点。
|
||||
|
||||
NVIDIA 和 AMD 的步骤完全一样。安装程序会识别你的显卡,并装好对应的引擎。它会问你几个问题:
|
||||
### 内存不够?试试 Gyro
|
||||
|
||||
- 用哪个模型、哪个规格,
|
||||
- 上下文多大(模型能记住多少文字),
|
||||
- 是否要识别图片。
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)(作者 agentionai)是运行这个模型占用最小的方式。
|
||||
|
||||
每次直接按回车就是推荐选项。之后它会下载模型(约 70 GB)并启动。如果下载中断了,再运行一次即可:它会从中断的地方继续。
|
||||
浏览器会打开 Strata 应用,地址是 `http://127.0.0.1:8080`。
|
||||
| 尺寸 | 所需内存(RAM + VRAM) | 下载大小 | 说明 |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27.6 GB | 58.5 GB | 给其他尺寸都放不下的电脑。质量低于 IQ2_XS。 |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | 质量约相当于 IQ3_XXS,显存占用少约 20%。输出回答比 Q2_0 慢,12 GB 的 RTX 3060 上慢 16%,R9700 上慢 21%。 |
|
||||
|
||||
> **模型启动时,你的电脑可能会变慢或卡住 1-3 分钟**(第一次最久)。
|
||||
> Strata 会把 35-55 GB 加载到内存,并为显卡锁定其中一部分。这是正常的。请耐心等待,不要关闭窗口。
|
||||
> 窗口里会显示 Strata 正在做什么。
|
||||
Gyro 可在 NVIDIA 和 AMD 显卡(AMD 为实验性支持)以及 Linux 上的 Intel Arc 显卡(已在 Arc A750 上测试)上运行,自 0.2.2 起也可在 Apple Silicon 的 Mac 上运行(32 GB 的 Mac mini 上 Gyro-S 为 9 token/秒,专家权重从 SSD 读取)。详情和测试数据:
|
||||
[docs/GYRO.md](docs/GYRO.md)。用 `strata launch --model gyro-s` 启动。
|
||||
|
||||
**下次使用时**,再运行一次 `START-HERE.bat`(或 `./setup.sh`)。它会马上启动,已下载的东西不会重复下载。关闭它的窗口就能停止模型。
|
||||
`UPDATE.bat`(`./update.sh`)只更新 Strata,不启动。更新、Docker、多张显卡、文件存放位置以及所有选项:
|
||||
[docs/INSTALL.md](docs/INSTALL.md)。
|
||||
### 其他尺寸
|
||||
|
||||
## 该选哪个模型
|
||||
|
||||
安装程序会根据你的内存推荐一个。同一个模型有几种规格,压缩程度不同。规格越小越快,越大越聪明一些。
|
||||
|
||||
| 你的内存 | 选择 | 原因 |
|
||||
| 你的内存(RAM + VRAM) | 选择 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| **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) | 开着其他程序也能放下最大的规格 |
|
||||
| **32 GB** | **Gyro-S**,写代码可选 **[Coder](docs/MODELS.md#coder)** | Coder 需要 32 GB 内存,只保留一半的专家,所以代码以外的能力较弱 |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB),或 **Gyro-M**(35 GB) | Swift 回答前思考的时间更短。Gyro-M 的质量接近 IQ3_XXS,但输出回答比 Q2_0 慢 16-21% |
|
||||
| **48 GB** | **IQ3_XXS**(47.0 GB) | 更慢,更好 |
|
||||
| **64 GB** | **IQ3_S**(54.8 GB),原版或 Swift 1.5 | 最慢,最好:在公开测试中与完整模型持平 |
|
||||
| **96 GB 及以上** | Unsloth 的 **UD-IQ4_XS**(约 72 GB 内存)到 **Q8_0**(约 136 GB),4 到 8 位 | 更接近完整模型。内存不足时,Strata 会从固态硬盘读取一部分,速度会慢很多 |
|
||||
|
||||
- **[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、UD-Q5_K_XL、UD-Q6_K_XL 和 Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** 4 到 8 位,更接近完整模型(下一个 token
|
||||
与 Q8_0 相同的比例为 93-95%)。所有专家都能放进内存时速度很快(UD-Q4_K_XL 约需 88 GB,Q8_0 约需 136 GB);内存不够时,
|
||||
Strata 回答时要从 SSD 读取一部分,会慢很多(64 GB 的电脑上 UD-Q4_K_XL 每秒 7-8.5 个 token)。
|
||||
- **[OrcaRouter 的 Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** 需要手动设置,不在安装程序的菜单里。
|
||||
内存比这一行写的少,也可能能用:Strata 会自动切换到[低内存模式](docs/MODELS.md#a-big-graphics-card-and-little-ram)。
|
||||
速度会变慢,显卡较小时会慢得多。
|
||||
|
||||
规格、下载以及各配置能装下什么:[docs/MODELS.md](docs/MODELS.md)。以后想再添加模型,运行
|
||||
`SETUP.bat`(Linux:`./setup.sh --setup`)。
|
||||
更多选择:
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** 是微调版本,回答前思考的时间更短,质量和内存占用大致相同。
|
||||
- **[NVFP4](docs/NVFP4.md)** 是 NVIDIA 为 NVIDIA 显卡提供的 4 位版本。要把所有专家放进内存,需要约 80 GB 内存。
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** 是另一个更小的模型,能看图片,还能同时回答多个请求。
|
||||
- **[OrcaRouter 的 Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** 需要手动设置。
|
||||
|
||||
所有尺寸、下载大小,以及各种配置能跑什么:[docs/MODELS.md](docs/MODELS.md)。
|
||||
|
||||
## 速度如何?
|
||||
|
||||
以下数据基于 12 GB 显卡(RTX 5070、Ryzen 5 7600、64 GB 内存)。其他显卡的结果见:
|
||||
[各模型的速度](docs/MODELS.md#how-fast-is-each-size)、[社区测试结果](docs/COMMUNITY_BENCHMARKS.md)。
|
||||
|
||||
| 模型 | 回答输出速度 | 读取提示词速度 |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
|
||||
一个 token 约等于 ¾ 个英文单词,所以每秒 60 个 token 比你阅读的速度还快。“读取提示词”指 Strata 吸收长文档、代码或聊天历史的速度。
|
||||
|
||||
在 16 GB 的 AMD 显卡上测得的结果(RX 9070 XT、Ryzen 9 3900X、47 GB 内存):
|
||||
|
||||
| 模型 | 回答输出速度 | 读取提示词速度 |
|
||||
| --- | --- | --- |
|
||||
| **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 |
|
||||
|
||||
两张表的进度条使用同一个比例。显存更大的显卡更快:RTX 3090(24 GB)每秒大约能输出 100-140 个 token。每个数字对应的引擎版本见
|
||||
[DETAILS.md](docs/DETAILS.md#speed-measured)。
|
||||
|
||||
## 使用方法
|
||||
|
||||
<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 分钟。之后的消息几秒内就开始回答。
|
||||
应用包含**聊天**、显示模型以及 GPU、CPU 和内存状态的**监控**、**模型**页面和**设置**。聊天可以在你电脑上的某个文件夹里工作。
|
||||
在**手动**模式下,修改文件或运行命令之前它会先询问你。输入 `/loop 10m <要做的事>`,在聊天保持打开期间每 10 分钟重复发送一次消息。
|
||||
|
||||
更多:[聊天记录存在哪里](docs/INSTALL.md#where-things-are-stored)、[API](docs/DETAILS.md#using-it)。
|
||||
聊天可以用 DuckDuckGo(或 Bing)搜索网页,不需要账号。你也可以在**设置,工具**里添加搜索密钥。搜索是唯一会把内容发出电脑的功能,
|
||||
发出的就是搜索词。
|
||||
|
||||
### 可选:整个模型都能放进内存时,单卡再提速
|
||||
要同时运行两个模型,请打开**运行多个模型**,并为每个模型分配同一品牌的单独一张显卡。
|
||||
|
||||
两个可选设置(来自 RubenTorney 的 #1904 和 #1905)。适用于只用一张显卡、并且内存能放下所有专家的情况(Strata 的 arena 模式,例如 64 GB 电脑上的 Q2_0 或 IQ3_XXS)。打开 Strata 数据文件夹里的配置文件 `strata-<model>.json`([在哪里](docs/INSTALL.md#where-things-are-stored)),加入:
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>Strata 应用中的一次对话:智能体用一个 HTML 文件写出了一个小型体素宝塔花园,页面在对话旁的预览中打开</sub></p>
|
||||
|
||||
- **生成更快:** 在 `"args"` 列表里加 `"--adapt-async", "1"`。专家缓存改在辅助线程里更新,不再暂停回答。在 RTX 5070(Windows,Q2_0)上实测:生成 +4-5%,代码和长文档 +7-10%。回答可能与默认设置略有不同(但每次运行都一样)。
|
||||
- **Windows 上读取提示更快:** 在 `"env"` 块里加 `"STRATA_PIN_BY_NEED": "1"`。Windows 只能为显卡锁定大约一半的内存;这个设置会先锁定显卡真正需要的专家。在 RTX 5070 上实测:读取提示 +4.4%。
|
||||
### 在终端里
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<sub>模型回复时的 <code>strata monitor</code>:速度、GPU、内存和最近的请求</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # 引导界面:模型、上下文、图片、访问方式,然后开始服务
|
||||
strata launch --model q2_0 --yes --no-tui # 不提问:开始服务,打印地址,然后返回
|
||||
strata launch --plan --model q2_0 # 显示将要执行的操作;不做任何更改
|
||||
strata launch --status # 当前正在提供什么服务
|
||||
strata launch --stop # 停止服务
|
||||
strata monitor --url http://127.0.0.1:8080 # 查看速度、GPU、内存和请求
|
||||
strata update --check # 有新引擎吗?(strata update 会安装它)
|
||||
strata doctor # 检查已安装的引擎文件
|
||||
```
|
||||
|
||||
`strata --help` 会列出所有命令。启动和提供服务时不会下载任何东西。只有安装、`strata launch`(在你选择模型时)和
|
||||
`strata update` 会下载。要从手机或另一台电脑访问 Strata,请使用 `strata launch --access lan`。它会生成一个 API 密钥,
|
||||
没有密钥就不会在网络上启动。除非你加上 `--open`,否则不会打开浏览器。指南:[docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。
|
||||
完整的客户端说明:[docs/TERMINAL.md](docs/TERMINAL.md)。
|
||||
|
||||
### 配合 AI 智能体
|
||||
|
||||
智能体用方式 B 中的一行命令安装 Strata,然后运行 `strata launch --model NAME --yes --no-tui`。步骤、参数和退出码见
|
||||
[docs/AI_SETUP.md](docs/AI_SETUP.md),仓库规则见 [AGENTS.md](AGENTS.md)。
|
||||
|
||||
### 连接你的其他应用
|
||||
|
||||
| 应用类型 | 基础 URL |
|
||||
| --- | --- |
|
||||
| 兼容 OpenAI(大多数应用和智能体) | `http://127.0.0.1:8080/v1`(API 密钥和模型名称随便填) |
|
||||
| Anthropic API(Claude Code 等) | `http://127.0.0.1:8080`,不带 `/v1`。这些应用会自己加上 `/v1/messages`。Claude Code:`ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API(Codex CLI) | `http://127.0.0.1:8080/v1/responses`([设置方法](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` 会打印 OpenCode、Codex CLI、Claude Code、oh-my-pi、pi、Hermes Agent 和 OpenClaw 的具体设置。
|
||||
`strata connect opencode --write` 会在询问后写入配置文件,并保留备份。
|
||||
|
||||
### 图片
|
||||
|
||||
能看图片的模型默认开启图片功能。在聊天里附加或粘贴图片,或者从你的应用发送图片。显卡的显存小于 24 GB 时,读图由处理器完成
|
||||
(每张图片几秒钟,不占用显卡的显存);显存达到 24 GB 时,由显卡完成。NVIDIA 和 AMD 的规则相同。你可以按模型关闭图片功能,
|
||||
也可以用 `--vision off`、`--vision gpu` 或 `--vision cpu` 选择读图方式。
|
||||
|
||||
这条规则背后的数据:在 Radeon AI PRO R9700 上,显卡读一张 260 token 的图片用了 57 ms,处理器用了 3.1 秒。在 16 GB 的显卡
|
||||
(Tesla P100 和 RTX A4000,Linux)上,用显卡读图会让文字速度下降 3-5%,因为它占用约 1.4 GB 显存,否则这部分会被模型使用。
|
||||
在 Mac 上,无论内存多大,都由 GPU 读图(32 GB 的 M4 上读一张 260 token 的图片用 0.45 秒,占用约 1.2 GB 共享内存);处理器读一张约需 50 秒,所以只有指定 `--vision cpu` 时才用。
|
||||
|
||||
### 使用须知
|
||||
|
||||
- 在聊天菜单或你的应用的“reasoning effort”里选择思考级别(**off、low、medium 或 high**)。off 最快。
|
||||
- Strata 默认一次回答一个请求。要同时处理多个,请在 `strata launch` 中使用 `--parallel N`,或在配置里写
|
||||
`"parallel": 2`([BATCHING.md](docs/BATCHING.md))。在 12 GB 显卡上,这会让每个回答变慢。
|
||||
- Strata 会完整读取聊天的第一条消息,每 30,000 个 token 约需 1 分钟。之后的消息几秒钟就开始。
|
||||
- 聊天记录存放的位置和完整的 API:[docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored)、[docs/DETAILS.md](docs/DETAILS.md#using-it)。
|
||||
|
||||
## 从源码构建
|
||||
|
||||
<details>
|
||||
<summary>从仓库构建 Windows、Linux 和 Mac 版本</summary>
|
||||
|
||||
[下载 Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip)并解压,或者用 `git clone` 获取。
|
||||
**Windows:** 双击 `START-HERE.bat`。**Linux:** 在 Strata 文件夹里运行 `./setup.sh`。
|
||||
|
||||
**Mac(Apple 芯片,macOS 15 及以上):** 先用 `xcode-select --install` 安装一次 Apple 的命令行工具(不需要完整的 Xcode)。
|
||||
在 Strata 文件夹里打开终端,运行 `./setup.sh`。它会在你的 Mac 上构建 Mac 引擎(约 7 分钟),问同样的问题,下载模型并启动。
|
||||
下次运行 `./setup.sh` 会直接启动。你的 Mac 适合哪个尺寸:[docs/MAC.md](docs/MAC.md)。
|
||||
|
||||
安装程序会找到你的显卡,并设置合适的引擎。它会问你用哪个模型和尺寸、上下文多长(模型能记住多少文字),以及是否要读取图片。
|
||||
直接按回车就是推荐的选择。然后它会下载模型(约 70 GB)并启动。如果下载中断,再运行一次就会继续。浏览器会打开
|
||||
`http://127.0.0.1:8080` 上的 Strata 页面。
|
||||
|
||||
> 模型启动期间,你的电脑可能会变慢,或者在 1-3 分钟内没有响应(第一次最久)。Strata 会把 35-55 GB 加载到内存里,并锁定其中一部分
|
||||
> 给显卡使用。请等待,不要关闭窗口。
|
||||
|
||||
下次再运行 `START-HERE.bat`(或 `./setup.sh`)会立即启动,不会重复下载。关闭它的窗口就能停止模型。`UPDATE.bat`
|
||||
(`./update.sh`)只更新 Strata,不启动。更新、Docker、多显卡、文件存放位置和所有选项:[docs/INSTALL.md](docs/INSTALL.md)。
|
||||
|
||||
要测试你的电脑,运行 `BENCH.bat`(`./bench.sh`)。需要 5-10 分钟,并生成一份可以分享的报告
|
||||
([方法](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark))。
|
||||
|
||||
**可选:整个模型都放得进内存时,让单卡更快。** 这是 RubenTorney 提供的两个需要手动开启的设置(#1904、#1905)。
|
||||
当你只用一张显卡,并且内存装得下所有专家时,它们会有帮助,例如在 64 GB 的电脑上跑 Q2_0 或 IQ3_XXS。打开 Strata 数据文件夹里的
|
||||
`strata-<model>.json`([位置](docs/INSTALL.md#where-things-are-stored)),然后添加:
|
||||
|
||||
- 在 `"args"` 列表中加入 `"--adapt-async", "1"`。这样专家缓存会在辅助线程上更新,不会让回答暂停。在 RTX 5070
|
||||
(Windows,Q2_0)上测得:解码快 4-5%,代码和长文档快 7-10%。回答可能与默认设置略有不同,但每次运行的结果保持一致。
|
||||
- 在 `"env"` 块中加入 `"STRATA_PIN_BY_NEED": "1"`(Windows)。Windows 只能为显卡锁定大约一半的内存,
|
||||
这个设置会优先锁定显卡最先需要的专家。在 RTX 5070 上测得:读取提示词快 4.4%。
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
修改后重启 Strata(`START-HERE.bat` 或 `./setup.sh`)。要撤销,删掉这两项即可。
|
||||
修改后请重启 Strata。要撤销,删除这两项即可。
|
||||
|
||||
</details>
|
||||
|
||||
## 工作原理
|
||||
|
||||
这类模型通常运行在有几百 GB 显存的服务器上。你的显卡只有 12-24 GB。Strata 把工作分给整台电脑,让模型装得下。
|
||||
可以把它想成一座图书馆。
|
||||
|
||||
- 模型是一个由 24,576 位小专家(“experts”)组成的团队,好比 24,576 本参考书。每个词只需要其中 10 本。
|
||||
- **你的显卡是书桌。** 它小而快。最常用的大约 4,000 本书摊开放在上面。
|
||||
- **你的内存是阅览室的书架。** 24,576 本书都在这里。某个词需要一本不在书桌上的书时,处理器会立刻从书架上取来读,显卡则继续工作。
|
||||
- **你的固态硬盘是档案室。** 里面存着一张 29 GB 的查找表。模型每个词只读其中几行,所以不用常驻内存。
|
||||
|
||||
<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>
|
||||
|
||||
另外有一个小助手会猜接下来的几个词。大模型一次检查所有猜测,保留正确的部分。得到的答案不变,但快了 1.6-1.8 倍。
|
||||
长文本按大块读取,一次最多 8,192 个 token,速度超过每秒 1,000 个 token。
|
||||
|
||||
<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>
|
||||
|
||||
更详细的解释:[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)。每个部分和它的数据:
|
||||
[详细说明](docs/DETAILS.md#how-it-works)和[论文](docs/paper/Strata-Paper.pdf)。
|
||||
|
||||
## 出问题了?
|
||||
|
||||
- **Strata 第一次启动时电脑卡死了。** 加载模型时这是正常的。请耐心等待,不要关闭窗口。
|
||||
10 分钟后还卡着?重启电脑,关掉其他程序再试一次,或者换一个更小的规格。
|
||||
- **下载或安装时中断了。** 再运行一次 `START-HERE.bat`(或 `./setup.sh`)。它会从中断的地方继续。
|
||||
- **非常慢,硬盘灯一直闪,或者提示“the engine stopped unexpectedly”。** 你的电脑可用内存不够。
|
||||
关掉其他程序(浏览器很占内存),或者换一个更小的规格(Q2_0 或 IQ2_XS)。
|
||||
- **第一次启动 Strata 时我的电脑卡住了。** 它在把模型加载到内存。请等待,不要关闭窗口。10 分钟后还是卡住?重启电脑,
|
||||
关闭其他程序后再试,或者选一个更小的尺寸。
|
||||
- **下载或安装时中断了。** 重新启动应用或 `strata launch`,或者再运行一次 `START-HERE.bat`(或 `./setup.sh`)。
|
||||
它会从中断的地方继续。
|
||||
- **非常慢、硬盘灯一直闪,或者提示“引擎意外停止”。** 你的电脑可用内存不够。关闭其他程序(浏览器很占内存),
|
||||
或者选一个更小的尺寸,比如 Q2_0 或 IQ2_XS。
|
||||
- **提示这台电脑没有可用的引擎。** 在应用里打开**设置,引擎**,点击**更新引擎**。在终端里运行 `strata update`。
|
||||
`strata doctor` 会显示已安装哪些引擎文件。
|
||||
- **提示 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)。
|
||||
更多问题和解决办法:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。还是解决不了?请提交
|
||||
[issue](https://github.com/Niko1221/Strata/issues)。在应用里,附上**设置,引擎,诊断,查看日志,保存日志**得到的文件。
|
||||
在终端里,粘贴 `strata doctor` 的输出。从源码运行时,附上 Strata 文件夹里的 `strata-<model>.log`。
|
||||
如果是安全问题,请私下报告:[SECURITY.md](SECURITY.md)。
|
||||
|
||||
## 致谢与许可证
|
||||
|
||||
模型是 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))。
|
||||
模型是 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)、agentionai(Gyro)
|
||||
和 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))。
|
||||
|
||||
## 参与贡献
|
||||
|
||||
发现了 bug,或者想提交修改或基准测试结果?请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。其中说明了需要附上哪些内容,
|
||||
方便其他人核实你的工作。
|
||||
|
||||
## 支持 Strata
|
||||
|
||||
Strata 免费且开源。如果它对你有用,欢迎支持它的开发:
|
||||
Strata 免费,采用 MIT 许可证。我在能拿到的各种硬件上开发和测试它。如果它在你的电脑上跑得不错,请我喝杯咖啡,能帮我继续测试新显卡、
|
||||
修复你们反馈的问题。不用有压力。使用它并反馈你的发现,同样很有帮助。
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
有兴趣赞助 Strata?请通过 [GitHub](https://github.com/Niko1221) 联系我。
|
||||
@@ -43,4 +43,53 @@ this PC only. The details and every setting are in [docs/DETAILS.md](docs/DETAIL
|
||||
last few prompts, whatever `"api_monitor"` says, to encode the next turn faster; no endpoint shows them, and they
|
||||
are gone when the server stops.
|
||||
|
||||
## The desktop app and the Terminal
|
||||
|
||||
Strata comes in three parts that share one engine: the **desktop app**, the **Terminal** (`strata`) and the **engine** that runs
|
||||
the model. The section above is about the engine's HTTP server. This is what the other two add.
|
||||
|
||||
- **The app's host listens on this PC only.** The app serves its page from a local host process on `127.0.0.1` (port 8791 unless
|
||||
changed). A request to its `/api/` endpoints that carries a browser `Origin` from any other site gets 403. The app shell holds a
|
||||
random secret (32 characters or more, compared in constant time) that is needed to quit the host or tell it which window is open,
|
||||
and the host gives its own helper processes a second random secret. By default the models the app starts are served on this PC only.
|
||||
- **Sharing is a separate, key-protected listener.** "Share with other devices" (and the Cloudflare tunnel) will not start until an
|
||||
API key is set. It opens a different listener that answers only `/v1/models`, `/v1/chat/completions` and `/v1/messages`, never the
|
||||
app's page or its host endpoints.
|
||||
- **App and Terminal talk through an owner channel.** One owner process per installation starts and ends the engine, and a second
|
||||
client (the Terminal) attaches to it. On macOS and Linux that is a private Unix socket in a folder only you can open. On Windows it
|
||||
is a TCP listener on `127.0.0.1` that answers only requests carrying a 256-bit token kept in an owner-only folder. That is a weaker
|
||||
guarantee than a socket other users cannot open: on Windows the token is what protects it, as with any loopback service that has a
|
||||
token (`strata_control/owner_transport.py`). The bridge between the app shell and the host (used for the app update) is also loopback and protected by a token.
|
||||
- **Starting a model and serving never download anything.** Downloads and updates are one explicit step: `strata update`, or "Update
|
||||
engine" and "Update Strata" in the app's Settings. Models and saved tuning are never replaced by an update.
|
||||
- **Updates come from a signed feed.** The release publishes `strata-feed.json` and `strata-feed.json.sig` (Ed25519). An installed
|
||||
Strata checks the signature against the public key it carries (`strata_control/update_keys.json`), refuses a feed older than the last
|
||||
one it accepted (a replayed old feed) or expired, and checks every engine package against its SHA-256, its signed manifest and the
|
||||
card in your PC before it test-starts it and switches to it. Any failure keeps the engine you had. The app installer itself is signed
|
||||
with a separate key (Tauri's minisign key) that the app and the Terminal both check. The private keys are kept offline by the owner.
|
||||
See [packaging/UPDATES.md](packaging/UPDATES.md).
|
||||
|
||||
### How far to trust `curl | sh` and `irm | iex`
|
||||
|
||||
The Terminal installs with one line:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
Piping a script into a shell runs whatever the server sends, so it deserves the same care as running any downloaded program.
|
||||
What the script does is short and written at its top: it fetches the Terminal archive named in the release's `install-manifest.txt`,
|
||||
checks its SHA-256, unpacks it into your home folder (no administrator rights, nothing outside your home), checks the release's signed feed
|
||||
with the Terminal it just unpacked and removes it again if that fails, and puts `strata` on your PATH.
|
||||
|
||||
Those checks have a limit. The checksum comes from the same release as the archive, and the public key that checks the feed arrives inside
|
||||
that archive, so for the first install all trust rests on HTTPS and on the `Niko1221/Strata` GitHub release itself. The checks catch a
|
||||
damaged or half-finished download and a feed that was not signed by the release key; they do not protect you if the release has been replaced
|
||||
by someone who controls it. Later updates are checked against the key already on your PC, which is a stronger position.
|
||||
|
||||
If that is not enough: save the script (`curl -fsSLO https://github.com/Niko1221/Strata/releases/latest/download/install.sh`), read it, and
|
||||
run it from the file; or install the desktop app or build from source instead and check the code you build.
|
||||
|
||||
Strata has not had an outside security audit yet.
|
||||
What Strata downloads, when it uses the internet, what stays on your PC and how downloads are checked is written up in [docs/SAFETY.md](docs/SAFETY.md).
|
||||
@@ -2,8 +2,13 @@
|
||||
rem Strata for Windows: the first run installs everything and starts the model; later runs just start it.
|
||||
rem Needs only an NVIDIA or AMD graphics driver. Python is installed for your user account if it is missing (no admin needed).
|
||||
setlocal
|
||||
rem Python reads and writes text as UTF-8, not in the Windows code page (cp1251, cp866, ...): a folder or user name with Cyrillic or other letters stays intact (#3202).
|
||||
set "PYTHONUTF8=1"
|
||||
title Strata
|
||||
cd /d "%~dp0"
|
||||
rem A bundled Python runtime (the python\ folder of the terminal archive) is used when it is there: nothing to find,
|
||||
rem install or download. SETUP.bat and UPDATE.bat run this file, so they use it too.
|
||||
if exist "python\python.exe" goto bundled
|
||||
if exist ".venv\Scripts\python.exe" goto run
|
||||
|
||||
call :findpy
|
||||
@@ -36,6 +41,14 @@ echo Could not create the Python environment in .venv
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:bundled
|
||||
set "PYTHONNOUSERSITE=1"
|
||||
set "PYTHONHOME="
|
||||
set "PYTHONPATH="
|
||||
"python\python.exe" setup.py %*
|
||||
if errorlevel 1 pause
|
||||
exit /b
|
||||
|
||||
:run
|
||||
".venv\Scripts\python.exe" setup.py %*
|
||||
if errorlevel 1 pause
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
rem Update Strata without starting the model (#475): the newest code (git pull, when this folder is a git clone), then
|
||||
rem what START-HERE.bat does before a start - the engine (a new one when this version needs it), the Python packages,
|
||||
rem each installed model's settings and draft subset. The model files are not touched. Start the model later with
|
||||
rem START-HERE.bat. Options are passed on to setup.py.
|
||||
rem START-HERE.bat. Options are passed on to setup.py. The bundled Python (python\ beside this file) is used when
|
||||
rem there is one: START-HERE.bat, which this file runs, picks it.
|
||||
setlocal
|
||||
title Strata - update
|
||||
cd /d "%~dp0"
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"name": "strata-desktop",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "strata-desktop",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "2.12.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.12.3.tgz",
|
||||
"integrity": "sha512-f7/LsY74SVb1ChmOPw1ajOpxii0GSzDAgmWHRci8JUYuuai04rXsSbFobpmB0sP2DdlvMD/B5ZotEn8GOGMRUg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
"tauri": "tauri.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.12.3",
|
||||
"@tauri-apps/cli-darwin-x64": "2.12.3",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.12.3",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.12.3",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.12.3",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.12.3",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.12.3",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.12.3",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.12.3",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.12.3",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.12.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.12.3.tgz",
|
||||
"integrity": "sha512-xmVNoJOOKzKLj7GLHXmXLimRby9M42aBIJa0sB80N7o24eKV5JiVmToKTRLVH9yhL05GZ/B6rSSOmbzNFJwBAA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.12.3.tgz",
|
||||
"integrity": "sha512-URd+0usfmUQQyb5kNPiKt2OT8gp7oGU7QR+qOppB6e35mpVBFFVNhWc9oWb3p0u71fDJ09DpF4T6m9xYxoIB8g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.12.3.tgz",
|
||||
"integrity": "sha512-0kwUVx2LbmOoTRlwF1oDww7th4/NSluv4ZBQ6KRZsFKg4g0TlTcIiJ0VehqkQYUeC9xIT1Mk6XtF52mI155VSQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.12.3.tgz",
|
||||
"integrity": "sha512-gKSGDx6fx+HBLQmG6guChFdisbrCSdx9Yli+j5teOa64gm1CeM+C6NLru5Pj3ZkkEep05BmCFjxvX+cjqAycZQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.12.3.tgz",
|
||||
"integrity": "sha512-Yr59qMy/gcvAfBuFdCGlAmGiLsYWMFDbsP4bAI6iNBzcJTGrYWZGeiQhGDo29MQwfF/NUBqyM2qljXu5tZ6wjQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.12.3.tgz",
|
||||
"integrity": "sha512-jKr0/REBMnLKC0OwPQZkyge0mKDaNZ0b/AbfGwVyCgFfwS730EVLfVDo3nt7MUu6jJaytRq08+xSj5qfy+U1+Q==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.12.3.tgz",
|
||||
"integrity": "sha512-gTkEqJAGCMROGanxHnzaJzQ+/rXl39HWVg0/hDODS1wDWBGqoceqGC6Qm1+mQbyT11xnsB8mKu/gyVID/YpFIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.12.3.tgz",
|
||||
"integrity": "sha512-T2dTxL1svRym71fZtaW4pZYXbthbfFsEJr1NeOUg/p7laWXZbUt/8DMSZf8recu9f7+1F/Lv00HtzsEu6vDO9w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.12.3.tgz",
|
||||
"integrity": "sha512-eoiQwpEZWbpsVHcM9BAzqqe4TCHszvr3rsM7pUQ4ofLK0lF1i1d/jX0Qo5DYMqcrUtwQuDdDuOCbcT0Izf1VwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.12.3.tgz",
|
||||
"integrity": "sha512-kE+KUg6Dbbkgvcw0m99wJVIToFQzJqpMBAXah74WqxaLzssNwSTUccrHp6+QVzMhjQP330tvekFmnQF+0NbE6Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.12.3.tgz",
|
||||
"integrity": "sha512-L7wndWAgX2mLW7alswpL3d1R0P9FN2WYBgHaQkAYEBxHHGkY4vFX9CjcRQ1LAxDAMIJxrY7/aDziFJeChS84NQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "strata-desktop",
|
||||
"productName": "Strata",
|
||||
"version": "0.2.3",
|
||||
"private": true,
|
||||
"description": "Strata Desktop - local AI, tuned to one model.",
|
||||
"scripts": {
|
||||
"build:ui": "npm --prefix ../host run build",
|
||||
"typecheck:ui": "npm --prefix ../host run typecheck",
|
||||
"test:ui": "npm --prefix ../host test",
|
||||
"tauri": "tauri",
|
||||
"dev": "npm run build:ui && tauri dev",
|
||||
"build": "npm run build:ui && tauri build",
|
||||
"build:app": "npm run build:ui && tauri build --bundles app",
|
||||
"dmg": "npm run build:ui && tauri build --bundles dmg"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "2.12.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The installed Windows app, closed every way, with a stub engine: nothing may be left running.
|
||||
|
||||
python apps/desktop/scripts/app-close-matrix.py --install <folder the installer wrote> [--scenarios all] [--report out.json]
|
||||
|
||||
For each case it starts the REAL installed `strata-desktop.exe` (its window opens), loads a stub engine through the host's API
|
||||
(profile + `runtime start` under Control's owner runtime, exactly the app's path), records every process of the tree (the app,
|
||||
the host, Control, the owner host, the server, the engine), ends the app one way, and asserts:
|
||||
* no process of the tree is left, the engine's port is free, the runtime lease can be taken again;
|
||||
* the engine got the clean stop where the way of closing allows one (normal close, window X);
|
||||
* the next load of the same slot works (a lost owner's state is cleaned, not a blocker).
|
||||
It also proves a second launch focuses the open window and starts nothing, and that one app runs two engines and closes both.
|
||||
Everything per-user lives under a scratch folder; the stub engine is a script. No real engine ever runs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from ctypes import wintypes
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import psutil # noqa: E402
|
||||
|
||||
from strata_control import procs, runtime # noqa: E402
|
||||
from tests.terminal.lifecycle_matrix import install_fake_engine, port_open # noqa: E402
|
||||
|
||||
if os.name != "nt":
|
||||
raise SystemExit("this matrix drives the Windows installer's app")
|
||||
|
||||
user32 = ctypes.WinDLL("user32", use_last_error=True)
|
||||
WM_CLOSE = 0x0010
|
||||
|
||||
|
||||
def main_window(pid: int) -> int | None:
|
||||
found = []
|
||||
|
||||
@ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
||||
def visit(hwnd, _):
|
||||
owner = wintypes.DWORD()
|
||||
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(owner))
|
||||
if owner.value == pid and user32.IsWindowVisible(hwnd):
|
||||
buf = ctypes.create_unicode_buffer(256)
|
||||
user32.GetWindowTextW(hwnd, buf, 256)
|
||||
if buf.value.strip() == "Strata":
|
||||
found.append(hwnd)
|
||||
return True
|
||||
user32.EnumWindows(visit, 0)
|
||||
return found[0] if found else None
|
||||
|
||||
|
||||
class Scratch:
|
||||
def __init__(self, root: Path, install: Path):
|
||||
self.root, self.install = root, install
|
||||
self.components = root / "components"
|
||||
self.models = root / "models"
|
||||
self.mark = root / "mark"
|
||||
for d in (self.components, self.models, self.mark, root / "local", root / "home", root / "appdata", root / "webview"):
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
install_fake_engine(self.components)
|
||||
from tools.strata_tokenizer import BYTE_TO_UNICODE
|
||||
tok = self.models / "tokenizer"
|
||||
tok.mkdir()
|
||||
tokens = [BYTE_TO_UNICODE[i] for i in range(256)] + ["ab", "<|im_start|>", "<|im_end|>", "<|endoftext|>"]
|
||||
(tok / "vocab.json").write_text(json.dumps({t: i for i, t in enumerate(tokens)}))
|
||||
(tok / "merges.txt").write_text("a b")
|
||||
(tok / "token_type.json").write_text(json.dumps([0] * 257 + [3, 3, 3]))
|
||||
for name in ("stub", "stub2"):
|
||||
(self.models / f"strata-{name}.json").write_text(json.dumps({
|
||||
"exe": "component:strata-engine", "backend": "cuda", "tokenizer": str(tok), "args": [], "cwd": str(root),
|
||||
"model_name": name, "host": "127.0.0.1", "port": 0}))
|
||||
self.env = {**os.environ, "STRATA_COMPONENTS_DIR": str(self.components), "STRATA_PROFILE_DIR": str(root / "profiles"),
|
||||
"STRATA_RUNTIME_DIR": str(root / "runtime"), "STRATA_INSTALL_DIR": str(self.models),
|
||||
"STRATA_MODELS_DIR": str(root / "models-unused"), "STRATA_ENGINE_BACKEND": "cuda",
|
||||
"STRATA_HOME": str(root / "home"), "APPDATA": str(root / "appdata"), "LOCALAPPDATA": str(root / "local"),
|
||||
"WEBVIEW2_USER_DATA_FOLDER": str(root / "webview"), "FAKE_ENGINE_MARK": str(self.mark),
|
||||
"FAKE_ENGINE_SLOW_STEPS": "0", "PYTHONDONTWRITEBYTECODE": "1"}
|
||||
self.env.pop("STRATA_API_KEY", None)
|
||||
for key in ("STRATA_COMPONENTS_DIR", "STRATA_PROFILE_DIR", "STRATA_RUNTIME_DIR", "LOCALAPPDATA", "STRATA_ENGINE_BACKEND",
|
||||
"STRATA_NO_ACQUIRE", "FAKE_ENGINE_MARK"):
|
||||
os.environ[key] = self.env.get(key, "1") # this process reads the same runtime and records folders
|
||||
|
||||
|
||||
class App:
|
||||
def __init__(self, scratch: Scratch):
|
||||
self.s = scratch
|
||||
self.proc = subprocess.Popen([str(scratch.install / "strata-desktop.exe")], cwd=scratch.install, env=scratch.env,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
self.base = None
|
||||
|
||||
def host(self):
|
||||
for child in psutil.Process(self.proc.pid).children(recursive=True):
|
||||
if child.name().lower() == "node.exe":
|
||||
return child
|
||||
return None
|
||||
|
||||
def wait_up(self, seconds=90):
|
||||
deadline = time.monotonic() + seconds
|
||||
while time.monotonic() < deadline:
|
||||
if self.proc.poll() is not None:
|
||||
raise RuntimeError(f"the app exited with {self.proc.returncode}")
|
||||
try:
|
||||
host = self.host()
|
||||
if host:
|
||||
for c in host.net_connections(kind="tcp"):
|
||||
if c.status == "LISTEN" and c.laddr.ip == "127.0.0.1":
|
||||
base = f"http://127.0.0.1:{c.laddr.port}"
|
||||
try:
|
||||
if urllib.request.urlopen(base + "/api/health", timeout=2).status == 200 and main_window(self.proc.pid):
|
||||
self.base = base
|
||||
return
|
||||
except OSError:
|
||||
pass
|
||||
except psutil.Error:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
raise RuntimeError("the app did not come up")
|
||||
|
||||
def call(self, path, method="GET", body=None):
|
||||
req = urllib.request.Request(self.base + path, method=method, data=None if body is None else json.dumps(body).encode(),
|
||||
headers={"content-type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=120) as r:
|
||||
return json.loads(r.read() or b"null")
|
||||
except urllib.error.HTTPError as exc:
|
||||
raise RuntimeError(f"{method} {path} -> {exc.code} {exc.read()[:300]!r}") from None
|
||||
|
||||
def load(self, model, multiple=False):
|
||||
self.call("/api/native?refresh=1")
|
||||
started = self.call("/api/native/operations", "POST", {"kind": "load", "args": {"model": model, **({"multiple": True} if multiple else {})}})
|
||||
deadline = time.monotonic() + 180
|
||||
while time.monotonic() < deadline:
|
||||
state = self.call("/api/native")
|
||||
job = next((j for j in state["jobs"] if j["id"] == started["id"]), None)
|
||||
if job and job["state"] != "running":
|
||||
if job["state"] != "done":
|
||||
raise RuntimeError(f"load {model}: {job['state']} {job.get('error')}")
|
||||
return state
|
||||
time.sleep(0.3)
|
||||
raise RuntimeError("load did not finish")
|
||||
|
||||
|
||||
def tree_of(app: App, slots=("app-1",)) -> tuple[list[procs.Ident], list[int]]:
|
||||
idents, ports = [], []
|
||||
members = [psutil.Process(app.proc.pid), *psutil.Process(app.proc.pid).children(recursive=True)]
|
||||
pids = {p.pid for p in members}
|
||||
for slot in slots:
|
||||
state = runtime._read_state(slot)
|
||||
if state:
|
||||
pids.add(state["host_pid"])
|
||||
for r in procs.records():
|
||||
if r.get("profile") in slots: # the app's own slots; a Terminal service is not part of the app's tree
|
||||
pids.add(r["pid"])
|
||||
for pid in pids:
|
||||
token = procs.start_token(pid)
|
||||
if token:
|
||||
idents.append(procs.Ident(pid, token, tuple(procs.command_line(pid) or [])))
|
||||
return idents, ports
|
||||
|
||||
|
||||
def survivors(idents):
|
||||
return [f"{i.pid} {' '.join(i.cmd)[:80]}" for i in idents if procs.start_token(i.pid) == i.token]
|
||||
|
||||
|
||||
def wait_gone(idents, seconds):
|
||||
deadline = time.monotonic() + seconds
|
||||
left = survivors(idents)
|
||||
while left and time.monotonic() < deadline:
|
||||
time.sleep(0.25)
|
||||
left = survivors(idents)
|
||||
return left
|
||||
|
||||
|
||||
def lease_free() -> bool:
|
||||
try:
|
||||
fd = runtime.acquire_activity_lease(shared=False)
|
||||
except Exception: # noqa: BLE001
|
||||
return False
|
||||
runtime._unlock_file(fd)
|
||||
os.close(fd)
|
||||
return True
|
||||
|
||||
|
||||
def control(scratch: Scratch, *argv: str, timeout: int = 300) -> dict:
|
||||
py = scratch.install / "control" / "python" / "python.exe"
|
||||
out = subprocess.run([str(py), str(scratch.install / "control" / "strata.py"), *argv, "--output", "json"],
|
||||
env={**scratch.env, "PYTHONNOUSERSITE": "1", "STRATA_NO_ACQUIRE": "1"},
|
||||
capture_output=True, text=True, timeout=timeout)
|
||||
try:
|
||||
return json.loads(out.stdout)
|
||||
except ValueError:
|
||||
return {"ok": False, "raw": out.stdout[-300:] + out.stderr[-300:]}
|
||||
|
||||
|
||||
def stop_cli(scratch: Scratch, slot: str) -> dict:
|
||||
py = scratch.install / "control" / "python" / "python.exe"
|
||||
out = subprocess.run([str(py), str(scratch.install / "control" / "strata.py"), "runtime", "stop", "--profile", slot, "--wait", "30",
|
||||
"--output", "json"], env={**scratch.env, "PYTHONNOUSERSITE": "1", "STRATA_NO_ACQUIRE": "1"},
|
||||
capture_output=True, text=True, timeout=120)
|
||||
try:
|
||||
return json.loads(out.stdout)
|
||||
except ValueError:
|
||||
return {"state": "?", "raw": out.stdout[-200:] + out.stderr[-200:]}
|
||||
|
||||
|
||||
def case(name: str, install: Path, how: str) -> dict:
|
||||
with tempfile.TemporaryDirectory(prefix="strata-app-matrix-", ignore_cleanup_errors=True) as tmp:
|
||||
saved = dict(os.environ)
|
||||
scratch = Scratch(Path(tmp), install)
|
||||
app = App(scratch)
|
||||
problems, note = [], ""
|
||||
try:
|
||||
app.wait_up()
|
||||
slots = ("app-1", "app-2") if how == "two-engines" else ("app-1",)
|
||||
terminal = None
|
||||
if how == "window-x-terminal-engine":
|
||||
# an engine the Terminal started (its own profile, no tie to the app) must survive the app's close
|
||||
control(scratch, "profile", "create", "terminal-1", "--from-config", str(scratch.models / "strata-stub2.json"))
|
||||
terminal = control(scratch, "runtime", "start", "--profile", "terminal-1", "--wait", "60")
|
||||
if terminal.get("state") != "ready":
|
||||
raise RuntimeError(f"the Terminal's service did not start: {terminal}")
|
||||
terminal_pids = {r["pid"] for r in procs.records(profile="terminal-1")} | {runtime._read_state("terminal-1")["host_pid"]}
|
||||
state = app.load("stub")
|
||||
if how == "two-engines":
|
||||
state = app.load("stub2", multiple=True)
|
||||
engine_ports = [i["port"] for i in state["instances"]]
|
||||
idents, _ = tree_of(app, slots)
|
||||
kinds = sorted({("engine" if "engine" in " ".join(i.cmd) else "server" if "serve.server" in " ".join(i.cmd) else
|
||||
"owner" if "strata_control.runtime" in " ".join(i.cmd) else "app" if "strata-desktop" in i.cmd[0].lower()
|
||||
else "host" if "node" in i.cmd[0].lower() else "other") for i in idents})
|
||||
note = f"tree {len(idents)} processes: {', '.join(kinds)}; engine ports {engine_ports}"
|
||||
t0 = time.monotonic()
|
||||
if how == "normal-close":
|
||||
subprocess.run(["taskkill", "/PID", str(app.proc.pid)], capture_output=True)
|
||||
elif how in ("window-x", "two-engines", "window-x-terminal-engine"):
|
||||
hwnd = main_window(app.proc.pid)
|
||||
if not hwnd:
|
||||
raise RuntimeError("no main window")
|
||||
user32.PostMessageW(hwnd, WM_CLOSE, 0, 0)
|
||||
elif how == "kill-app":
|
||||
subprocess.run(["taskkill", "/F", "/PID", str(app.proc.pid)], capture_output=True)
|
||||
elif how == "host-crash":
|
||||
host = app.host()
|
||||
subprocess.run(["taskkill", "/F", "/PID", str(host.pid)], capture_output=True)
|
||||
elif how == "second-launch":
|
||||
second = subprocess.Popen([str(install / "strata-desktop.exe")], cwd=install, env=scratch.env,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
try:
|
||||
code = second.wait(timeout=30)
|
||||
except subprocess.TimeoutExpired:
|
||||
second.kill()
|
||||
problems.append("the second launch did not exit")
|
||||
code = None
|
||||
time.sleep(2)
|
||||
hosts = [p for p in psutil.process_iter(["name", "pid"]) if p.info["name"].lower() == "node.exe"
|
||||
and (p.parent() and p.parent().pid == app.proc.pid)]
|
||||
apps = [p for p in psutil.process_iter(["name"]) if p.info["name"].lower() == "strata-desktop.exe"]
|
||||
if app.proc.poll() is not None:
|
||||
problems.append("the first app went away")
|
||||
if len(apps) != 1:
|
||||
problems.append(f"{len(apps)} app processes (want 1)")
|
||||
if len(hosts) != 1:
|
||||
problems.append(f"{len(hosts)} hosts under the first app (want 1)")
|
||||
after, _ = tree_of(app, slots)
|
||||
new = [i for i in after if i.pid not in {x.pid for x in idents}]
|
||||
if new:
|
||||
problems.append(f"the second launch started: {[' '.join(i.cmd)[:60] for i in new]}")
|
||||
note += f"; second launch exit code {code}, app processes {len(apps)}, hosts {len(hosts)}"
|
||||
subprocess.run(["taskkill", "/PID", str(app.proc.pid)], capture_output=True)
|
||||
if how == "host-crash":
|
||||
# the shell and its window stay (nothing here restarts a dead host); the host, Control's owner, the server and
|
||||
# the engine must not
|
||||
idents = [i for i in idents if any(k in " ".join(i.cmd) for k in ("strata_control.runtime", "serve.server", "engine", "node.exe"))]
|
||||
left = wait_gone(idents, 90)
|
||||
if left:
|
||||
problems.append(f"processes left: {left}")
|
||||
for port in engine_ports:
|
||||
deadline = time.monotonic() + 10
|
||||
while port_open(port) and time.monotonic() < deadline:
|
||||
time.sleep(0.2)
|
||||
if port_open(port):
|
||||
problems.append(f"engine port {port} still open")
|
||||
if terminal is None and not lease_free():
|
||||
problems.append("lease still held")
|
||||
marks = sorted(p.name for p in scratch.mark.iterdir())
|
||||
if terminal is not None:
|
||||
status = control(scratch, "runtime", "status", "--profile", "terminal-1")
|
||||
alive_pids = [p for p in terminal_pids if procs.start_token(p)]
|
||||
if status.get("state") != "ready" or len(alive_pids) != len(terminal_pids):
|
||||
problems.append(f"closing the app disturbed the Terminal's service: {status.get('state')}, alive {alive_pids} of {sorted(terminal_pids)}")
|
||||
note += f"; the Terminal's service is {status.get('state')} after the app closed"
|
||||
stopped = control(scratch, "runtime", "stop", "--profile", "terminal-1", "--wait", "30")
|
||||
if stopped.get("state") != "stopped" or [p for p in terminal_pids if procs.start_token(p)]:
|
||||
problems.append(f"the Terminal's service did not stop on request: {stopped}")
|
||||
time.sleep(1)
|
||||
if not lease_free():
|
||||
problems.append("lease still held after the Terminal's service stopped")
|
||||
if how in ("normal-close", "window-x", "two-engines", "second-launch", "window-x-terminal-engine") and not ({"quit", "eof"} & set(marks)):
|
||||
problems.append(f"the engine was not stopped the clean way (marks {marks})")
|
||||
seconds = round(time.monotonic() - t0, 1)
|
||||
if how == "host-crash":
|
||||
subprocess.run(["taskkill", "/PID", str(app.proc.pid)], capture_output=True)
|
||||
note += "; the app shell stays open after a host crash (by design today)"
|
||||
# the slot must be usable again: stopping a lost owner's slot cleans its state; nothing blocks the next load
|
||||
for slot in slots:
|
||||
res = stop_cli(scratch, slot)
|
||||
already = "not managed or is stopped" in json.dumps(res) # nothing was left to stop: the clean outcome
|
||||
if res.get("state") not in ("stopped",) and not already:
|
||||
problems.append(f"runtime stop {slot} after the close: {res}")
|
||||
if runtime._read_state(slot) is not None:
|
||||
problems.append(f"state of {slot} left behind")
|
||||
return {"name": f"app-{how}", "ok": not problems, "problems": problems, "seconds": seconds, "marks": marks, "note": note}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"name": f"app-{how}", "ok": False, "problems": [f"{type(exc).__name__}: {exc}"] + problems, "note": note}
|
||||
finally:
|
||||
for p in psutil.process_iter(["name", "exe"]):
|
||||
try:
|
||||
if (p.info["exe"] or "").lower().startswith(str(install).lower()):
|
||||
p.kill()
|
||||
except psutil.Error:
|
||||
pass
|
||||
for r in procs.records():
|
||||
try:
|
||||
psutil.Process(r["pid"]).kill()
|
||||
except psutil.Error:
|
||||
pass
|
||||
os.environ.clear(); os.environ.update(saved)
|
||||
|
||||
|
||||
SCENARIOS = ["normal-close", "window-x", "window-x-terminal-engine", "kill-app", "host-crash", "second-launch", "two-engines"]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--install", required=True)
|
||||
ap.add_argument("--scenarios", default="all")
|
||||
ap.add_argument("--report")
|
||||
a = ap.parse_args()
|
||||
names = SCENARIOS if a.scenarios == "all" else a.scenarios.split(",")
|
||||
results = []
|
||||
for how in names:
|
||||
r = case(how, Path(a.install), how)
|
||||
results.append(r)
|
||||
print(("PASS " if r["ok"] else "FAIL ") + r["name"], r.get("seconds", ""), r.get("problems") or "", r.get("note", ""), flush=True)
|
||||
if a.report:
|
||||
Path(a.report).write_text(json.dumps({"platform": "windows", "engine": "fake", "results": results}, indent=1), encoding="utf-8")
|
||||
return 0 if all(r["ok"] for r in results) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* The Graphics cards choice of the app, end to end on a box with two cards, through the installed app layout (packaged host + bundled Control + a real
|
||||
* engine component): the model list carries the options (one rule, strata_control/gpu_fit.py), a model that fits one card recommends that card, then
|
||||
* load with ONE card -> answers, only that card holds the model; load with BOTH cards (the layer split) -> answers, both cards hold the model.
|
||||
*
|
||||
* node cards-e2e.mjs --install <folder with node/ host/ control/> --components <folder holding engine/<package>> --work <scratch> \
|
||||
* --family qwen --model IQ3_XXS --data <data folder that has the model files> [--smi "nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits"]
|
||||
*/
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readdirSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') || all[i + 1] === undefined ? true : all[i + 1]]] : acc), []));
|
||||
for (const need of ['install', 'components', 'work', 'model']) if (!args[need]) { console.error('missing --' + need); process.exit(2); }
|
||||
const install = resolve(args.install), work = resolve(args.work), components = resolve(args.components), family = String(args.family ?? 'qwen'), modelId = String(args.model);
|
||||
const node = join(install, 'node', 'node'), hostEntry = join(install, 'host', 'server', 'index.mjs');
|
||||
const dirs = { models: join(work, 'models'), profiles: join(work, 'profiles'), runtime: join(work, 'runtime'), home: join(work, 'home'), xdg: join(work, 'xdg'), appdata: join(work, 'appdata') };
|
||||
for (const d of [dirs.models, dirs.home, dirs.xdg, dirs.appdata]) mkdirSync(d, { recursive: true });
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
let failed = false;
|
||||
const record = (name, ok, detail = '') => { if (!ok) failed = true; console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' ' + String(detail).slice(0, 500) : ''}`); };
|
||||
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
|
||||
const env = { ...process.env, STRATA_COMPONENTS_DIR: components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime, STRATA_INSTALL_DIR: dirs.models,
|
||||
STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(work, 'models-unused'), APPDATA: dirs.appdata, XDG_CONFIG_HOME: dirs.xdg, PYTHONDONTWRITEBYTECODE: '1' };
|
||||
delete env.STRATA_API_KEY;
|
||||
const port = await freePort();
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const host = spawn(node, [hostEntry], { cwd: join(install, 'host'), stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...env, STRATA_PORT: String(port), STRATA_DIST: join(install, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1',
|
||||
STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()) } });
|
||||
let hostLog = '';
|
||||
host.stdout.on('data', c => { hostLog += c; }); host.stderr.on('data', c => { hostLog += c; });
|
||||
const call = async (path, method = 'GET', body) => {
|
||||
const response = await fetch(base + path, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
||||
return { status: response.status, body: await response.json().catch(() => null) };
|
||||
};
|
||||
async function waitJob(id, seconds) {
|
||||
let last = '';
|
||||
for (let i = 0; i < seconds * 2; i++) {
|
||||
const state = (await call('/api/native')).body;
|
||||
const job = state.jobs.find(j => j.id === id);
|
||||
if (job && job.summary !== last) { last = job.summary; console.log(' ... ' + String(last).slice(0, 120)); }
|
||||
if (job && job.state !== 'running') return { job, state };
|
||||
await delay(500);
|
||||
}
|
||||
throw new Error('operation did not finish');
|
||||
}
|
||||
const smi = () => {
|
||||
const cmd = String(args.smi ?? 'nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits').split(' ');
|
||||
const r = spawnSync(cmd[0], cmd.slice(1), { encoding: 'utf8' });
|
||||
return (r.stdout ?? '').split('\n').map(s => Number(s.trim())).filter(Number.isFinite);
|
||||
};
|
||||
async function chatPong(instance) {
|
||||
const chat = await call('/v1/chat/completions', 'POST', { model: instance?.apiModelId ?? modelId.toLowerCase(), max_tokens: 400, stream: false, temperature: 0, messages: [{ role: 'user', content: 'Reply with exactly one word: pong' }] });
|
||||
const m = chat.body?.choices?.[0]?.message ?? {};
|
||||
return { ok: chat.status === 200 && /pong/i.test(`${m.content ?? ''}`), text: `${m.content ?? ''}`.slice(0, 60), status: chat.status };
|
||||
}
|
||||
async function main() {
|
||||
let up = false;
|
||||
for (let i = 0; i < 200 && !up; i++) { try { up = (await fetch(base + '/api/health')).ok; } catch { /* not yet */ } if (!up) await delay(150); }
|
||||
record('host healthy', up, up ? base : hostLog.slice(-500));
|
||||
if (!up) return;
|
||||
const packages = existsSync(join(components, 'engine')) ? readdirSync(join(components, 'engine')) : [];
|
||||
record('an engine component is installed', packages.length > 0, packages.join(', '));
|
||||
|
||||
// ---- the model list carries the choice
|
||||
const native = (await call('/api/native?refresh=1')).body;
|
||||
const gpus = native.hardware?.gpus ?? [];
|
||||
record('the host sees two or more graphics cards', gpus.filter(g => g.usable !== false).length >= 2, JSON.stringify(gpus.map(g => ({ i: g.index, n: g.name, gb: g.vram_gb }))));
|
||||
const size = native.catalog?.families?.find(f => f.family === family)?.sizes.find(s => s.model === modelId);
|
||||
const opt = size?.gpu_options;
|
||||
record('the model list offers the cards choice for this model', !!opt && opt.options.length >= 2, JSON.stringify(opt?.options?.map(o => ({ key: o.key, cards: o.cards, verdict: o.verdict, rec: o.recommended }))));
|
||||
record('a model that fits one card recommends the single largest card (the split is offered, not forced)', opt?.recommended === 'single' && opt.options.find(o => o.key === 'split'), `recommended=${opt?.recommended}`);
|
||||
const single = opt?.options.find(o => o.key === 'single')?.cards ?? [0];
|
||||
const both = opt?.options.find(o => o.key === 'split')?.cards ?? [0, 1];
|
||||
|
||||
// ---- Download and prepare with the recommended card
|
||||
const values = { family, model: modelId, context: 32768, vision: 'no', gpu: single[0], ...(args.data ? { data_dir: resolve(args.data) } : {}) };
|
||||
const started = await call('/api/native/operations', 'POST', { kind: 'install', args: values });
|
||||
record('the install operation starts', started.status === 200 || started.status === 202, JSON.stringify(started.body).slice(0, 160));
|
||||
const done = await waitJob(started.body.id, 3600);
|
||||
record('Download and prepare finishes', done.job.state === 'done', `${done.job.state} ${done.job.error ?? done.job.summary}`);
|
||||
const installed = (await call('/api/native?refresh=1')).body.installed?.models?.find(m => String(m.model).toLowerCase() === modelId.toLowerCase());
|
||||
const model = installed?.model ?? modelId.toLowerCase();
|
||||
|
||||
for (const [label, cards] of [['ONE card', single], ['BOTH cards (layer split)', both]]) {
|
||||
const before = smi();
|
||||
const load = await call('/api/native/operations', 'POST', { kind: 'load', args: { model, gpus: cards } });
|
||||
const loaded = await waitJob(load.body.id, 900);
|
||||
const instance = loaded.state.instances[0];
|
||||
record(`load with ${label} starts the engine`, loaded.job.state === 'done' && instance?.status === 'ready', `${loaded.job.state} ${loaded.job.error ?? ''}; instance=${instance?.status}; gpus=${JSON.stringify(instance?.gpus)}`);
|
||||
record(`the instance runs on exactly the chosen card(s)`, JSON.stringify([...(instance?.gpus ?? [])].sort()) === JSON.stringify([...cards].sort()), `want ${JSON.stringify(cards)} got ${JSON.stringify(instance?.gpus)}`);
|
||||
await delay(3000);
|
||||
const used = smi();
|
||||
const holding = used.map((mb, i) => (mb - (before[i] ?? 0)) > 1000 ? i : -1).filter(i => i >= 0);
|
||||
record(`only the chosen card(s) hold the model (graphics memory in use: ${used.join(', ')} MiB)`, JSON.stringify(holding) === JSON.stringify([...cards].sort()), `holding ${JSON.stringify(holding)}`);
|
||||
const pong = await chatPong(instance);
|
||||
record(`it answers (${label})`, pong.ok, `${pong.status} ${JSON.stringify(pong.text)}`);
|
||||
const unloaded = await call(`/api/native/instances/${instance?.id}/unload`, 'POST', {});
|
||||
record(`unload (${label})`, unloaded.status === 200, unloaded.status);
|
||||
await delay(4000);
|
||||
}
|
||||
}
|
||||
try { await main(); } catch (e) { record('run', false, e.stack ?? e); }
|
||||
host.kill();
|
||||
await delay(1500);
|
||||
console.log(failed ? 'CARDS-E2E FAIL' : 'CARDS-E2E PASS');
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,72 @@
|
||||
# Fetch and verify the private Node runtime the Windows desktop bundle ships (win-x64).
|
||||
#
|
||||
# The PowerShell twin of fetch-node.sh: same pinned version, same SHA-256 pin, same refusal to
|
||||
# install anything whose digest does not match. Not committed output: node.exe is a ~90 MB binary.
|
||||
#
|
||||
# Usage:
|
||||
# powershell -NoProfile -ExecutionPolicy Bypass -File apps\desktop\scripts\fetch-node.ps1
|
||||
# ... -File fetch-node.ps1 -Check # verify what is already installed
|
||||
# ... -File fetch-node.ps1 -FromFile node.zip # verify and install a zip you already have (offline)
|
||||
#
|
||||
# The Node version must satisfy the App Host's own engines.node (>= 24.12.0).
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$Check,
|
||||
[string]$FromFile = '',
|
||||
[string]$Dest = ''
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$NodeVersion = if ($env:STRATA_NODE_VERSION) { $env:STRATA_NODE_VERSION } else { '24.12.0' }
|
||||
$Target = 'win-x64'
|
||||
# SHA-256 of node-v24.12.0-win-x64.zip from nodejs.org's SHASUMS256.txt; keep equal to fetch-node.sh.
|
||||
$Expected = '9c125f61ae947b52e779095830f9cac267846a043ef7192183c84016aaad2812'
|
||||
if (-not $Dest) { $Dest = Join-Path $PSScriptRoot '..\src-tauri\resources\node' }
|
||||
$Installed = Join-Path $Dest 'node.exe'
|
||||
|
||||
function Fail([string]$Message) { [Console]::Error.WriteLine("fetch-node: $Message"); exit 1 }
|
||||
|
||||
if ($Check) {
|
||||
if (-not (Test-Path -LiteralPath $Installed)) { Fail "not installed at $Installed" }
|
||||
$got = (& $Installed --version).Trim()
|
||||
Write-Output "fetch-node: $Installed is $got"
|
||||
if ($got -ne "v$NodeVersion") { Fail "expected v$NodeVersion" }
|
||||
exit 0
|
||||
}
|
||||
|
||||
$Archive = "node-v$NodeVersion-$Target.zip"
|
||||
$Work = Join-Path ([IO.Path]::GetTempPath()) ("strata-node-" + [Guid]::NewGuid().ToString('N'))
|
||||
New-Item -ItemType Directory -Path $Work | Out-Null
|
||||
try {
|
||||
$Zip = Join-Path $Work $Archive
|
||||
if ($FromFile) {
|
||||
Copy-Item -LiteralPath $FromFile -Destination $Zip
|
||||
} else {
|
||||
$Url = "https://nodejs.org/dist/v$NodeVersion/$Archive"
|
||||
Write-Output "fetch-node: $Url"
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
Invoke-WebRequest -UseBasicParsing -Uri $Url -OutFile $Zip
|
||||
}
|
||||
$Actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $Zip).Hash.ToLowerInvariant()
|
||||
if ($NodeVersion -ne '24.12.0') {
|
||||
# another version has no recorded digest: say so rather than implying a verification
|
||||
[Console]::Error.WriteLine("fetch-node: WARNING - no recorded digest for v$NodeVersion; this download is NOT authenticated.")
|
||||
[Console]::Error.WriteLine("fetch-node: computed sha256 $Actual - record it in this script and fetch-node.sh.")
|
||||
} elseif ($Actual -ne $Expected) {
|
||||
Fail "REFUSING - $Archive hashes $Actual, expected $Expected"
|
||||
} else {
|
||||
Write-Output 'fetch-node: sha256 verified against the recorded digest'
|
||||
}
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$zipFile = [IO.Compression.ZipFile]::OpenRead($Zip)
|
||||
try {
|
||||
$entry = $zipFile.GetEntry("node-v$NodeVersion-$Target/node.exe")
|
||||
if (-not $entry) { Fail "node.exe is not inside $Archive" }
|
||||
New-Item -ItemType Directory -Force -Path $Dest | Out-Null
|
||||
[IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $Installed, $true)
|
||||
} finally { $zipFile.Dispose() }
|
||||
Write-Output "fetch-node: installed at $Installed"
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $Work -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fetch and verify the private Node runtime the desktop bundle ships.
|
||||
#
|
||||
# The App Host is an ordinary Node program, and the packaging requirement is that an end user
|
||||
# supplies no runtime of their own. The desktop bundle therefore carries a pinned standalone Node
|
||||
# beside the host.
|
||||
#
|
||||
# It is NOT committed. It is a ~117 MB platform binary: committing it would bloat every clone, it
|
||||
# cannot be reviewed in a diff, and it is not source. This script is the reproducible substitute —
|
||||
# it downloads the official build for the target and refuses to install anything whose SHA-256 does
|
||||
# not match the recorded digest.
|
||||
#
|
||||
# Usage:
|
||||
# apps/desktop/scripts/fetch-node.sh # host platform
|
||||
# apps/desktop/scripts/fetch-node.sh --target linux-x64
|
||||
# apps/desktop/scripts/fetch-node.sh --check # verify what is already installed
|
||||
#
|
||||
# The Node version must satisfy the App Host's own `engines.node` (>= 24.12.0).
|
||||
set -euo pipefail
|
||||
|
||||
NODE_VERSION="${STRATA_NODE_VERSION:-24.12.0}"
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEST="$HERE/../src-tauri/resources/node"
|
||||
|
||||
# Published SHA-256 of each official nodejs.org archive, recorded so a tampered or truncated
|
||||
# download is refused rather than shipped. Add a row when bumping the version.
|
||||
# Taken from nodejs.org's own SHASUMS256.txt for v24.12.0, which is published over HTTPS beside
|
||||
# the archives. Recorded here so the fetch is verified against a value that does not come from the
|
||||
# same response as the payload.
|
||||
digest_for() {
|
||||
case "$1" in
|
||||
darwin-arm64) echo "319f221adc5e44ff0ed57e8a441b2284f02b8dc6fc87b8eb92a6a93643fd8080" ;;
|
||||
linux-x64) echo "6159227e0af7d7c3c6bb2fa900452b04a6cb8841a702a79acc613209d70b04d0" ;;
|
||||
linux-arm64) echo "9b2a2eeb98a8eb37361224e2a1d060300ad2dd143af58dfdb16de785df0f1228" ;;
|
||||
win-x64) echo "9c125f61ae947b52e779095830f9cac267846a043ef7192183c84016aaad2812" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
target() {
|
||||
if [ "${1:-}" = "--target" ]; then echo "${2:?--target needs a value}"; return; fi
|
||||
case "$(uname -s)-$(uname -m)" in
|
||||
Darwin-arm64) echo darwin-arm64 ;;
|
||||
Linux-x86_64) echo linux-x64 ;;
|
||||
Linux-aarch64) echo linux-arm64 ;;
|
||||
MINGW*|MSYS*|CYGWIN*) echo win-x64 ;;
|
||||
*) echo "unsupported host: $(uname -s)-$(uname -m)" >&2; exit 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
TARGET="$(target "$@")"
|
||||
EXE="node"
|
||||
case "$TARGET" in win-*) EXE="node.exe" ;; esac
|
||||
INSTALLED="$DEST/$EXE"
|
||||
|
||||
if [ "${1:-}" = "--check" ]; then
|
||||
if [ ! -x "$INSTALLED" ]; then echo "fetch-node: not installed at $INSTALLED" >&2; exit 1; fi
|
||||
got="$("$INSTALLED" --version)"
|
||||
echo "fetch-node: $INSTALLED is $got"
|
||||
case "$got" in "v$NODE_VERSION") exit 0 ;; *) echo "fetch-node: expected v$NODE_VERSION" >&2; exit 1 ;; esac
|
||||
fi
|
||||
|
||||
# The official archive for this target.
|
||||
case "$TARGET" in
|
||||
darwin-arm64) ARCHIVE="node-v$NODE_VERSION-darwin-arm64.tar.gz" ;;
|
||||
darwin-x64) ARCHIVE="node-v$NODE_VERSION-darwin-x64.tar.gz" ;;
|
||||
linux-x64) ARCHIVE="node-v$NODE_VERSION-linux-x64.tar.gz" ;;
|
||||
linux-arm64) ARCHIVE="node-v$NODE_VERSION-linux-arm64.tar.gz" ;;
|
||||
win-x64) ARCHIVE="node-v$NODE_VERSION-win-x64.zip" ;;
|
||||
*) echo "fetch-node: no archive known for $TARGET" >&2; exit 2 ;;
|
||||
esac
|
||||
URL="https://nodejs.org/dist/v$NODE_VERSION/$ARCHIVE"
|
||||
|
||||
EXPECTED="$(digest_for "$TARGET")"
|
||||
WORK="$(mktemp -d "${TMPDIR:-/tmp}/strata-node.XXXXXX")" # GNU mktemp needs the X template; BSD accepts it too
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
echo "fetch-node: $URL"
|
||||
curl -fsSL -o "$WORK/$ARCHIVE" "$URL"
|
||||
|
||||
ACTUAL="$(shasum -a 256 "$WORK/$ARCHIVE" 2>/dev/null | cut -d' ' -f1 || sha256sum "$WORK/$ARCHIVE" | cut -d' ' -f1)"
|
||||
if [ -n "$EXPECTED" ]; then
|
||||
if [ "$ACTUAL" != "$EXPECTED" ]; then
|
||||
echo "fetch-node: REFUSING — $ARCHIVE hashes $ACTUAL, expected $EXPECTED" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "fetch-node: sha256 verified against the recorded digest"
|
||||
else
|
||||
# Without a recorded digest the download cannot be authenticated. Say so loudly and keep the
|
||||
# computed value so it can be recorded, rather than implying a verification that did not happen.
|
||||
echo "fetch-node: WARNING — no recorded digest for $TARGET; this download is NOT authenticated." >&2
|
||||
echo "fetch-node: computed sha256 $ACTUAL — record it in $(basename "${BASH_SOURCE[0]}") to enable verification." >&2
|
||||
fi
|
||||
|
||||
mkdir -p "$DEST"
|
||||
case "$ARCHIVE" in
|
||||
*.zip) unzip -q -j "$WORK/$ARCHIVE" "node-v$NODE_VERSION-win-x64/node.exe" -d "$DEST" ;;
|
||||
*) tar xzf "$WORK/$ARCHIVE" -C "$WORK"
|
||||
cp "$WORK/node-v$NODE_VERSION-$TARGET/bin/node" "$DEST/node" ;;
|
||||
esac
|
||||
chmod +x "$DEST/$EXE"
|
||||
|
||||
echo "fetch-node: installed $("$DEST/$EXE" --version) at $DEST/$EXE"
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* The first run of the installed app, headless: the host as the Tauri shell starts it (STRATA_PACKAGED=1, STRATA_NO_ACQUIRE=1,
|
||||
* the staged Control, nothing installed, nothing in the per-user folders) and Chrome on its onboarding.
|
||||
*
|
||||
* node apps/desktop/scripts/first-run-ui.mjs [--control <staged control>] [--node <node.exe>] [--work <scratch>] [--shots <dir>]
|
||||
*
|
||||
* Checks, one line each (exit 1 if any fails):
|
||||
* the host answers; /api/native has a real catalog (families with sizes), a recommendation and the GPUs of this PC
|
||||
* (from Control's bundled installer on Control's Python); the page shows "Let's get started", and after it the model
|
||||
* list with sizes (no "couldn't show this screen"); no page error; the install plan for the recommended model is a
|
||||
* plan (nothing is downloaded or started: a plan changes nothing); the spy sees no pip/git/compiler/engine fetch.
|
||||
* Per-user folders (STRATA_HOME, APPDATA, LOCALAPPDATA, the components and install folders) are all under --work.
|
||||
* No model is downloaded and no engine runs.
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') ? true : all[i + 1]]] : acc), []));
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const repoHost = resolve(here, '../../host');
|
||||
const staged = resolve(here, '../src-tauri/resources');
|
||||
const control = resolve(args.control ?? join(staged, 'control'));
|
||||
const nodeExe = resolve(args.node ?? join(staged, 'node', process.platform === 'win32' ? 'node.exe' : 'node'));
|
||||
const work = resolve(args.work ?? mkdtempSync(join(tmpdir(), 'strata-first-run-')));
|
||||
const shots = resolve(args.shots ?? join(work, 'shots'));
|
||||
mkdirSync(work, { recursive: true }); mkdirSync(shots, { recursive: true });
|
||||
const spy = pathToFileURL(join(repoHost, 'tests/helpers/acquire-spy.mjs')).href;
|
||||
const spyLog = join(work, 'spy.log'); writeFileSync(spyLog, '');
|
||||
|
||||
const { chromium } = await import(pathToFileURL(join(repoHost, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
|
||||
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'].find(existsSync);
|
||||
|
||||
let failed = false;
|
||||
const check = (ok, label, detail = '') => { console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ' ' + detail : ''}`); if (!ok) failed = true; };
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
|
||||
|
||||
for (const need of [nodeExe, join(control, 'strata.py'), join(repoHost, 'dist', 'index.html')]) if (!existsSync(need)) { console.error(`missing ${need} (run stage-control.py, fetch-node and "npm run build" in apps/host first)`); process.exit(2); }
|
||||
|
||||
const dirs = { home: join(work, 'home'), appdata: join(work, 'appdata'), local: join(work, 'local'), components: join(work, 'local', 'Strata', 'components') };
|
||||
for (const d of Object.values(dirs)) mkdirSync(d, { recursive: true });
|
||||
const port = await freePort();
|
||||
const env = {
|
||||
...process.env,
|
||||
STRATA_PORT: String(port), STRATA_DIST: join(repoHost, 'dist'), STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1',
|
||||
STRATA_CONTROL_DIR: control, STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(work, 'models'), STRATA_COMPONENTS_DIR: dirs.components,
|
||||
APPDATA: dirs.appdata, LOCALAPPDATA: dirs.local, XDG_CONFIG_HOME: join(work, 'xdg'), XDG_DATA_HOME: join(work, 'xdgdata'),
|
||||
STRATA_PROFILE_DIR: join(work, 'profiles'), STRATA_RUNTIME_DIR: join(work, 'runtime'), ACQUIRE_SPY_LOG: spyLog,
|
||||
};
|
||||
delete env.STRATA_INSTALL_DIR;
|
||||
mkdirSync(env.STRATA_MODELS_DIR, { recursive: true });
|
||||
const host = spawn(nodeExe, [...(args['no-spy'] === undefined ? ['--import', spy] : []), join(repoHost, 'server', 'index.mjs')], { cwd: repoHost, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let hostLog = '';
|
||||
host.stdout.on('data', c => { hostLog += c; }); host.stderr.on('data', c => { hostLog += c; });
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const api = async (path, init) => { const r = await fetch(base + path, init); return { status: r.status, body: await r.json().catch(() => null) }; };
|
||||
|
||||
let browser;
|
||||
try {
|
||||
let up = false;
|
||||
for (let i = 0; i < 150 && !up; i++) { try { up = (await api('/api/health')).status === 200; } catch { await delay(200); } }
|
||||
check(up, 'the packaged host starts', `port ${port}`);
|
||||
if (!up) throw new Error(hostLog.slice(-2000));
|
||||
|
||||
const native = await api('/api/native?refresh=1');
|
||||
const state = native.body;
|
||||
check(native.status === 200 && state?.available === true, 'the host reads Control', state?.error ?? '');
|
||||
const sizes = (state?.catalog?.families ?? []).reduce((n, f) => n + (f.sizes?.length ?? 0), 0);
|
||||
check((state?.catalog?.families?.length ?? 0) > 0 && sizes >= 3, 'the catalog has real families and sizes', `${state?.catalog?.families?.length} families, ${sizes} sizes`);
|
||||
check(!!state?.catalog?.recommendation, 'the catalog has a recommendation', JSON.stringify(state?.catalog?.recommendation ?? null).slice(0, 120));
|
||||
check(Array.isArray(state?.hardware?.gpus), 'the hardware lists GPUs', (state?.hardware?.gpus ?? []).map(g => `${g.name} ${g.vram_gb} GB`).join('; ') || 'none on this PC');
|
||||
check(state?.installDir && realpathSync.native(state.installDir).toLowerCase().startsWith(realpathSync.native(work).toLowerCase()), 'models go to a writable per-user folder, not the bundle', state?.installDir);
|
||||
const hw = (await api('/api/hardware')).body;
|
||||
console.log('info host hardware probe:', JSON.stringify((hw?.gpus ?? []).map(g => ({ name: g.name, backend: g.backend, vramGb: g.vramGb, unified: g.unified }))));
|
||||
check(!state?.installerError, 'the bundled installer answers', state?.installerError ?? '');
|
||||
|
||||
browser = await chromium.launch({ executablePath: CHROME, headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1400, height: 950 } });
|
||||
const problems = [];
|
||||
page.on('pageerror', e => problems.push(`pageerror: ${e.message}`));
|
||||
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('text=/Let.s get started|Choose a version|Connect your Strata/i', { timeout: 30000 }).catch(() => {});
|
||||
await page.screenshot({ path: join(shots, '1-welcome.png') });
|
||||
const broken = await page.locator('text=/couldn.t show this screen/i').count();
|
||||
check(broken === 0, 'the first screen renders (not "Strata couldn\'t show this screen")');
|
||||
const start = page.getByRole('button', { name: /get started/i });
|
||||
check(await start.count() > 0, 'the welcome step offers "Let\'s get started"');
|
||||
if (await start.count()) await start.first().click();
|
||||
await page.waitForSelector('text=/Choose a version/i', { timeout: 30000 }).catch(() => {});
|
||||
await delay(1200);
|
||||
await page.screenshot({ path: join(shots, '2-models.png') });
|
||||
check(await page.locator('text=/couldn.t show this screen/i').count() === 0, 'the model step renders');
|
||||
check(await page.getByText(/Choose a version/i).count() > 0, 'the model step lists the versions');
|
||||
const labels = await page.locator('[role=radio]').allInnerTexts();
|
||||
check(labels.length >= 3, 'the versions are the real catalog\'s sizes', labels.map(l => l.replace(/\s+/g, ' ').slice(0, 40)).join(' | '));
|
||||
check(labels.some(l => /\d+\.\d\s*GB/.test(l)), 'each version shows its download size');
|
||||
check(problems.length === 0, 'no page error', problems.join('; '));
|
||||
|
||||
const recommended = state?.catalog?.recommendation;
|
||||
if (recommended?.model) {
|
||||
const plan = await api('/api/native/plan', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ family: recommended.family, model: recommended.model, context: recommended.context, data_dir: join(work, 'Strata-data') }) });
|
||||
check(plan.status === 200 && !!plan.body?.plan, 'the install plan for the recommended model is a plan (nothing downloaded)', plan.body?.summary?.slice(0, 100) ?? JSON.stringify(plan.body).slice(0, 150));
|
||||
check(!existsSync(join(work, 'Strata-data', 'models')), 'a plan wrote no model files');
|
||||
}
|
||||
} catch (error) {
|
||||
check(false, 'the run', String(error?.message ?? error).slice(0, 600));
|
||||
} finally {
|
||||
await browser?.close().catch(() => {});
|
||||
host.kill();
|
||||
await new Promise(r => host.once('exit', r)); // only the PID this script started
|
||||
const attempts = readFileSync(spyLog, 'utf8').split('\n').filter(Boolean);
|
||||
check(attempts.length === 0, 'the spy saw no pip, git, compiler, engine or internet access', attempts.slice(0, 3).join(' '));
|
||||
console.log(`shots in ${shots}`);
|
||||
if (!args.work) rmSync(work, { recursive: true, force: true, maxRetries: 3 });
|
||||
}
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Turn a pnpm "isolated" node_modules (a virtual store in `.pnpm` full of links) into an ordinary hoisted tree of real folders.
|
||||
*
|
||||
* Why: the installer copies the Harness folder file by file. pnpm's layout is thousands of links (junctions on Windows); a copy
|
||||
* that follows them duplicates every package once per dependent and one that does not breaks on the first dangling link. A hoisted
|
||||
* tree has each package once (nested only where two versions of the same name are needed), no links, and Node resolves it
|
||||
* exactly as it resolves any npm install.
|
||||
*
|
||||
* Algorithm (the one npm uses, in short): the packages the root names come first, at the top. Then, walking each placed package's
|
||||
* own dependencies: look the name up from the package's own folder upwards; if the same instance is already visible, nothing to
|
||||
* do; if a DIFFERENT instance of that name is visible, nest this one inside the dependent; if none is visible, hoist it to the top.
|
||||
*
|
||||
* @param {string} out folder holding `node_modules`
|
||||
* @param {{skip?: RegExp}} [options] package names never copied (the caller removes them anyway)
|
||||
* @returns {{packages:number, copied:number, nested:number}}
|
||||
*/
|
||||
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync } from 'node:fs';
|
||||
import { dirname, join, relative, sep } from 'node:path';
|
||||
|
||||
export function flattenPnpm(out, { skip = null } = {}) {
|
||||
const nm = join(out, 'node_modules');
|
||||
const store = join(nm, '.pnpm');
|
||||
const flatRoot = join(out, 'node_modules.flat');
|
||||
rmSync(flatRoot, { recursive: true, force: true });
|
||||
mkdirSync(join(flatRoot, 'node_modules'), { recursive: true });
|
||||
|
||||
const manifest = dir => { try { return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')); } catch { return null; } };
|
||||
const nameOf = dir => manifest(dir)?.name ?? null;
|
||||
/**
|
||||
* What makes two instances "the same package": name and version. pnpm keeps one copy per set of peer packages (the same workspace
|
||||
* package under many hashed folders); in one hoisted tree they are one package.
|
||||
*/
|
||||
const identity = dir => { const m = manifest(dir); return m ? `${m.name}@${m.version}` : dir; };
|
||||
/** The sibling links of a store instance: its dependencies, by name -> real instance folder. */
|
||||
function storeDeps(instance, name) {
|
||||
const container = name.startsWith('@') ? dirname(dirname(instance)) : dirname(instance);
|
||||
const found = new Map();
|
||||
for (const e of readdirSync(container, { withFileTypes: true })) {
|
||||
if (e.name === '.bin') continue;
|
||||
const entries = e.name.startsWith('@') ? readdirSync(join(container, e.name), { withFileTypes: true }).map(x => [`${e.name}/${x.name}`, join(container, e.name, x.name)]) : [[e.name, join(container, e.name)]];
|
||||
for (const [dep, p] of entries) {
|
||||
if (dep === name) continue;
|
||||
try { if (lstatSync(p).isSymbolicLink() || existsSync(p)) found.set(dep, realpathSync(p)); } catch { /* a dangling link: the package was removed on purpose */ }
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
/** Every instance in the store by package name, for packages that arrived without a store entry (copied peers). */
|
||||
const byName = new Map();
|
||||
if (existsSync(store)) {
|
||||
for (const id of readdirSync(store, { withFileTypes: true })) {
|
||||
if (!id.isDirectory()) continue;
|
||||
const container = join(store, id.name, 'node_modules');
|
||||
if (!existsSync(container)) continue;
|
||||
for (const e of readdirSync(container, { withFileTypes: true })) {
|
||||
if (e.name === '.bin') continue;
|
||||
const dirs = e.name.startsWith('@') ? readdirSync(join(container, e.name)).map(x => join(container, e.name, x)) : [join(container, e.name)];
|
||||
for (const d of dirs) {
|
||||
let real; try { real = realpathSync(d); } catch { continue; }
|
||||
if (!real.startsWith(store)) continue; // a link to a workspace package outside the store is handled through the instance table
|
||||
const n = nameOf(real);
|
||||
if (n && !byName.has(n)) byName.set(n, real);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const placed = new Map(); // folder (in the flat tree) -> real instance folder it holds
|
||||
let copied = 0, nested = 0;
|
||||
|
||||
const visibleFrom = (chain, name) => {
|
||||
for (const dir of chain) {
|
||||
const slot = join(dir, 'node_modules', ...name.split('/'));
|
||||
if (placed.has(slot)) return { slot, instance: placed.get(slot) };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const copyInto = (slot, instance) => {
|
||||
mkdirSync(dirname(slot), { recursive: true });
|
||||
cpSync(instance, slot, { recursive: true, dereference: true, filter: f => !relative(instance, f).split(sep).includes('node_modules') });
|
||||
placed.set(slot, identity(instance));
|
||||
copied++;
|
||||
};
|
||||
|
||||
const queue = []; // { slotDir (folder of the placed package), chain (folders searched upwards), instance, name }
|
||||
const rootChain = [flatRoot];
|
||||
const place = (name, instance, chain, forceHere) => {
|
||||
if (skip?.test(name)) return;
|
||||
const seen = visibleFrom(chain, name);
|
||||
if (seen && seen.instance === identity(instance)) return;
|
||||
const hoist = !seen && !forceHere;
|
||||
const base = hoist ? flatRoot : chain[0];
|
||||
const slot = join(base, 'node_modules', ...name.split('/'));
|
||||
if (placed.has(slot)) return; // another instance already sits where this one would go (it nests below, handled by the caller's chain)
|
||||
copyInto(slot, instance);
|
||||
if (!hoist && base !== flatRoot) nested++;
|
||||
queue.push({ slotDir: slot, chain: [slot, ...(hoist ? rootChain : chain)], instance, name });
|
||||
};
|
||||
|
||||
// 1. the packages the root names
|
||||
const rootEntries = [];
|
||||
for (const e of readdirSync(nm, { withFileTypes: true })) {
|
||||
if (e.name === '.pnpm' || e.name === '.bin' || e.name.startsWith('.')) continue;
|
||||
const entries = e.name.startsWith('@') ? readdirSync(join(nm, e.name)).map(x => [`${e.name}/${x}`, join(nm, e.name, x)]) : [[e.name, join(nm, e.name)]];
|
||||
for (const [name, p] of entries) rootEntries.push([name, p]);
|
||||
}
|
||||
for (const [name, p] of rootEntries) {
|
||||
let real; try { real = realpathSync(p); } catch { continue; }
|
||||
place(name, real, rootChain, true);
|
||||
}
|
||||
// 2. their dependencies, breadth first
|
||||
while (queue.length) {
|
||||
const { slotDir, chain, instance, name } = queue.shift();
|
||||
let wants;
|
||||
if (instance.startsWith(store)) wants = storeDeps(instance, name);
|
||||
else {
|
||||
// a package copied in without a store entry: its declared requirements, taken from the store by name when not visible yet
|
||||
const m = manifest(instance) ?? {};
|
||||
wants = new Map();
|
||||
for (const dep of [...Object.keys(m.dependencies ?? {}), ...Object.keys(m.peerDependencies ?? {}), ...Object.keys(m.optionalDependencies ?? {})]) {
|
||||
if (visibleFrom(chain, dep)) continue;
|
||||
const real = byName.get(dep);
|
||||
if (real) wants.set(dep, real);
|
||||
}
|
||||
}
|
||||
for (const [dep, real] of wants) place(dep, real, chain, false);
|
||||
}
|
||||
|
||||
rmSync(nm, { recursive: true, force: true });
|
||||
renameSync(join(flatRoot, 'node_modules'), nm);
|
||||
rmSync(flatRoot, { recursive: true, force: true });
|
||||
return { packages: placed.size, copied, nested };
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Which files and folders of the staged Harness are dropped, and the check that nothing the Harness imports is gone.
|
||||
*
|
||||
* The 2026-10-17 tester report: on Linux and Mac every agent chat failed ("session/create ... waiting for workflowEngine").
|
||||
* Cause: the old rule "drop any folder named after another operating system" also removed the Harness's own JavaScript package
|
||||
* `@deepseek-ai/dsh-win32-process`. It is plain JS (no binary), and `dsh-subprocess-local` and `dsh-sandbox-local` import it by
|
||||
* name on every platform, so those two plugins failed to load, and everything waiting on them (shell, subprocess, ptcRuntime,
|
||||
* workflowEngine) never started. Windows kept it (the rule differs there), so the Windows tests passed.
|
||||
*/
|
||||
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
||||
import { join, sep } from 'node:path';
|
||||
|
||||
export const OTHER_OS = {
|
||||
win32: /(^|[\\/._-])(darwin|linux|android|freebsd|musl)([\\/._-]|$)/i,
|
||||
darwin: /(^|[\\/._-])(win32|linux|android|freebsd|musl)([\\/._-]|$)/i,
|
||||
// Linux: the AppImage bundler (linuxdeploy) resolves the libraries of every ELF file under resources and stops on one it
|
||||
// cannot satisfy (koffi's musl build wants libc.musl). The app is glibc x86_64, so musl and other-CPU builds are dropped.
|
||||
linux: /(^|[\\/._-])(win32|darwin|android|freebsd|musl|arm64|aarch64|armv7|arm|ia32|riscv64|ppc64|s390x|loong64)([\\/._-]|$)/i,
|
||||
};
|
||||
const NATIVE_FOLDER = /prebuild|bin|native|platform|-(win32|darwin|linux)/i;
|
||||
|
||||
/** The Harness's own JavaScript packages (`@deepseek-ai/dsh-*`) are code every platform imports by name; never drop one. */
|
||||
export function isHarnessCodePackage(path) {
|
||||
const parts = path.split(sep);
|
||||
const i = parts.lastIndexOf('@deepseek-ai');
|
||||
return i >= 0 && i === parts.length - 2 && parts[i + 1].startsWith('dsh-');
|
||||
}
|
||||
|
||||
/** True when the folder `path` (a directory named `name`) is another operating system's prebuilt binary and can go. */
|
||||
export function isOtherOsBinaryFolder(platform, name, path) {
|
||||
const rule = OTHER_OS[platform] ?? OTHER_OS.linux;
|
||||
if (isHarnessCodePackage(path)) return false;
|
||||
return rule.test(name) && NATIVE_FOLDER.test(path);
|
||||
}
|
||||
|
||||
/** Imports the Windows copy (which works) is also without: browser-side or dev-only bundles, and the document converter the app drops on purpose. */
|
||||
const KNOWN_ABSENT = new Set([
|
||||
'@deepseek-ai/libreoffice-kit', '@deepseek-ai/dsh-client-ui-dockkit', '@deepseek-ai/dsh-client-web',
|
||||
'@deepseek-ai/dsh-experimental-webworker-runtime', '@deepseek-ai/dsh-remote-mock', '@deepseek-ai/dsh-client-test-runtime',
|
||||
]);
|
||||
|
||||
function jsFiles(dir, out = []) {
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = join(dir, e.name);
|
||||
if (e.isSymbolicLink()) continue;
|
||||
if (e.isDirectory()) jsFiles(p, out);
|
||||
else if (/\.(m?js|cjs)$/.test(e.name)) out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every bare `@deepseek-ai/<package>` the staged Harness's own packages import (static `from "x"` and `import("x")`) must exist
|
||||
* in the staged node_modules. Returns the list of `{from, missing}` problems (empty = good). Plugins that are only loaded on
|
||||
* another platform still count: Node resolves a static import whether or not the code runs.
|
||||
*/
|
||||
export function missingHarnessImports(harnessDir) {
|
||||
const scope = join(harnessDir, 'node_modules', '@deepseek-ai');
|
||||
if (!existsSync(scope)) return [{ from: scope, missing: '@deepseek-ai (no installed packages)' }];
|
||||
const problems = [];
|
||||
const seen = new Set();
|
||||
const re = /(?:from|import)\s*\(?\s*["'](@deepseek-ai\/[^"'/]+)(?:\/[^"']*)?["']/g;
|
||||
for (const pkg of readdirSync(scope)) {
|
||||
const lib = join(scope, pkg, 'lib');
|
||||
if (!existsSync(lib)) continue;
|
||||
for (const file of jsFiles(lib)) {
|
||||
const text = readFileSync(file, 'utf8');
|
||||
for (const m of text.matchAll(re)) {
|
||||
const name = m[1];
|
||||
const key = `${pkg}>${name}`;
|
||||
if (seen.has(key) || KNOWN_ABSENT.has(name)) continue;
|
||||
seen.add(key);
|
||||
if (!existsSync(join(harnessDir, 'node_modules', ...name.split('/')))
|
||||
&& !existsSync(join(scope, pkg, 'node_modules', ...name.split('/')))) problems.push({ from: `${pkg}`, missing: name });
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems;
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Prove the INSTALLED app layout can start and stop an engine with nothing acquired, using a stub engine.
|
||||
*
|
||||
* node apps/desktop/scripts/installed-flow.mjs --install <folder the installer wrote> [--work <scratch>] [--spy <acquire-spy.mjs>]
|
||||
*
|
||||
* It starts the installed host with the installed node.exe exactly as the Tauri shell does (STRATA_PACKAGED=1,
|
||||
* STRATA_NO_ACQUIRE=1; Control found beside the host), against a scratch component folder holding a fake engine
|
||||
* package and a scratch install folder holding one run config. Everything per-user (profiles, runtime state, host data,
|
||||
* components) lives under --work, so the real profile of whoever runs this is never read or written.
|
||||
*
|
||||
* Steps (each one printed, the run exits 1 if any fails):
|
||||
* host health, Control status (engine usable, acquiring forbidden), models from Control, load through the host
|
||||
* (profile + `runtime start` under the owner runtime), a chat answer from the stub engine, a second client attaches to
|
||||
* the same owner/instance, unload stops it, no engine for this PC is a notice with nothing started, the Tauri-style
|
||||
* clean shutdown stops the engines the host started, a hard-killed host takes its engine with it.
|
||||
* With --spy the host runs under tests/helpers/acquire-spy.mjs and the run fails on any attempt to reach the internet or
|
||||
* run an installer.
|
||||
*
|
||||
* REAL mode (a test box with a GPU): --components <folder holding engine/<package>> --models <folder with strata-<id>.json
|
||||
* whose engine is "component:strata-engine"> --model <id> [--backend cuda]. The same host and Control, a real engine and a
|
||||
* real model: load, a real answer, ownership, unload, the missing-engine notice, load again, clean shutdown. The stub-only
|
||||
* steps (the marker file, the hard-kill report) are left out.
|
||||
*/
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, chmodSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') || all[i + 1] === undefined ? true : all[i + 1]]] : acc), []));
|
||||
if (!args.install) { console.error('usage: installed-flow.mjs --install <folder> [--work <dir>] [--spy <acquire-spy.mjs>]'); process.exit(2); }
|
||||
const install = resolve(args.install);
|
||||
const isWin = process.platform === 'win32';
|
||||
const node = join(install, 'node', isWin ? 'node.exe' : 'node');
|
||||
const hostEntry = join(install, 'host', 'server', 'index.mjs');
|
||||
const controlDir = join(install, 'control');
|
||||
const python = isWin ? join(controlDir, 'python', 'python.exe') : join(controlDir, 'python', 'bin', 'python3');
|
||||
const real = !!args.components;
|
||||
const modelId = String(args.model ?? 'stub');
|
||||
const work = resolve(args.work ?? mkdtempSync(join(tmpdir(), 'strata-installed-flow-')));
|
||||
mkdirSync(work, { recursive: true });
|
||||
const dirs = { components: real ? resolve(args.components) : join(work, 'components'), models: real ? resolve(args.models) : join(work, 'models'), profiles: join(work, 'profiles'), runtime: join(work, 'runtime'), home: join(work, 'home') };
|
||||
// the runtime and profile folders are created by Control itself, with the owner-only ACL it insists on
|
||||
for (const [name, d] of Object.entries(dirs)) if (!['runtime', 'profiles'].includes(name) && !(real && ['components', 'models'].includes(name))) mkdirSync(d, { recursive: true });
|
||||
|
||||
const steps = [];
|
||||
const record = (name, ok, detail = '') => { steps.push({ name, ok: !!ok, detail: String(detail) }); console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' ' + detail : ''}`); };
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
|
||||
const sha = buf => createHash('sha256').update(buf).digest('hex');
|
||||
|
||||
// ------------------------------------------------------------------------------ fixtures
|
||||
const baseEnv = {
|
||||
...process.env,
|
||||
STRATA_COMPONENTS_DIR: dirs.components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime,
|
||||
STRATA_INSTALL_DIR: dirs.models, STRATA_ENGINE_BACKEND: String(args.backend ?? 'cuda'), STRATA_HOME: dirs.home, STRATA_MODELS_DIR: real ? join(work, 'models-unused') : dirs.models,
|
||||
APPDATA: join(work, 'appdata'), XDG_CONFIG_HOME: join(work, 'xdg'), PYTHONDONTWRITEBYTECODE: '1',
|
||||
};
|
||||
delete baseEnv.STRATA_API_KEY;
|
||||
mkdirSync(baseEnv.APPDATA, { recursive: true });
|
||||
const mark = join(work, 'engine-ran.json');
|
||||
|
||||
const FAKE_ENGINE = `import os, sys, json
|
||||
from pathlib import Path
|
||||
Path(os.environ['FAKE_ENGINE_MARK']).write_text(json.dumps({'pid': os.getpid(), 'script': os.path.abspath(__file__), 'argv': sys.argv[1:]}))
|
||||
print('READY 4096 stop', flush=True)
|
||||
for line in sys.stdin:
|
||||
if line.strip() == 'QUIT': break
|
||||
if line.startswith(('GEN ', 'BGEN ')):
|
||||
slot = int(line.split()[1]) if line.startswith('BGEN ') else None
|
||||
print('T 97', flush=True)
|
||||
print('DONE 1 1 1.0 1.0 length 0 0 0', flush=True)
|
||||
if slot is not None: print(f'BADM {slot} 0', flush=True)
|
||||
`;
|
||||
|
||||
function installFakeEngine() {
|
||||
const platform = isWin ? 'windows-x86_64' : 'linux-x86_64';
|
||||
const folder = join(dirs.components, 'engine', `0.1.45-${platform}-cuda`);
|
||||
mkdirSync(folder, { recursive: true });
|
||||
writeFileSync(join(folder, 'engine_main.py'), FAKE_ENGINE);
|
||||
const entry = isWin ? 'engine.cmd' : 'engine';
|
||||
writeFileSync(join(folder, entry), isWin
|
||||
? `@echo off\r\n"${python}" "%~dp0engine_main.py" %*\r\n`
|
||||
: `#!/bin/sh\nexec "${python}" "$(dirname "$0")/engine_main.py" "$@"\n`);
|
||||
if (!isWin) chmodSync(join(folder, entry), 0o755);
|
||||
const files = [entry, 'engine_main.py'].map(p => { const b = readFileSync(join(folder, p)); return { path: p, sha256: sha(b), bytes: b.length }; });
|
||||
writeFileSync(join(folder, 'engine-component.json'), JSON.stringify({ schema_version: 1, id: 'strata-engine', version: '0.1.45', platform, backend: 'cuda', entrypoint: entry, files }));
|
||||
return folder;
|
||||
}
|
||||
|
||||
function makeTokenizerAndConfig() {
|
||||
const tok = join(dirs.models, 'tokenizer');
|
||||
mkdirSync(tok, { recursive: true });
|
||||
const script = `import json, sys\nfrom tools.strata_tokenizer import BYTE_TO_UNICODE\ntok = sys.argv[1]\ntokens = [BYTE_TO_UNICODE[i] for i in range(256)] + ['ab', '<|im_start|>', '<|im_end|>', '<|endoftext|>']\njson.dump({t: i for i, t in enumerate(tokens)}, open(tok + '/vocab.json', 'w'))\nopen(tok + '/merges.txt', 'w').write('a b')\njson.dump([0] * 257 + [3, 3, 3], open(tok + '/token_type.json', 'w'))\n`;
|
||||
const done = spawnSync(python, ['-c', script, tok], { cwd: controlDir, env: { ...baseEnv, PYTHONNOUSERSITE: '1' }, encoding: 'utf8' });
|
||||
if (done.status !== 0) throw new Error('tokenizer fixture failed: ' + done.stderr);
|
||||
writeFileSync(join(dirs.models, 'strata-stub.json'), JSON.stringify({ exe: 'component:strata-engine', backend: 'cuda', tokenizer: tok, args: [], cwd: work, model_name: 'stub', host: '127.0.0.1', port: 0 }));
|
||||
}
|
||||
|
||||
const control = (...argv) => spawnSync(python, [join(controlDir, 'strata.py'), ...argv, '--output', 'json'], { cwd: controlDir, env: { ...baseEnv, PYTHONNOUSERSITE: '1', STRATA_NO_ACQUIRE: '1', FAKE_ENGINE_MARK: mark }, encoding: 'utf8', timeout: 120000 });
|
||||
const controlJson = (...argv) => { const r = control(...argv); let json = null; try { json = JSON.parse(r.stdout.slice(r.stdout.indexOf('{'))); } catch { /* not json */ } return { code: r.status, json, text: r.stdout + r.stderr }; };
|
||||
const alive = pid => { try { process.kill(pid, 0); return true; } catch { return false; } };
|
||||
|
||||
// ------------------------------------------------------------------------------ the host
|
||||
let host = null, port = 0, base = '', spyLog = null, shutdownToken = randomBytes(32).toString('hex');
|
||||
async function startHost() {
|
||||
port = await freePort(); base = `http://127.0.0.1:${port}`;
|
||||
const hostArgs = [...(args.spy ? ['--import', pathToFileURL(resolve(args.spy)).href] : []), hostEntry];
|
||||
if (args.spy) { spyLog = join(work, 'acquire-spy.log'); writeFileSync(spyLog, ''); }
|
||||
host = spawn(node, hostArgs, {
|
||||
cwd: join(install, 'host'), stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
|
||||
env: { ...baseEnv, STRATA_PORT: String(port), STRATA_DIST: join(install, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1',
|
||||
STRATA_NO_ACQUIRE: '1', STRATA_SHUTDOWN_TOKEN: shutdownToken, FAKE_ENGINE_MARK: mark, STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()),
|
||||
...(spyLog ? { ACQUIRE_SPY_LOG: spyLog } : {}) },
|
||||
});
|
||||
let log = '';
|
||||
host.stdout.on('data', c => { log += c; }); host.stderr.on('data', c => { log += c; });
|
||||
host.hostLog = () => log;
|
||||
for (let i = 0; i < 200; i++) { try { if ((await fetch(base + '/api/health')).ok) return true; } catch { /* not yet */ } await delay(150); }
|
||||
return false;
|
||||
}
|
||||
/** The host started the way the Tauri shell starts it, by a stand-in shell (a node process) that can then be killed hard. */
|
||||
async function startHostViaShell() {
|
||||
port = await freePort(); base = `http://127.0.0.1:${port}`;
|
||||
const spec = { command: node, args: [hostEntry], options: {
|
||||
cwd: join(install, 'host'), stdio: 'ignore', windowsHide: true,
|
||||
env: { ...baseEnv, STRATA_PORT: String(port), STRATA_DIST: join(install, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1',
|
||||
STRATA_NO_ACQUIRE: '1', STRATA_SHUTDOWN_TOKEN: shutdownToken, STRATA_EXIT_WITH_PARENT: '1', FAKE_ENGINE_MARK: mark,
|
||||
STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()) } } };
|
||||
const shell = spawn(node, ['-e', "const s=JSON.parse(process.env.HOST_SPEC);const c=require('node:child_process').spawn(s.command,s.args,s.options);console.log(c.pid);setInterval(()=>{},1000);"],
|
||||
{ env: { ...process.env, HOST_SPEC: JSON.stringify(spec) }, stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true });
|
||||
const hostPid = await new Promise(r => shell.stdout.once('data', c => r(Number(String(c).trim()))));
|
||||
for (let i = 0; i < 200; i++) { try { if ((await fetch(base + '/api/health')).ok) return { shell, hostPid }; } catch { /* not yet */ } await delay(150); }
|
||||
return { shell, hostPid, failed: true };
|
||||
}
|
||||
const call = async (path, method = 'GET', body) => {
|
||||
const response = await fetch(base + path, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
||||
return { status: response.status, body: await response.json().catch(() => null) };
|
||||
};
|
||||
async function waitJob(id, seconds = 120) {
|
||||
for (let i = 0; i < seconds * 5; i++) {
|
||||
const state = (await call('/api/native')).body;
|
||||
const job = state.jobs.find(j => j.id === id);
|
||||
if (job && job.state !== 'running') return { job, state };
|
||||
await delay(200);
|
||||
}
|
||||
throw new Error('operation did not finish');
|
||||
}
|
||||
async function stopHost(hard = false) {
|
||||
if (!host || host.exitCode !== null) return;
|
||||
if (hard) { if (isWin) spawnSync('taskkill', ['/PID', String(host.pid), '/T', '/F'], { windowsHide: true }); else host.kill('SIGKILL'); }
|
||||
else host.kill();
|
||||
for (let i = 0; i < 100 && host.exitCode === null; i++) await delay(100);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
for (const need of [node, hostEntry, python, join(controlDir, 'strata.py')]) if (!existsSync(need)) { record('installed layout', false, `missing ${need}`); return; }
|
||||
record('installed layout', true, install);
|
||||
if (real) {
|
||||
const packages = existsSync(join(dirs.components, 'engine')) ? readdirSync(join(dirs.components, 'engine')) : [];
|
||||
record('fixtures', packages.length > 0 && existsSync(join(dirs.models, `strata-${modelId}.json`)), `engine package(s) ${packages.join(', ')} in ${dirs.components}; run config strata-${modelId}.json in ${dirs.models}`);
|
||||
} else {
|
||||
installFakeEngine();
|
||||
makeTokenizerAndConfig();
|
||||
record('fixtures', true, `stub engine package in ${dirs.components}; run config strata-stub.json`);
|
||||
}
|
||||
|
||||
if (!(await startHost())) { record('host healthy', false, host.hostLog().slice(-600)); return; }
|
||||
record('host healthy', true, base);
|
||||
|
||||
const status = (await call('/api/control/status')).body;
|
||||
record('Control status: bundled, engine usable, acquiring forbidden', status.control?.python?.startsWith(controlDir) && status.engine?.usable === true && status.acquisition?.forbidden === true,
|
||||
`python=${status.control?.python}; engine=${status.engine?.engine?.version}; forbidden=${status.acquisition?.forbidden}`);
|
||||
|
||||
const native = (await call('/api/native?refresh=1')).body;
|
||||
const model = native.installed?.models?.find(m => m.model === modelId);
|
||||
record('models come from Control', native.available && model?.ready === true && native.packaged === true, `available=${native.available}; model ready=${model?.ready}; ${native.summary ?? native.error}`);
|
||||
|
||||
// ---- load
|
||||
const started = (await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } })).body;
|
||||
const loaded = await waitJob(started.id, 400);
|
||||
const instance = loaded.state.instances[0];
|
||||
record('load through the host starts the engine under Control', loaded.job.state === 'done' && instance?.status === 'ready', `job=${loaded.job.state} ${loaded.job.error ?? ''}; instance=${instance?.status}`);
|
||||
let ran = existsSync(mark) ? JSON.parse(readFileSync(mark, 'utf8')) : null;
|
||||
if (real) {
|
||||
const listed = spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => l.includes(`${dirs.components}/engine/`) && l.includes('--serve'));
|
||||
ran = listed.length ? { pid: Number(listed[0].trim().split(/\s+/)[0]), script: listed[0].trim().split(/\s+/)[1] } : null;
|
||||
record('the engine process running is the binary from the component folder', !!ran, listed[0] ? listed[0].trim().slice(0, 160) : 'no engine process under the component folder');
|
||||
} else {
|
||||
record('the engine that ran is the one in the component folder', ran && ran.script.toLowerCase().startsWith(dirs.components.toLowerCase()), ran?.script ?? 'engine never ran');
|
||||
}
|
||||
|
||||
const runtime = controlJson('runtime', 'status', '--profile', 'app-1');
|
||||
record('a second client sees the same owner and instance (ownership)', runtime.json?.state === 'ready' && !!runtime.json.owner_id && !!runtime.json.instance_id && runtime.json.host_pid !== host.pid,
|
||||
`state=${runtime.json?.state}; owner=${String(runtime.json?.owner_id).slice(0, 8)}; instance=${String(runtime.json?.instance_id).slice(0, 8)}; owner host pid ${runtime.json?.host_pid} is not the app host (${host.pid})`);
|
||||
|
||||
// ---- answer
|
||||
const chat = await call('/v1/chat/completions', 'POST', real
|
||||
? { model: instance?.apiModelId ?? modelId, messages: [{ role: 'user', content: 'Reply with exactly one word: pong' }], max_tokens: 200, temperature: 0, reasoning_effort: 'none', stream: false }
|
||||
: { model: instance?.apiModelId ?? modelId, messages: [{ role: 'user', content: 'hi' }], max_tokens: 4, stream: false });
|
||||
const message = chat.body?.choices?.[0]?.message ?? {};
|
||||
const answer = `${message.content ?? ''}${message.reasoning_content ?? ''}`; // the stub emits token 97 ("a"); the server files it as reasoning
|
||||
record(real ? 'the real model answers through the host (the answer text is checked)' : 'the engine answers through the host (the stub token, not an error)',
|
||||
chat.status === 200 && (real ? /pong/i.test(answer) : answer === 'a'), `status=${chat.status}; answer=${JSON.stringify(answer.slice(0, 200))}${chat.status === 200 ? '' : ' ' + JSON.stringify(chat.body).slice(0, 160)}`);
|
||||
|
||||
// ---- unload
|
||||
const unloaded = await call(`/api/native/instances/${instance.id}/unload`, 'POST', {});
|
||||
await delay(500);
|
||||
const after = controlJson('runtime', 'status', '--profile', 'app-1');
|
||||
for (let i = 0; i < 100 && ran && alive(ran.pid); i++) await delay(100); // a real engine takes a moment to release the GPU
|
||||
record('unload stops the engine', unloaded.status === 200 && after.json?.state === 'stopped' && (!ran || !alive(ran.pid)), `status=${unloaded.status}; runtime=${after.json?.state}; engine pid ${ran?.pid} alive=${ran ? alive(ran.pid) : 'n/a'}`);
|
||||
|
||||
// ---- no engine for this PC
|
||||
const hidden = join(dirs.components, real ? 'engine.hidden-by-flow' : 'engine.gone');
|
||||
renameSync(join(dirs.components, 'engine'), hidden);
|
||||
rmSync(mark, { force: true });
|
||||
const missing = await waitJob((await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } })).body.id);
|
||||
record('no engine for this PC is a notice and nothing is started', missing.job.state === 'error' && /No engine for this PC/.test(missing.job.error) && /never downloads/.test(missing.job.error) && !existsSync(mark),
|
||||
`${missing.job.state}: ${String(missing.job.error).slice(0, 160)}`);
|
||||
const refused = await waitJob((await call('/api/native/operations', 'POST', { kind: 'install', args: { family: 'qwen', model: 'IQ2_XS' } })).body.id);
|
||||
record('installing from the app is refused (Terminal only)', refused.job.state === 'error' && /Terminal/.test(refused.job.error), refused.job.error);
|
||||
const binary = await call('/api/engine/binary', 'POST');
|
||||
record('the llama.cpp download route is refused', binary.status === 409 && /never downloads/.test(binary.body?.error ?? ''), `${binary.status} ${binary.body?.error}`);
|
||||
|
||||
// ---- clean shutdown (what the Tauri shell does on exit)
|
||||
renameSync(hidden, join(dirs.components, 'engine'));
|
||||
rmSync(mark, { force: true });
|
||||
const again = await waitJob((await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } })).body.id, 400);
|
||||
let ran2 = existsSync(mark) ? JSON.parse(readFileSync(mark, 'utf8')) : null;
|
||||
if (real) {
|
||||
const listed = spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => l.includes(`${dirs.components}/engine/`) && l.includes('--serve'));
|
||||
ran2 = listed.length ? { pid: Number(listed[0].trim().split(/\s+/)[0]) } : null;
|
||||
}
|
||||
record('load again after the engine package returns', again.job.state === 'done', again.job.error ?? '');
|
||||
const wrong = await fetch(base + '/api/control/shutdown', { method: 'POST', headers: { 'x-strata-shutdown': 'nope' } });
|
||||
record('shutdown without the shell\'s secret is refused', wrong.status === 403, String(wrong.status));
|
||||
const exited = new Promise(r => host.once('exit', code => r(code)));
|
||||
const stopping = await fetch(base + '/api/control/shutdown', { method: 'POST', headers: { 'x-strata-shutdown': shutdownToken } });
|
||||
const code = await Promise.race([exited, delay(90000).then(() => 'timeout')]);
|
||||
for (let i = 0; i < 100 && ran2 && alive(ran2.pid); i++) await delay(100);
|
||||
const final = controlJson('runtime', 'status', '--profile', 'app-1');
|
||||
record('clean shutdown stops the engine the app started and exits', stopping.status === 202 && code === 0 && final.json?.state === 'stopped' && (!ran2 || !alive(ran2.pid)), `status=${stopping.status}; host exit=${code}; runtime=${final.json?.state}; engine alive=${ran2 ? alive(ran2.pid) : 'n/a'}`);
|
||||
|
||||
// ---- hard kill: an engine whose app host dies ends with it (the owner follows the host: runtime start --die-with-pid)
|
||||
if (!(await startHost())) { record('host restarts', false, host.hostLog().slice(-400)); return; }
|
||||
await call('/api/native?refresh=1');
|
||||
rmSync(mark, { force: true });
|
||||
const third = await waitJob((await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } })).body.id, 400);
|
||||
let ran3 = existsSync(mark) ? JSON.parse(readFileSync(mark, 'utf8')) : null;
|
||||
if (real) {
|
||||
const listed = spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => l.includes(`${dirs.components}/engine/`) && l.includes('--serve'));
|
||||
ran3 = listed.length ? { pid: Number(listed[0].trim().split(/\s+/)[0]) } : null;
|
||||
record('(real) the engine is up before the host is killed', !!ran3, listed[0] ? listed[0].trim().slice(0, 120) : 'no engine process');
|
||||
}
|
||||
await stopHost(true);
|
||||
for (let i = 0; i < 600 && ran3 && alive(ran3.pid); i++) await delay(100); // the owner notices within a second, drains, stops
|
||||
const orphan = controlJson('runtime', 'status', '--profile', 'app-1');
|
||||
record('a hard-killed app host takes its engine with it', third.job.state === 'done' && (!ran3 || !alive(ran3.pid)), `engine alive=${ran3 ? alive(ran3.pid) : 'n/a'}; runtime=${orphan.json?.state ?? orphan.json?.error?.message}`);
|
||||
const cleanup = controlJson('runtime', 'stop', '--profile', 'app-1');
|
||||
record('nothing is left to stop afterwards', /stopped|not managed/.test(JSON.stringify(cleanup.json)) && controlJson('runtime', 'status', '--profile', 'app-1').json?.state === 'stopped', JSON.stringify(cleanup.json).slice(0, 160));
|
||||
|
||||
// ---- the shell itself is killed (what the desktop shell's crash or kill looks like to the host): the host leaves, and
|
||||
// its engine with it, with nothing announced. Windows also has the job object; this is the path that works everywhere.
|
||||
const started2 = await startHostViaShell();
|
||||
if (started2.failed) { record('host starts under a stand-in shell', false, ''); try { started2.shell.kill('SIGKILL'); } catch { /* gone */ } }
|
||||
else {
|
||||
await call('/api/native?refresh=1');
|
||||
rmSync(mark, { force: true });
|
||||
const fourth = await waitJob((await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } })).body.id, 400);
|
||||
let ran4 = existsSync(mark) ? JSON.parse(readFileSync(mark, 'utf8')) : null;
|
||||
if (real) {
|
||||
const listed = spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => l.includes(`${dirs.components}/engine/`) && l.includes('--serve'));
|
||||
ran4 = listed.length ? { pid: Number(listed[0].trim().split(/\s+/)[0]) } : null;
|
||||
}
|
||||
if (isWin) spawnSync('taskkill', ['/PID', String(started2.shell.pid), '/F'], { windowsHide: true }); else started2.shell.kill('SIGKILL');
|
||||
for (let i = 0; i < 100 && alive(started2.hostPid); i++) await delay(100);
|
||||
const hostGone = !alive(started2.hostPid);
|
||||
for (let i = 0; i < 900 && ran4 && alive(ran4.pid); i++) await delay(100);
|
||||
const engineGone = !ran4 || !alive(ran4.pid);
|
||||
record('a killed shell takes the host and its engine with it', fourth.job.state === 'done' && hostGone && engineGone,
|
||||
`host pid ${started2.hostPid} gone=${hostGone}; engine pid ${ran4?.pid} gone=${engineGone}; runtime=${controlJson('runtime', 'status', '--profile', 'app-1').json?.state}`);
|
||||
controlJson('runtime', 'stop', '--profile', 'app-1');
|
||||
}
|
||||
|
||||
await finishSpy();
|
||||
}
|
||||
|
||||
async function finishSpy() {
|
||||
if (spyLog) {
|
||||
const attempts = readFileSync(spyLog, 'utf8').split('\n').filter(Boolean);
|
||||
record('no acquisition attempt by the host during the whole run (spy)', attempts.length === 0, attempts.join(' | '));
|
||||
}
|
||||
}
|
||||
|
||||
try { await main(); }
|
||||
catch (error) { record('flow', false, error.stack ?? error.message); }
|
||||
finally {
|
||||
try { const hid = join(dirs.components, 'engine.hidden-by-flow'); if (real && existsSync(hid) && !existsSync(join(dirs.components, 'engine'))) renameSync(hid, join(dirs.components, 'engine')); } catch { /* reported by the run */ }
|
||||
await stopHost(true);
|
||||
controlJson('runtime', 'stop', '--profile', 'app-1');
|
||||
controlJson('runtime', 'stop', '--profile', 'app-2');
|
||||
}
|
||||
const failed = steps.filter(s => !s.ok);
|
||||
writeFileSync(join(work, 'installed-flow.json'), JSON.stringify({ ok: failed.length === 0, install, work, steps }, null, 1));
|
||||
console.log(`\n${steps.length - failed.length}/${steps.length} ok; report ${join(work, 'installed-flow.json')}`);
|
||||
if (!args.keep && !args.work && failed.length === 0) { try { rmSync(work, { recursive: true, force: true }); } catch { /* leave it */ } }
|
||||
process.exit(failed.length ? 1 : 0);
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Mock-ups of the Windows installer in Strata's own look: four screens (welcome, where, installing, done), light and dark, drawn with
|
||||
* the app's built CSS (apps/host/dist) and brand files, then photographed with Chrome. Nothing here is built or shipped: it is what
|
||||
* the owner is shown before an installer is chosen.
|
||||
*
|
||||
* node apps/desktop/scripts/installer-mockups.mjs [--out <folder>]
|
||||
*/
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]]] : acc), []));
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const host = resolve(here, '../../host');
|
||||
const out = resolve(args.out ?? join(here, '../installer-mockups'));
|
||||
mkdirSync(out, { recursive: true });
|
||||
const dist = join(host, 'dist');
|
||||
const css = readdirSync(join(dist, 'assets')).filter(f => f.endsWith('.css')).map(f => readFileSync(join(dist, 'assets', f), 'utf8')).join('\n');
|
||||
const logo = theme => readFileSync(join(dist, 'brand', `strata-lockup-${theme}.svg`), 'utf8');
|
||||
const { chromium } = await import(pathToFileURL(join(host, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
|
||||
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
|
||||
|
||||
const check = '<svg class="st-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
|
||||
const folder = '<svg class="st-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>';
|
||||
|
||||
const screens = {
|
||||
welcome: theme => `<header class="onboard-header"><span class="mock-logo">${logo(theme)}</span></header>
|
||||
<div class="onboard-welcome-copy"><h1>State of the art. At home.</h1><p>Strata runs AI on your own computer.<br>Setting it up takes a few minutes.</p>
|
||||
<div class="onboard-benefits"><span>${check}Private</span><span>${check}Fast</span><span>${check}Easy to use</span></div></div>
|
||||
<footer class="onboard-footer onboard-footer--welcome"><button class="st-btn st-btn--primary">Install Strata</button><small>Installs for you only. No administrator needed.</small></footer>`,
|
||||
where: theme => `<header class="onboard-header"><span class="mock-logo">${logo(theme)}</span></header>
|
||||
<div class="mock-body"><h2 class="mock-title">Where should Strata go?</h2><p class="mock-sub">This is the right place for most people.</p>
|
||||
<div class="onboard-folder">${folder}<div><strong>Your Strata folder</strong><small>C:\\Users\\Alex\\AppData\\Local\\Strata</small></div><button class="st-btn st-btn--sm">Change</button></div>
|
||||
<p class="mock-note">About 310 MB. Your models and chats are kept somewhere else and are never removed by an update.</p></div>
|
||||
<footer class="onboard-footer"><button class="st-btn st-btn--ghost">Back</button><span></span><button class="st-btn st-btn--primary">Install</button></footer>`,
|
||||
installing: theme => `<header class="onboard-header"><span class="mock-logo">${logo(theme)}</span></header>
|
||||
<div class="mock-body"><h2 class="mock-title">Installing Strata…</h2><p class="mock-sub">Copying the app and its engine room. You can keep working meanwhile.</p>
|
||||
<div class="onboard-progress" style="--p:62%"><progress max="100" value="62"></progress></div>
|
||||
<div class="onboard-progress-meta"><span>62% · 190 / 310 MB</span><div class="onboard-progress-actions"><span>About 1 min left</span><button>Cancel</button></div></div></div>
|
||||
<footer class="onboard-footer"><span></span><span class="mock-hint">Next: choose your first model inside Strata.</span><button class="st-btn st-btn--primary" disabled>Installing…</button></footer>`,
|
||||
done: theme => `<header class="onboard-header"><span class="mock-logo">${logo(theme)}</span></header>
|
||||
<div class="onboard-welcome-copy"><h1>Strata is ready.</h1><p>Open it to pick your first model. Strata downloads it for you and gets it ready.</p></div>
|
||||
<footer class="onboard-footer onboard-footer--welcome"><button class="st-btn st-btn--primary">Open Strata</button><small>A shortcut was added to your Start menu and desktop.</small></footer>`,
|
||||
};
|
||||
|
||||
const page = (name, theme) => `<!doctype html><html data-theme="${theme}"><head><meta charset="utf-8"><style>${css}
|
||||
body{background:${theme === 'dark' ? '#0b0d0e' : '#e9edef'};margin:0;display:grid;place-items:center;min-height:100vh}
|
||||
.mock-window{width:720px;border-radius:14px;overflow:hidden;background:var(--st-surface,#fff);border:1px solid var(--st-line,#dde);box-shadow:0 30px 80px #0003}
|
||||
.mock-titlebar{height:34px;display:flex;align-items:center;justify-content:space-between;padding:0 14px;font-size:12px;color:var(--st-ink-muted);background:var(--st-surface-2,#f3f5f6);border-bottom:1px solid var(--st-line-soft,#e6e9eb)}
|
||||
.mock-logo svg{height:22px;width:auto;display:block}
|
||||
.mock-body{padding:8px 36px 28px}.mock-title{font-size:28px;margin:16px 0 8px}.mock-sub{color:var(--st-ink-muted);margin:0 0 22px;line-height:1.6}
|
||||
.mock-note{color:var(--st-ink-muted);font-size:12px;margin-top:16px;line-height:1.6}.mock-hint{font-size:12px;color:var(--st-ink-muted)}
|
||||
.onboard-progress progress{width:100%}
|
||||
.guided-onboarding{min-height:430px;display:flex;flex-direction:column}.onboard-footer{margin-top:auto}
|
||||
</style></head><body><div class="mock-window"><div class="mock-titlebar"><span>Strata setup</span><span>– ☐ ✕</span></div>
|
||||
<div class="setup guided-onboarding" data-step="${name === 'welcome' || name === 'done' ? 1 : 2}">${screens[name](theme)}</div></div></body></html>`;
|
||||
|
||||
const browser = await chromium.launch({ executablePath: CHROME, headless: true });
|
||||
const written = [];
|
||||
for (const theme of ['light', 'dark']) {
|
||||
for (const name of Object.keys(screens)) {
|
||||
const tab = await browser.newPage({ viewport: { width: 860, height: 620 }, deviceScaleFactor: 1 });
|
||||
await tab.setContent(page(name, theme));
|
||||
await tab.waitForTimeout(250);
|
||||
const file = join(out, `${name}-${theme}.png`);
|
||||
await tab.screenshot({ path: file });
|
||||
written.push(file);
|
||||
await tab.close();
|
||||
}
|
||||
}
|
||||
await browser.close();
|
||||
console.log(written.join('\n'));
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* "Use your own GGUF" end to end on a box, through the installed app layout (packaged host + bundled Control + a real engine component):
|
||||
* preview a folder of a supported model, preview an unsupported GGUF (must be refused in plain terms), prepare the supported one
|
||||
* (pack, from the folder, nothing copied), load it, get a chat answer, unload, shut down, and check nothing is left running.
|
||||
*
|
||||
* node local-gguf-e2e.mjs --root /data/ui4-e2e --good /data/141-gyro/models/IQ2_XS --bad /data/ui4-e2e/bad [--context 8192] [--backend cuda|metal]
|
||||
* (the app folder is <root>/app: node/, host/, control/ ...; <root>/components gets the engine, from <root>/app/engine-bundle when the app carries one)
|
||||
*/
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]]] : acc), []));
|
||||
const root = resolve(args.root);
|
||||
const good = args.good, bad = args.bad;
|
||||
const context = Number(args.context ?? 8192);
|
||||
const app = join(root, 'app');
|
||||
const dirs = { components: join(root, 'components'), install: join(root, 'install'), profiles: join(root, 'profiles'), runtime: join(root, 'runtime'), home: join(root, 'home'), xdg: join(root, 'xdg'), appdata: join(root, 'appdata') };
|
||||
for (const d of [dirs.install, dirs.home, dirs.xdg, dirs.appdata]) mkdirSync(d, { recursive: true });
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
const steps = [];
|
||||
let failed = false;
|
||||
const record = (name, ok, detail = '') => { steps.push({ name, ok: !!ok, detail: String(detail).slice(0, 400) }); console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' ' + String(detail).slice(0, 300) : ''}`); if (!ok) failed = true; };
|
||||
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
|
||||
|
||||
const env = { ...process.env, STRATA_COMPONENTS_DIR: dirs.components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime, STRATA_INSTALL_DIR: dirs.install, STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(root, 'Strata-data'), STRATA_ENGINE_BACKEND: args.backend ?? 'cuda', XDG_CONFIG_HOME: dirs.xdg, APPDATA: dirs.appdata, PYTHONDONTWRITEBYTECODE: '1' };
|
||||
delete env.STRATA_API_KEY;
|
||||
const port = await freePort();
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const host = spawn(join(app, 'node', 'node'), [join(app, 'host', 'server', 'index.mjs')], { cwd: join(app, 'host'), stdio: ['ignore', 'pipe', 'pipe'], detached: false,
|
||||
env: { ...env, STRATA_PORT: String(port), STRATA_DIST: join(app, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1', STRATA_EXIT_WITH_PARENT: '1',
|
||||
STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()), STRATA_WORKSPACE: join(root, 'workspace') } });
|
||||
mkdirSync(join(root, 'workspace'), { recursive: true });
|
||||
let hostLog = ''; host.stdout.on('data', c => { hostLog += c; }); host.stderr.on('data', c => { hostLog += c; });
|
||||
const call = async (path, method = 'GET', body) => { const r = await fetch(base + path, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) }); return { status: r.status, body: await r.json().catch(() => null) }; };
|
||||
async function waitJob(id, seconds) {
|
||||
const until = Date.now() + seconds * 1000;
|
||||
let last = '';
|
||||
while (Date.now() < until) {
|
||||
const state = (await call('/api/native')).body;
|
||||
const job = state.jobs.find(j => j.id === id);
|
||||
if (job && job.state !== 'running') return { job, state };
|
||||
if (job && job.summary !== last) { last = job.summary; console.log(' ...', String(job.summary).slice(0, 140)); }
|
||||
await delay(2000);
|
||||
}
|
||||
throw new Error('operation did not finish in time');
|
||||
}
|
||||
const engineProcs = () => spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => l.includes(join(dirs.components, 'engine')) && /\/strata( |$)/.test(l));
|
||||
|
||||
async function main() {
|
||||
let up = false;
|
||||
for (let i = 0; i < 200 && !up; i++) { try { up = (await fetch(base + '/api/health')).ok; } catch { await delay(150); } }
|
||||
record('host healthy', up, up ? base : hostLog.slice(-500));
|
||||
if (!up) return;
|
||||
let status = (await call('/api/control/status')).body;
|
||||
for (let i = 0; i < 180 && status?.engineSetup; i++) { await delay(1000); status = (await call('/api/control/status')).body; } // the engine that came with the app is copied on the first start
|
||||
record('Control status: bundled, engine usable', status?.engine?.usable === true, `engine=${status?.engine?.engine?.version} ${status?.engine?.notice ?? ''}`);
|
||||
await call('/api/native?refresh=1');
|
||||
|
||||
// ---- the unsupported GGUF: a plain refusal
|
||||
const badPreview = await call('/api/native/local/preview', 'POST', { path: bad });
|
||||
const badIssues = badPreview.body?.preview?.issues ?? badPreview.body?.issues ?? [];
|
||||
const badStatus = badPreview.body?.preview?.file_set_status;
|
||||
console.log(' bad preview:', badPreview.status, JSON.stringify(badPreview.body).slice(0, 400));
|
||||
record('an unsupported GGUF is refused, nothing adopted', badPreview.status === 200 ? (badStatus !== 'plausible' || !badPreview.body.preview.candidate_selection) : badPreview.status >= 400, `status=${badPreview.status}; file_set_status=${badStatus}; issues=${JSON.stringify(badIssues)}`);
|
||||
|
||||
// ---- the supported folder
|
||||
const goodPreview = await call('/api/native/local/preview', 'POST', { path: good });
|
||||
const selection = goodPreview.body?.preview?.candidate_selection;
|
||||
console.log(' good preview:', JSON.stringify(goodPreview.body).slice(0, 500));
|
||||
record('a folder of a supported model is recognised', goodPreview.status === 200 && goodPreview.body?.preview?.file_set_status === 'plausible' && !!selection, `family=${selection?.family}; model=${selection?.model}; files=${goodPreview.body?.preview?.file_count}`);
|
||||
if (!selection || args.only === 'preview') return;
|
||||
|
||||
// ---- prepare (what the page's button does): install from the folder, nothing copied or downloaded
|
||||
const native0 = (await call('/api/native?refresh=1')).body;
|
||||
const catalogSize = native0.catalog?.families?.find(f => f.family === selection.family)?.sizes.find(s => s.model === selection.model);
|
||||
record('the catalog lists the recognised version', !!catalogSize, catalogSize ? `id=${catalogSize.id}` : `no ${selection.family}/${selection.model} in the catalog`);
|
||||
const started = await call('/api/native/operations', 'POST', { kind: 'install', args: { family: selection.family, model: selection.model, gguf_dir: good, context, vision: 'no' } });
|
||||
record('prepare starts', started.status < 300 && !!started.body?.id, `status=${started.status} ${JSON.stringify(started.body).slice(0, 200)}`);
|
||||
if (!started.body?.id) return;
|
||||
const prepared = await waitJob(started.body.id, 3600);
|
||||
record('prepare finishes', prepared.job.state === 'done', `${prepared.job.state}: ${prepared.job.error ?? prepared.job.summary}`);
|
||||
if (prepared.job.state !== 'done') return;
|
||||
const native1 = (await call('/api/native?refresh=1')).body;
|
||||
const modelId = catalogSize?.id ?? `${selection.family === 'qwen' ? '' : selection.family + '-'}${String(selection.model).toLowerCase()}`;
|
||||
const entry = native1.installed?.models?.find(m => m.model === modelId);
|
||||
record('the model is ready and points at the folder, not a copy', entry?.ready === true, `model=${modelId}; ready=${entry?.ready}; dataFolder=${entry?.dataFolder}`);
|
||||
if (!entry?.ready) return;
|
||||
|
||||
// ---- load
|
||||
const load = await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } });
|
||||
const loaded = await waitJob(load.body.id, 1800);
|
||||
const instance = loaded.state.instances[0];
|
||||
record('load starts the engine from the component folder', loaded.job.state === 'done' && instance?.status === 'ready', `job=${loaded.job.state} ${loaded.job.error ?? ''}; instance=${instance?.status}`);
|
||||
if (instance?.status !== 'ready') return;
|
||||
record('one engine process is running', engineProcs().length === 1, engineProcs().map(l => l.trim().slice(0, 140)).join(' | '));
|
||||
|
||||
// ---- a chat answer, checked
|
||||
const chat = await fetch(base + '/api/chat/stream', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId: 'e2e-chat', message: 'Reply with exactly one word: pong', history: [], nativeModelId: instance.id, thinking: 'Off', agent: false, workspaceId: null }) });
|
||||
let text = '', done = null;
|
||||
for await (const chunk of chat.body) {
|
||||
for (const line of Buffer.from(chunk).toString('utf8').split('\n')) {
|
||||
if (!line.trim()) continue;
|
||||
try { const f = JSON.parse(line); if (f.type === 'delta') text += f.text ?? ''; if (f.type === 'done') done = f; if (f.type === 'error') text += ` [error: ${f.message}]`; } catch { /* partial line */ }
|
||||
}
|
||||
}
|
||||
record('the model answers through the app (the answer text is checked)', chat.status === 200 && /pong/i.test(text), `status=${chat.status}; answer=${JSON.stringify(text.slice(0, 120))}; tokensOut=${done?.tokensOut}; tps=${done?.tps}`);
|
||||
|
||||
// ---- unload, shutdown, nothing left
|
||||
const unload = await call(`/api/native/instances/${instance.id}/unload`, 'POST');
|
||||
await delay(3000);
|
||||
record('unload stops the engine', unload.status < 300 && engineProcs().length === 0, `status=${unload.status}; engines left=${engineProcs().length}`);
|
||||
}
|
||||
|
||||
try { await main(); } catch (e) { record('the flow', false, e?.stack ?? e); }
|
||||
host.kill();
|
||||
for (let i = 0; i < 100 && host.exitCode === null; i++) await delay(100);
|
||||
if (host.exitCode === null) host.kill('SIGKILL');
|
||||
await delay(2000);
|
||||
const left = spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => (l.includes(dirs.install) || l.includes(join(app, 'host'))) && !l.includes('ps -eo'));
|
||||
record('nothing of this run is left running', left.length === 0, left.join(' | ').slice(0, 300));
|
||||
writeFileSync(join(root, 'e2e-result.json'), JSON.stringify({ steps, hostLogTail: hostLog.slice(-2000) }, null, 1));
|
||||
console.log(failed ? 'FAILED' : 'ALL OK');
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the Strata DMG without a Finder session.
|
||||
#
|
||||
# Tauri's default DMG step runs an AppleScript that asks Finder to arrange the window (icon
|
||||
# positions, background). Finder AppleEvents time out in a non-GUI or sandboxed context — observed
|
||||
# here as `Finder got an error: AppleEvent timed out. (-1712)` — so the default path cannot be
|
||||
# relied on for an automated build. The upstream `create-dmg` script already supports exactly this
|
||||
# case via `--skip-jenkins`, which produces a plain, correct DMG with the app and the Applications
|
||||
# drop link and no cosmetic arrangement.
|
||||
#
|
||||
# Usage: scripts/make-dmg.sh [output.dmg]
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DESKTOP="$(cd "$HERE/.." && pwd)"
|
||||
BUNDLE="$DESKTOP/src-tauri/target/release/bundle"
|
||||
APP="$BUNDLE/macos/Strata.app"
|
||||
VERSION="$(grep -m1 '^version = ' "$DESKTOP/src-tauri/Cargo.toml" | cut -d'"' -f2)" # the one place the app version is written
|
||||
OUT="${1:-$BUNDLE/dmg/Strata_${VERSION}_aarch64.dmg}"
|
||||
|
||||
if [ ! -d "$APP" ]; then
|
||||
echo "error: $APP does not exist; run 'cargo tauri build --bundles app' first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK="$(mktemp -d -t strata-dmg)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
# A staging folder is what create-dmg copies into the image: the app, and nothing else.
|
||||
#
|
||||
# The Applications drop link is created by `--app-drop-link` below. Adding one here as well makes
|
||||
# create-dmg fail with "File exists", because it links into the volume it is assembling.
|
||||
mkdir -p "$WORK/stage"
|
||||
cp -R "$APP" "$WORK/stage/"
|
||||
|
||||
# Reuse Tauri's vendored create-dmg if the bundler left it behind, otherwise take it from the
|
||||
# bundle cache. Building it yourself is not necessary and not expected.
|
||||
SCRIPT="$BUNDLE/dmg/bundle_dmg.sh"
|
||||
if [ ! -x "$SCRIPT" ]; then
|
||||
echo "error: $SCRIPT not found; run 'cargo tauri build --bundles dmg' once to vendor it" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$OUT")"
|
||||
rm -f "$OUT"
|
||||
|
||||
# --skip-jenkins: no Finder AppleScript. Everything else is the same code path Tauri uses.
|
||||
"$SCRIPT" \
|
||||
--volname "Strata $VERSION" \
|
||||
--volicon "$BUNDLE/macos/Strata.app/Contents/Resources/Strata.icns" \
|
||||
--app-drop-link 400 200 \
|
||||
--skip-jenkins \
|
||||
--no-internet-enable \
|
||||
"$OUT" "$WORK/stage"
|
||||
|
||||
echo
|
||||
echo "built: $OUT"
|
||||
ls -lh "$OUT" | awk '{print "size: " $5}'
|
||||
shasum -a 256 "$OUT"
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* The app and installer icon, refined from the existing Strata mark (apps/host/public/brand/strata-mark-*.svg and the current
|
||||
* icons/icon.png): the same three rounded bars on the same dark tile, in the same colours. Nothing is redrawn. What changes is
|
||||
* how it is cut for small sizes: at 16-48 px the bars are snapped to whole pixels and made a little bolder and wider so they
|
||||
* stay crisp instead of blurring; from 64 px up the geometry is the mark's own proportions.
|
||||
*
|
||||
* node apps/desktop/scripts/make-icons.mjs [--out <folder>] writes icon-<size>.png for every size and icon.ico
|
||||
*/
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]]] : acc), []));
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const host = resolve(here, '../../host');
|
||||
const out = resolve(args.out ?? join(here, '../src-tauri/icons'));
|
||||
mkdirSync(out, { recursive: true });
|
||||
const { chromium } = await import(pathToFileURL(join(host, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
|
||||
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
|
||||
|
||||
const TILE = '#161a1d', LIGHT = '#eef1f3', GREEN = '#34d399'; // the colours of the current icon.png
|
||||
|
||||
/** Integer geometry for the small sizes: bars on whole pixels, centred. Returns [{x,y,w,h,fill}]. */
|
||||
function hinted(size) {
|
||||
const bar = Math.max(2, Math.round(size * 0.14)), gap = Math.max(2, Math.round(size * 0.1)), mid = bar + 1;
|
||||
const total = 2 * bar + mid + 2 * gap, top = Math.floor((size - total) / 2);
|
||||
const even = w => (w % 2 === size % 2 ? w : w + 1);
|
||||
const wide = even(Math.round(size * 0.5)), long = even(Math.round(size * 0.76));
|
||||
return [
|
||||
{ x: (size - wide) / 2, y: top, w: wide, h: bar, fill: LIGHT },
|
||||
{ x: (size - long) / 2, y: top + bar + gap, w: long, h: mid, fill: GREEN },
|
||||
{ x: (size - wide) / 2, y: top + bar + gap + mid + gap, w: wide, h: bar, fill: LIGHT },
|
||||
];
|
||||
}
|
||||
/** The mark's own proportions (from icon.png at 512): bars 34% / 50% / 34% wide, 9% high, on a tile with 21.5% corners. */
|
||||
function proportional(size) {
|
||||
const k = size / 512;
|
||||
return [
|
||||
{ x: 170 * k, y: 156 * k, w: 173 * k, h: 46 * k, fill: LIGHT },
|
||||
{ x: 128 * k, y: 232 * k, w: 256 * k, h: 48 * k, fill: GREEN },
|
||||
{ x: 170 * k, y: 309 * k, w: 173 * k, h: 46 * k, fill: LIGHT },
|
||||
];
|
||||
}
|
||||
function svg(size) {
|
||||
const small = size <= 48;
|
||||
const bars = (small ? hinted(size) : proportional(size));
|
||||
const radius = small ? Math.round(size * 0.22) : size * 0.215;
|
||||
const rects = bars.map(b => `<rect x="${b.x}" y="${b.y}" width="${b.w}" height="${b.h}" rx="${small ? Math.min(b.h / 2, 1.5 + size / 32) : b.h / 2}" fill="${b.fill}"${small ? ' shape-rendering="crispEdges"' : ''}/>`).join('');
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}"><rect width="${size}" height="${size}" rx="${radius}" fill="${TILE}"/>${rects}</svg>`;
|
||||
}
|
||||
|
||||
const SIZES = [16, 20, 24, 32, 40, 48, 64, 96, 128, 256, 512, 1024];
|
||||
const browser = await chromium.launch({ executablePath: CHROME, headless: true });
|
||||
const png = {};
|
||||
for (const size of SIZES) {
|
||||
const page = await browser.newPage({ viewport: { width: size, height: size }, deviceScaleFactor: 1 });
|
||||
await page.setContent(`<!doctype html><body style="margin:0;background:transparent">${svg(size)}</body>`);
|
||||
png[size] = await page.screenshot({ omitBackground: true, clip: { x: 0, y: 0, width: size, height: size } });
|
||||
await page.close();
|
||||
writeFileSync(join(out, `icon-${size}.png`), png[size]);
|
||||
}
|
||||
await browser.close();
|
||||
|
||||
// The .ico: one PNG image per size (16-256), the sizes Windows asks for at 100-250% scaling.
|
||||
const ICO = [16, 20, 24, 32, 40, 48, 64, 96, 128, 256];
|
||||
const head = Buffer.alloc(6); head.writeUInt16LE(0, 0); head.writeUInt16LE(1, 2); head.writeUInt16LE(ICO.length, 4);
|
||||
let offset = 6 + 16 * ICO.length;
|
||||
const entries = [], blobs = [];
|
||||
for (const size of ICO) {
|
||||
const data = png[size], e = Buffer.alloc(16);
|
||||
e.writeUInt8(size === 256 ? 0 : size, 0); e.writeUInt8(size === 256 ? 0 : size, 1); e.writeUInt8(0, 2); e.writeUInt8(0, 3);
|
||||
e.writeUInt16LE(1, 4); e.writeUInt16LE(32, 6); e.writeUInt32LE(data.length, 8); e.writeUInt32LE(offset, 12);
|
||||
offset += data.length; entries.push(e); blobs.push(data);
|
||||
}
|
||||
writeFileSync(join(out, 'icon.ico'), Buffer.concat([head, ...entries, ...blobs]));
|
||||
writeFileSync(join(out, 'icon.png'), png[512]);
|
||||
console.log('icons in', out, ICO.join(','));
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* The installer's pictures, drawn from the app's own design: the light surface and ink colours of the app (src/styles tokens), the
|
||||
* brand lockup and mark files, GT Walsheim Bold for the words (baked into the pictures, because NSIS cannot use a web font).
|
||||
*
|
||||
* node apps/desktop/scripts/make-installer-images.mjs [--out <folder>] [--preview <folder>]
|
||||
*
|
||||
* Writes sidebar.bmp (164x314, the welcome and finish pages) and header.bmp (150x57, the other pages) as 24-bit BMPs, and with
|
||||
* --preview the same as PNGs at 3x.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]]] : acc), []));
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const host = resolve(here, '../../host');
|
||||
const out = resolve(args.out ?? join(here, '../src-tauri/nsis'));
|
||||
mkdirSync(out, { recursive: true });
|
||||
const { chromium } = await import(pathToFileURL(join(host, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
|
||||
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
|
||||
const font = pathToFileURL(join(host, 'public/fonts/GTWalsheimPro-Bold.woff2')).href;
|
||||
const fontMedium = pathToFileURL(join(host, 'public/fonts/GTWalsheimPro-Medium.woff2')).href;
|
||||
const lockup = readFileSync(join(host, 'public/brand/strata-lockup-light.svg'), 'utf8');
|
||||
const mark = readFileSync(join(host, 'public/brand/strata-mark-light.svg'), 'utf8');
|
||||
|
||||
// the app's light tokens
|
||||
const SURFACE = '#f9fafb', INK = '#171b1e', MUTED = '#5b646b', ACCENT = '#10b981', LINE = '#e3e8ea';
|
||||
const css = `@font-face{font-family:GT;font-weight:700;src:url(${font})}@font-face{font-family:GT;font-weight:500;src:url(${fontMedium})}
|
||||
*{box-sizing:border-box}body{margin:0;font-family:GT,'Segoe UI',sans-serif;color:${INK};background:${SURFACE}}`;
|
||||
|
||||
const pages = {
|
||||
sidebar: { w: 164, h: 314, html: `<div style="width:164px;height:314px;padding:22px 18px;display:flex;flex-direction:column;background:linear-gradient(180deg,#f9fafb 0%,#eef6f2 100%);border-right:1px solid ${LINE}">
|
||||
<div style="height:26px">${lockup.replace('<svg', '<svg style="height:26px;width:auto;display:block"')}</div>
|
||||
<div style="margin-top:auto">
|
||||
<div style="font-weight:700;font-size:22px;line-height:1.12;letter-spacing:-.01em">State of<br>the art.<br>At home.</div>
|
||||
<div style="margin-top:10px;font-weight:500;font-size:11px;line-height:1.5;color:${MUTED}">AI on your own computer.<br>Private. Fast. Easy.</div>
|
||||
<div style="margin-top:16px;height:3px;width:34px;border-radius:2px;background:${ACCENT}"></div>
|
||||
</div></div>` },
|
||||
header: { w: 150, h: 57, html: `<div style="width:150px;height:57px;display:flex;align-items:center;justify-content:flex-end;padding-right:12px;background:${SURFACE}">${lockup.replace('<svg', '<svg style="height:22px;width:auto;display:block"')}</div>` },
|
||||
};
|
||||
|
||||
const browser = await chromium.launch({ executablePath: CHROME, headless: true });
|
||||
const work = join(tmpdir(), 'strata-installer-images'); mkdirSync(work, { recursive: true });
|
||||
for (const [name, page] of Object.entries(pages)) {
|
||||
for (const scale of [1, 3]) {
|
||||
const file = join(work, `${name}.html`);
|
||||
writeFileSync(file, `<!doctype html><meta charset="utf-8"><style>${css}</style><body>${page.html}</body>`);
|
||||
const tab = await browser.newPage({ viewport: { width: page.w, height: page.h }, deviceScaleFactor: scale });
|
||||
await tab.goto(pathToFileURL(file).href);
|
||||
await tab.evaluate(() => document.fonts.ready);
|
||||
await tab.waitForTimeout(200);
|
||||
const png = join(work, `${name}@${scale}.png`);
|
||||
await tab.screenshot({ path: png, clip: { x: 0, y: 0, width: page.w, height: page.h } });
|
||||
await tab.close();
|
||||
if (scale === 3 && args.preview) { mkdirSync(resolve(args.preview), { recursive: true }); writeFileSync(join(resolve(args.preview), `${name}.png`), readFileSync(png)); }
|
||||
}
|
||||
}
|
||||
await browser.close();
|
||||
// 24-bit BMP, the format NSIS wants: render at 3x, shrink with a good filter
|
||||
const py = `import sys\nfrom PIL import Image\nfor name in ('sidebar','header'):\n im = Image.open(sys.argv[1] + '/' + name + '@3.png').convert('RGB')\n w, h = im.width // 3, im.height // 3\n im.resize((w, h), Image.LANCZOS).save(sys.argv[2] + '/' + name + '.bmp', 'BMP')\n`;
|
||||
const done = spawnSync('python', ['-c', py, work, out], { encoding: 'utf8' });
|
||||
if (done.status !== 0) { console.error(done.stderr); process.exit(1); }
|
||||
console.log('wrote', join(out, 'sidebar.bmp'), join(out, 'header.bmp'));
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# Write a tester's README with the checksums and sizes of the files it names, computed from the files themselves.
|
||||
#
|
||||
# apps/desktop/scripts/make-tester-readme.sh <template> <out.txt> --appimage <file name> --files-dir <dir> [--files-dir <dir> ...]
|
||||
#
|
||||
# Why: a README that carries a hand-pasted sha256 goes stale the moment the file is rebuilt (the Ubuntu 26.04 tester compared the file
|
||||
# with the old build's hash). The template holds placeholders instead, and this script fills them from the real files:
|
||||
# @APPIMAGE@ the AppImage's file name
|
||||
# @APP_VERSION@ the app version, read from apps/desktop/src-tauri/Cargo.toml
|
||||
# @SHA256:<name>@ sha256 of the file <name> found in one of the --files-dir folders
|
||||
# @SIZE_GB:<name>@ its size in GB with one decimal
|
||||
# @INCLUDE:<file>@ (alone on a line) the contents of <file> from the template's folder, with the same placeholders
|
||||
# It refuses to write the README when a named file is missing or a placeholder is left over, so a stale or empty hash cannot ship.
|
||||
# The same check is done by `--verify <readme> --files-dir <dir>`: every "<64 hex> <name>" or "sha256 <64 hex>" pair that names a file
|
||||
# present in the folders must match that file.
|
||||
set -euo pipefail
|
||||
|
||||
usage() { echo "usage: $0 <template> <out.txt> --appimage <name> --files-dir <dir> [--files-dir <dir> ...]" >&2; echo " $0 --verify <readme> --files-dir <dir> [...]" >&2; exit 2; }
|
||||
|
||||
VERIFY=""; TEMPLATE=""; OUT=""; APPIMAGE=""; DIRS=()
|
||||
if [ "${1:-}" = "--verify" ]; then VERIFY="${2:?readme}"; shift 2; else TEMPLATE="${1:?template}"; OUT="${2:?out}"; shift 2 || usage; fi
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--appimage) APPIMAGE="$2"; shift 2 ;;
|
||||
--files-dir) DIRS+=("$2"); shift 2 ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
[ ${#DIRS[@]} -gt 0 ] || usage
|
||||
|
||||
find_file() { local d; for d in "${DIRS[@]}"; do [ -f "$d/$1" ] && { echo "$d/$1"; return 0; }; done; return 1; }
|
||||
sha_of() { sha256sum "$1" | awk '{print $1}'; }
|
||||
size_gb() { awk -v b="$(stat -c %s "$1" 2>/dev/null || stat -f %z "$1")" 'BEGIN{printf "%.1f", b/1000000000}'; }
|
||||
|
||||
if [ -n "$VERIFY" ]; then
|
||||
# every checksum in the README must be the checksum of a file that is here, and a line that names a file and carries a checksum must carry that file's checksum
|
||||
hashes=""; bad=0; seen=0
|
||||
for d in "${DIRS[@]}"; do for f in "$d"/*; do [ -f "$f" ] && grep -qF "$(basename "$f")" "$VERIFY" && hashes="$hashes $(sha_of "$f")"; done; done # only files the README names are hashed (a folder may hold big unrelated files)
|
||||
readme_hashes="$(grep -Eo '[0-9a-f]{64}' "$VERIFY" | sort -u || true)"
|
||||
for h in $readme_hashes; do
|
||||
seen=$((seen + 1))
|
||||
case " $hashes " in *" $h "*) ;; *) echo "STALE: the README lists $h, which is the checksum of no file in: ${DIRS[*]}" >&2; bad=$((bad + 1)) ;; esac
|
||||
done
|
||||
for d in "${DIRS[@]}"; do for f in "$d"/*; do
|
||||
[ -f "$f" ] || continue
|
||||
grep -qF "$(basename "$f")" "$VERIFY" || continue
|
||||
# a line that names this file AND carries a checksum must carry this file's checksum (a sentence that merely mentions the file is fine)
|
||||
line_hash="$(grep -F "$(basename "$f")" "$VERIFY" | grep -Eo '[0-9a-f]{64}' | head -1 || true)"
|
||||
if [ -n "$line_hash" ] && [ "$line_hash" != "$(sha_of "$f")" ]; then
|
||||
echo "STALE: $(basename "$f") is $(sha_of "$f"), the README says $line_hash" >&2; bad=$((bad + 1))
|
||||
fi
|
||||
done; done
|
||||
echo "$seen checksum(s) in the README checked, $bad stale"
|
||||
[ "$bad" = 0 ] && [ "$seen" -gt 0 ]
|
||||
exit
|
||||
fi
|
||||
|
||||
[ -n "$APPIMAGE" ] || usage
|
||||
TDIR="$(cd "$(dirname "$TEMPLATE")" && pwd)"
|
||||
# the app version has one source (the same file Tauri and the shell use)
|
||||
APP_VERSION="$(grep -m1 '^version = ' "$TDIR/../src-tauri/Cargo.toml" | cut -d'"' -f2)"
|
||||
[ -n "$APP_VERSION" ] || { echo "make-tester-readme: no version in $TDIR/../src-tauri/Cargo.toml" >&2; exit 1; }
|
||||
TMP="$(mktemp)"; trap 'rm -f "$TMP" "$TMP.2"' EXIT
|
||||
# includes first (one level), then the placeholders
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
if [[ "$line" =~ ^@INCLUDE:([^@]+)@$ ]]; then tr -d '\r' < "$TDIR/${BASH_REMATCH[1]}"; else printf '%s\n' "$line"; fi
|
||||
done < <(tr -d '\r' < "$TEMPLATE") | sed "s|@APPIMAGE@|$APPIMAGE|g; s|@APP_VERSION@|$APP_VERSION|g" > "$TMP"
|
||||
|
||||
# @SHA256:name@ and @SIZE_GB:name@
|
||||
while IFS= read -r token; do
|
||||
kind="${token#@}"; kind="${kind%%:*}"; name="${token#@*:}"; name="${name%@}"
|
||||
f="$(find_file "$name")" || { echo "make-tester-readme: no file named $name in: ${DIRS[*]} (a README must not carry a hash it did not compute)" >&2; exit 1; }
|
||||
case "$kind" in
|
||||
SHA256) value="$(sha_of "$f")" ;;
|
||||
SIZE_GB) value="$(size_gb "$f")" ;;
|
||||
*) echo "make-tester-readme: unknown placeholder $token" >&2; exit 1 ;;
|
||||
esac
|
||||
# replace every occurrence of this exact token (plain text, no pattern characters)
|
||||
text="$(cat "$TMP"; printf x)"; text="${text%x}"
|
||||
printf '%s' "${text//"$token"/$value}" > "$TMP"
|
||||
done < <(grep -Eo '@(SHA256|SIZE_GB):[^@ ]+@' "$TMP" | sort -u)
|
||||
|
||||
if grep -Eq '@[A-Z0-9_]+[:@]' "$TMP"; then echo "make-tester-readme: placeholders left over:" >&2; grep -En '@[A-Z0-9_]+[:@]' "$TMP" >&2; exit 1; fi
|
||||
cp "$TMP" "$OUT"
|
||||
echo "wrote $OUT"
|
||||
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Attached pictures reaching a model, end to end on a test box, through the installed app layout (packaged host + bundled Control + an engine
|
||||
* component that carries strata-vision):
|
||||
* 1. the model catalog marks the picture-capable models (images: true) when the picture reader is installed;
|
||||
* 2. API models: a llama-server with a small picture model (--mmproj) is saved as a Custom model API; the host works out that it reads pictures,
|
||||
* and a real picture goes out as image_url and is answered. A server without a picture reader, a model set "not to read pictures", and an API
|
||||
* of another style each get a plain refusal (409) that says how to fix it; ticking "This model reads pictures" lets the picture through;
|
||||
* 3. a local model (unless --skip-local): Download and prepare with Read pictures ON, load, and a picture through the app's own chat.
|
||||
*
|
||||
* node pics-e2e.mjs --install <node/ host/ control/> --components <engine components> --work <scratch> --image shapes.png \
|
||||
* Also: --silent <url> (a server whose model list says nothing about pictures: ON by default), --agent (the same picture through the agent; needs a Harness)
|
||||
* --vlm http://127.0.0.1:18090/v1 --vlm-model smolvlm --text-only http://127.0.0.1:18091/v1 [--skip-local --family qwen --model IQ3_XXS --data <folder> --device cpu]
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') || all[i + 1] === undefined ? true : all[i + 1]]] : acc), []));
|
||||
for (const need of ['install', 'components', 'work', 'image']) if (!args[need]) { console.error('missing --' + need); process.exit(2); }
|
||||
const install = resolve(args.install), work = resolve(args.work), components = resolve(args.components);
|
||||
const want = String(args.match ?? args.model ?? 'IQ3_XXS').toLowerCase(), contextSize = Number(args.context ?? 32768), skipLocal = args['skip-local'] === true, device = String(args.device ?? 'cpu'), family = String(args.family ?? 'qwen'), modelId = String(args.model ?? 'IQ3_XXS');
|
||||
const node = join(install, 'node', 'node'), hostEntry = join(install, 'host', 'server', 'index.mjs');
|
||||
const dirs = { models: join(work, 'models'), profiles: join(work, 'profiles'), runtime: join(work, 'runtime'), home: join(work, 'home'), xdg: join(work, 'xdg'), appdata: join(work, 'appdata') };
|
||||
for (const d of [dirs.models, dirs.home, dirs.xdg, dirs.appdata]) mkdirSync(d, { recursive: true });
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
let failed = false;
|
||||
const record = (name, ok, detail = '') => { if (!ok) failed = true; console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' ' + String(detail).slice(0, 600) : ''}`); };
|
||||
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
|
||||
const env = { ...process.env, STRATA_COMPONENTS_DIR: components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime, STRATA_INSTALL_DIR: dirs.models,
|
||||
STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(work, 'models-unused'), APPDATA: dirs.appdata, XDG_CONFIG_HOME: dirs.xdg, PYTHONDONTWRITEBYTECODE: '1' };
|
||||
delete env.STRATA_API_KEY;
|
||||
const port = await freePort();
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const host = spawn(node, [hostEntry], { cwd: join(install, 'host'), stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...env, STRATA_PORT: String(port), STRATA_DIST: join(install, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1',
|
||||
STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()) } });
|
||||
let hostLog = '';
|
||||
host.stdout.on('data', c => { hostLog += c; }); host.stderr.on('data', c => { hostLog += c; });
|
||||
const call = async (path, method = 'GET', body) => {
|
||||
const response = await fetch(base + path, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
||||
return { status: response.status, body: await response.json().catch(() => null) };
|
||||
};
|
||||
const png = 'data:image/png;base64,' + readFileSync(resolve(args.image)).toString('base64');
|
||||
const QUESTION = 'Name each shape in this picture with its colour, and read out any text.';
|
||||
|
||||
/** One chat turn through the app's own endpoint: the plain-text answer, or the refusal (status + message). */
|
||||
async function chat(extra, message = QUESTION) {
|
||||
const response = await fetch(base + '/api/chat/stream', { method: 'POST', headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ message, agent: false, images: [{ url: png, name: 'shapes.png' }], ...extra }) });
|
||||
if (response.status !== 200) { const b = await response.json().catch(() => ({})); return { status: response.status, message: b.error ?? b.message ?? '' }; }
|
||||
let text = '', error = '';
|
||||
for (const line of (await response.text()).split('\n')) {
|
||||
if (!line.trim()) continue;
|
||||
let f; try { f = JSON.parse(line); } catch { continue; }
|
||||
if (f.type === 'error') error = f.message ?? 'error';
|
||||
else if (typeof f.text === 'string' && f.type === 'delta') text += f.text;
|
||||
}
|
||||
return { status: 200, text, error };
|
||||
}
|
||||
const saveApi = async (name, baseUrl, models, protocol = 'openai-chat') => {
|
||||
const existing = (await call('/api/model-providers')).body.connections.find(c => c.name === name);
|
||||
return (await call('/api/model-providers', 'POST', { ...(existing ? { id: existing.id } : {}), kind: 'custom', provider: null, name, baseUrl, protocol, apiKey: '', models })).body;
|
||||
};
|
||||
async function readsAfter(connectionName, id, want, seconds = 20) {
|
||||
for (let i = 0; i < seconds * 2; i++) {
|
||||
const c = (await call('/api/model-providers')).body.connections.find(x => x.name === connectionName);
|
||||
const m = c?.models.find(x => x.id === id);
|
||||
if (m && m.reads === want) return { c, m };
|
||||
await delay(500);
|
||||
}
|
||||
const c = (await call('/api/model-providers')).body.connections.find(x => x.name === connectionName);
|
||||
return { c, m: c?.models.find(x => x.id === id) };
|
||||
}
|
||||
async function waitJob(id, seconds) {
|
||||
let last = '';
|
||||
for (let i = 0; i < seconds * 2; i++) {
|
||||
const state = (await call('/api/native')).body;
|
||||
const job = state.jobs.find(j => j.id === id);
|
||||
if (job && job.summary !== last) { last = job.summary; console.log(' ... ' + String(last).slice(0, 120)); }
|
||||
if (job && job.state !== 'running') return { job, state };
|
||||
await delay(500);
|
||||
}
|
||||
throw new Error('operation did not finish');
|
||||
}
|
||||
const names = low => /(red|blue|green|circle|square|triangle|strata|42)/.test(low);
|
||||
|
||||
async function main() {
|
||||
let up = false;
|
||||
for (let i = 0; i < 200 && !up; i++) { try { up = (await fetch(base + '/api/health')).ok; } catch { /* not yet */ } if (!up) await delay(150); }
|
||||
record('host healthy', up, up ? base : hostLog.slice(-500));
|
||||
if (!up) return;
|
||||
|
||||
// ---- 1. the catalog
|
||||
const packages = existsSync(join(components, 'engine')) ? readdirSync(join(components, 'engine')) : [];
|
||||
const reader = packages.some(p => existsSync(join(components, 'engine', p, 'strata-vision')));
|
||||
const native = (await call('/api/native?refresh=1')).body;
|
||||
record('the engine component carries the picture reader and the host knows it (visionReader)', reader && native.visionReader === true, `component=${reader} host=${native.visionReader}`);
|
||||
const rows = (native.catalog?.families ?? []).flatMap(f => (f.sizes ?? []).map(s => ({ family: f.title ?? f.family, size: s.id, images: s.images, capable: s.imagesCapable })));
|
||||
console.log(' catalog (family / size / images / imagesCapable):');
|
||||
for (const r of rows) console.log(` ${String(r.family).padEnd(26)} ${String(r.size).padEnd(30)} images=${r.images} capable=${r.capable}`);
|
||||
record('some models in the catalog read pictures', rows.some(r => r.images === true), `${rows.filter(r => r.images).length} of ${rows.length}`);
|
||||
record('every picture-capable model reads pictures when the reader is installed', rows.every(r => r.capable === r.images), '');
|
||||
const flash = rows.filter(r => /flash-next/i.test(`${r.family} ${r.size}`) && !/gyro|qad/i.test(`${r.family} ${r.size}`));
|
||||
record('the Qwen3.8-Flash-Next models are marked images: true', flash.length > 0 && flash.every(r => r.images === true), flash.map(r => r.size + '=' + r.images).join(' '));
|
||||
|
||||
// ---- 2. API models
|
||||
if (args.vlm) {
|
||||
const smol = args['vlm-model'] ?? 'smolvlm';
|
||||
await saveApi('Small picture model', args.vlm, [{ id: smol, name: 'SmolVLM' }]);
|
||||
const found = await readsAfter('Small picture model', smol, true);
|
||||
record('the host works out that the llama-server reads pictures (no tick needed)', found.m?.reads === true, JSON.stringify(found.m));
|
||||
const conn = found.c;
|
||||
const sent = await chat({ remoteModel: { connectionId: conn?.id, modelId: smol } });
|
||||
console.log(` answer from the API model: ${JSON.stringify((sent.text ?? sent.message ?? '').slice(0, 300))}`);
|
||||
record('the picture goes to the API model and is answered', sent.status === 200 && !sent.error && (sent.text ?? '').trim().length > 0 && names((sent.text ?? '').toLowerCase()), `status=${sent.status} err=${sent.error ?? ''}`);
|
||||
|
||||
if (args['text-only']) {
|
||||
await saveApi('Text only server', args['text-only'], [{ id: 'plain', name: 'Plain model' }]);
|
||||
const t = await readsAfter('Text only server', 'plain', false);
|
||||
record('a server without a picture reader is known not to read pictures', t.m?.reads === false, JSON.stringify(t.m));
|
||||
const refused = await chat({ remoteModel: { connectionId: t.c.id, modelId: 'plain' } });
|
||||
console.log(` refusal: ${JSON.stringify(refused.message)}`);
|
||||
record('a picture for it is refused in plain words that say how to fix it', refused.status === 409 && /Read pictures with this model/.test(refused.message ?? '') && /Settings/.test(refused.message ?? ''), `${refused.status}`);
|
||||
const ticked = await saveApi('Text only server', args['text-only'], [{ id: 'plain', name: 'Plain model', vision: true }]);
|
||||
const after = (await call('/api/model-providers')).body.connections.find(x => x.name === 'Text only server');
|
||||
record('ticking "Read pictures with this model" is saved and makes the model read pictures', after?.models[0]?.vision === true && after?.models[0]?.reads === true, JSON.stringify(after?.models[0]));
|
||||
const through = await chat({ remoteModel: { connectionId: after.id, modelId: 'plain' } });
|
||||
record('with the tick the picture is sent on (the server itself decides what to do with it)', through.status === 200 && (through.error ?? '') !== '' ? /turn off/.test(through.error) : through.status === 200, `${through.status} ${through.message ?? through.error ?? ''}`);
|
||||
}
|
||||
|
||||
await saveApi('Small picture model', args.vlm, [{ id: smol, name: 'SmolVLM', vision: false }]);
|
||||
const off = await readsAfter('Small picture model', smol, false);
|
||||
const notRead = await chat({ remoteModel: { connectionId: off.c.id, modelId: smol } });
|
||||
record('a model switched OFF refuses pictures and says where to turn them on again', notRead.status === 409 && /not to read pictures/.test(notRead.message ?? '') && /Settings/.test(notRead.message ?? ''), `${notRead.status} ${notRead.message ?? ''}`);
|
||||
await saveApi('Small picture model', args.vlm, [{ id: smol, name: 'SmolVLM', vision: null }]);
|
||||
|
||||
// unknown is ON by default: a server whose list and /props say nothing is sent the picture
|
||||
if (args.silent) {
|
||||
await saveApi('Silent server', args.silent, [{ id: 'quiet', name: 'Quiet model' }]);
|
||||
const q = await readsAfter('Silent server', 'quiet', true, 8);
|
||||
record('a server that says nothing about pictures is ON by default', q.m?.reads === true && q.m?.vision === null, JSON.stringify(q.m));
|
||||
const sentQuiet = await chat({ remoteModel: { connectionId: q.c.id, modelId: 'quiet' } });
|
||||
record('and the picture is sent to it (not refused)', sentQuiet.status === 200, `${sentQuiet.status} ${sentQuiet.message ?? sentQuiet.error ?? ''}`);
|
||||
}
|
||||
await saveApi('Other style', 'https://api.anthropic.com/v1', [{ id: 'claude-x', name: 'Claude' }], 'anthropic');
|
||||
const other = (await call('/api/model-providers')).body.connections.find(x => x.name === 'Other style');
|
||||
const style = await chat({ remoteModel: { connectionId: other.id, modelId: 'claude-x' } });
|
||||
console.log(` other style: ${JSON.stringify(style.message)}`);
|
||||
record('an API of another style says plainly why pictures stay in the preview', style.status === 409 && /OpenAI/.test(style.message ?? ''), `${style.status}`);
|
||||
|
||||
}
|
||||
if (skipLocal) return;
|
||||
|
||||
// ---- 3. a local model
|
||||
const values = { family, model: modelId, context: contextSize, vision: 'yes', loadVision: device, ...(args.data ? { data_dir: resolve(args.data) } : {}) };
|
||||
const started = await call('/api/native/operations', 'POST', { kind: 'install', args: values });
|
||||
record('the install operation starts', started.status === 200 || started.status === 202, JSON.stringify(started.body).slice(0, 200));
|
||||
const done = await waitJob(started.body.id, 3600);
|
||||
record('Download and prepare finishes', done.job.state === 'done', `${done.job.state} ${done.job.error ?? done.job.summary}`);
|
||||
const refreshed = (await call('/api/native?refresh=1')).body;
|
||||
const model = refreshed.installed?.models?.find(m => [m.id, m.model, m.tag].some(x => String(x ?? '').toLowerCase() === want));
|
||||
record('the model is prepared with its picture reader', !!model?.visionPrepared, JSON.stringify({ ready: model?.ready, visionPrepared: model?.visionPrepared }));
|
||||
const load = await call('/api/native/operations', 'POST', { kind: 'load', args: { model: model?.model ?? want } });
|
||||
const loaded = await waitJob(load.body.id, 900);
|
||||
const instance = loaded.state.instances[0];
|
||||
record('the model loads with the picture reader', loaded.job.state === 'done' && instance?.status === 'ready' && instance?.vision === true, `${loaded.job.state} ${loaded.job.error ?? ''} vision=${instance?.vision}`);
|
||||
record('the model loaded with pictures ON without being asked per load (default)', instance?.vision === true, '');
|
||||
const t0 = Date.now();
|
||||
let local = { status: 0 };
|
||||
for (let i = 0; i < 2 && !(local.text ?? '').trim(); i++) local = await chat({ nativeModelId: instance?.id, maxTokens: 4096 });
|
||||
console.log(` answer from the local model (${Math.round((Date.now() - t0) / 1000)} s): ${JSON.stringify((local.text ?? local.message ?? '').slice(0, 300))}`);
|
||||
const low = (local.text ?? '').toLowerCase();
|
||||
record('the local model answers about the picture (names the shapes)', local.status === 200 && /red|circle/.test(low) && /blue|square/.test(low) && /green|triangle/.test(low), `status=${local.status} err=${local.error ?? ''}`);
|
||||
record('the local model reads STRATA 42', low.includes('42'));
|
||||
// the same picture through the agent (the Harness route must declare image input: the "does not declare image input" error of the tester)
|
||||
if (args.agent) { // needs a Harness beside the host (the installed layout has one; a bare test box does not)
|
||||
const t1 = Date.now(); let a = { status: 0 };
|
||||
for (let i = 0; i < 2 && !/red|circle|square|triangle/.test((a.text ?? '').toLowerCase()); i++) a = await chat({ nativeModelId: instance?.id, agent: true, sessionId: 'pics-agent-' + i, mode: 'plan', maxTokens: 4096 });
|
||||
console.log(` answer through the agent (${Math.round((Date.now() - t1) / 1000)} s): ${JSON.stringify((a.text ?? a.message ?? '').slice(0, 300))}`);
|
||||
record('the agent route reads the picture too', a.status === 200 && /red|circle|square|triangle/.test((a.text ?? '').toLowerCase()) && !/does not declare image input/.test(a.text ?? ''), `status=${a.status} err=${a.error ?? ''}`);
|
||||
}
|
||||
// OFF: loading with Read pictures off refuses a picture in plain words and says what to do
|
||||
await call(`/api/native/instances/${instance?.id}/unload`, 'POST', {});
|
||||
const loadOff = await call('/api/native/operations', 'POST', { kind: 'load', args: { model: model?.model ?? want, vision: 'no' } });
|
||||
const loadedOff = await waitJob(loadOff.body.id, 900);
|
||||
const off2 = loadedOff.state.instances[0];
|
||||
record('loaded with Read pictures OFF', loadedOff.job.state === 'done' && off2?.vision === false, `${loadedOff.job.state} vision=${off2?.vision}`);
|
||||
const refusedLocal = await chat({ nativeModelId: off2?.id, maxTokens: 64 });
|
||||
console.log(` refusal: ${JSON.stringify(refusedLocal.message)}`);
|
||||
record('a picture for the OFF model is refused with a next step', refusedLocal.status === 409 && /Read pictures/.test(refusedLocal.message ?? '') && /Models/.test(refusedLocal.message ?? ''), `${refusedLocal.status}`);
|
||||
const unloaded = await call(`/api/native/instances/${off2?.id}/unload`, 'POST', {});
|
||||
record('unload', unloaded.status === 200, unloaded.status);
|
||||
}
|
||||
try { await main(); } catch (e) { record('run', false, e.stack ?? e); }
|
||||
host.kill();
|
||||
await delay(1500);
|
||||
console.log(failed ? 'PICS-E2E FAIL' : 'PICS-E2E PASS');
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# Put the bundled payloads back to the bytes they were staged with, inside a finished AppImage.
|
||||
#
|
||||
# apps/desktop/scripts/repack-appimage.sh <in.AppImage> <out.AppImage> [--resources <src-tauri/resources>] [--check]
|
||||
#
|
||||
# Why: Tauri hands the AppDir to linuxdeploy, which treats every ELF file under usr/lib as a library to fix up: it rewrites the RPATH of
|
||||
# the engine (strata, libstrata_kq.so, libcudart), of the bundled Python and Node and of the Harness's native modules. The files then no
|
||||
# longer match the SHA-256 and size their manifests list (engine-component.json), so the host refuses the bundled engine, and the
|
||||
# installer's own integrity promise ("what we staged is what runs") is broken. This script unpacks the AppImage, copies the staged
|
||||
# folders (control, harness, node, engine-bundle) over the AppDir byte for byte, and packs it again with the same runtime.
|
||||
#
|
||||
# --check does not write anything: it lists every engine-bundle file whose bytes differ from the manifest in the AppImage.
|
||||
set -euo pipefail
|
||||
|
||||
IN="$(realpath "${1:?in.AppImage}")"; OUT="${2:-}"
|
||||
RES="$(cd "$(dirname "${BASH_SOURCE[0]}")/../src-tauri/resources" && pwd)"
|
||||
CHECK=0
|
||||
shift 2 || true
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--resources) RES="$(cd "$2" && pwd)"; shift 2 ;;
|
||||
--check) CHECK=1; shift ;;
|
||||
*) echo "unknown option $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
OFFSET="$("$IN" --appimage-offset)"
|
||||
WORK="$(mktemp -d -t strata-repack.XXXXXX)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
unsquashfs -no-progress -o "$OFFSET" -d "$WORK/AppDir" "$IN" >/dev/null
|
||||
# the product folder under usr/lib (Tauri names it after productName)
|
||||
LIB="$(find "$WORK/AppDir/usr/lib" -maxdepth 2 -type d -name engine-bundle -printf '%h\n' | head -1)"
|
||||
[ -n "$LIB" ] || LIB="$(find "$WORK/AppDir/usr/lib" -maxdepth 2 -type d -name control -printf '%h\n' | head -1)"
|
||||
[ -n "$LIB" ] || { echo "no Strata resource folder in the AppImage" >&2; exit 1; }
|
||||
|
||||
if [ "$CHECK" = 1 ]; then
|
||||
bad=0
|
||||
while IFS= read -r f; do
|
||||
rel="${f#$RES/}"
|
||||
if ! cmp -s "$f" "$LIB/$rel"; then echo "differs: $rel"; bad=$((bad + 1)); fi
|
||||
done < <(find "$RES/engine-bundle" "$RES/node" -type f ! -name .gitignore)
|
||||
echo "$bad file(s) differ from what was staged"
|
||||
[ "$bad" = 0 ]
|
||||
exit
|
||||
fi
|
||||
|
||||
[ -n "$OUT" ] || { echo "give out.AppImage" >&2; exit 2; }
|
||||
for d in control harness node engine-bundle; do
|
||||
[ -d "$RES/$d" ] || continue
|
||||
rsync -aL --checksum --delete --exclude .gitignore "$RES/$d/" "$LIB/$d/"
|
||||
done
|
||||
# The launcher records the environment the AppImage was started from, before its own hooks add the AppImage's libraries and GTK/GIO
|
||||
# settings (LD_LIBRARY_PATH, GTK_*, XDG_DATA_DIRS ...). The host gives it back to every program that is not Strata's own (zenity,
|
||||
# kdialog, xdg-open, the assistant's shell): apps/host/server/external-env.mjs. Without it a system program loads the AppImage's libssl
|
||||
# and dies ("OPENSSL_3.2.0 not found"). The line goes right after `this_dir=...`, ahead of every hook.
|
||||
if ! grep -q STRATA_ORIGINAL_ENV "$WORK/AppDir/AppRun"; then
|
||||
sed -i '/^this_dir=/a # Strata: the environment this AppImage was started from (given back to programs that are not Strata'"'"'s own)\nexport STRATA_ORIGINAL_ENV="$(env -0 | base64 -w0)"' "$WORK/AppDir/AppRun"
|
||||
grep -q STRATA_ORIGINAL_ENV "$WORK/AppDir/AppRun" || { echo "could not add the environment record to AppRun" >&2; exit 1; }
|
||||
fi
|
||||
mksquashfs "$WORK/AppDir" "$WORK/app.squashfs" -root-owned -noappend -comp zstd -b 131072 -no-progress >/dev/null
|
||||
head -c "$OFFSET" "$IN" > "$OUT"
|
||||
cat "$WORK/app.squashfs" >> "$OUT"
|
||||
chmod +x "$OUT"
|
||||
# The checksum is computed here, from the finished file, every time: a README or a note must take it from this file
|
||||
# (apps/desktop/scripts/make-tester-readme.sh) and never from an older build.
|
||||
( cd "$(dirname "$OUT")" && sha256sum "$(basename "$OUT")" > "$(basename "$OUT").sha256" )
|
||||
echo "repacked: $OUT ($(stat -c %s "$OUT") bytes)"
|
||||
cat "$OUT.sha256"
|
||||
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stage the Control component the installer ships: src-tauri/resources/control.
|
||||
|
||||
python apps/desktop/scripts/stage-control.py # build the terminal bundle, stage it
|
||||
python apps/desktop/scripts/stage-control.py --archive X.zip # stage an archive you already built
|
||||
python apps/desktop/scripts/stage-control.py --wheelhouse DIR # build with no network (wheels from DIR)
|
||||
python apps/desktop/scripts/stage-control.py --check # verify what is staged
|
||||
|
||||
What lands in `control/`: the pinned CPython 3.12.15 (python-build-standalone, digest-checked), the hash-locked wheels
|
||||
(`packaging/locks/runtime-<target>-cp312.txt`, installed with --require-hashes at build time), strata_control,
|
||||
strata_cli, serve, tools/strata_tokenizer.py + gguf_reader.py, and BUNDLE.json (version, interpreter and lock digests,
|
||||
the digest of the archive it came from). It is the same bundle as the Terminal archive (packaging/package_terminal.py):
|
||||
one Python runtime-core for both products. The app then never runs pip, npm, git or a compiler to start or serve.
|
||||
|
||||
This script is a BUILD step. It may download the interpreter and the wheels (verified); the app never does.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[2]
|
||||
OUT = HERE.parent / "src-tauri" / "resources" / "control"
|
||||
sys.path.insert(0, str(ROOT / "packaging"))
|
||||
import package_terminal as pt # noqa: E402
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1 << 20), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def build_archive(work: Path, wheelhouse: str | None) -> Path:
|
||||
command = [sys.executable, str(ROOT / "packaging" / "package_terminal.py"), "--out", str(work)]
|
||||
if wheelhouse:
|
||||
command += ["--wheelhouse", wheelhouse]
|
||||
subprocess.run(command, check=True, cwd=str(ROOT))
|
||||
archives = sorted(work.glob("strata-terminal-*.zip")) + sorted(work.glob("strata-terminal-*.tar.gz"))
|
||||
if len(archives) != 1:
|
||||
raise SystemExit(f"expected one archive in {work}, found {archives}")
|
||||
return archives[0]
|
||||
|
||||
|
||||
def extract(archive: Path, destination: Path) -> Path:
|
||||
"""Unpack the archive; returns the folder that holds python/ and strata.py."""
|
||||
scratch = destination.parent / (destination.name + ".incoming")
|
||||
shutil.rmtree(scratch, ignore_errors=True)
|
||||
scratch.mkdir(parents=True)
|
||||
if archive.name.endswith(".zip"):
|
||||
with zipfile.ZipFile(archive) as zf:
|
||||
for member in zf.namelist():
|
||||
target = (scratch / member).resolve()
|
||||
if scratch.resolve() not in target.parents and target != scratch.resolve():
|
||||
raise SystemExit(f"unsafe member in {archive.name}: {member}")
|
||||
zf.extractall(scratch)
|
||||
else:
|
||||
with tarfile.open(archive) as tf:
|
||||
tf.extractall(scratch, filter="data")
|
||||
(inner,) = [p for p in scratch.iterdir() if p.is_dir()]
|
||||
return inner
|
||||
|
||||
|
||||
#: What the app's installer needs on top of the Terminal bundle: the model catalog, GPU probe and install job. The list and the copy
|
||||
#: live in packaging/package_terminal.py (the Terminal archive carries the same files, for `strata launch`).
|
||||
INSTALLER_FILES, INSTALLER_TOOLS, INSTALLER_DATA = pt.INSTALLER_FILES, pt.INSTALLER_TOOLS, pt.INSTALLER_DATA
|
||||
local_imports = pt.local_imports
|
||||
|
||||
|
||||
def add_installer(folder: Path) -> dict:
|
||||
"""Copy the installer's files from the source tree into the staged Control (after the Terminal bundle is in place)."""
|
||||
return pt.add_installer(folder, ROOT)
|
||||
|
||||
|
||||
#: What the other names of the interpreter become: Tauri copies a symlink as a full copy of its target, so `python`, `python3` and
|
||||
#: `python3.12` in a bundle would be three real files (68 MB each on Linux). Only `python3.12` stays the real one.
|
||||
#: The launcher runs inside the AppImage's environment (LD_LIBRARY_PATH holds the bundled libraries), where an external program such as
|
||||
#: `dirname` can fail to start (Ubuntu 26.04's coreutils: "libsystemd.so.0: version LIBSYSTEMD_254 not found"), so it uses shell built-ins
|
||||
#: only. The first version called `dirname`; that left the folder empty and "Control stopped" on such a computer.
|
||||
LAUNCHER = """#!/bin/sh
|
||||
# Strata: this name only starts the real interpreter beside it (the bundle keeps one copy, not one per name). Built-ins only: no dirname.
|
||||
case "$0" in */*) dir=${0%%/*} ;; *) dir=. ;; esac
|
||||
here=$(cd "$dir" && pwd)
|
||||
exec "$here/%s" "$@"
|
||||
"""
|
||||
|
||||
|
||||
def dedupe_python(folder: Path) -> list[str]:
|
||||
"""Make every other name of the interpreter in `python/bin` a small launcher of the real binary (Linux and macOS; Windows has one
|
||||
python.exe). A symlink would not survive the bundling, and a plain copy is what costs the space. Returns the names changed."""
|
||||
bindir = folder / "python" / "bin"
|
||||
if not bindir.is_dir():
|
||||
return []
|
||||
real = bindir / f"python{pt.PYTHON_VERSION.rsplit('.', 1)[0]}"
|
||||
if not real.exists():
|
||||
return []
|
||||
if real.is_symlink(): # the real file is a link itself: make the target the real one
|
||||
target = real.resolve()
|
||||
real.unlink()
|
||||
shutil.move(str(target), str(real))
|
||||
changed = []
|
||||
for name in ("python", "python3"):
|
||||
path = bindir / name
|
||||
if not (path.is_symlink() or (path.is_file() and path.stat().st_size == real.stat().st_size)):
|
||||
continue
|
||||
path.unlink()
|
||||
path.write_bytes((LAUNCHER % real.name).encode("utf-8"))
|
||||
path.chmod(0o755)
|
||||
changed.append(name)
|
||||
return changed
|
||||
|
||||
|
||||
|
||||
def write_bundle_record(folder: Path, archive: Path) -> dict:
|
||||
manifest_path = archive.with_name(archive.name.rsplit(".", 2 if archive.name.endswith(".tar.gz") else 1)[0] + ".manifest.json")
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8")) if manifest_path.exists() else {}
|
||||
target = manifest.get("target") or pt.host_target()
|
||||
record = {
|
||||
"schema_version": 1, "component": "strata-control",
|
||||
"version": (folder / "VERSION").read_text(encoding="utf-8").strip(),
|
||||
"target": target,
|
||||
"python": {"version": pt.PYTHON_VERSION, "build": pt.PBS_RELEASE, "sha256": pt.PYTHON_PINS[target]["sha256"]},
|
||||
"lock": manifest.get("lock"),
|
||||
"installed": manifest.get("installed"),
|
||||
"archive": {"name": archive.name, "sha256": sha256(archive), "bytes": archive.stat().st_size},
|
||||
"acquires_at_runtime": False,
|
||||
"installer": add_installer(folder),
|
||||
}
|
||||
(folder / "BUNDLE.json").write_text(json.dumps(record, indent=1) + "\n", encoding="utf-8")
|
||||
return record
|
||||
|
||||
|
||||
def check(folder: Path) -> int:
|
||||
target = pt.host_target()
|
||||
python = folder / pt.PYTHON_PINS[target]["python"]
|
||||
problems = []
|
||||
for need in (python, folder / "strata.py", folder / "strata_control", folder / "serve" / "server.py", folder / "BUNDLE.json",
|
||||
folder / "setup.py", folder / "tools" / "strata_mcp.py", folder / "tools" / "iq_pack.py", folder / "data" / "draft_vocab.bin"):
|
||||
if not need.exists():
|
||||
problems.append(f"missing {need}")
|
||||
if problems:
|
||||
print("\n".join(problems), file=sys.stderr)
|
||||
return 1
|
||||
env = {"PYTHONNOUSERSITE": "1", "PYTHONDONTWRITEBYTECODE": "1", "STRATA_NO_ACQUIRE": "1", "SYSTEMROOT": __import__("os").environ.get("SYSTEMROOT", "")}
|
||||
done = subprocess.run([str(python), str(folder / "strata.py"), "--version"], capture_output=True, text=True, env=env,
|
||||
cwd=str(folder), timeout=60)
|
||||
print(done.stdout.strip() or done.stderr.strip())
|
||||
if done.returncode:
|
||||
return done.returncode
|
||||
# the installer: its catalog, a GPU probe and its imports work on the bundled Python alone (nothing is installed or fetched)
|
||||
probe = ("import sys; sys.path.insert(0, %r); import importlib.util as u; s = u.spec_from_file_location('s', %r); m = u.module_from_spec(s); "
|
||||
"s.loader.exec_module(m); import iq_pack, strata_pack, pack_index, mtp_fetch; "
|
||||
"print('installer ok: %%d models, %%d families' %% (len(m.MODELS), len(m.FAMILIES)))") % (str(folder / "tools"), str(folder / "setup.py"))
|
||||
env["STRATA_CONTROL_INSTALL"] = "1"
|
||||
again = subprocess.run([str(python), "-c", probe], capture_output=True, text=True, env=env, cwd=str(folder), timeout=120)
|
||||
print(again.stdout.strip() or again.stderr.strip()[-600:])
|
||||
return again.returncode
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--archive", type=Path, help="stage this terminal archive instead of building one")
|
||||
ap.add_argument("--wheelhouse", help="build with the wheels in this folder (no network)")
|
||||
ap.add_argument("--out", type=Path, default=OUT)
|
||||
ap.add_argument("--check", action="store_true", help="verify the staged component and exit")
|
||||
a = ap.parse_args()
|
||||
if a.check:
|
||||
return check(a.out)
|
||||
with tempfile.TemporaryDirectory(prefix="strata-stage-control-") as work:
|
||||
archive = a.archive.resolve() if a.archive else build_archive(Path(work), a.wheelhouse)
|
||||
keep = [p for p in a.out.iterdir() if p.name == ".gitignore"] if a.out.exists() else []
|
||||
if a.out.exists():
|
||||
for child in a.out.iterdir():
|
||||
if child.name != ".gitignore":
|
||||
shutil.rmtree(child, ignore_errors=True) if child.is_dir() else child.unlink()
|
||||
a.out.mkdir(parents=True, exist_ok=True)
|
||||
inner = extract(archive, a.out)
|
||||
record = write_bundle_record(inner, archive)
|
||||
for child in inner.iterdir():
|
||||
shutil.move(str(child), str(a.out / child.name))
|
||||
shutil.rmtree(inner.parent, ignore_errors=True)
|
||||
launched = dedupe_python(a.out)
|
||||
if launched:
|
||||
print(f"python: {', '.join(launched)} are launchers of the one real interpreter")
|
||||
print(f"staged {record['component']} {record['version']} ({record['target']}) into {a.out}")
|
||||
return check(a.out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Stage the DeepSeek Harness (pinned, MIT) as the installer's `harness/` folder: a pruned PRODUCTION install, built at
|
||||
* build time, so nothing is fetched or compiled on the person's PC.
|
||||
*
|
||||
* node apps/desktop/scripts/stage-harness.mjs --src <built Harness checkout> [--out apps/desktop/src-tauri/resources/harness]
|
||||
*
|
||||
* <built Harness checkout> is a clone of the pinned commit after `pnpm install --frozen-lockfile` and `pnpm run build:artifacts`
|
||||
* (see phaseA-tests/notes-split-s4a.md, step 1). The script never touches it: it runs `pnpm deploy --legacy --prod` into
|
||||
* the output folder, removes what a running Harness does not read (sources, tests, maps, docs, other operating systems'
|
||||
* prebuilt binaries, the LibreOffice document converter and the speech engine, which the app does not use), keeps the
|
||||
* MIT licence and third-party notices, writes BUNDLE.json, and finally boots the staged copy once as proof.
|
||||
*
|
||||
* The host finds the result at `<install>/harness` (server/agent/harness-runtime.mjs), or in the component folder.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { join, relative, resolve, sep } from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { flattenPnpm } from './flatten-pnpm.mjs';
|
||||
import { isOtherOsBinaryFolder, missingHarnessImports } from './harness-prune.mjs';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const opt = (name, fallback) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : fallback; };
|
||||
const src = resolve(opt('src', process.env.STRATA_HARNESS_SRC ?? ''));
|
||||
const out = resolve(opt('out', join(import.meta.dirname, '..', 'src-tauri', 'resources', 'harness')));
|
||||
if (!opt('src', process.env.STRATA_HARNESS_SRC) || !existsSync(join(src, 'apps', 'cli', 'lib', 'bin.js'))) {
|
||||
console.error('Give --src <a built Harness checkout> (apps/cli/lib/bin.js must exist).');
|
||||
process.exit(2);
|
||||
}
|
||||
const platform = process.platform;
|
||||
const run = (cmd, argv, cwd, extra = {}) => {
|
||||
const r = spawnSync(cmd, argv, { cwd, stdio: 'inherit', shell: false, ...extra });
|
||||
if (r.status !== 0) { console.error(`${cmd} ${argv.join(' ')} failed (${r.status ?? r.error})`); process.exit(1); }
|
||||
};
|
||||
|
||||
// The output folder in the repo carries a .gitignore that keeps the staged files out of git: keep it across the clean-up.
|
||||
const keepIgnore = existsSync(join(out, '.gitignore')) ? readFileSync(join(out, '.gitignore')) : null;
|
||||
rmSync(out, { recursive: true, force: true });
|
||||
mkdirSync(out, { recursive: true });
|
||||
const pnpm = platform === 'win32' ? ['corepack.cmd', ['pnpm']] : ['corepack', ['pnpm']];
|
||||
run(pnpm[0], [...pnpm[1], 'deploy', '--legacy', '--prod', '--filter', '@deepseek-ai/dsh', out], src, { shell: platform === 'win32' });
|
||||
|
||||
// --- peers -------------------------------------------------------------------------------------------------------------
|
||||
// `pnpm deploy --prod` leaves out peer dependencies, but the Harness loads some of them by name at run time (for example the
|
||||
// plugin-group helper that app-boot imports). Walk the installed packages; every dependency or peer that cannot be
|
||||
// resolved from the output is copied from the source checkout's own node_modules, and its own requirements after it.
|
||||
function packageJsonsUnder(dir, out2 = []) {
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = join(dir, e.name);
|
||||
if (e.isSymbolicLink()) continue;
|
||||
if (e.isDirectory()) packageJsonsUnder(p, out2);
|
||||
else if (e.name === 'package.json' && p.includes(`${sep}node_modules${sep}`)) out2.push(p);
|
||||
}
|
||||
return out2;
|
||||
}
|
||||
function requirementsOf(file) {
|
||||
try { const j = JSON.parse(readFileSync(file, 'utf8')); return [...Object.keys(j.dependencies ?? {}), ...Object.keys(j.peerDependencies ?? {}), ...Object.keys(j.optionalDependencies ?? {})]; } catch { return []; }
|
||||
}
|
||||
/** The checkout's own workspace packages (packages/**, vendor/*, apps/*) by name: where a peer that is one of ours comes from. */
|
||||
const workspace = new Map();
|
||||
function indexWorkspace(dir, depth = 0) {
|
||||
if (depth > 4) return;
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (!e.isDirectory() || e.isSymbolicLink() || e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
||||
const p = join(dir, e.name);
|
||||
const manifest = join(p, 'package.json');
|
||||
if (existsSync(manifest)) { try { workspace.set(JSON.parse(readFileSync(manifest, 'utf8')).name, p); } catch { /* not a package */ } }
|
||||
indexWorkspace(p, depth + 1);
|
||||
}
|
||||
}
|
||||
for (const top of ['packages', 'vendor', 'apps']) if (existsSync(join(src, top))) indexWorkspace(join(src, top));
|
||||
const resolvable = name => existsSync(join(out, 'node_modules', ...name.split('/')));
|
||||
const queue = [...new Set(packageJsonsUnder(join(out, 'node_modules')).flatMap(requirementsOf))].filter(n => !resolvable(n));
|
||||
const added = [];
|
||||
while (queue.length) {
|
||||
const name = queue.shift();
|
||||
if (resolvable(name)) continue;
|
||||
const from = workspace.get(name) ?? join(src, 'node_modules', ...name.split('/'));
|
||||
if (!existsSync(from)) continue; // an optional package for another platform, or not installed in the source: nothing to copy
|
||||
const to = join(out, 'node_modules', ...name.split('/'));
|
||||
mkdirSync(join(to, '..'), { recursive: true });
|
||||
const realFrom = realpathSync(from);
|
||||
cpSync(realFrom, to, { recursive: true, dereference: true, filter: f => !relative(realFrom, f).split(sep).includes('node_modules') });
|
||||
added.push(name);
|
||||
queue.push(...requirementsOf(join(to, 'package.json')).filter(n => !resolvable(n)));
|
||||
}
|
||||
if (added.length) console.log(`peer packages added: ${added.join(', ')}`);
|
||||
|
||||
// --- the browser tool server ---------------------------------------------------------------------------------------------
|
||||
// Settings -> Tools can switch on a browser the assistant drives (Playwright's MCP server, Apache-2.0) on the person's own Chrome or Edge,
|
||||
// so no browser is downloaded. The server and the two Playwright libraries it needs are not part of the Harness's production closure;
|
||||
// they are copied from the source checkout's package store (~20 MB) and the host starts it with Node (server/agent/browser-probe.mjs).
|
||||
const pnpmStore = join(src, 'node_modules', '.pnpm');
|
||||
function storePackage(name, version) {
|
||||
if (!existsSync(pnpmStore)) return null;
|
||||
const prefix = `${name.replace('/', '+')}@`;
|
||||
const hit = readdirSync(pnpmStore).filter(n => n.startsWith(prefix) && (!version || n === `${prefix}${version}` || n.startsWith(`${prefix}${version}_`))).sort().at(-1);
|
||||
const dir = hit ? join(pnpmStore, hit, 'node_modules', ...name.split('/')) : null;
|
||||
return dir && existsSync(dir) ? dir : null;
|
||||
}
|
||||
const browserDir = storePackage('@playwright/mcp');
|
||||
if (browserDir) {
|
||||
const wanted = JSON.parse(readFileSync(join(browserDir, 'package.json'), 'utf8')).dependencies ?? {};
|
||||
const parts = [['@playwright/mcp', browserDir], ...['playwright', 'playwright-core'].map(n => [n, storePackage(n, wanted[n])])];
|
||||
if (parts.every(([, dir]) => dir)) {
|
||||
for (const [name, dir] of parts) {
|
||||
const to = join(out, 'node_modules', ...name.split('/'));
|
||||
mkdirSync(join(to, '..'), { recursive: true });
|
||||
cpSync(realpathSync(dir), to, { recursive: true, dereference: true, filter: f => !relative(dir, f).split(sep).includes('node_modules') });
|
||||
}
|
||||
console.log(`browser tool server added: ${parts.map(([n]) => n).join(', ')}`);
|
||||
} else console.log('browser tool server NOT added: Playwright libraries missing from the package store');
|
||||
} else console.log('browser tool server NOT added: @playwright/mcp is not in the source checkout');
|
||||
|
||||
// --- one real folder per package, no links -------------------------------------------------------------------------------
|
||||
// The installer copies files; pnpm's layout is links. See flatten-pnpm.mjs. The two packages the app does not use are never copied.
|
||||
const flat = flattenPnpm(out, { skip: /libreoffice-kit|sherpa-onnx/i });
|
||||
console.log(`flattened: ${flat.packages} placed (${flat.nested} nested for a second version)`);
|
||||
|
||||
// --- prune ------------------------------------------------------------------------------------------------------------
|
||||
/** Whole packages the app never loads: office-document conversion (hundreds of MB) and speech input. */
|
||||
const DROP_PACKAGES = [/libreoffice-kit/i, /sherpa-onnx/i];
|
||||
/** Only when sitting directly in a package's root (a library may have a runtime folder called `doc`, e.g. yaml/dist/doc). */
|
||||
const DROP_DIRS = new Set(['test', 'tests', '__tests__', 'docs', 'website', 'benchmarks', 'examples', '.github']);
|
||||
const DROP_FILES = /\.(map|md|markdown|pdb|tsbuildinfo)$/i;
|
||||
/** Licences and notices travel with the files they cover. */
|
||||
const KEEP_FILES = /^(LICENSE|LICENCE|NOTICE|THIRD_PARTY)/i;
|
||||
let removed = 0;
|
||||
function prune(dir) {
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = join(dir, e.name);
|
||||
if (e.isSymbolicLink()) continue;
|
||||
if (e.isDirectory()) {
|
||||
const inLib = p.split(sep).includes('lib');
|
||||
if ((DROP_DIRS.has(e.name) && !inLib && existsSync(join(dir, 'package.json'))) || DROP_PACKAGES.some(r => r.test(e.name)) || isOtherOsBinaryFolder(platform, e.name, p)) {
|
||||
rmSync(p, { recursive: true, force: true }); removed++; continue;
|
||||
}
|
||||
prune(p);
|
||||
} else if (DROP_FILES.test(e.name) && !KEEP_FILES.test(e.name)) {
|
||||
rmSync(p, { force: true }); removed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
prune(out);
|
||||
|
||||
// --- licence: the MIT text and the third-party notices travel with the files ---------------------------------------------
|
||||
for (const name of ['LICENSE', 'THIRD_PARTY_NOTICES.md']) {
|
||||
if (existsSync(join(src, name))) cpSync(join(src, name), join(out, name));
|
||||
}
|
||||
const cli = JSON.parse(readFileSync(join(out, 'package.json'), 'utf8'));
|
||||
let commit = process.env.STRATA_HARNESS_COMMIT ?? null;
|
||||
if (!commit) { const r = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: src, encoding: 'utf8' }); if (r.status === 0) commit = r.stdout.trim(); }
|
||||
function size(dir) { let t = 0, n = 0; for (const e of readdirSync(dir, { withFileTypes: true })) { const p = join(dir, e.name); if (e.isDirectory()) { const s = size(p); t += s.bytes; n += s.files; } else if (e.isFile()) { t += statSync(p).size; n++; } } return { bytes: t, files: n }; }
|
||||
const total = size(out);
|
||||
writeFileSync(join(out, 'BUNDLE.json'), JSON.stringify({
|
||||
component: 'deepseek-harness', version: cli.version, commit, license: 'MIT', platform, node: process.version,
|
||||
bytes: total.bytes, files: total.files, entry: 'apps/cli/lib/bin.js', builtAt: new Date().toISOString(),
|
||||
note: 'Pruned production install (pnpm deploy --prod). No package manager runs on the user\'s PC.',
|
||||
}, null, 2));
|
||||
|
||||
if (keepIgnore) writeFileSync(join(out, '.gitignore'), keepIgnore);
|
||||
|
||||
// --- proof 1: nothing the Harness imports by name was pruned ------------------------------------------------------------
|
||||
// (2026-10-17: the other-OS rule once removed @deepseek-ai/dsh-win32-process, which every platform imports; Linux/Mac agent chats died.)
|
||||
const lost = missingHarnessImports(out);
|
||||
if (lost.length) { console.error(['staged Harness is missing imported packages:', ...lost.map(l => ` ${l.from} -> ${l.missing}`)].join(String.fromCharCode(10))); process.exit(1); }
|
||||
console.log('import check: every @deepseek-ai package the Harness imports is staged');
|
||||
|
||||
// --- proof 2: the staged copy answers --version and boots the web profile once ----------------------------------------
|
||||
const bin = join(out, 'apps', 'cli', 'lib', 'bin.js');
|
||||
const entry = existsSync(bin) ? bin : join(out, 'lib', 'bin.js');
|
||||
const v = spawnSync(process.execPath, [entry, '--version'], { encoding: 'utf8' });
|
||||
if (v.status !== 0) { console.error('staged Harness does not start:', v.stderr); process.exit(1); }
|
||||
console.log(`staged ${cli.version} (${commit?.slice(0, 8) ?? 'no commit'}) in ${out}: ${(total.bytes / 1048576).toFixed(0)} MiB, ${total.files} files, ${removed} pruned items; --version -> ${v.stdout.trim()}`);
|
||||
console.log(`sha256(BUNDLE.json)=${createHash('sha256').update(readFileSync(join(out, 'BUNDLE.json'))).digest('hex')}`);
|
||||
@@ -0,0 +1,721 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Headless UI flows of the installed app against a packaged host with a FAKE installer and a FAKE engine (nothing is
|
||||
* downloaded, no real engine runs; the real Control starts the fake engine through the owner runtime):
|
||||
*
|
||||
* node apps/desktop/scripts/ui-flows.mjs [--control <staged control>] [--node <node.exe>] [--shots <dir>] [--only download,models,tune,fail,theme]
|
||||
*
|
||||
* download the onboarding download: ONE combined bar over both files, an ETA in minutes left of Stop, Finish later closes the
|
||||
* pop-up with the download still running, the Models screen shows the same progress, Stop stops it
|
||||
* models an installed model is the one Models selects and loads, without a download pop-up
|
||||
* tune the tuning step offers no picture reading, tunes, writes a RAM budget for "leave 20%", opens chat; reading size
|
||||
* fail an engine that stops while loading: the error is in words and the pop-up does not scroll
|
||||
* theme the app follows the system light/dark setting
|
||||
* Per-user folders are all under a scratch folder. Exit 1 if a check fails.
|
||||
*/
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, rmdirSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') ? true : all[i + 1]]] : acc), []));
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const repoHost = resolve(here, '../../host');
|
||||
const staged = resolve(here, '../src-tauri/resources');
|
||||
const stagedControl = resolve(args.control ?? join(staged, 'control'));
|
||||
const nodeExe = resolve(args.node ?? join(staged, 'node', process.platform === 'win32' ? 'node.exe' : 'node'));
|
||||
const work = mkdtempSync(join(tmpdir(), 'strata-ui-flows-'));
|
||||
const shots = resolve(args.shots ?? join(work, 'shots'));
|
||||
mkdirSync(shots, { recursive: true });
|
||||
const only = new Set(String(args.only ?? 'download,models,profile,ui2,ui2d,tune,fail,delete,theme,restart,perf,polish,stop,latest').split(','));
|
||||
const isWin = process.platform === 'win32';
|
||||
const { chromium } = await import(pathToFileURL(join(repoHost, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
|
||||
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
const sha = buf => createHash('sha256').update(buf).digest('hex');
|
||||
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
|
||||
let failed = false;
|
||||
const check = (ok, label, detail = '') => { console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ' ' + detail : ''}`); if (!ok) failed = true; };
|
||||
|
||||
// ------------------------------------------------------------------ the app folder: the real Control with a fake installer
|
||||
const control = join(work, 'control');
|
||||
mkdirSync(control, { recursive: true });
|
||||
for (const name of ['python', 'strata_control', 'strata_cli', 'serve']) symlinkSync(join(stagedControl, name), join(control, name), 'junction');
|
||||
for (const name of ['strata.py', 'VERSION', 'BUNDLE.json', 'requirements.txt']) copyFileSync(join(stagedControl, name), join(control, name));
|
||||
cpSync(join(stagedControl, 'tools'), join(control, 'tools'), { recursive: true });
|
||||
const python = isWin ? join(control, 'python', 'python.exe') : join(control, 'python', 'bin', 'python3');
|
||||
|
||||
const FAKE_INSTALLER = String.raw`
|
||||
import json, os, sys, time
|
||||
from pathlib import Path
|
||||
ROOT = Path(sys.argv[sys.argv.index('--root') + 1]) if '--root' in sys.argv else Path('.')
|
||||
STATE = Path(os.environ['FAKE_STATE']); STATE.mkdir(parents=True, exist_ok=True)
|
||||
RATE = float(os.environ.get('FAKE_RATE_GB_S', '0.5'))
|
||||
def size(model, gb, ram, mid):
|
||||
return {'model': model, 'about': model, 'download_gb': gb, 'ram_needed_gb': ram, 'experts_gb': ram - 8, 'on_this_pc': 'fits', 'installed': False, 'images': True, 'experimental': False, 'id': mid}
|
||||
CATALOG = {'summary': 'fake', 'families': [{'family': 'qwen', 'title': 'Qwen3.8-Flash-Next', 'about': 'x', 'experimental': False, 'images': True, 'sizes': [
|
||||
size('Q2_0', 66.4, 48, 'q2_0'), size('IQ3_XXS', 75.8, 60, 'iq3_xxs'), size('IQ3_S', 83.6, 62, 'iq3_s')]}],
|
||||
'contexts': [8192, 32768, 65536], 'recommendation': {'family': 'qwen', 'model': 'IQ3_XXS', 'title': 'Qwen3.8-Flash-Next IQ3_XXS', 'context': 32768}, 'context_note': ''}
|
||||
HARDWARE = {'ram_gb': 63.1, 'cpu': {'name': 'cpu', 'cores': 12}, 'gpus': [{'index': 0, 'name': 'NVIDIA GeForce RTX 5070', 'vram_gb': 11.9, 'usable': True}]}
|
||||
TOTAL = 83.6
|
||||
def job():
|
||||
f = STATE / 'job.json'
|
||||
if not f.exists(): return None
|
||||
j = json.loads(f.read_text())
|
||||
if j.get('cancelled'):
|
||||
return {'running': False, 'exit_code': 1, 'error': 'stopped', 'result': 'failed'}
|
||||
done = min(TOTAL, RATE * (time.time() - j['start']))
|
||||
return {'running': True, 'step': '5/7: downloading Qwen3.8-Flash-Next IQ3_S', 'model': 'IQ3_S',
|
||||
'download': {'file': 'model files', 'done_gb': round(done, 3), 'total_gb': TOTAL, 'remaining_gb': round(TOTAL - done, 3), 'percent': int(100 * done / TOTAL)}}
|
||||
def call(name, a):
|
||||
if name == 'strata_models': return CATALOG
|
||||
if name == 'strata_remove':
|
||||
cfg = ROOT / ('strata-%s.json' % a['model'])
|
||||
if a.get('confirm') and cfg.exists(): cfg.unlink()
|
||||
return {'summary': 'Deleted', 'plan': {'gb': 83.6}, 'failed': []}
|
||||
if name == 'strata_status': return {'summary': 'fake', 'installed': {'models': [{'model': 'iq3_s', 'disk_gb': 83.6, 'disk_bytes': 83600000000}]}, 'hardware': HARDWARE, 'recommendation': CATALOG['recommendation'], 'install': job(), 'running': []}
|
||||
if name == 'strata_install':
|
||||
if a.get('cancel'):
|
||||
f = STATE / 'job.json'
|
||||
if f.exists(): j = json.loads(f.read_text()); j['cancelled'] = True; f.write_text(json.dumps(j))
|
||||
return {'summary': 'stopped', 'install': job()}
|
||||
if a.get('confirm'):
|
||||
(STATE / 'job.json').write_text(json.dumps({'start': time.time()}))
|
||||
return {'summary': 'started', 'plan': {'download_gb': TOTAL}, 'install': job()}
|
||||
if not a: return {'summary': 'progress', 'install': job()}
|
||||
return {'summary': 'Plan', 'plan': {'title': 'Qwen3.8-Flash-Next IQ3_S', 'download_gb': TOTAL, 'disk_short': None}}
|
||||
raise Exception('unknown tool ' + name)
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line: continue
|
||||
m = json.loads(line)
|
||||
if 'id' not in m: continue
|
||||
method = m.get('method')
|
||||
if method == 'initialize': res = {'protocolVersion': '2024-11-05'}
|
||||
elif method == 'tools/list': res = {'tools': [{'name': n} for n in ('strata_models', 'strata_status', 'strata_install', 'strata_remove')]}
|
||||
else:
|
||||
p = m['params']
|
||||
res = {'structuredContent': call(p['name'], p.get('arguments') or {})}
|
||||
sys.stdout.write(json.dumps({'jsonrpc': '2.0', 'id': m['id'], 'result': res}) + '\n'); sys.stdout.flush()
|
||||
`;
|
||||
writeFileSync(join(control, 'tools', 'strata_mcp.py'), FAKE_INSTALLER);
|
||||
|
||||
const FAKE_ENGINE = `import os, sys, json, time
|
||||
from pathlib import Path
|
||||
if os.environ.get('FAKE_ENGINE_FAIL'):
|
||||
print('memory guard: 5.1 GiB of RAM free is under the 6 GiB limit (limit 63.1 GiB)', flush=True)
|
||||
sys.exit(3)
|
||||
ctx = '4096'
|
||||
if '--max-context' in sys.argv: ctx = sys.argv[sys.argv.index('--max-context') + 1]
|
||||
print('READY %s stop' % ctx, flush=True)
|
||||
for line in sys.stdin:
|
||||
if line.strip() == 'QUIT': break
|
||||
if line.startswith(('GEN ', 'BGEN ')):
|
||||
slot = int(line.split()[1]) if line.startswith('BGEN ') else None
|
||||
t0 = time.time(); n = 0
|
||||
for ch in os.environ.get('FAKE_REPLY', 'a'):
|
||||
print('T %d' % ord(ch), flush=True); n += 1
|
||||
if os.environ.get('FAKE_TOKEN_DELAY'): time.sleep(float(os.environ['FAKE_TOKEN_DELAY']))
|
||||
print('DONE %d 12 300.0 %.1f length 0 0 0' % (n, max(1.0, (time.time() - t0) * 1000)), flush=True)
|
||||
if slot is not None: print(f'BADM {slot} 0', flush=True)
|
||||
`;
|
||||
|
||||
// one scenario = one fresh host with its own per-user folders
|
||||
async function scenario(name, { installed = false, env = {} } = {}, body) {
|
||||
if (!only.has(name)) return;
|
||||
console.log(`--- ${name}`);
|
||||
const dir = join(work, name);
|
||||
const dirs = { home: join(dir, 'home'), appdata: join(dir, 'appdata'), local: join(dir, 'local'), components: join(dir, 'local', 'Strata', 'components'), install: join(dir, 'local', 'Strata', 'install'), state: join(dir, 'fake-state'), profiles: join(dir, 'profiles'), runtime: join(dir, 'runtime') };
|
||||
for (const d of [dirs.home, dirs.appdata, dirs.local, dirs.components, dirs.install, dirs.state, join(dir, 'models')]) mkdirSync(d, { recursive: true });
|
||||
const baseEnv = { ...process.env, STRATA_COMPONENTS_DIR: dirs.components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime, STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(dir, 'models'), APPDATA: dirs.appdata, LOCALAPPDATA: dirs.local, USERPROFILE: dirs.home, XDG_CONFIG_HOME: join(dir, 'xdg'), XDG_DATA_HOME: join(dir, 'xdgdata'), PYTHONDONTWRITEBYTECODE: '1', STRATA_ENGINE_BACKEND: 'cuda', FAKE_STATE: dirs.state, ...env };
|
||||
delete baseEnv.STRATA_API_KEY; delete baseEnv.STRATA_INSTALL_DIR;
|
||||
if (installed) {
|
||||
const platform = isWin ? 'windows-x86_64' : 'linux-x86_64';
|
||||
const folder = join(dirs.components, 'engine', `0.1.45-${platform}-cuda`);
|
||||
mkdirSync(folder, { recursive: true });
|
||||
writeFileSync(join(folder, 'engine_main.py'), FAKE_ENGINE);
|
||||
const entry = isWin ? 'engine.cmd' : 'engine';
|
||||
writeFileSync(join(folder, entry), isWin ? `@echo off\r\n"${python}" "%~dp0engine_main.py" %*\r\n` : `#!/bin/sh\nexec "${python}" "$(dirname "$0")/engine_main.py" "$@"\n`);
|
||||
const files = [entry, 'engine_main.py'].map(p => { const b = readFileSync(join(folder, p)); return { path: p, sha256: sha(b), bytes: b.length }; });
|
||||
writeFileSync(join(folder, 'engine-component.json'), JSON.stringify({ schema_version: 1, id: 'strata-engine', version: '0.1.45', platform, backend: 'cuda', entrypoint: entry, files }));
|
||||
const tok = join(dirs.install, 'tokenizer');
|
||||
mkdirSync(tok, { recursive: true });
|
||||
const script = `import json, sys\nfrom tools.strata_tokenizer import BYTE_TO_UNICODE\ntok = sys.argv[1]\ntokens = [BYTE_TO_UNICODE[i] for i in range(256)] + ['ab', '<|im_start|>', '<|im_end|>', '<|endoftext|>']\njson.dump({t: i for i, t in enumerate(tokens)}, open(tok + '/vocab.json', 'w'))\nopen(tok + '/merges.txt', 'w').write('a b')\njson.dump([0] * 257 + [3, 3, 3], open(tok + '/token_type.json', 'w'))\n`;
|
||||
const made = spawnSync(python, ['-c', script, tok], { cwd: control, env: { ...baseEnv, PYTHONNOUSERSITE: '1' }, encoding: 'utf8' });
|
||||
if (made.status !== 0) throw new Error('tokenizer fixture: ' + made.stderr);
|
||||
writeFileSync(join(dirs.install, 'strata-iq3_s.json'), JSON.stringify({ exe: 'component:strata-engine', backend: 'cuda', tokenizer: tok, args: ['--max-context', '32768', '--pool-workers', '12'], cwd: dir, model_name: 'stub', host: '127.0.0.1', port: 0, kv: 'int8' }));
|
||||
}
|
||||
const port = env.FIXED_PORT ? Number(env.FIXED_PORT) : await freePort(), base = `http://127.0.0.1:${port}`;
|
||||
const host = spawn(nodeExe, [join(repoHost, 'server', 'index.mjs')], { cwd: repoHost, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
|
||||
env: { ...baseEnv, STRATA_PORT: String(port), STRATA_DIST: join(repoHost, 'dist'), STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1', STRATA_CONTROL_DIR: control, STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()) } });
|
||||
let log = ''; host.stdout.on('data', c => { log += c; }); host.stderr.on('data', c => { log += c; });
|
||||
const api = async (path, init) => { const r = await fetch(base + path, init); return { status: r.status, body: await r.json().catch(() => null) }; };
|
||||
const browser = await chromium.launch({ executablePath: CHROME, headless: true });
|
||||
try {
|
||||
let up = false;
|
||||
for (let i = 0; i < 200 && !up; i++) { try { up = (await api('/api/health')).status === 200; } catch { await delay(150); } }
|
||||
check(up, `${name}: the packaged host starts`);
|
||||
await api('/api/native?refresh=1');
|
||||
await body({ base, api, browser, dirs, shot: (page, n) => page.screenshot({ path: join(shots, `${name}-${n}.png`) }), log: () => log });
|
||||
} catch (error) {
|
||||
check(false, `${name}: the flow`, String(error?.stack ?? error).slice(0, 700) + '\n' + log.slice(-600));
|
||||
} finally {
|
||||
await browser.close().catch(() => {});
|
||||
host.kill(); await new Promise(r => host.once('exit', r));
|
||||
}
|
||||
}
|
||||
const newPage = async (browser, base, options = {}) => {
|
||||
const context = await browser.newContext({ viewport: { width: 1400, height: 950 }, ...(process.env.SCHEME ? { colorScheme: process.env.SCHEME } : {}), ...options });
|
||||
const page = await context.newPage();
|
||||
const problems = [];
|
||||
page.on('pageerror', e => problems.push(e.message));
|
||||
page.problems = problems;
|
||||
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
||||
return page;
|
||||
};
|
||||
const clickText = async (page, re, timeout = 30000) => { const b = page.getByRole('button', { name: re }).first(); await b.waitFor({ timeout }); await b.click(); };
|
||||
const noScroll = async page => page.evaluate(() => {
|
||||
const root = document.querySelector('.guided-onboarding');
|
||||
const bad = [];
|
||||
for (let el = document.querySelector('.onboard-tune-stage, .onboard-prepare-stage, .onboard-model-stage') ?? root; el && el !== document.body; el = el.parentElement) {
|
||||
const style = getComputedStyle(el);
|
||||
if (/(auto|scroll)/.test(style.overflowY) && el.scrollHeight > el.clientHeight + 1) bad.push(`${el.className || el.tagName}: ${el.scrollHeight} > ${el.clientHeight}`);
|
||||
}
|
||||
return bad;
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ download
|
||||
await scenario('download', {}, async ({ base, api, browser, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /get started/i);
|
||||
await page.getByText(/Choose a version/i).waitFor({ timeout: 30000 });
|
||||
await page.locator('[role=radio]').filter({ hasText: 'IQ3_S' }).first().click();
|
||||
await clickText(page, /^Continue/);
|
||||
await clickText(page, /Download model/i, 60000);
|
||||
await page.locator('.onboard-progress progress').waitFor({ timeout: 30000 });
|
||||
// ONE bar: sample the percent while it runs, it only goes up
|
||||
const samples = [];
|
||||
let eta = '';
|
||||
for (let i = 0; i < 40 && !eta; i++) {
|
||||
const state = (await api('/api/native')).body;
|
||||
const d = state.jobs.at(-1)?.result?.install?.download;
|
||||
if (d) samples.push(d.done_gb);
|
||||
eta = (await page.locator('.onboard-progress-actions span').first().textContent({ timeout: 1000 }).catch(() => '')) ?? '';
|
||||
await delay(1000);
|
||||
}
|
||||
await shot(page, '1-wizard-progress');
|
||||
check(/About \d+ (min left|h)/.test(eta), 'the wizard shows about how many minutes are left', eta.trim());
|
||||
const order = await page.evaluate(() => { const a = document.querySelector('.onboard-progress-actions'); const kids = a ? [...a.children].map(k => k.tagName + ':' + Math.round(k.getBoundingClientRect().left)) : []; return kids; });
|
||||
check(order.length === 2 && order[0].startsWith('SPAN') && order[1].startsWith('BUTTON') && Number(order[0].split(':')[1]) < Number(order[1].split(':')[1]), 'the time left sits left of Stop', order.join(' '));
|
||||
check(samples.length >= 3 && samples.every((v, i) => i === 0 || v >= samples[i - 1]), 'one combined count that only goes up', samples.map(v => v.toFixed(1)).join(', '));
|
||||
const meta = await page.locator('.onboard-progress-meta span').first().textContent();
|
||||
check(/GB/.test(meta ?? '') && /83\.6/.test(meta ?? ''), 'the bar says done / total over the whole model', meta);
|
||||
// close the pop-up while it downloads
|
||||
await clickText(page, /Finish later/i);
|
||||
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
|
||||
await page.getByText(/Preparing your model/i).waitFor({ timeout: 15000 }).catch(() => {});
|
||||
await delay(1500);
|
||||
await shot(page, '2-models-progress');
|
||||
await page.locator('.native-progress').first().screenshot({ path: join(shots, 'download-3-models-progress-card.png') }).catch(() => {});
|
||||
check(await page.locator('.guided-onboarding').count() === 0, 'the setup pop-up closed');
|
||||
check(await page.getByText(/Preparing your model/i).count() > 0, 'the Models screen shows the running download');
|
||||
const still = (await api('/api/native')).body.jobs.at(-1);
|
||||
check(still?.state === 'running', 'the download keeps running in the background', still?.state);
|
||||
const modelsEta = await page.locator('.native-progress .onboard-progress-actions span').first().textContent({ timeout: 15000 }).catch(() => '');
|
||||
check(/About \d+ (min left|h)/.test(modelsEta ?? ''), 'the Models screen shows the time left too', (modelsEta ?? '').trim());
|
||||
await clickText(page, /^Stop$/);
|
||||
await delay(2500);
|
||||
check((await api('/api/native')).body.jobs.at(-1)?.state !== 'running', 'Stop stops it');
|
||||
check(page.problems.length === 0, 'no page error', page.problems.join('; '));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ models
|
||||
await scenario('models', { installed: true }, async ({ base, api, browser, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /Go straight to the app/i);
|
||||
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
|
||||
await page.getByText(/Conversation context/i).first().waitFor({ timeout: 30000 });
|
||||
await delay(1500);
|
||||
await shot(page, '1-models');
|
||||
await page.locator('.version-trigger').first().waitFor({ timeout: 20000 }).catch(async () => { await shot(page, '0-models-missing-summary'); console.log('info page text:', JSON.stringify((await page.locator('body').innerText()).slice(0, 500))); });
|
||||
const summary = await page.locator('.version-trigger').first().textContent({ timeout: 5000 }).catch(() => '');
|
||||
// the Models tab fits the first view: nothing to scroll while no list is open and Expert mode is closed
|
||||
if (await page.getByRole('button', { name: /^Close expert mode$/ }).count()) { await page.getByRole('button', { name: /^Close expert mode$/ }).click(); await delay(500); }
|
||||
await shot(page, '1b-models-one-view');
|
||||
const overflow = await page.evaluate(() => { const el = document.querySelector('.main__body'); return el ? el.scrollHeight - el.clientHeight : 0; });
|
||||
check(overflow <= 2, 'the Models tab fits the first view without scrolling', String(overflow) + ' px over');
|
||||
check(await page.locator('.family-switch').count() === 1, 'the model family switch is in the navbar');
|
||||
check(await page.locator('h1').filter({ hasText: /^Model$/ }).count() === 0, 'and the navbar does not say "Model"');
|
||||
check(/IQ3_S|High quality/.test(summary ?? ''), 'Models selects the model that is on this PC, not the recommendation', (summary ?? '').replace(/\s+/g, ' ').trim());
|
||||
check(!/IQ3_XXS|Balanced/.test(summary ?? ''), 'the recommendation (IQ3_XXS) is not what is selected');
|
||||
const load = page.getByRole('button', { name: /^Load model$/ }).first();
|
||||
await load.click();
|
||||
await delay(1200);
|
||||
check(await page.getByRole('dialog').filter({ hasText: /Download and prepare/i }).count() === 0, 'loading an installed model opens no download pop-up');
|
||||
let ready = false;
|
||||
for (let i = 0; i < 90 && !ready; i++) { const s = (await api('/api/native')).body; ready = s.instances.some(x => x.status === 'ready'); const err = s.instances.find(x => x.status === 'error'); if (err) throw new Error('load failed: ' + err.error); await delay(1000); }
|
||||
if (!ready) { const st = (await api('/api/native')).body; console.log('info jobs', JSON.stringify(st.jobs.map(j => [j.kind, j.state, j.error ?? j.summary])), 'instances', JSON.stringify(st.instances.map(i => [i.status, i.error]))); await shot(page, '2-models-after-click'); }
|
||||
check(ready, 'the model loads (a fake engine under the real Control)');
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.getByRole('button', { name: /^Unload$/ }).first().waitFor({ timeout: 30000 }).catch(() => {});
|
||||
await shot(page, '3-models-loaded');
|
||||
check(await page.getByRole('button', { name: /^Unload$/ }).count() > 0, 'after a reload the Models screen shows the model as loaded, with Unload');
|
||||
check(await page.getByRole('button', { name: /^Load model$/ }).count() === 0, 'and no longer offers Load model');
|
||||
await page.getByRole('button', { name: /^Unload$/ }).first().click();
|
||||
let gone = false;
|
||||
for (let i = 0; i < 40 && !gone; i++) { gone = (await api('/api/native')).body.instances.length === 0; await delay(500); }
|
||||
check(gone, 'Unload stops the engine');
|
||||
await page.getByRole('button', { name: /^Load model$/ }).first().waitFor({ timeout: 15000 }).catch(() => {});
|
||||
check(await page.getByRole('button', { name: /^Load model$/ }).count() > 0, 'and Load model is back');
|
||||
check(page.problems.length === 0, 'no page error', page.problems.join('; '));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ the profile used, the popover, the pop-up
|
||||
await scenario('profile', { installed: true }, async ({ base, api, browser, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /Go straight to the app/i);
|
||||
await page.getByText(/Conversation context/i).first().waitFor({ timeout: 30000 });
|
||||
await delay(1500);
|
||||
const work = () => page.getByRole('button', { name: /Leave 20% for my work/ }).first();
|
||||
const full = () => page.getByRole('button', { name: /Full performance/ }).first();
|
||||
await work().click();
|
||||
check(await work().getAttribute('aria-pressed') === 'true', 'Leave 20% is chosen before loading');
|
||||
await page.getByRole('button', { name: /^Load model$/ }).first().click();
|
||||
let ready = false;
|
||||
for (let i = 0; i < 90 && !ready; i++) { const s = (await api('/api/native')).body; ready = s.instances.some(x => x.status === 'ready'); await delay(500); }
|
||||
check(ready, 'the model loads');
|
||||
const inst = (await api('/api/native')).body.instances.find(x => x.status === 'ready');
|
||||
check(inst?.resourceProfile === 'work', 'the running model reports the profile it was started with', String(inst?.resourceProfile));
|
||||
await delay(1500);
|
||||
check(await work().getAttribute('aria-pressed') === 'true' && await full().getAttribute('aria-pressed') === 'false', 'after the load the page still shows Leave 20%, not Full performance');
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.getByRole('button', { name: /^Unload$/ }).first().waitFor({ timeout: 30000 });
|
||||
await delay(1000);
|
||||
check(await work().getAttribute('aria-pressed') === 'true', 'and after a reload it is still the profile in use');
|
||||
await shot(page, '1-profile-used');
|
||||
// the reasoning popover names the loaded model
|
||||
await page.evaluate(() => { location.hash = '#/chat'; });
|
||||
const effort = page.getByRole('button', { name: /^Reasoning effort/ }).first();
|
||||
await effort.waitFor({ timeout: 30000 });
|
||||
await effort.click();
|
||||
const header = await page.locator('.reasoning-menu-model strong').first().textContent({ timeout: 10000 });
|
||||
check(header && !/No model loaded/i.test(header), 'the Reasoning popover names the loaded model, not "No model loaded"', String(header));
|
||||
await shot(page, '2-reasoning-popover');
|
||||
await page.keyboard.press('Escape');
|
||||
// an installed model asked for with other settings: the pop-up never speaks of downloading
|
||||
await page.evaluate(() => { location.hash = '#/models'; });
|
||||
await page.getByRole('button', { name: /^Unload$/ }).first().waitFor({ timeout: 30000 });
|
||||
await page.getByRole('button', { name: /^Unload$/ }).first().click();
|
||||
await page.getByRole('button', { name: /^Load model$/ }).first().waitFor({ timeout: 30000 });
|
||||
await page.getByRole('combobox', { name: 'Conversation context' }).first().click();
|
||||
const options = page.getByRole('option');
|
||||
const pick = (await options.allTextContents()).findIndex(t => /^8K$/.test(t.trim()));
|
||||
if (pick >= 0) await options.nth(pick).click();
|
||||
await page.getByRole('button', { name: /^Load model$|^Checking/ }).first().click();
|
||||
const dialog = page.getByRole('dialog').first();
|
||||
await dialog.waitFor({ timeout: 15000 }).catch(() => {});
|
||||
const text = (await dialog.textContent().catch(() => '')) ?? '';
|
||||
await shot(page, '3-apply-dialog');
|
||||
if (text) {
|
||||
check(!/Download and prepare|Download . load|approximately|[0-9] GB/i.test(text), 'for a downloaded model the pop-up has no download wording or size', text.replace(/\s+/g, ' ').slice(0, 600));
|
||||
check(/Apply/.test(text), 'it says what happens: apply settings and load');
|
||||
} else console.log('info the settings were the saved ones, so loading needed no pop-up at all');
|
||||
check(page.problems.length === 0, 'no page error', page.problems.join('; '));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ version list, This computer, Tune, a task
|
||||
for (const scheme of ['light', 'dark']) await scenario(scheme === 'light' ? 'ui2' : 'ui2d', { installed: true, env: { FAKE_TOKEN_DELAY: '0.02', FAKE_REPLY: 'I am thinking about it. </think> ' + 'Here is the answer, word by word. '.repeat(12), STRATA_TUNE_NO_QUIET_CHECK: '1' } }, async ({ base, api, browser, shot }) => {
|
||||
const page = await newPage(browser, base, { colorScheme: scheme });
|
||||
await clickText(page, /Go straight to the app/i);
|
||||
await page.getByText(/Conversation context/i).first().waitFor({ timeout: 30000 });
|
||||
await delay(1500);
|
||||
// the version list: Recommended (the fixed four), then folded groups, one line per model
|
||||
console.log('info triggers', await page.locator('.version-trigger').count(), await page.getByRole('button', { name: 'Model version' }).count());
|
||||
await page.locator('.version-trigger').first().click();
|
||||
await delay(300);
|
||||
console.log('info expanded after click', await page.locator('.version-trigger').first().getAttribute('aria-expanded'));
|
||||
await delay(800); await shot(page, `0-after-click-${scheme}`);
|
||||
console.log('info menu', await page.evaluate(() => { const m = document.querySelector('.version-menu'); const t = document.querySelector('.version-trigger'); return JSON.stringify({ menu: m ? m.getBoundingClientRect() : null, parent: m ? [m.parentElement.className, getComputedStyle(m.parentElement).display, getComputedStyle(m.parentElement).visibility, m.parentElement.style.cssText.slice(0, 120)] : null, expanded: t?.getAttribute('aria-expanded'), roles: [...document.querySelectorAll('[role=menu]')].length }); }));
|
||||
await page.locator('.version-menu').waitFor({ timeout: 10000 });
|
||||
const heads = await page.locator('.version-head span:first-child, .version-group__head').allTextContents();
|
||||
const names = await page.locator('.version-menu > .version-row .version-row__name strong').allTextContents();
|
||||
console.log('info version list', JSON.stringify(heads.map(t => t.replace(/\s+/g, ' ').trim())), JSON.stringify(names));
|
||||
check(heads[0]?.trim() === 'Recommended', `${scheme}: the list starts with Recommended`);
|
||||
check(names.length >= 1 && names.length <= 4, `${scheme}: Recommended shows only the picks this catalog has (at most the fixed four)`, names.join(' | '));
|
||||
check(heads.some(t => /Swift models|GSQ-RCO models|Gyro models|NVFP4 models|Unsloth models/.test(t)), `${scheme}: the other models are in folded groups`);
|
||||
check(heads.at(-1)?.includes('Use your own GGUF'), `${scheme}: the last row is Use your own GGUF`);
|
||||
const tall = await page.locator('.version-row').evaluateAll(els => els.filter(e => e.getBoundingClientRect().height > 44).length);
|
||||
check(tall === 0, `${scheme}: every model is one line`, String(tall));
|
||||
await shot(page, `1-versions-${scheme}`);
|
||||
await page.keyboard.press('Escape');
|
||||
// This computer
|
||||
const pc = page.locator('.pc-card');
|
||||
await pc.waitFor({ timeout: 10000 });
|
||||
const pcText = ((await pc.textContent()) ?? '').replace(/\s+/g, ' ');
|
||||
check(/This computer/.test(pcText) && /(Fits fully in memory|read from the SSD)/.test(pcText), `${scheme}: This computer says in one line whether it fits`, pcText.slice(0, 200));
|
||||
check(await pc.locator('.pc-meter').count() >= 1, `${scheme}: it shows the memory bars`);
|
||||
check(await pc.locator('.chip, .st-pill, [class*="badge"]').count() === 0, `${scheme}: no chips or badges in it`);
|
||||
await shot(page, `2-computer-${scheme}`);
|
||||
// Tune: five steps, a live speed, a result card
|
||||
await page.getByRole('button', { name: /^Tune model$/ }).first().click({ timeout: 10000 });
|
||||
await delay(1500); await shot(page, `2b-tune-dialog-${scheme}`);
|
||||
// The fake engine has no other setting worth trying, so its tune passes through "trying" too fast to be seen. The page is shown the progress a
|
||||
// real tune reports (Control's status file: stage, speeds) for the running view; the start, the result and every other call are real.
|
||||
let fakePhase = 'run';
|
||||
await page.route('**/api/native*', async route => {
|
||||
const response = await route.fetch(); let json = null;
|
||||
try { json = await response.json(); } catch { return route.fulfill({ response }); }
|
||||
for (const job of json?.jobs ?? []) if (job.kind === 'autotune' && fakePhase === 'done') { job.state = 'done'; job.progress = null; job.result = { outcome: 'improved', gain_pct: 18, base: { decode_tok_s: 41.2 }, chosen: { decode_tok_s: 48.6 }, preset: { id: 'fast', name: 'Fast · 64K' }, answers: { class: 'exact', fields: [] } }; } else if (job.kind === 'autotune' && job.state === 'running') job.progress = { stage: 'rounds', text: null, minutesLeft: 9, runsDone: 3, runsPlanned: 6, elapsedS: 300, step: 'Trying: more experts kept in memory (round 1 of 2)', speeds: [{ label: 'base', tok_s: 41.2 }, { label: 'Fewer helper threads', tok_s: 40.1 }, { label: 'More experts kept in memory', tok_s: 46.8 }] };
|
||||
return route.fulfill({ response, json });
|
||||
});
|
||||
await page.getByRole('button', { name: /Start tuning|Tune again/ }).first().click({ timeout: 15000 });
|
||||
await page.locator('.tune-step').first().waitFor({ timeout: 60000 });
|
||||
check(await page.locator('.tune-step').count() === 5, `${scheme}: the tuner shows its five steps`);
|
||||
let sawRun = false, sawSpeed = false;
|
||||
for (let i = 0; i < 400; i++) {
|
||||
const done = await page.locator('.tune-result, .tune-progress').count();
|
||||
if (i % 4 === 0) { const jobs = (await api('/api/native')).body?.jobs ?? []; const j = jobs.find(x => x.kind === 'autotune'); if (j) console.log('info tune job', j.state, JSON.stringify(j.progress ?? null).slice(0, 300)); }
|
||||
if (await page.locator('.tune-step[data-s=run]').count()) sawRun = true;
|
||||
const big = await page.locator('.tune-live__big').first().textContent().catch(() => '');
|
||||
if (/[0-9]/.test(big ?? '')) sawSpeed = true;
|
||||
if (sawRun && sawSpeed) { await delay(700); await shot(page, `3-tune-running-${scheme}`); break; }
|
||||
if (done) break;
|
||||
await delay(600);
|
||||
}
|
||||
check(sawRun, `${scheme}: a step is shown as running`);
|
||||
check(sawSpeed, `${scheme}: the live speed shows a number while it runs`);
|
||||
if (sawSpeed) { fakePhase = 'done'; await page.locator('.tune-outcome').waitFor({ timeout: 15000 }).catch(() => {}); check(/18% faster/.test((await page.locator('.tune-outcome__body h3').textContent().catch(() => '')) ?? ''), `${scheme}: the result card shows the gain as its headline`); check(/Fast/.test((await page.locator('.tune-outcome__facts').textContent().catch(() => '')) ?? ''), `${scheme}: and what it was saved as`); }
|
||||
await shot(page, `4-tune-result-${scheme}`);
|
||||
await page.getByRole('button', { name: /^Close$/ }).first().click({ timeout: 10000 }).catch(() => {});
|
||||
// a task: the opened task is skimmable, with pause / delete / Edit / Run now
|
||||
const made = await api('/api/tasks', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title: 'Open Downloads folder', prompt: 'Open my Downloads folder and tell me what is new.', frequency: 'weekdays', at: '08:00', weekday: 1, timeZone: 'Europe/Ljubljana', mode: 'manual', enabled: true }) });
|
||||
const id = made.body?.id ?? made.body?.task?.id;
|
||||
check(!!id, `${scheme}: a task is created`, JSON.stringify(made.body).slice(0, 120));
|
||||
await page.evaluate(i => { location.hash = '#/tasks/' + i; }, id);
|
||||
await page.getByRole('button', { name: /^Edit$/ }).first().waitFor({ timeout: 15000 });
|
||||
console.log('info header buttons', await page.getByRole('button', { name: /^Pause / }).count(), await page.getByRole('button', { name: /^Delete / }).count(), await page.getByRole('button', { name: /Run now/ }).count(), JSON.stringify(await page.locator('.task-header-actions button').evaluateAll(b => b.map(x => (x.getAttribute('aria-label') ?? x.textContent ?? '').trim()))));
|
||||
check(await page.getByRole('button', { name: /^Pause / }).count() === 1 && await page.getByRole('button', { name: /^Delete / }).count() >= 1 && await page.getByRole('button', { name: /Run now/ }).count() >= 1, `${scheme}: pause, delete, Edit and Run now are in the header`);
|
||||
check(await page.locator('.main__back').count() === 0 && await page.locator('.main__title-line .task-state').count() === 0, `${scheme}: nothing sits on the left of an opened task's navbar`);
|
||||
const factsText = ((await page.locator('.task-facts').textContent()) ?? '').replace(/\s+/g, ' ');
|
||||
check(/Weekdays/.test(factsText) && /Schedule/i.test(factsText) && /Last result/i.test(factsText), `${scheme}: schedule, what it may do and the last result are at the top`, factsText.slice(0, 160));
|
||||
await shot(page, `5-task-${scheme}`);
|
||||
await page.getByRole('button', { name: /^Pause / }).first().click();
|
||||
await page.getByRole('button', { name: /^Resume / }).first().waitFor({ timeout: 10000 });
|
||||
check(true, `${scheme}: Pause becomes Resume`);
|
||||
check(page.problems.length === 0, `${scheme}: no page error`, page.problems.join('; '));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ tune + reading size
|
||||
await scenario('tune', { installed: true, env: { FAKE_TOKEN_DELAY: '0.02', FAKE_REPLY: ['I am thinking about it.', '</think>', '', 'Here is a list:', '', '- one', '- two', '', '## Heading', '', 'Some `code` and **bold** text.', '', '| a | b |', '|---|---|', '| 1 | 2 |', '', '```', 'code line', '```', ''].join(String.fromCharCode(10)) } }, async ({ base, api, browser, dirs, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /get started/i);
|
||||
await page.getByText(/Choose a version/i).waitFor({ timeout: 30000 });
|
||||
await page.locator('[role=radio]').filter({ hasText: 'IQ3_S' }).first().click();
|
||||
await clickText(page, /^Continue/);
|
||||
await page.getByText(/Leave 20% for my work/i).waitFor({ timeout: 30000 });
|
||||
await delay(800);
|
||||
await shot(page, '1-tune-step');
|
||||
check(await page.getByText(/Read pictures when this model loads/i).count() === 0, 'the tuning step offers no picture reading for an app install');
|
||||
await clickText(page, /Tune & start chatting/i);
|
||||
let done = false, job = null;
|
||||
for (let i = 0; i < 120 && !done; i++) { job = (await api('/api/native')).body.jobs.findLast?.(j => j.kind === 'tune') ?? null; done = !!job && job.state !== 'running'; await delay(1000); }
|
||||
check(job?.state === 'done', 'tuning finishes without a vision error', job?.error ?? job?.state);
|
||||
const cfg = JSON.parse(readFileSync(join(dirs.install, 'strata-iq3_s.json'), 'utf8'));
|
||||
check(cfg.args.includes('--resident-budget-gib'), '"leave 20%" wrote a RAM budget for the experts', cfg.args.join(' '));
|
||||
const gib = Number(cfg.args[cfg.args.indexOf('--resident-budget-gib') + 1]);
|
||||
check(gib + 10 <= 63.1 * 0.8, 'the budget keeps the model within 80% of the PC RAM', `${gib} GiB`);
|
||||
// chat + reading size
|
||||
await page.getByRole('textbox', { name: /Message Strata/i }).waitFor({ timeout: 60000 });
|
||||
await page.getByRole('textbox', { name: /Message Strata/i }).fill('hello');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.st-msg--assistant .st-bubble').first().waitFor({ timeout: 60000 });
|
||||
await page.locator('.st-msg--assistant .st-bubble p').first().waitFor({ timeout: 60000 }).catch(async () => console.log('info reply so far:', JSON.stringify(await page.locator('.st-msg--assistant').first().innerText()).slice(0, 300)));
|
||||
await page.waitForFunction(() => !document.querySelector('.st-msg--assistant .caret'), null, { timeout: 90000 }).catch(() => {});
|
||||
// the chat's telemetry chips: speed and context
|
||||
const chips = page.locator('.telemetry__chip');
|
||||
const chipTexts = await chips.allInnerTexts();
|
||||
console.log('info chips', JSON.stringify(chipTexts));
|
||||
let speedText = '', contextText = '';
|
||||
for (let i = 0; i < await chips.count(); i++) {
|
||||
await chips.nth(i).click();
|
||||
await delay(400);
|
||||
const pop = await page.locator('.telepop').last().innerText().catch(() => '');
|
||||
if (/tok\/s|speed/i.test(pop) && !speedText) speedText = pop;
|
||||
if (/context/i.test(pop) && !contextText) contextText = pop;
|
||||
await page.keyboard.press('Escape');
|
||||
}
|
||||
const tps = Number(speedText.match(/([\d.]+)\s*tok(?:ens)?\/s/)?.[1] ?? NaN);
|
||||
check(tps > 15 && tps < 120, 'the speed shown is the decode speed (the engine paced at ~50 tokens a second)', speedText.replace(/\s+/g, ' ').slice(0, 160));
|
||||
check(/32\.8K|32K|32,768|32768/.test(contextText) && !/128K/.test(contextText), 'the context shown is the one the model runs with (32K), not 128K', contextText.replace(/\s+/g, ' ').slice(0, 160));
|
||||
const sizes = [];
|
||||
for (const px of [12, 14, 16, 22]) {
|
||||
await page.evaluate(v => { const k = 'strata.prefs.v2'; const cur = JSON.parse(localStorage.getItem(k) || '{}'); localStorage.setItem(k, JSON.stringify({ ...cur, contentFontSize: v })); }, px);
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.locator('.st-msg--assistant .st-bubble').first().waitFor({ timeout: 30000 });
|
||||
await page.screenshot({ path: join(shots, `tune-3-chat-${px}px.png`) });
|
||||
sizes.push(await page.evaluate(() => { const b = document.querySelector('.st-msg--assistant .st-bubble'); const t = b.querySelector('p, .md-li, span') ?? b; const u = document.querySelector('.st-msg--user .st-bubble'); const all = {}; for (const el of b.querySelectorAll('*')) { if (!el.childNodes.length || ![...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim())) continue; all[el.tagName.toLowerCase() + (el.className ? '.' + String(el.className).split(' ')[0] : '')] = parseFloat(getComputedStyle(el).fontSize); } const extra = {}; for (const [k, q] of Object.entries({ reasoning: '.st-msg--assistant .st-collapse__body', codeblock: '.st-msg--assistant .st-code pre', codehead: '.st-msg--assistant .st-code__head' })) { const e = document.querySelector(q); if (e) extra[k] = parseFloat(getComputedStyle(e).fontSize); } Object.assign(all, extra); const chain = []; for (let e = b.querySelector('p'); e && e !== b; e = e.parentElement) chain.push(e.tagName + '.' + String(e.className).split(' ')[0] + '=' + getComputedStyle(e).fontSize); all.chain = chain; globalThis.__all = all; return [parseFloat(getComputedStyle(b).fontSize), parseFloat(getComputedStyle(t).fontSize), parseFloat(getComputedStyle(u).fontSize), all]; }));
|
||||
}
|
||||
console.log('info chain', JSON.stringify(await page.evaluate(() => { const el = document.querySelector('.st-msg--assistant .st-bubble p'); const out = []; for (let e = el; e && !e.classList.contains('st-msg'); e = e.parentElement) out.push(e.tagName + '.' + String(e.className).split(' ')[0] + '=' + getComputedStyle(e).fontSize); return out; })));
|
||||
// which visible text does NOT follow the reading size? (same elements at 12 and 22)
|
||||
const survey = async px => { await page.evaluate(v => { const k = 'strata.prefs.v2'; const cur = JSON.parse(localStorage.getItem(k) || '{}'); localStorage.setItem(k, JSON.stringify({ ...cur, contentFontSize: v })); }, px); await page.reload({ waitUntil: 'domcontentloaded' }); await page.locator('.st-msg--assistant .st-bubble').first().waitFor({ timeout: 30000 }); await delay(600);
|
||||
return page.evaluate(() => { const out = {}; for (const el of document.querySelectorAll('main *, .chat *, textarea')) { if (![...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim()) && el.tagName !== 'TEXTAREA') continue; const r = el.getBoundingClientRect(); if (!r.width || !r.height) continue; const key = el.tagName.toLowerCase() + '.' + String(el.className).split(' ')[0] + ' <' + (el.parentElement?.className ?? '').toString().split(' ')[0] + '>'; out[key] = parseFloat(getComputedStyle(el).fontSize); } return out; }); };
|
||||
const a = await survey(12), b = await survey(22);
|
||||
// a fresh app (nothing saved) uses 14 px
|
||||
await page.evaluate(() => localStorage.removeItem('strata.prefs.v2'));
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.locator('.st-msg--assistant .st-bubble').first().waitFor({ timeout: 30000 });
|
||||
check(await page.evaluate(() => getComputedStyle(document.querySelector('.st-msg--assistant .st-bubble')).fontSize) === '14px', 'the default reading size is 14 px');
|
||||
const fixed = Object.keys(a).filter(k => k in b && a[k] === b[k]);
|
||||
console.log('info text that does not follow the reading size:', JSON.stringify(fixed.map(k => k + '=' + a[k])));
|
||||
await shot(page, '2-chat-22px');
|
||||
console.log('info sizes', JSON.stringify(sizes), JSON.stringify(await page.locator('.st-msg--assistant .st-bubble').first().innerHTML()).slice(0, 400));
|
||||
check(sizes[0][1] === 12 && sizes[1][1] === 14 && sizes[2][1] === 16 && sizes[3][1] === 22 && sizes[0][2] === 12 && sizes[3][2] === 22, 'the reading size changes the text size of the conversation', JSON.stringify(sizes));
|
||||
// the slider itself
|
||||
await page.evaluate(() => { location.hash = '#/chat?settings=1'; });
|
||||
const slider = page.getByLabel('Reading text size');
|
||||
if (await slider.count()) {
|
||||
await slider.first().focus();
|
||||
for (let i = 0; i < 4; i++) await page.keyboard.press('ArrowRight');
|
||||
await delay(500);
|
||||
const now = await page.evaluate(() => getComputedStyle(document.querySelector('.st-msg--assistant .st-bubble') ?? document.body).fontSize);
|
||||
check(parseFloat(now) === 18, 'moving the slider changes the conversation text at once', now);
|
||||
const box = await slider.first().boundingBox();
|
||||
await page.mouse.click(box.x + box.width * 0.9, box.y + box.height / 2);
|
||||
await delay(400);
|
||||
const dragged = await page.evaluate(() => [getComputedStyle(document.querySelector('.st-msg--assistant .st-bubble') ?? document.body).fontSize, getComputedStyle(document.documentElement).getPropertyValue('--st-content-fs'), JSON.parse(localStorage.getItem('strata.prefs.v2') || '{}').contentFontSize]);
|
||||
check(parseFloat(dragged[0]) >= 20 && parseFloat(dragged[2]) >= 20, 'clicking the slider with the mouse changes it too, and it is saved', dragged.join(' '));
|
||||
} else console.log('info the settings slider was not reachable by hash here; the stored size was checked above');
|
||||
check(page.problems.length === 0, 'no page error', page.problems.join('; '));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ delete
|
||||
await scenario('delete', { installed: true }, async ({ base, api, browser, dirs, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /Go straight to the app/i);
|
||||
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
|
||||
await page.getByRole('button', { name: /^Load model$/ }).first().click();
|
||||
let ready = false;
|
||||
for (let i = 0; i < 90 && !ready; i++) { ready = (await api('/api/native')).body.instances.some(x => x.status === 'ready'); await delay(1000); }
|
||||
check(ready, 'a model is loaded');
|
||||
await clickText(page, /Manage models/i);
|
||||
await page.getByRole('button', { name: /^Delete$/ }).first().click();
|
||||
await delay(600);
|
||||
await shot(page, '1-delete-confirm');
|
||||
const dialog = await page.getByRole('dialog').last().innerText();
|
||||
check(/Delete this version\?/.test(dialog) && /Remove 83\.6 GB of downloaded files\. You can download this version again\./.test(dialog), 'the confirm dialog uses the existing words and the real size', dialog.replace(/\s+/g, ' ').slice(0, 600));
|
||||
check(/running model will be unloaded/i.test(dialog), 'and says the loaded model is unloaded first');
|
||||
await page.getByRole('button', { name: /^Delete version$/ }).click();
|
||||
let state = null;
|
||||
for (let i = 0; i < 40; i++) { state = (await api('/api/native')).body; if (state.instances.length === 0 && !state.installed?.models?.some(m => m.model === 'iq3_s')) break; await delay(500); }
|
||||
check(state.instances.length === 0, 'the loaded model was unloaded');
|
||||
check(!existsSync(join(dirs.install, 'strata-iq3_s.json')), 'the installer removed the run config of the model');
|
||||
check(!state.installed?.models?.some(m => m.model === 'iq3_s'), 'the model is no longer listed as downloaded');
|
||||
await shot(page, '2-after-delete');
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ perf
|
||||
await scenario('perf', { installed: true }, async ({ base, api, browser, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /Go straight to the app/i);
|
||||
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
|
||||
await page.getByRole('button', { name: /^Load model$/ }).first().click();
|
||||
let ready = false;
|
||||
for (let i = 0; i < 90 && !ready; i++) { ready = (await api('/api/native')).body.instances.some(x => x.status === 'ready'); await delay(1000); }
|
||||
check(ready, 'a model is loaded');
|
||||
await delay(2500);
|
||||
const requests = [];
|
||||
const started = new Map();
|
||||
page.on('request', r => started.set(r, Date.now()));
|
||||
page.on('requestfinished', r => requests.push([r.url().replace(base, '').slice(0, 60), Date.now() - (started.get(r) ?? Date.now())]));
|
||||
await page.evaluate(() => { window.__long = []; new PerformanceObserver(l => { for (const e of l.getEntries()) window.__long.push(Math.round(e.duration)); }).observe({ entryTypes: ['longtask'] }); });
|
||||
const t0 = Date.now();
|
||||
await page.evaluate(() => { location.hash = '#/chat'; });
|
||||
await page.getByRole('textbox', { name: /Message Strata/i }).waitFor({ timeout: 30000 });
|
||||
const open = Date.now() - t0;
|
||||
await delay(1500);
|
||||
const long = await page.evaluate(() => window.__long);
|
||||
console.log('info chat opened in', open, 'ms; long tasks', JSON.stringify(long), 'requests', JSON.stringify(requests.filter(r => r[1] > 100)), 'all', requests.length);
|
||||
check(open < 1500 && Math.max(0, ...long) < 300, 'Models to Chat opens at once, with no long block', open + ' ms, longest task ' + Math.max(0, ...long) + ' ms');
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ polish
|
||||
await scenario('polish', { installed: true }, async ({ base, api, browser, dirs, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /Go straight to the app/i);
|
||||
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
|
||||
await page.getByText(/Conversation context/i).first().waitFor({ timeout: 30000 });
|
||||
check(await page.getByRole('button', { name: /^Tune model$/ }).count() === 1, 'the Models tab has the Tune model button (the real tuner, not the old short benchmark)');
|
||||
await clickText(page, /Manage models/i);
|
||||
await delay(500);
|
||||
const rows = await page.locator('.native-manage-row').count();
|
||||
check(rows === 1, 'Manage models lists only the downloaded model, not the catalog', rows + ' row(s)');
|
||||
await shot(page, '1-manage-models');
|
||||
await page.getByRole('button', { name: /^Done$/ }).click();
|
||||
await page.getByRole('button', { name: /Other local models/i }).click().catch(async () => { await clickText(page, /Manage models/i); await clickText(page, /Use a local GGUF/i); });
|
||||
await page.getByText(/Folder with the GGUF files/i).first().waitFor({ timeout: 15000 }).catch(() => {});
|
||||
await shot(page, '2-local-gguf');
|
||||
check(await page.getByText(/Folder with the GGUF files/i).count() > 0, 'the local GGUF page opens in the same design as Models');
|
||||
await page.getByPlaceholder(/Full path to the folder/i).fill(join(dirs.install, 'does-not-exist'));
|
||||
await page.getByRole('button', { name: /^Check files$/ }).click();
|
||||
await delay(3000);
|
||||
check(await page.locator('.error-notice').count() > 0, 'a folder that cannot be used says so in words');
|
||||
await shot(page, '2b-local-gguf-problem');
|
||||
await page.evaluate(() => { location.hash = '#/chat?settings=1'; });
|
||||
await delay(1500);
|
||||
const text = await page.locator('body').innerText();
|
||||
check(!/Contrast/.test(text), 'Settings has no Contrast option');
|
||||
check(!/View request log/.test(text), 'and no "View request log" button');
|
||||
await page.getByText(/^Engine$/).first().click().catch(() => {});
|
||||
await delay(1500);
|
||||
await shot(page, '3-settings-engine');
|
||||
const engine = await page.locator('body').innerText();
|
||||
check(/Update component/.test(engine) && /Diagnostics/.test(engine), 'Settings > Engine has Update component and Diagnostics');
|
||||
check(!/Performance|Model settings|Calibrate|Engine installation/.test(engine), 'and nothing else');
|
||||
await page.getByRole('button', { name: /^View logs$/ }).click(); // the logs open in a pop-up that holds Save logs
|
||||
await page.getByRole('button', { name: /^Save logs$/ }).click();
|
||||
await page.getByText(/Saved to/).first().waitFor({ timeout: 15000 }).catch(() => {});
|
||||
const savedText = await page.getByText(/Saved to/).first().innerText().catch(() => '');
|
||||
const savedPath = savedText.replace(/^Saved to\s*/, '').trim();
|
||||
check(!!savedPath && existsSync(savedPath), 'Save logs writes a file to Downloads and says where', savedPath);
|
||||
await shot(page, '4-logs-saved');
|
||||
await page.evaluate(() => { location.hash = '#/monitor'; });
|
||||
await delay(6000);
|
||||
await shot(page, '5-monitor');
|
||||
const mon = await page.locator('body').innerText();
|
||||
check(/°C/.test(mon), 'the Monitor shows GPU data with no model loaded', (mon.match(/.{0,30}°C.{0,20}/)?.[0] ?? 'no temperature text').replace(/\s+/g, ' '));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ stop
|
||||
await scenario('stop', { installed: true, env: { FAKE_TOKEN_DELAY: '0.1', FAKE_REPLY: 'thinking '.repeat(30) } }, async ({ base, api, browser, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /Go straight to the app/i);
|
||||
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
|
||||
await page.getByRole('button', { name: /^Load model$/ }).first().click();
|
||||
let ready = false;
|
||||
for (let i = 0; i < 90 && !ready; i++) { ready = (await api('/api/native')).body.instances.some(x => x.status === 'ready'); await delay(1000); }
|
||||
if (!ready) { const st = (await api('/api/native')).body; console.log('info jobs', JSON.stringify(st.jobs.map(j => [j.kind, j.state, j.error ?? j.summary])), JSON.stringify(st.instances.map(i => [i.status, i.error]))); await shot(page, '0-stop-notloaded'); }
|
||||
check(ready, 'a model is loaded');
|
||||
await page.evaluate(() => { location.hash = '#/chat'; });
|
||||
const box = page.getByRole('textbox', { name: /Message Strata/i });
|
||||
await box.waitFor({ timeout: 30000 });
|
||||
await box.fill('hello');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.typing').first().waitFor({ timeout: 30000 });
|
||||
await delay(1500);
|
||||
await page.getByRole('button', { name: /^Stop/i }).first().click().catch(async () => { await page.keyboard.press('Escape'); });
|
||||
await delay(4000);
|
||||
const dots = await page.locator('.typing').count();
|
||||
await shot(page, '1-after-stop');
|
||||
check(dots === 0, 'after Stop in the middle of thinking the three dots are gone', dots + ' left');
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ latest
|
||||
await scenario('latest', {}, async ({ base, browser, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
const now = Date.now();
|
||||
await page.evaluate(now => {
|
||||
const messages = [];
|
||||
for (let i = 0; i < 40; i++) { messages.push({ id: 'u' + i, role: 'user', content: 'question ' + i, at: now }, { id: 'a' + i, role: 'assistant', content: 'answer number ' + i + ' with some text to fill the line and more of it.', at: now, tokens: 20, inputTokens: 5 }); }
|
||||
localStorage.setItem('strata.sessions.v1', JSON.stringify([{ id: 's_latest', title: 'long', updatedAt: now, createdAt: now, messages }]));
|
||||
localStorage.setItem('strata.active-session.v1', 's_latest');
|
||||
}, now);
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await delay(1500);
|
||||
if (await page.getByRole('button', { name: /Go straight to the app/i }).count()) { await clickText(page, /Go straight to the app/i); await delay(1500); }
|
||||
await page.goto(base + '/#/chat', { waitUntil: 'domcontentloaded' });
|
||||
await page.getByRole('textbox', { name: /Message Strata/i }).waitFor({ timeout: 30000 });
|
||||
await delay(800);
|
||||
await page.locator('.main__body.chat').first().waitFor({ timeout: 15000 }).catch(async () => { await shot(page, '0-latest-missing'); console.log('info body', JSON.stringify((await page.locator('body').innerText()).slice(0, 300))); });
|
||||
await page.locator('.main__body.chat').first().evaluate(el => { el.scrollTop = 3000; });
|
||||
await delay(600);
|
||||
await page.mouse.wheel(0, -400);
|
||||
await delay(600);
|
||||
await page.locator('.main__body.chat').first().evaluate(el => { el.scrollTop = 0; });
|
||||
await delay(800);
|
||||
const gaps = await page.evaluate(() => { const b = document.querySelector('.jumplatest'), c = document.querySelector('.main__body.chat'); if (!b || !c) return null; const r = b.getBoundingClientRect(), cr = c.getBoundingClientRect(); return { top: Math.round(r.top - cr.top), bottom: Math.round(cr.bottom - r.bottom) }; });
|
||||
console.log('info chat dom', JSON.stringify(await page.evaluate(() => { const c = document.querySelector('.main__body.chat'); return { msgs: document.querySelectorAll('.st-msg').length, scrollH: c?.scrollHeight, clientH: c?.clientHeight, top: c?.scrollTop, jump: document.querySelectorAll('.jumplatest').length }; })));
|
||||
await shot(page, '1-latest-button');
|
||||
check(!!gaps && gaps.top >= 12 && gaps.bottom >= 12, 'the Latest button keeps a gap from the top navigation and from the bottom', JSON.stringify(gaps));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ fail
|
||||
await scenario('fail', { installed: true, env: { FAKE_ENGINE_FAIL: '1' } }, async ({ base, api, browser, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /get started/i);
|
||||
await page.getByText(/Choose a version/i).waitFor({ timeout: 30000 });
|
||||
await page.locator('[role=radio]').filter({ hasText: 'IQ3_S' }).first().click();
|
||||
await clickText(page, /^Continue/);
|
||||
await clickText(page, /Tune & start chatting/i, 30000);
|
||||
await page.getByText(/ran low on memory/i).first().waitFor({ timeout: 120000 });
|
||||
await delay(1000);
|
||||
await shot(page, '1-tune-error');
|
||||
const text = await page.locator('.guided-onboarding').innerText();
|
||||
check(/ran low on memory/.test(text), 'the error says in words that the PC ran low on memory');
|
||||
check(/Leave 20% for my work/.test(text) && /low-RAM/.test(text), 'and what to try');
|
||||
check(!/server_exited|memory guard/.test(text), 'no code or log line in the main text');
|
||||
check(!/vision files/i.test(text), 'no picture-reading error');
|
||||
const bad = await noScroll(page);
|
||||
check(bad.length === 0, 'the tuning pop-up does not scroll when an error shows', bad.join('; '));
|
||||
await page.locator('.error-notice summary').first().click();
|
||||
await delay(500);
|
||||
await shot(page, '2-tune-error-details');
|
||||
const details = await page.locator('.error-notice pre').first().innerText();
|
||||
check(/server_exited/.test(details) && /exited before/.test(details), 'Details holds the code and the reason the server logged', details.replace(/\s+/g, ' ').slice(0, 120));
|
||||
const badOpen = await noScroll(page);
|
||||
check(badOpen.length === 0, 'and the pop-up still does not scroll with Details open', badOpen.join('; '));
|
||||
await page.setViewportSize({ width: 1280, height: 700 });
|
||||
await delay(600);
|
||||
const small = await noScroll(page);
|
||||
check(small.length === 0, 'nor in a small window (1280x700), Details open', small.join('; '));
|
||||
await shot(page, '3-tune-error-small-window');
|
||||
void api;
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ restart
|
||||
// The app keeps its theme, text size, language and chat list in the browser's storage, which belongs to the window's origin
|
||||
// (127.0.0.1:<port>). The shell now uses the same port every launch: stop the host, start it again on that port, reopen the
|
||||
// window with the same browser profile, and everything is still there.
|
||||
if (only.has('restart')) {
|
||||
console.log('--- restart');
|
||||
const fixed = await freePort();
|
||||
const profile = join(work, 'browser-profile');
|
||||
const run = async (body) => scenario('restart', { env: { FIXED_PORT: String(fixed) } }, async ctx => {
|
||||
const context = await chromium.launchPersistentContext(profile, { executablePath: CHROME, headless: true, viewport: { width: 1200, height: 800 } });
|
||||
try { await body(context, ctx); } finally { await context.close(); }
|
||||
});
|
||||
only.add('restart');
|
||||
await run(async (context, { base }) => {
|
||||
const page = context.pages()[0] ?? await context.newPage();
|
||||
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
||||
await page.evaluate(() => { localStorage.setItem('strata.prefs.v2', JSON.stringify({ theme: 'dark', contentFontSize: 19, language: 'de' })); localStorage.setItem('strata.sessions.v1', JSON.stringify([{ id: 'chat-1', title: 'My chat' }])); });
|
||||
});
|
||||
await run(async (context, { base }) => {
|
||||
const page = context.pages()[0] ?? await context.newPage();
|
||||
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
||||
const saved = await page.evaluate(() => ({ prefs: JSON.parse(localStorage.getItem('strata.prefs.v2') || '{}'), chats: localStorage.getItem('strata.sessions.v1'), theme: document.documentElement.dataset.theme }));
|
||||
check(saved.prefs.contentFontSize === 19 && saved.prefs.language === 'de', 'text size and language survive a restart on the same port', JSON.stringify(saved.prefs));
|
||||
check(saved.theme === 'dark', 'the theme survives too', saved.theme);
|
||||
check(/My chat/.test(saved.chats ?? ''), 'and the chat list', saved.chats);
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ theme
|
||||
await scenario('theme', {}, async ({ base, browser, shot }) => {
|
||||
for (const scheme of ['dark', 'light']) {
|
||||
const page = await newPage(browser, base, { colorScheme: scheme });
|
||||
await delay(1500);
|
||||
const theme = await page.evaluate(() => document.documentElement.dataset.theme);
|
||||
check(theme === scheme, `a fresh app follows the system (${scheme})`, theme);
|
||||
await shot(page, `1-${scheme}`);
|
||||
await page.context().close();
|
||||
}
|
||||
});
|
||||
|
||||
// the junctions into the staged Control go first (rmdir on a junction removes only the link)
|
||||
for (const name of ['python', 'strata_control', 'strata_cli', 'serve']) { try { rmdirSync(join(control, name)); } catch { /* already gone */ } }
|
||||
try { rmSync(work, { recursive: true, force: true, maxRetries: 3 }); } catch (e) { console.log('info scratch folder left behind:', work); }
|
||||
console.log(`shots in ${shots}`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Picture reading in the APP, end to end on a test box, through the installed app layout (packaged host + bundled Control + a real engine component
|
||||
* that carries strata-vision):
|
||||
* Download and prepare with "Read pictures" ON (the picture-reader file is fetched, the vision settings are written), load the model with the picture
|
||||
* reader, send a real picture through the host's own /v1 endpoint and check that the answer names what is in it, unload, shut down.
|
||||
*
|
||||
* node vision-e2e.mjs --install <folder with node/ host/ control/> --components <folder holding engine/<package with strata-vision>> \
|
||||
* --work <scratch> --family qwen --model IQ3_XXS --data <data folder that has (or will get) the model files> --image shapes.png [--device cpu|gpu]
|
||||
*
|
||||
* The picture has a red circle, a blue square, a green triangle and the text "STRATA 42". The run exits 1 if any step fails.
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') || all[i + 1] === undefined ? true : all[i + 1]]] : acc), []));
|
||||
for (const need of ['install', 'components', 'work', 'model', 'image']) if (!args[need]) { console.error('missing --' + need); process.exit(2); }
|
||||
const install = resolve(args.install), work = resolve(args.work), components = resolve(args.components), family = String(args.family ?? 'qwen'), modelId = String(args.model);
|
||||
const device = String(args.device ?? 'cpu');
|
||||
const node = join(install, 'node', 'node'), hostEntry = join(install, 'host', 'server', 'index.mjs'), controlDir = join(install, 'control');
|
||||
const dirs = { models: join(work, 'models'), profiles: join(work, 'profiles'), runtime: join(work, 'runtime'), home: join(work, 'home'), xdg: join(work, 'xdg'), appdata: join(work, 'appdata') };
|
||||
for (const d of [dirs.models, dirs.home, dirs.xdg, dirs.appdata]) mkdirSync(d, { recursive: true });
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
let failed = false;
|
||||
const record = (name, ok, detail = '') => { if (!ok) failed = true; console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' ' + String(detail).slice(0, 600) : ''}`); };
|
||||
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
|
||||
const env = { ...process.env, STRATA_COMPONENTS_DIR: components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime, STRATA_INSTALL_DIR: dirs.models,
|
||||
STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(work, 'models-unused'), APPDATA: dirs.appdata, XDG_CONFIG_HOME: dirs.xdg, PYTHONDONTWRITEBYTECODE: '1' };
|
||||
delete env.STRATA_API_KEY;
|
||||
const port = await freePort();
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const host = spawn(node, [hostEntry], { cwd: join(install, 'host'), stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...env, STRATA_PORT: String(port), STRATA_DIST: join(install, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1',
|
||||
STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()) } });
|
||||
let hostLog = '';
|
||||
host.stdout.on('data', c => { hostLog += c; }); host.stderr.on('data', c => { hostLog += c; });
|
||||
const call = async (path, method = 'GET', body) => {
|
||||
const response = await fetch(base + path, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
||||
return { status: response.status, body: await response.json().catch(() => null) };
|
||||
};
|
||||
async function waitJob(id, seconds) {
|
||||
let last = '';
|
||||
for (let i = 0; i < seconds * 2; i++) {
|
||||
const state = (await call('/api/native')).body;
|
||||
const job = state.jobs.find(j => j.id === id);
|
||||
if (job && job.summary !== last) { last = job.summary; console.log(' ... ' + String(last).slice(0, 120)); }
|
||||
if (job && job.state !== 'running') return { job, state };
|
||||
await delay(500);
|
||||
}
|
||||
throw new Error('operation did not finish');
|
||||
}
|
||||
async function main() {
|
||||
let up = false;
|
||||
for (let i = 0; i < 200 && !up; i++) { try { up = (await fetch(base + '/api/health')).ok; } catch { /* not yet */ } if (!up) await delay(150); }
|
||||
record('host healthy', up, up ? base : hostLog.slice(-500));
|
||||
if (!up) return;
|
||||
const packages = existsSync(join(components, 'engine')) ? readdirSync(join(components, 'engine')) : [];
|
||||
const reader = packages.some(p => existsSync(join(components, 'engine', p, 'strata-vision')));
|
||||
record('the engine component carries the picture reader (strata-vision)', reader, packages.join(', '));
|
||||
const status = (await call('/api/control/status')).body;
|
||||
record('Control: engine usable', status.engine?.usable === true, `engine=${status.engine?.engine?.version}`);
|
||||
|
||||
// ---- Download and prepare, "Read pictures" on (the app sends vision "yes" for the download)
|
||||
const values = { family, model: modelId, context: 32768, vision: 'yes', loadVision: device, ...(args.data ? { data_dir: resolve(args.data) } : {}) };
|
||||
const plan = await call('/api/native/plan', 'POST', values);
|
||||
record('the plan for the download is made', plan.status === 200, JSON.stringify(plan.body).slice(0, 300));
|
||||
const started = await call('/api/native/operations', 'POST', { kind: 'install', args: values });
|
||||
record('the install operation starts', started.status === 200 || started.status === 202, JSON.stringify(started.body).slice(0, 200));
|
||||
const done = await waitJob(started.body.id, 3600);
|
||||
record('Download and prepare finishes', done.job.state === 'done', `${done.job.state} ${done.job.error ?? done.job.summary}`);
|
||||
const native = (await call('/api/native?refresh=1')).body;
|
||||
console.log(' installed models: ' + JSON.stringify((native.installed?.models ?? []).map(m => ({ id: m.id, model: m.model, ready: m.ready, visionPrepared: m.visionPrepared, images: m.images, config: m.config }))));
|
||||
const want = modelId.toLowerCase();
|
||||
const model = native.installed?.models?.find(m => [m.id, m.model, m.tag].some(x => String(x ?? '').toLowerCase() === want));
|
||||
record('the model is installed with the picture reader prepared (visionPrepared)', !!model?.visionPrepared, JSON.stringify({ ready: model?.ready, visionPrepared: model?.visionPrepared, images: model?.images }));
|
||||
const configFile = model?.configPath ?? (model?.config ? join(dirs.models, model.config) : null);
|
||||
let cfg = {};
|
||||
try { cfg = JSON.parse(readFileSync(configFile, 'utf8')); } catch { /* reported below */ }
|
||||
const mm = cfg.vision?.mmproj;
|
||||
record('the run settings carry the vision entry and --vision, and the mmproj file is on disk',
|
||||
!!cfg.vision && (cfg.args ?? []).includes('--vision') && !!mm && existsSync(mm) && statSync(mm).size > 800e6,
|
||||
`vision=${cfg.vision ? 'yes' : 'no'} --vision=${(cfg.args ?? []).includes('--vision')} mmproj=${mm ? Math.round(statSync(mm).size / 1048576) + ' MB' : 'none'}`);
|
||||
|
||||
// ---- load with the picture reader
|
||||
const load = await call('/api/native/operations', 'POST', { kind: 'load', args: { model: model?.model ?? modelId.toLowerCase(), vision: device } });
|
||||
const loaded = await waitJob(load.body.id, 900);
|
||||
const instance = loaded.state.instances[0];
|
||||
record('load starts the engine with the picture reader', loaded.job.state === 'done' && instance?.status === 'ready', `${loaded.job.state} ${loaded.job.error ?? ''}; instance=${instance?.status}; vision=${instance?.vision}`);
|
||||
record('the host says the loaded model reads pictures', !!instance?.vision, `vision=${instance?.vision}`);
|
||||
|
||||
// ---- a real picture through the host's own /v1 endpoint
|
||||
const png = readFileSync(resolve(args.image)).toString('base64');
|
||||
const t = Date.now();
|
||||
let answer = '', finish = '';
|
||||
for (const budget of [1536, 4096]) {
|
||||
const chat = await call('/v1/chat/completions', 'POST', { model: instance?.apiModelId ?? modelId, max_tokens: budget, stream: false, messages: [{ role: 'user', content: [
|
||||
{ type: 'text', text: 'Describe this picture: name each shape with its colour and read out any text.' },
|
||||
{ type: 'image_url', image_url: { url: 'data:image/png;base64,' + png } }] }] });
|
||||
const choice = chat.body?.choices?.[0] ?? {};
|
||||
answer = choice.message?.content ?? '';
|
||||
finish = choice.finish_reason ?? String(chat.status);
|
||||
if (answer.trim() || finish !== 'length') break;
|
||||
}
|
||||
console.log(` answer (${Math.round((Date.now() - t) / 1000)} s, finish=${finish}): ${JSON.stringify(answer.slice(0, 300))}`);
|
||||
const low = answer.toLowerCase();
|
||||
record('the answer names the red circle', /red|circle/.test(low));
|
||||
record('the answer names the blue square', /blue|square/.test(low));
|
||||
record('the answer names the green triangle', /green|triangle/.test(low));
|
||||
record('the answer reads STRATA 42', low.includes('42'));
|
||||
|
||||
// ---- unload
|
||||
const unloaded = await call(`/api/native/instances/${instance?.id}/unload`, 'POST', {});
|
||||
record('unload', unloaded.status === 200, unloaded.status);
|
||||
}
|
||||
try { await main(); } catch (e) { record('run', false, e.stack ?? e); }
|
||||
host.kill();
|
||||
await delay(1500);
|
||||
console.log(failed ? 'VISION-E2E FAIL' : 'VISION-E2E PASS');
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,2 @@
|
||||
# Rust build output. Regenerated by `cargo tauri build`; never committed.
|
||||
/target/
|
||||
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "strata-desktop"
|
||||
version = "0.2.3"
|
||||
description = "Strata Desktop — local AI, tuned to one model."
|
||||
edition = "2021"
|
||||
rust-version = "1.77"
|
||||
license = "MIT"
|
||||
|
||||
# Linux: the drop of a file on the window is read straight from the GTK drag signals (see hook_linux_drops in main.rs); the versions are the ones Tauri itself uses.
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
gtk = "0.18"
|
||||
webkit2gtk = "2.0"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-single-instance = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = "s"
|
||||
panic = "abort"
|
||||
strip = true
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Tauri uses the system WKWebView, so this bundle needs no JIT entitlement of its own;
|
||||
the web content process gets what it needs from the OS. The App Host runs as an ordinary
|
||||
child process and needs no special rights. Kept minimal on purpose. -->
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1 @@
|
||||
{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-exit":{"identifier":"allow-exit","description":"Enables the exit command without any pre-configured scope.","commands":{"allow":["exit"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-exit":{"identifier":"deny-exit","description":"Denies the exit command without any pre-configured scope.","commands":{"allow":[],"deny":["exit"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"DenLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 433 B |
|
After Width: | Height: | Size: 1004 B |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 771 B |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 949 B |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 504 B |