mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-11 17:35:15 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfa3f4025c | ||
|
|
ac08a0820d | ||
|
|
05041e5db0 | ||
|
|
3157c38c68 | ||
|
|
d2d198a492 | ||
|
|
8c9bd206ce | ||
|
|
46bddbbb74 | ||
|
|
6953d896d0 | ||
|
|
baa8f616de | ||
|
|
ac669f199d | ||
|
|
0003604859 | ||
|
|
e168b76b8c | ||
|
|
f53664c159 | ||
|
|
44f8c334de | ||
|
|
a22d557eee | ||
|
|
51d23fdd1e | ||
|
|
a62ca064a3 | ||
|
|
8183c6b671 | ||
|
|
c33fa7ba6d | ||
|
|
b7958c044f | ||
|
|
06e51dfaef |
No files matched your search
@@ -19,7 +19,7 @@ Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**. For an agent that has a shell,
|
||||
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
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh # Linux, Mac
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex # Windows PowerShell
|
||||
```
|
||||
|
||||
|
||||
+8
-1
@@ -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.49 LANGUAGES CXX)
|
||||
project(strata VERSION 0.1.49.2 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)
|
||||
@@ -1667,6 +1667,13 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
foreach(_f GGML_AVX GGML_AVX2 GGML_FMA GGML_F16C GGML_BMI2)
|
||||
set(${_f} ON CACHE BOOL "" FORCE)
|
||||
endforeach()
|
||||
# The DeepSeek engine (STRATA_BUILD_DSV41) builds ggml-cpu with BMI2 OFF: ggml's IQ1_S / IQ1_M dots use _pdep_u64 with it, and AMD
|
||||
# Zen 1/2 run PDEP in microcode (EPYC 7452, IQ1_M one-token dot 1.8 GB/s against 32 GB/s without it; IQ1_S 2.3 against 37). The
|
||||
# results are bit-identical either way (ds4_gg_bench --dump, types 19 and 29, 1-16 tokens). Other engines keep BMI2 until their
|
||||
# next rebuild is looked at: no Qwen package changes with this commit.
|
||||
if(STRATA_BUILD_DSV41)
|
||||
set(GGML_BMI2 OFF CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
elseif(STRATA_ISA_FLOOR)
|
||||
# the experimental older-CPU build (STRATA_ISA_FLOOR above): one ggml-cpu for the floor, ggml's "sandybridge"
|
||||
# feature set (SSE4.2 + AVX) or SSE4.2 alone - not the build host's, which may be newer than the PC it runs on.
|
||||
|
||||
+2
-2
@@ -108,7 +108,7 @@ 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
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
@@ -197,7 +197,7 @@ Alle Größen, Downloads und was wo läuft: [docs/MODELS.md](docs/MODELS.md).
|
||||
| Set | Download | Qualität im Vergleich zum Originalmodell | Antwortet mit |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M** (1,75 Bit) | 239 GB | KL 0,217, Perplexität 3,80 (+21 %); Code +8,9 %, Agent-Aufgaben +10,2 %; schwächer bei einfacher englischer Prosa. Laut Modellkarte von ISTA-DASLab behält es 98,7 % des Aufgabendurchschnitts des Basismodells (AIME25, GPQA-Diamond, LiveCodeBench v6). | noch nicht gemessen |
|
||||
| **IQ2_XXS** (2.00 Bit) | 257 GB | KL 0,170, Perplexität 3,63 (+15,5 %); Code +8,6 %, Agent-Aufgaben +5,3 % | 6 bis 9 Tokens/s auf einer RTX 3060, etwa 5 auf einer RTX 5070 |
|
||||
| **IQ2_XXS** (2.00 Bit) | 257 GB | KL 0,170, Perplexität 3,63 (+15,5 %); Code +8,6 %, Agent-Aufgaben +5,3 % | etwa 8 bis 10 Tokens/s auf einer RTX 3060 (Code 9,9, Agent-Aufgaben 7,8), etwa 5 auf einer RTX 5070 |
|
||||
| **IQ2_XS** (2.25 Bit) | 274 GB | KL 0,107, Perplexität 3,37 (+7,3 %); Code +3,2 %, Agent-Aufgaben +2,2 % | etwa 11 Tokens/s auf einer Radeon AI PRO R9700 |
|
||||
| **IQ4_NL** (Engram-Tabelle, kein drittes Modell) | 111 GB, schon in allen drei Downloads oben enthalten | Die 4-Bit-Engram-Nachschlagetabelle, die das Modell aus RAM oder SSD liest. Alle drei Sets nutzen diese eine Datei (`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`). | n/a |
|
||||
|
||||
|
||||
+2
-2
@@ -109,7 +109,7 @@ La Terminal es el comando `strata`, pensado para scripts, servidores y agentes d
|
||||
Linux y Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
@@ -197,7 +197,7 @@ Todos los tamaños, descargas y lo que cabe en cada equipo: [docs/MODELS.md](doc
|
||||
| Conjunto | Descarga | Calidad frente al modelo original | Responde a |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M** (1,75 bits) | 239 GB | KL 0,217, perplejidad 3,80 (+21 %); código +8,9 %, tareas de agente +10,2 %; más débil con prosa inglesa corriente. Según la ficha del modelo de ISTA-DASLab, conserva el 98,7 % del promedio de tareas del modelo base (AIME25, GPQA-Diamond, LiveCodeBench v6). | aún sin medir |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0,170, perplejidad 3,63 (+15,5 %); código +8,6 %, tareas de agente +5,3 % | de 6 a 9 tokens/s en una RTX 3060, unos 5 en una RTX 5070 |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0,170, perplejidad 3,63 (+15,5 %); código +8,6 %, tareas de agente +5,3 % | de 8 a 10 tokens/s en una RTX 3060 (código 9,9, tareas de agente 7,8), unos 5 en una RTX 5070 |
|
||||
| **IQ2_XS** (2.25 bits) | 274 GB | KL 0,107, perplejidad 3,37 (+7,3 %); código +3,2 %, tareas de agente +2,2 % | unos 11 tokens/s en una Radeon AI PRO R9700 |
|
||||
| **IQ4_NL** (tabla Engram, no es un tercer modelo) | 111 GB, ya incluidos en las tres descargas de arriba | La tabla de consulta Engram de 4 bits que el modelo lee de la RAM o del SSD. Los tres conjuntos usan este único archivo (`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`). | n/a |
|
||||
|
||||
|
||||
+2
-2
@@ -109,7 +109,7 @@ Le Terminal est la commande `strata`, pensée pour les scripts, les serveurs et
|
||||
Linux et Mac :
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell) :
|
||||
@@ -198,7 +198,7 @@ Toutes les tailles, les téléchargements et ce qui tient où : [docs/MODELS.md]
|
||||
| Version | Téléchargement | Qualité par rapport au modèle d’origine | Écrit à |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M** (1,75 bit) | 239 Go | KL 0,217, perplexité 3,80 (+21 %) ; code +8,9 %, tâches d’agent +10,2 % ; plus faible sur la prose anglaise courante. D’après la fiche du modèle d’ISTA-DASLab, elle garde 98,7 % de la moyenne des tâches du modèle de base (AIME25, GPQA-Diamond, LiveCodeBench v6). | pas encore mesurée |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0,170, perplexité 3,63 (+15,5 %) ; code +8,6 %, tâches d’agent +5,3 % | 6 à 9 tokens/s sur une RTX 3060, environ 5 sur une RTX 5070 |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0,170, perplexité 3,63 (+15,5 %) ; code +8,6 %, tâches d’agent +5,3 % | environ 8 à 10 tokens/s sur une RTX 3060 (code 9,9, tâches d’agent 7,8), environ 5 sur une RTX 5070 |
|
||||
| **IQ2_XS** (2.25 bits) | 274 GB | KL 0,107, perplexité 3,37 (+7,3 %) ; code +3,2 %, tâches d’agent +2,2 % | environ 11 tokens/s sur une Radeon AI PRO R9700 |
|
||||
| **IQ4_NL** (table Engram, pas un troisième modèle) | 111 Go, déjà compris dans les trois téléchargements ci-dessus | La table de consultation Engram en 4 bits que le modèle lit dans la RAM ou sur le SSD. Les trois versions utilisent ce seul fichier (`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`). | n/a |
|
||||
|
||||
|
||||
+2
-2
@@ -105,7 +105,7 @@ Strata は **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next
|
||||
Linux と Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows(PowerShell):
|
||||
@@ -190,7 +190,7 @@ Gyro は NVIDIA と AMD のカード(AMD は実験的)、および Linux 上
|
||||
| セット | ダウンロード | 元のモデルとの品質差 | 出力速度 |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M**(1.75 ビット) | 239 GB | KL 0.217、パープレキシティ 3.80(+21%)、コード +8.9%、エージェント作業 +10.2%、通常の英語の散文では弱めです。ISTA-DASLab のモデルカードによると、基本モデルのタスク平均の 98.7% を保ちます(AIME25、GPQA-Diamond、LiveCodeBench v6)。 | 未測定 |
|
||||
| **IQ2_XXS** (2.00 ビット) | 257 GB | KL 0.170、パープレキシティ 3.63(+15.5%)、コード +8.6%、エージェント作業 +5.3% | RTX 3060 で毎秒 6 から 9 トークン、RTX 5070 で約 5 |
|
||||
| **IQ2_XXS** (2.00 ビット) | 257 GB | KL 0.170、パープレキシティ 3.63(+15.5%)、コード +8.6%、エージェント作業 +5.3% | RTX 3060 で毎秒約 8 から 10 トークン(コード 9.9、エージェント作業 7.8)、RTX 5070 で約 5 |
|
||||
| **IQ2_XS** (2.25 ビット) | 274 GB | KL 0.107、パープレキシティ 3.37(+7.3%)、コード +3.2%、エージェント作業 +2.2% | Radeon AI PRO R9700 1 枚で毎秒約 11 トークン |
|
||||
| **IQ4_NL**(Engram テーブル、3 つ目のモデルではありません) | 111 GB、上の 3 つのダウンロードに含まれています | モデルが RAM または SSD から読む 4 ビットの Engram 参照テーブルです。3 つのセットはこの 1 つのファイル(`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`)を共有します。 | 該当なし |
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ 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
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
@@ -195,7 +195,7 @@ Every size, download and what fits where: [docs/MODELS.md](docs/MODELS.md).
|
||||
| Set | Download | Quality against the original model | Writes at |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M** (1.75 bits) | 239 GB | KL 0.217, perplexity 3.80 (+21%); code +8.9%, agent work +10.2%; weaker on plain English prose. ISTA-DASLab's model card: keeps 98.7% of the base model's task average (AIME25, GPQA-Diamond, LiveCodeBench v6). | not measured yet |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0.170, perplexity 3.63 (+15.5%); code +8.6%, agent work +5.3% | 6 to 9 tokens/s on an RTX 3060, about 5 on an RTX 5070 |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0.170, perplexity 3.63 (+15.5%); code +8.6%, agent work +5.3% | about 8 to 10 tokens/s on an RTX 3060 (code 9.9, agent work 7.8), about 5 on an RTX 5070 |
|
||||
| **IQ2_XS** (2.25 bits) | 274 GB | KL 0.107, perplexity 3.37 (+7.3%); code +3.2%, agent work +2.2% | about 11 tokens/s on one Radeon AI PRO R9700 |
|
||||
| **IQ4_NL** (Engram table, not a third model) | 111 GB, already in all three downloads above | The 4-bit Engram lookup table the model reads from RAM or SSD. All three sets use this one file (`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`). | n/a |
|
||||
|
||||
|
||||
+2
-2
@@ -105,7 +105,7 @@ 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
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
@@ -190,7 +190,7 @@ Todos os tamanhos, downloads e o que cabe em cada configuração: [docs/MODELS.m
|
||||
| Conjunto | Download | Qualidade em relação ao modelo original | Responde a |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M** (1,75 bits) | 239 GB | KL 0,217, perplexidade 3,80 (+21%); código +8,9%, tarefas de agente +10,2%; mais fraco em prosa comum em inglês. Segundo o cartão do modelo da ISTA-DASLab, mantém 98,7% da média de tarefas do modelo base (AIME25, GPQA-Diamond, LiveCodeBench v6). | ainda não medida |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0,170, perplexidade 3,63 (+15,5%); código +8,6%, tarefas de agente +5,3% | 6 a 9 tokens/s em uma RTX 3060, cerca de 5 em uma RTX 5070 |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 GB | KL 0,170, perplexidade 3,63 (+15,5%); código +8,6%, tarefas de agente +5,3% | cerca de 8 a 10 tokens/s em uma RTX 3060 (código 9,9, tarefas de agente 7,8), cerca de 5 em uma RTX 5070 |
|
||||
| **IQ2_XS** (2.25 bits) | 274 GB | KL 0,107, perplexidade 3,37 (+7,3%); código +3,2%, tarefas de agente +2,2% | cerca de 11 tokens/s em uma Radeon AI PRO R9700 |
|
||||
| **IQ4_NL** (tabela Engram, não é um terceiro modelo) | 111 GB, já incluídos nos três downloads acima | A tabela de consulta Engram de 4 bits que o modelo lê da RAM ou do SSD. Os três conjuntos usam este único arquivo (`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`). | n/a |
|
||||
|
||||
|
||||
+2
-2
@@ -105,7 +105,7 @@ Strata 运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-N
|
||||
Linux 和 Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows(PowerShell):
|
||||
@@ -189,7 +189,7 @@ Gyro 可在 NVIDIA 和 AMD 显卡(AMD 为实验性支持)以及 Linux 上的
|
||||
| 版本 | 下载大小 | 与原模型相比的质量 | 生成速度 |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M**(1.75 位) | 239 GB | KL 0.217,困惑度 3.80(+21%);代码 +8.9%,智能体任务 +10.2%;普通英文散文表现较弱。据 ISTA-DASLab 的模型卡,它保留了基础模型任务平均分的 98.7%(AIME25、GPQA-Diamond、LiveCodeBench v6)。 | 尚未测量 |
|
||||
| **IQ2_XXS** (2.00 位) | 257 GB | KL 0.170,困惑度 3.63(+15.5%);代码 +8.6%,智能体任务 +5.3% | RTX 3060 上每秒 6 到 9 个 token,RTX 5070 上约 5 个 |
|
||||
| **IQ2_XXS** (2.00 位) | 257 GB | KL 0.170,困惑度 3.63(+15.5%);代码 +8.6%,智能体任务 +5.3% | RTX 3060 上每秒约 8 到 10 个 token(代码 9.9,智能体任务 7.8),RTX 5070 上约 5 个 |
|
||||
| **IQ2_XS** (2.25 位) | 274 GB | KL 0.107,困惑度 3.37(+7.3%);代码 +3.2%,智能体任务 +2.2% | 一块 Radeon AI PRO R9700 上每秒约 11 个 token |
|
||||
| **IQ4_NL**(Engram 表,不是第三个模型) | 111 GB,已包含在上面三个下载中 | 模型从内存或固态硬盘读取的 4 位 Engram 查找表。三个版本共用这一个文件(`DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf`)。 | 不适用 |
|
||||
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ the model. The section above is about the engine's HTTP server. This is what the
|
||||
The Terminal installs with one line:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Docker on a Mac runs Linux in a virtual machine that has no access to the Mac's GPU (no Metal), so a container could only
|
||||
use the CPU, and Strata has no CPU-only mode worth running. Use the Mac app or the Terminal instead:
|
||||
|
||||
curl -fsSL <release-folder>/install.sh | sh # then: strata launch
|
||||
curl -fsSL <release-folder>/install.sh -o install.sh && sh install.sh # then: strata launch
|
||||
|
||||
See docs/MAC.md and docs/TERMINAL_LAUNCH.md. The same goes for Windows with Docker Desktop and an AMD or Intel card:
|
||||
the WSL2 virtual machine passes NVIDIA cards through, not Radeon or Arc cards. Use the Windows app or Terminal.
|
||||
+1
-1
@@ -65,7 +65,7 @@ its SHA-256, unpacks it into the user's home folder, checks the release's signed
|
||||
PATH.
|
||||
|
||||
```
|
||||
Linux, Mac: curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
Linux, Mac: curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
Windows (PowerShell): irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
|
||||
+4
-5
@@ -42,7 +42,7 @@ perplexity +7.3% (code +3.2%, agent work +2.2%). IQ1_M: perplexity +21% (KL 0.21
|
||||
121 + 111 + 8 = 239 GB for 1.75; 138 + 111 + 8 = 257 GB for 2.00; 155 + 111 + 8 = 274 GB for 2.25.
|
||||
|
||||
Measured: an RTX 5070 (12 GB) with 64 GB of RAM and a 3.6 GB/s NVMe drive answers at about 4.7 tokens/s and reads prompts at 121
|
||||
tokens/s (4K) to 231 tokens/s (8K, layer-major path); an RTX 3060 (12 GB) with 46 GB of RAM about 7.2 tokens/s; four R9700 about 31
|
||||
tokens/s (4K) to 231 tokens/s (8K, layer-major path); an RTX 3060 (12 GB) with 46 GB of RAM about 8 to 10 tokens/s (code 9.9, agent work 7.8; 149 tokens/s at 8K; DSpark stays off there, `--dspark on` adds 2-13%); four R9700 about 31
|
||||
tokens/s. Under about 40 GB of RAM it is untested and slow; setup says so and still installs.
|
||||
|
||||
Setup downloads with resume and SHA-256, packs the model with the plain recipe (`pack_gguf.py --model SET --shards ... --engram
|
||||
@@ -81,11 +81,10 @@ The server speaks to the engine as it does for the other models, so `/v1/chat/co
|
||||
server turns them into OpenAI `tool_calls` and Anthropic `tool_use`, including `tool_choice`, streamed arguments and calls written inside the thinking.
|
||||
A value marked `string="true"` stays a string.
|
||||
- Defaults when a request names none: temperature 1.0, top_p 1.0.
|
||||
- Agents and sub-agents that take turns can skip reading their prompts again (off in this version, a switch for testing). With
|
||||
`STRATA_DS4_SLOT_KEEP=1` the engine sets a conversation aside when another request takes the model (about 17 MB per 6.5K tokens) and takes it
|
||||
- Agents and sub-agents that take turns do not read their prompts again. The engine sets a conversation aside when another request takes the model (about 17 MB per 6.5K tokens) and takes it
|
||||
back when the next prompt continues it, also after a restart. Slots live in `ds4-slots` next to the run log, are capped at 2 GB
|
||||
(`STRATA_DS4_SLOT_GB`) and are removed after a week. `STRATA_DS4_AUTO_PIN=1` also keeps the system prompt and tools shared by an agent
|
||||
and its sub-agents as a checkpoint.
|
||||
(`STRATA_DS4_SLOT_GB`) and are removed after a week. The system prompt and tools shared by an agent
|
||||
and its sub-agents are kept as a checkpoint too. `STRATA_DS4_SLOT_KEEP=0` and `STRATA_DS4_AUTO_PIN=0` turn this off.
|
||||
|
||||
Not in this version: several requests at once (`--parallel` is ignored with a warning) and pictures (`--vision` is ignored with a warning).
|
||||
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ way for scripts, servers and AI agents. Linux (x86-64), Mac (Apple Silicon) and
|
||||
Linux and Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
|
||||
@@ -7,7 +7,7 @@ in the Terminal's look, with presets, an Advanced table, an access choice and a
|
||||
## Install (one line)
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh # Linux, macOS
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh # Linux, macOS
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex # Windows PowerShell
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Strata 0.2.4.1: DeepSeek setup works from the installed Terminal
|
||||
|
||||
**Fixed:** setting up DeepSeek V4.1-Flash from the installed Terminal stopped at "Preparing your model" with "can't open file .../tools/dsv41/pack_gguf.py". The Terminal and Control packages of 0.2.4 did not contain the DeepSeek preparation scripts (`pack_gguf.py`, `smoke.py` and the short tune, `tune.py`); they ran only from a source checkout. They are in the packages now, and so are three scripts that other commands run by path (`strata_inspect.py`, `strata_bench.py`, `nvfp4_local_pack.py`).
|
||||
|
||||
Only the Terminal's Control changed (version 0.1.49.1). The app stays 0.2.3 and every engine package stays as it was, so there is nothing new to download for them.
|
||||
|
||||
## Corrected speed figure for the RTX 3060
|
||||
|
||||
The 0.2.4 notes said DSpark raised an RTX 3060 PC from 6.1-6.7 to 8.2-8.7 tokens/s. That was measured with DSpark forced on; on 12 GB cards DSpark stays off by default. Measured with the release defaults on an RTX 3060 PC with 46 GB of RAM: 8 to 10 tokens/s for answers (code 9.9, agent work 7.8, chat 8.9) and 149 tokens/s for an 8K prompt. Turning DSpark on (`--dspark on`) adds 2 to 13% there.
|
||||
|
||||
## Updating
|
||||
|
||||
- **From 0.2.4:** run `strata update` in the Terminal. It fetches Control 0.1.49.1; the next `strata` command already runs it (`strata --version` says 0.2.4.1). Then run your DeepSeek setup again.
|
||||
- **New installs:** the install command (`install.ps1` on Windows, `install.sh` on Linux) installs this version.
|
||||
- **Mac:** the Mac Terminal gets Control 0.1.49.1 too. DeepSeek is not on the Mac yet.
|
||||
|
||||
---
|
||||
|
||||
Strata is free and open source. If it runs well on your PC, a coffee keeps the work on it going:
|
||||
|
||||
<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>
|
||||
@@ -0,0 +1,29 @@
|
||||
# Strata 0.2.4.2: DeepSeek launch and install fixes
|
||||
|
||||
Fixes for `strata launch` with DeepSeek and for the one-line installer. The Terminal's Control (0.1.49.2), `install.sh` and the three DeepSeek engine packages (0.1.49.1) changed. The app stays 0.2.3 and every Qwen engine package stays as it was, so there is nothing new to download for them.
|
||||
|
||||
## What is fixed
|
||||
|
||||
- **Faster DeepSeek on AMD Ryzen 3000 / Threadripper 3000 / EPYC 7002 CPUs.** The 2.00 and IQ1_M sets answered up to 3x slower there; the answers are unchanged. The DeepSeek engine is now 0.1.49.1 (`strata-dsv41-0.1.49.1-windows-x86_64-cuda-cuda13`, `-linux-x86_64-cuda-cuda13` and `-linux-x86_64-hip-rx9070`).
|
||||
- **The right DeepSeek engine runs when you have more than one Strata install.** Launch now picks the newest DeepSeek engine package of the install that is running, and of the install your model's settings came from, instead of an old path saved in the model's settings.
|
||||
- **A plain message when GitHub cannot be reached.** DeepSeek setup checks for 10 seconds before it downloads anything and says in one line that GitHub is not reachable, instead of retrying for minutes.
|
||||
- **Faster returns to a conversation.** DeepSeek keeps recent conversations (and a shared system prompt) on disk, so a returning agent or a sub-agent with the same system prompt answers sooner; up to 2 GB, cleaned after a week; `STRATA_DS4_SLOT_KEEP=0` turns it off.
|
||||
- **The DeepSeek engine updates itself.** A newer DeepSeek engine package in the signed feed is installed on the next `strata launch` (see Updating).
|
||||
- **DeepSeek no longer downloads the Qwen engine.** A DeepSeek launch does not fetch or test-start the Strata (Qwen) engine, the llama.cpp source or CUDA libraries it never uses. `strata launch --plan` no longer promises that download.
|
||||
- **The disk check counts only what is still to download.** If most of the model files are already in your models folder (also under the first release's file names), the model list no longer says "tight: needs more disk space". The room for the prepared model is still counted.
|
||||
- **Smaller wording fixes.** A setup message that ended with a full stop no longer gets a second one ("engine.."), and the second sentence of "The model could not be installed. The model does not answer sensibly after the preparation: ..." starts with a capital.
|
||||
- **The install one-liners keep your terminal open on an error.** `install.sh` is now a function that returns an error instead of closing the shell it runs in, and the commands in the README and docs download it first: `curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh`.
|
||||
|
||||
## Updating
|
||||
|
||||
- **DeepSeek engine:** it updates itself on the next launch. When the signed feed has a newer DeepSeek engine package for your PC (0.1.49.1 over 0.1.49), `strata launch` downloads it, checks it, starts it once, switches to it and removes the old one. If that fails you see one line and the engine you have keeps running; with no internet the launch goes on without waiting. An engine you gave with `--dsv41-package` or `STRATA_DSV41_EXE` is never replaced.
|
||||
|
||||
- **From 0.2.4 or 0.2.4.1:** run `strata update` in the Terminal. It fetches Control 0.1.49.2 and the next `strata` command runs it (`strata --version` says 0.2.4.2).
|
||||
- **New installs:** use the command in the README (download `install.sh`, then run it).
|
||||
- **Mac:** the Mac Terminal gets Control 0.1.49.2 too. DeepSeek is not on the Mac yet.
|
||||
|
||||
---
|
||||
|
||||
Strata is free and open source. If it runs well on your PC, a coffee keeps the work on it going:
|
||||
|
||||
<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>
|
||||
@@ -25,7 +25,7 @@ Output is the speed of the answer. Prompt is how fast Strata reads what you send
|
||||
| PC | output | prompt |
|
||||
|---|---|---|
|
||||
| RTX 5070 12 GB, 64 GB RAM | about 5 tokens/s (code) | 231 tokens/s at 8K |
|
||||
| RTX 3060 12 GB, 46 GB RAM | about 6 to 9 tokens/s (6.1-6.7 without DSpark, 8.2-8.7 with) | about 103 tokens/s at 8K |
|
||||
| RTX 3060 12 GB, 46 GB RAM | about 8 to 10 tokens/s (code 9.9, agent work 7.8) | 149 tokens/s at 8K |
|
||||
| 1x Radeon AI PRO R9700 (AMD), IQ2_XS | about 11 tokens/s | not measured |
|
||||
| 4x Radeon AI PRO R9700 (AMD) | about 31 tokens/s (55 to 69 with DSpark) | about 505 tokens/s at 8K, about 818 at 32K |
|
||||
|
||||
@@ -39,7 +39,7 @@ These are the 2.00-bit set. The 2.25-bit set reads 13% more data per token, so e
|
||||
- **Faster prompts on Radeon AI PRO R9700.** New matrix-core kernels (RDNA4) read prompts 36% faster at 8K and 49% faster at 32K on 4 cards (370 to 505 and 550 to 818 tokens/s). Answer speed does not change. They are on by default on these cards.
|
||||
- **Faster long prompts.** A prompt over about 3,700 tokens skips work that the last 20 layers cannot use (a 16K prompt is about 34% faster). In 99.5% of the 799 positions we compared, the model's top choice for the next word was the same with and without the skip.
|
||||
- **Thinking budget.** When neither your request nor your config sets one, DeepSeek thinks for at most 32,768 tokens. A request can set its own budget; 0 turns the limit off.
|
||||
- **DSpark** (a small draft model from Unsloth) is on where it measured faster and off on CPU-bound PCs. On the RTX 3060 PC (20 threads) it raised decode from 6.1-6.7 to 8.2-8.7 tokens/s. On the 4x R9700 box it raised 31 tokens/s to 55 to 69. On the RTX 5070 PC with a 6-core Ryzen 5 7600 it gave no gain at any setting (code 4.64 tokens/s with it off against 4.33 at best with it on; agent 3.41 against 2.86 to 3.21), because checking the drafts costs the CPU about what the saved tokens are worth. There `--dspark auto` does not load the drafter.
|
||||
- **DSpark** (a small draft model from Unsloth) is on where it measured faster and off on CPU-bound PCs. On 12 GB cards DSpark stays off by default; turning it on (`--dspark on`) adds 2-13% on the RTX 3060 PC. On the 4x R9700 box it raised 31 tokens/s to 55 to 69. On the RTX 5070 PC with a 6-core Ryzen 5 7600 it gave no gain at any setting (code 4.64 tokens/s with it off against 4.33 at best with it on; agent 3.41 against 2.86 to 3.21), because checking the drafts costs the CPU about what the saved tokens are worth. There `--dspark auto` does not load the drafter.
|
||||
- **Faster CPU rows.** The engine has new exact rows for the experts the CPU computes: AVX-512 on Zen 4 and Zen 5, AVX2 on other CPUs, chosen automatically. On the Ryzen 5 7600 PC with an RTX 5070 (2.00 set, DSpark on, three alternating pairs, identical text in all six runs), decode went from 4.08 to 5.11 tokens/s (+25%) and prompt reading rose 26%. This was measured on one CPU, a Zen 4. Zen 5 gained more in the kernel tests.
|
||||
- **Cards and engines:** tested on an RTX 3060 (Linux, full model), an RTX 5070 (Windows) and the Radeon AI PRO R9700 (gfx1201, Linux). The RTX 30 kernels (sm_86) were also checked on an RTX A4000. Windows on RTX 30 is built but was not run here. RX 9070 (gfx1200) is built but untested. DeepSeek runs on its own engine package (see below).
|
||||
|
||||
|
||||
+96
-91
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
# Strata Terminal: one-line installer for Linux and macOS.
|
||||
#
|
||||
# curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
# curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
#
|
||||
# What it does, in order (and nothing else):
|
||||
# 1. finds out which computer this is (system, processor, graphics card) so it can fetch the right archive;
|
||||
@@ -31,96 +31,101 @@ die() { printf 'strata installer: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
[ -n "$FEED" ] || die "this installer does not know where the Strata release folder is. Set STRATA_FEED to its address and run it again, for example: STRATA_FEED=https://example.org/strata sh install.sh"
|
||||
|
||||
# ---- tools
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
fetch() { curl -fsSL --retry 3 -o "$2" "$1"; }
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
fetch() { wget -q -O "$2" "$1"; }
|
||||
else
|
||||
die "neither curl nor wget is installed, so nothing can be downloaded. Install one (for example: sudo apt install curl) and run this again."
|
||||
fi
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha() { sha256sum "$1" | cut -d' ' -f1; }
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
sha() { shasum -a 256 "$1" | cut -d' ' -f1; }
|
||||
else
|
||||
die "no sha256sum or shasum was found, so the download cannot be checked. Install coreutils and run this again."
|
||||
fi
|
||||
command -v tar >/dev/null 2>&1 || die "tar is not installed; install it and run this again."
|
||||
|
||||
# ---- 1. this computer
|
||||
os=$(uname -s); arch=$(uname -m)
|
||||
case "$os-$arch" in
|
||||
Linux-x86_64|Linux-amd64) platform=linux-x86_64 ;;
|
||||
Darwin-arm64|Darwin-aarch64) platform=macos-arm64 ;;
|
||||
Linux-aarch64|Linux-arm64) die "this computer ($os $arch) has an ARM processor. Strata needs an NVIDIA or AMD graphics card and has no engine for ARM Linux, so it could not run a model here. Use a Linux PC with an Intel or AMD processor (x86_64) and one of those cards." ;;
|
||||
Darwin-x86_64) die "this Mac has an Intel processor. Strata for Mac needs Apple silicon (M1 or newer), so it could not run a model here." ;;
|
||||
*) die "this computer ($os $arch) has no Strata Terminal. Supported: Linux x86_64 with an NVIDIA or AMD card, macOS on Apple silicon (Windows: use install.ps1)." ;;
|
||||
esac
|
||||
gpu=""
|
||||
if command -v nvidia-smi >/dev/null 2>&1; then
|
||||
gpu=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -1 || true)
|
||||
[ -n "$gpu" ] && gpu="NVIDIA $gpu"
|
||||
fi
|
||||
if [ -z "$gpu" ] && [ "$os" = Linux ] && command -v lspci >/dev/null 2>&1; then
|
||||
gpu=$(lspci 2>/dev/null | grep -iE 'vga|3d|display' | head -1 | sed 's/^[^:]*: *//' || true)
|
||||
fi
|
||||
if [ -z "$gpu" ] && [ "$os" = Darwin ]; then
|
||||
gpu=$(system_profiler SPDisplaysDataType 2>/dev/null | sed -n 's/^ *Chipset Model: //p' | head -1 || true)
|
||||
fi
|
||||
say "Strata Terminal installer"
|
||||
say " This computer: $os $arch ($platform)${gpu:+, graphics: $gpu}"
|
||||
say " Release folder: $FEED"
|
||||
|
||||
# ---- 2. download and check
|
||||
work=$(mktemp -d "${TMPDIR:-/tmp}/strata-install.XXXXXX") || die "cannot make a temporary folder"
|
||||
trap 'rm -rf "$work"' EXIT INT TERM
|
||||
fetch "$FEED/install-manifest.txt" "$work/manifest.txt" || die "could not read $FEED/install-manifest.txt. Check the address and your internet connection."
|
||||
line=$(awk -v p="$platform" '$1 == p { print; exit }' "$work/manifest.txt")
|
||||
[ -n "$line" ] || die "this release has no Terminal for $platform."
|
||||
set -- $line
|
||||
version=$2; rel=$3; want=$4
|
||||
say " Terminal $version ($(( ${5:-0} / 1048576 )) MB)"
|
||||
say "Downloading…"
|
||||
fetch "$FEED/$rel" "$work/terminal.tar.gz" || die "the download failed. Check your internet connection and run this again; nothing was installed."
|
||||
got=$(sha "$work/terminal.tar.gz")
|
||||
[ "$got" = "$want" ] || die "the download does not match the release's checksum (got $got, wanted $want). It was not installed; try again, and if it repeats the release folder is damaged."
|
||||
say " Checksum ok."
|
||||
|
||||
# ---- 3. unpack
|
||||
dest="$DATA_DIR/terminal/$version"
|
||||
mkdir -p "$DATA_DIR/terminal" "$BIN_DIR"
|
||||
rm -rf "$dest.incoming"; mkdir -p "$dest.incoming"
|
||||
tar -xzf "$work/terminal.tar.gz" -C "$dest.incoming" || die "the archive could not be unpacked (is the disk full?). Nothing was installed."
|
||||
if [ ! -f "$dest.incoming/bin/strata" ]; then # an archive with one top folder (a terminal archive instead of the feed's control package)
|
||||
inner=$(ls -d "$dest.incoming"/*/ 2>/dev/null | head -1)
|
||||
if [ -n "$inner" ] && [ -f "${inner}bin/strata" ]; then
|
||||
mv "${inner%/}" "$dest.inner" && rm -rf "$dest.incoming" && mv "$dest.inner" "$dest.incoming"
|
||||
# Everything below is one function, called on the last line: a download that stops half-way is a syntax error (nothing runs), not a half installer.
|
||||
main() {
|
||||
# ---- tools
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
fetch() { curl -fsSL --retry 3 -o "$2" "$1"; }
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
fetch() { wget -q -O "$2" "$1"; }
|
||||
else
|
||||
die "neither curl nor wget is installed, so nothing can be downloaded. Install one (for example: sudo apt install curl) and run this again."
|
||||
fi
|
||||
fi
|
||||
[ -f "$dest.incoming/bin/strata" ] || { rm -rf "$dest.incoming"; die "the archive does not look like a Strata Terminal (no bin/strata). Nothing was installed."; }
|
||||
chmod +x "$dest.incoming/bin/strata" 2>/dev/null || true
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha() { sha256sum "$1" | cut -d' ' -f1; }
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
sha() { shasum -a 256 "$1" | cut -d' ' -f1; }
|
||||
else
|
||||
die "no sha256sum or shasum was found, so the download cannot be checked. Install coreutils and run this again."
|
||||
fi
|
||||
command -v tar >/dev/null 2>&1 || die "tar is not installed; install it and run this again."
|
||||
|
||||
# ---- 4. the signed feed, checked with the Terminal just unpacked
|
||||
if [ "${STRATA_INSTALL_TEST_TRUST:-}" = 1 ]; then
|
||||
fetch "$FEED/test-trust.json" "$work/test-trust.json" || { rm -rf "$dest.incoming"; die "the test feed has no test-trust.json"; }
|
||||
STRATA_UPDATE_TRUSTED_KEYS="$work/test-trust.json"; export STRATA_UPDATE_TRUSTED_KEYS
|
||||
fi
|
||||
say "Checking the release's signature…"
|
||||
if ! "$dest.incoming/bin/strata" update --check --no-engine --feed "$FEED" >"$work/check.txt" 2>&1; then
|
||||
sed -e 's/^/ /' "$work/check.txt" >&2
|
||||
rm -rf "$dest.incoming"
|
||||
die "the release folder's signed feed could not be verified, so nothing was installed. The reason is printed above; if it names a newer feed already accepted here, use the newest release folder, or follow the step it gives. If you trust this source and see no such reason, ask its owner to publish a signed feed."
|
||||
fi
|
||||
say " Signature ok."
|
||||
# ---- 1. this computer
|
||||
os=$(uname -s); arch=$(uname -m)
|
||||
case "$os-$arch" in
|
||||
Linux-x86_64|Linux-amd64) platform=linux-x86_64 ;;
|
||||
Darwin-arm64|Darwin-aarch64) platform=macos-arm64 ;;
|
||||
Linux-aarch64|Linux-arm64) die "this computer ($os $arch) has an ARM processor. Strata needs an NVIDIA or AMD graphics card and has no engine for ARM Linux, so it could not run a model here. Use a Linux PC with an Intel or AMD processor (x86_64) and one of those cards." ;;
|
||||
Darwin-x86_64) die "this Mac has an Intel processor. Strata for Mac needs Apple silicon (M1 or newer), so it could not run a model here." ;;
|
||||
*) die "this computer ($os $arch) has no Strata Terminal. Supported: Linux x86_64 with an NVIDIA or AMD card, macOS on Apple silicon (Windows: use install.ps1)." ;;
|
||||
esac
|
||||
gpu=""
|
||||
if command -v nvidia-smi >/dev/null 2>&1; then
|
||||
gpu=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -1 || true)
|
||||
[ -n "$gpu" ] && gpu="NVIDIA $gpu"
|
||||
fi
|
||||
if [ -z "$gpu" ] && [ "$os" = Linux ] && command -v lspci >/dev/null 2>&1; then
|
||||
gpu=$(lspci 2>/dev/null | grep -iE 'vga|3d|display' | head -1 | sed 's/^[^:]*: *//' || true)
|
||||
fi
|
||||
if [ -z "$gpu" ] && [ "$os" = Darwin ]; then
|
||||
gpu=$(system_profiler SPDisplaysDataType 2>/dev/null | sed -n 's/^ *Chipset Model: //p' | head -1 || true)
|
||||
fi
|
||||
say "Strata Terminal installer"
|
||||
say " This computer: $os $arch ($platform)${gpu:+, graphics: $gpu}"
|
||||
say " Release folder: $FEED"
|
||||
|
||||
# ---- 5. in place
|
||||
rm -rf "$dest"
|
||||
mv "$dest.incoming" "$dest"
|
||||
ln -sfn "$dest" "$DATA_DIR/terminal/current"
|
||||
ln -sf "$DATA_DIR/terminal/current/bin/strata" "$BIN_DIR/strata"
|
||||
case ":$PATH:" in
|
||||
*":$BIN_DIR:"*) say "Installed. Run: strata launch" ;;
|
||||
*) say "Installed. Run: $BIN_DIR/strata launch"
|
||||
say " (To type just 'strata', add this line to your shell profile: export PATH=\"$BIN_DIR:\$PATH\")" ;;
|
||||
esac
|
||||
# ---- 2. download and check
|
||||
work=$(mktemp -d "${TMPDIR:-/tmp}/strata-install.XXXXXX") || die "cannot make a temporary folder"
|
||||
trap 'rm -rf "$work"' EXIT INT TERM
|
||||
fetch "$FEED/install-manifest.txt" "$work/manifest.txt" || die "could not read $FEED/install-manifest.txt. Check the address and your internet connection."
|
||||
line=$(awk -v p="$platform" '$1 == p { print; exit }' "$work/manifest.txt")
|
||||
[ -n "$line" ] || die "this release has no Terminal for $platform."
|
||||
set -- $line
|
||||
version=$2; rel=$3; want=$4
|
||||
say " Terminal $version ($(( ${5:-0} / 1048576 )) MB)"
|
||||
say "Downloading…"
|
||||
fetch "$FEED/$rel" "$work/terminal.tar.gz" || die "the download failed. Check your internet connection and run this again; nothing was installed."
|
||||
got=$(sha "$work/terminal.tar.gz")
|
||||
[ "$got" = "$want" ] || die "the download does not match the release's checksum (got $got, wanted $want). It was not installed; try again, and if it repeats the release folder is damaged."
|
||||
say " Checksum ok."
|
||||
|
||||
# ---- 3. unpack
|
||||
dest="$DATA_DIR/terminal/$version"
|
||||
mkdir -p "$DATA_DIR/terminal" "$BIN_DIR"
|
||||
rm -rf "$dest.incoming"; mkdir -p "$dest.incoming"
|
||||
tar -xzf "$work/terminal.tar.gz" -C "$dest.incoming" || die "the archive could not be unpacked (is the disk full?). Nothing was installed."
|
||||
if [ ! -f "$dest.incoming/bin/strata" ]; then # an archive with one top folder (a terminal archive instead of the feed's control package)
|
||||
inner=$(ls -d "$dest.incoming"/*/ 2>/dev/null | head -1)
|
||||
if [ -n "$inner" ] && [ -f "${inner}bin/strata" ]; then
|
||||
mv "${inner%/}" "$dest.inner" && rm -rf "$dest.incoming" && mv "$dest.inner" "$dest.incoming"
|
||||
fi
|
||||
fi
|
||||
[ -f "$dest.incoming/bin/strata" ] || { rm -rf "$dest.incoming"; die "the archive does not look like a Strata Terminal (no bin/strata). Nothing was installed."; }
|
||||
chmod +x "$dest.incoming/bin/strata" 2>/dev/null || true
|
||||
|
||||
# ---- 4. the signed feed, checked with the Terminal just unpacked
|
||||
if [ "${STRATA_INSTALL_TEST_TRUST:-}" = 1 ]; then
|
||||
fetch "$FEED/test-trust.json" "$work/test-trust.json" || { rm -rf "$dest.incoming"; die "the test feed has no test-trust.json"; }
|
||||
STRATA_UPDATE_TRUSTED_KEYS="$work/test-trust.json"; export STRATA_UPDATE_TRUSTED_KEYS
|
||||
fi
|
||||
say "Checking the release's signature…"
|
||||
if ! "$dest.incoming/bin/strata" update --check --no-engine --feed "$FEED" >"$work/check.txt" 2>&1; then
|
||||
sed -e 's/^/ /' "$work/check.txt" >&2
|
||||
rm -rf "$dest.incoming"
|
||||
die "the release folder's signed feed could not be verified, so nothing was installed. The reason is printed above; if it names a newer feed already accepted here, use the newest release folder, or follow the step it gives. If you trust this source and see no such reason, ask its owner to publish a signed feed."
|
||||
fi
|
||||
say " Signature ok."
|
||||
|
||||
# ---- 5. in place
|
||||
rm -rf "$dest"
|
||||
mv "$dest.incoming" "$dest"
|
||||
ln -sfn "$dest" "$DATA_DIR/terminal/current"
|
||||
ln -sf "$DATA_DIR/terminal/current/bin/strata" "$BIN_DIR/strata"
|
||||
case ":$PATH:" in
|
||||
*":$BIN_DIR:"*) say "Installed. Run: strata launch" ;;
|
||||
*) say "Installed. Run: $BIN_DIR/strata launch"
|
||||
say " (To type just 'strata', add this line to your shell profile: export PATH=\"$BIN_DIR:\$PATH\")" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -121,6 +121,11 @@ INSTALLER_FILES = ("setup.py", "ref/load.py") # ref/load.py: strata_pack.py (t
|
||||
#: module it starts to import is never forgotten again; tests/terminal/test_bundle_imports.py checks the staged bundle.
|
||||
INSTALLER_TOOLS = ("strata_mcp", "iq_pack", "strata_pack", "pack_index", "mtp_fetch", "mtp_pack", "mtp_rt", "draft_vocab",
|
||||
"strata_tokenizer", "gguf_reader", "calibrate")
|
||||
#: tools run BY PATH (`python tools/.../x.py`) by setup.py, strata_cli or strata_control: no import names them, so setup_tool_imports() cannot find
|
||||
#: them. DeepSeek's setup and launch run dsv41/pack_gguf.py (the model folder), dsv41/smoke.py (the engine's first answer) and dsv41/tune.py
|
||||
#: (the short tune); setup.py's other by-path tools (nvfp4_local_pack, strata_bench, strata_inspect) were missing from the bundle the same way.
|
||||
#: tests/terminal/test_bundle_tools_paths.py reads the by-path references out of the source and fails when one is missing from the bundle.
|
||||
INSTALLER_SCRIPTS = ("dsv41/pack_gguf", "dsv41/smoke", "dsv41/tune", "nvfp4_local_pack", "strata_bench", "strata_inspect")
|
||||
INSTALLER_DATA = "data" # draft vocabularies, expert profiles: 2 MB
|
||||
|
||||
#: packages the bundle must import after the install (the API server's and the CLI's).
|
||||
@@ -245,9 +250,15 @@ def local_imports(folder: Path, entry: tuple[str, ...]) -> list[str]:
|
||||
source = folder / "tools" / f"{name}.py"
|
||||
if not source.exists():
|
||||
raise SystemExit(f"installer tool missing: tools/{name}.py")
|
||||
sub = name.rsplit("/", 1)[0] + "/" if "/" in name else "" # (tools/dsv41/tune.py imports its neighbour smoke.py)
|
||||
for node in ast.walk(ast.parse(source.read_text(encoding="utf-8-sig"))):
|
||||
names = [a.name.split(".")[0] for a in node.names] if isinstance(node, ast.Import) else [node.module.split(".")[0]] if isinstance(node, ast.ImportFrom) and node.module and node.level == 0 else []
|
||||
todo += [m for m in names if (folder / "tools" / f"{m}.py").exists() and m not in seen]
|
||||
for m in names:
|
||||
for rel in ((sub + m,) if sub else ()) + (m,):
|
||||
if (folder / "tools" / f"{rel}.py").exists():
|
||||
if rel not in seen:
|
||||
todo.append(rel)
|
||||
break
|
||||
return sorted(seen)
|
||||
|
||||
|
||||
@@ -266,7 +277,7 @@ def setup_tool_imports(source: Path = ROOT) -> list[str]:
|
||||
|
||||
def add_installer(folder: Path, source: Path = ROOT) -> dict:
|
||||
"""Copy the installer's files from the source tree into a staged terminal folder (after the terminal payload is in place)."""
|
||||
tools = local_imports(source, tuple(sorted(set(INSTALLER_TOOLS) | set(setup_tool_imports(source)))))
|
||||
tools = local_imports(source, tuple(sorted(set(INSTALLER_TOOLS) | set(INSTALLER_SCRIPTS) | set(setup_tool_imports(source)))))
|
||||
copied = []
|
||||
for name in INSTALLER_FILES:
|
||||
(folder / name).parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -274,6 +285,7 @@ def add_installer(folder: Path, source: Path = ROOT) -> dict:
|
||||
copied.append(name)
|
||||
(folder / "tools").mkdir(exist_ok=True)
|
||||
for name in tools:
|
||||
(folder / "tools" / f"{name}.py").parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source / "tools" / f"{name}.py", folder / "tools" / f"{name}.py")
|
||||
copied.append(f"tools/{name}.py")
|
||||
if (folder / INSTALLER_DATA).exists():
|
||||
|
||||
+4
-4
@@ -3479,7 +3479,7 @@ def hip_auto_devices(cfg: dict, env: dict) -> list[int] | None:
|
||||
return visible
|
||||
|
||||
|
||||
DS41_SLOT_KEEP_DEFAULT = "0" # the engine keeps a displaced conversation in RAM and in the slot folder (see below)
|
||||
DS41_SLOT_KEEP_DEFAULT = "1" # the engine keeps a displaced conversation in RAM and in the slot folder (see below)
|
||||
DS41_SLOT_GB_DEFAULT = "2" # the folder's cap in GB (a 6.5K-token conversation is about 17 MB)
|
||||
|
||||
|
||||
@@ -3488,7 +3488,7 @@ def deepseek_slot_env(env: dict, cfg: dict) -> None:
|
||||
at its checkpoints (agents and sub-agents that take turns do not read their prompts again). STRATA_DS4_SLOT_KEEP
|
||||
switches it (the default above; a config's env or the shell wins); the folder is `ds4-slots` next to the run log, in
|
||||
Strata's data folder (STRATA_DS4_SLOT_DIR names another), and the engine caps it (STRATA_DS4_SLOT_GB, 2 here, 16 in the engine) and
|
||||
removes what is older than a week or not this model's. Off by default (0.2.4); STRATA_DS4_SLOT_KEEP=1 turns it on."""
|
||||
removes what is older than a week or not this model's. On by default; STRATA_DS4_SLOT_KEEP=0 turns it off."""
|
||||
env.setdefault("STRATA_DS4_SLOT_KEEP", DS41_SLOT_KEEP_DEFAULT)
|
||||
env.setdefault("STRATA_DS4_SLOT_GB", DS41_SLOT_GB_DEFAULT)
|
||||
log = cfg.get("log")
|
||||
@@ -4931,8 +4931,8 @@ class Service:
|
||||
"""DeepSeek-V4.1 only: how many leading messages are the system prompt (its tools are rendered with them). An
|
||||
agent and its sub-agents that work behind the same system prompt and tools share exactly this start, so the
|
||||
engine keeps a checkpoint of its state there (pin=N): the next agent behind the same start goes back to it
|
||||
instead of reading it again. 0: none (off unless STRATA_DS4_AUTO_PIN=1, no system message, or nothing after it)."""
|
||||
if os.environ.get("STRATA_DS4_AUTO_PIN", "0") != "1":
|
||||
instead of reading it again. 0: none (STRATA_DS4_AUTO_PIN=0, no system message, or nothing after it)."""
|
||||
if os.environ.get("STRATA_DS4_AUTO_PIN", "1") == "0":
|
||||
return 0
|
||||
n = 0
|
||||
for m in messages:
|
||||
|
||||
+7
-5
@@ -562,10 +562,12 @@ class Api(ApiBase):
|
||||
self.chat()
|
||||
self.assertNotIn("strata_prefix", self.engine.last_sampling) # no system message
|
||||
body = {"messages": [{"role": "system", "content": "s"}, {"role": "user", "content": "u"}], "max_tokens": 40}
|
||||
for off in (None, "0"): # off by default, and when switched off
|
||||
self.auto_pin(off)
|
||||
self.post("/v1/chat/completions", body)
|
||||
self.assertNotIn("strata_prefix", self.engine.last_sampling)
|
||||
self.auto_pin("0") # switched off
|
||||
self.post("/v1/chat/completions", body)
|
||||
self.assertNotIn("strata_prefix", self.engine.last_sampling)
|
||||
self.auto_pin(None) # on by default
|
||||
self.post("/v1/chat/completions", body)
|
||||
self.assertIn("strata_prefix", self.engine.last_sampling)
|
||||
|
||||
def test_a_clients_own_prefix_wins(self):
|
||||
self.start("ok")
|
||||
@@ -600,7 +602,7 @@ class SlotEnv(unittest.TestCase):
|
||||
def test_deepseek_gets_a_folder_in_the_data_folder_and_the_default_switch(self):
|
||||
env = self.env({"arch": "deepseek41", "log": str(Path("data") / "strata-deepseek.log"), "args": []})
|
||||
self.assertEqual(Path(env["STRATA_DS4_SLOT_DIR"]), Path("data") / "ds4-slots")
|
||||
self.assertEqual(env["STRATA_DS4_SLOT_KEEP"], "0")
|
||||
self.assertEqual(env["STRATA_DS4_SLOT_KEEP"], "1")
|
||||
self.assertEqual(env["STRATA_DS4_SLOT_GB"], "2")
|
||||
|
||||
def test_the_configs_own_settings_win(self):
|
||||
|
||||
@@ -7743,6 +7743,24 @@ def dsv41_engine_dir(eng: Path) -> Path:
|
||||
return (home() if CONTROL else Path(eng).parent) / DSV41_ENGINE_DIR
|
||||
|
||||
|
||||
def github_reachable(timeout: float = 10.0) -> bool:
|
||||
"""Does GitHub answer within `timeout` seconds (any HTTP answer counts; no answer, a refused or timed-out connection does not)? One quick check
|
||||
instead of minutes of retries when the connection is down (a SYN that is never answered)."""
|
||||
try:
|
||||
_urlopen(urllib.request.Request("https://github.com", method="HEAD", headers={"User-Agent": "strata-setup"}), timeout=timeout).close()
|
||||
return True
|
||||
except urllib.error.HTTPError:
|
||||
return True
|
||||
except (OSError, urllib.error.URLError):
|
||||
return False
|
||||
|
||||
|
||||
def dsv41_engine_pending(eng: Path) -> bool:
|
||||
"""A DeepSeek engine package file the person put in engine-dsv41/ is there to be installed (no download needed)."""
|
||||
from strata_control import dsv41_engine
|
||||
return bool(dsv41_engine.pending(dsv41_engine_dir(eng)))
|
||||
|
||||
|
||||
def dsv41_manual_steps(eng: Path) -> str:
|
||||
return (f"If you downloaded the DeepSeek engine package from the release page (the file strata-dsv41-...-{'windows' if WIN else 'linux'}-....{'zip' if WIN else 'tar.gz'}), "
|
||||
f"run the same command again with --dsv41-package PATH-OF-THE-FILE (strata launch accepts it too), or put the file in {dsv41_engine_dir(eng)} "
|
||||
@@ -7778,6 +7796,22 @@ def get_dsv41_engine(eng: Path, hip: bool, package: str | None = None) -> Path |
|
||||
return exe
|
||||
|
||||
|
||||
def upgrade_dsv41_engine(eng: Path, exe: Path) -> Path:
|
||||
"""The installed DeepSeek engine package is older than the signed feed's: updated next to it, switched after its test start, the old one removed
|
||||
(strata_control.dsv41_engine.upgrade). Silent when the feed cannot be reached; a failure is one plain line and `exe` keeps running."""
|
||||
from strata_control import dsv41_engine
|
||||
try:
|
||||
from strata_cli import update_cmd
|
||||
root = dsv41_engine_dir(eng)
|
||||
source = update_cmd._feed_source(argparse.Namespace(feed=None), root.parent)
|
||||
if not dsv41_engine.source_reachable(source):
|
||||
return exe
|
||||
new = dsv41_engine.upgrade(root, source, installed=exe, emit=lambda m: say(" " + m))
|
||||
except Exception: # noqa: BLE001 - never stop a setup for this
|
||||
return exe
|
||||
return new or exe
|
||||
|
||||
|
||||
def dsv41_exe(eng: Path) -> Path | None:
|
||||
"""The DeepSeek-V4.1 engine (strata-dsv41): STRATA_DSV41_EXE (a build of your own), else its own package folder beside the engine (engine-dsv41/),
|
||||
else beside the engine in `eng` (an older all-in-one package); None when there is none."""
|
||||
@@ -8000,12 +8034,20 @@ def install_dsv41(a, family, gpu, hip, amd, cuda_tk, multi, port, ram, adopted,
|
||||
pip_install(runtime_packages(),
|
||||
"numpy, jinja2, regex, pyyaml, tqdm, requests, cmake, ninja, pillow, psutil, textual (the screens)")
|
||||
|
||||
step(4, "the Strata engine")
|
||||
llama, eng, meta, lib_dirs, gpu, _vision = engine_step(a, gpu, hip, amd, cuda_tk, "none")
|
||||
# DeepSeek runs on its own engine package: the Strata (Qwen) engine, its llama.cpp source and its CUDA libraries are not needed for it, so none of
|
||||
# them is downloaded or test-started here (they were ~700 MB, and a stop on every one of them when GitHub could not be reached).
|
||||
step(4, "the DeepSeek engine")
|
||||
eng, meta, lib_dirs = ROOT / "engine", {"version": None}, []
|
||||
exe = dsv41_exe(eng)
|
||||
if exe is None and not getattr(a, "dsv41_package", None) and not dsv41_engine_pending(eng) and not github_reachable():
|
||||
fail("Strata can't reach GitHub to download the DeepSeek engine.",
|
||||
"Check the internet connection and run the same command again. " + dsv41_manual_steps(eng))
|
||||
if exe is None and not getattr(a, "build", False):
|
||||
get_dsv41_engine(eng, hip, getattr(a, "dsv41_package", None))
|
||||
exe = dsv41_exe(eng)
|
||||
if (exe is not None and not getattr(a, "dsv41_package", None) and not getattr(a, "build", False)
|
||||
and not (os.environ.get("STRATA_DSV41_EXE") or "").strip() and exe.parent.parent == dsv41_engine_dir(eng)):
|
||||
exe = upgrade_dsv41_engine(eng, exe) # an installed package older than the signed feed's: updated next to it, switched after it starts
|
||||
if exe is not None and exe.parent != Path(eng):
|
||||
lib_dirs = [*lib_dirs, str(exe.parent)] # its own package carries the libraries it needs
|
||||
if exe is None:
|
||||
|
||||
@@ -92,7 +92,10 @@ void gg_cpu_quant(int act, const float* x, int k, void* dst) {
|
||||
// min_tokens(type) and these rows from there. DS4_GG_IQ256=0 never takes them, =1 always (the old opt-in).
|
||||
static int g_iq256 = -1; // -1: from the environment and the CPU, 0 off, 1 always, 2 from min_tokens(type)
|
||||
void gg_cpu_set_iq256(int mode) { g_iq256 = mode; }
|
||||
static int min_tokens(int type) { return type == 21 ? 1 : type == 29 ? 3 : 2; }
|
||||
// IQ1_M from one token too: ggml's own IQ1_M dot (and IQ1_S's) uses _pdep_u64 where the build has BMI2, which AMD Zen 1/2 run in
|
||||
// microcode - 1.8 GB/s against these rows' 33 GB/s on an EPYC 7452 (a 2.00 set decodes 3x slower on one card), 19-31 GB/s on Intel.
|
||||
// The rows are bit-identical, so taking them at every token count only changes the speed.
|
||||
static int min_tokens(int type) { return type == 21 ? 1 : type == 29 ? 1 : 2; }
|
||||
static bool own_ok(int type, int impl) {
|
||||
static std::once_flag once[2][32];
|
||||
static bool ok[2][32];
|
||||
|
||||
@@ -7,7 +7,7 @@ SCHEMA_VERSION = 1
|
||||
|
||||
#: The version people are told: the Strata app's own (apps/desktop/src-tauri/Cargo.toml `version`; tests/terminal/test_tuiqa_version.py keeps the
|
||||
#: two equal, except for a Terminal-only release such as 0.2.3.1 or 0.2.4, whose app stays 0.2.3). `version()` below is the files' build number, which the engine-compatibility checks compare, so it stays numeric.
|
||||
PRODUCT_VERSION = "0.2.4"
|
||||
PRODUCT_VERSION = "0.2.4.2"
|
||||
|
||||
|
||||
def version() -> str:
|
||||
|
||||
+80
-14
@@ -13,6 +13,7 @@ tests see the same thing. Nothing here prints or reads the keyboard. The steps r
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -116,7 +117,7 @@ def setup_failure(lines: list[str], err: str, hint: str, details: str | None, ow
|
||||
"Pick a folder you own for the model files (the Folder step, or --data-dir D:\\Strata-models) and start again. "
|
||||
"Nothing was downloaded." + where, 6)
|
||||
return LaunchError(f"The model could not be installed. Setup stopped with an error ({crashed}).", "Run it again." + where, 6)
|
||||
return LaunchError("The model could not be installed." + (f" {err}." if err else ""),
|
||||
return LaunchError("The model could not be installed." + (f" {capital(err.rstrip('.'))}." if err else ""),
|
||||
(hint or ("Run it again: finished steps are skipped." if own_files else
|
||||
"Run it again: finished steps are skipped and a download continues where it stopped."))
|
||||
+ (f" Details: {details}" if details else ""), 6)
|
||||
@@ -543,6 +544,9 @@ class Runner:
|
||||
|
||||
# ---- 1. the engine
|
||||
def ensure_engine(self) -> None:
|
||||
if self.multi_file(): # DeepSeek: its own engine package (setup fetches it); the Strata engine is not used
|
||||
self.emit("engine", "DeepSeek runs on its own engine package, so the Strata engine is not downloaded for it.", 100)
|
||||
return
|
||||
self.emit("engine", "Checking the engine for this computer…")
|
||||
from strata_control import engine_component as ec
|
||||
root = Path(self.components_dir) if self.components_dir else None
|
||||
@@ -947,7 +951,7 @@ class Runner:
|
||||
env.pop("STRATA_NEED_ENGINE", None)
|
||||
if self.engine_problem:
|
||||
env["STRATA_ALLOW_NO_ENGINE"] = "1" # the person said go on: setup warns about the engine instead of stopping
|
||||
else:
|
||||
elif not self.multi_file(): # (DeepSeek does not use the Strata engine: nothing to require)
|
||||
env["STRATA_NEED_ENGINE"] = "1"
|
||||
for k in ("PYTHONHOME", "PYTHONPATH", "VIRTUAL_ENV"):
|
||||
env.pop(k, None)
|
||||
@@ -1088,6 +1092,27 @@ class Runner:
|
||||
self.emit("model", f"Used your prepared model files and wrote the run settings ({path.name}); this card is not one the built-in downloader sets up.", 100, level="warn")
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
def deepseek_set_in(mod, model_path, prefer: str | None = None):
|
||||
"""(set name, its catalog entry, the folder) when `model_path` (a folder, the folder above the set's own folder, or one shard) holds shards
|
||||
of an official DeepSeek-V4.1-Flash set - recognised by the pinned shard names (the first release's names too), never by the generic GGUF
|
||||
fingerprint table, which does not know DeepSeek (its shard 8 is a table, not a layer: "could not be read", #3914) - else None."""
|
||||
fam = getattr(mod, "MULTI_FILE_FAMILIES", {}).get("deepseek41")
|
||||
if fam is None or not model_path:
|
||||
return None
|
||||
p = Path(model_path)
|
||||
legacy = getattr(mod, "DSV41_LEGACY_NAMES", {})
|
||||
order = ([prefer] if prefer in fam["models"] else []) + [m for m in fam["models"] if m != prefer]
|
||||
for m in order:
|
||||
d = fam["models"][m]
|
||||
subs = [d.get("subdir"), d.get("legacy_subdir"), m]
|
||||
folders = [p, *[p / x for x in subs if x]] if p.is_dir() else [p.parent]
|
||||
names = [n for s in d["shards"] for n in (s, legacy.get(s)) if n]
|
||||
folder = next((f for f in folders if any((f / n).exists() for n in names)), None)
|
||||
if folder is not None:
|
||||
return m, d, folder
|
||||
return None
|
||||
|
||||
def deepseek_files(self) -> bool:
|
||||
"""--model-path names DeepSeek-V4.1-Flash's shards (a folder, the repository's folder with the set's own folder inside - UD-Q8_K_XL,
|
||||
2.00bpw, 2.25bpw - or one shard): they are that model, whatever the fingerprint table of the Flash-Next files says. True when it did
|
||||
@@ -1099,16 +1124,7 @@ class Runner:
|
||||
fam = getattr(mod, "MULTI_FILE_FAMILIES", {}).get("deepseek41")
|
||||
if fam is None or not s.model_path:
|
||||
return False
|
||||
p = Path(s.model_path)
|
||||
found = None
|
||||
order = ([s.model] if s.model in fam["models"] else []) + [m for m in fam["models"] if m != s.model]
|
||||
for m in order:
|
||||
d = fam["models"][m]
|
||||
folders = [p, p / (d.get("subdir") or m)] if p.is_dir() else [p.parent]
|
||||
folder = next((f for f in folders if any((f / n).exists() for n in d["shards"])), None)
|
||||
if folder is not None:
|
||||
found = (m, d, folder)
|
||||
break
|
||||
found = self.deepseek_set_in(mod, s.model_path, s.model)
|
||||
except Exception: # noqa: BLE001 - not DeepSeek's: the usual check goes on
|
||||
return False
|
||||
if found is None:
|
||||
@@ -1118,7 +1134,8 @@ class Runner:
|
||||
raise LaunchError(f"These are DeepSeek-V4.1-Flash's files, not {s.family} {s.model}'s.",
|
||||
"Leave --model out, or use --model " + model + ".", 2)
|
||||
others = list(d["shards"])
|
||||
missing = [n for n in others if not (folder / n).exists()]
|
||||
legacy = getattr(mod, "DSV41_LEGACY_NAMES", {})
|
||||
missing = [n for n in others if not (folder / n).exists() and not (legacy.get(n) and (folder / legacy[n]).exists())]
|
||||
if missing:
|
||||
parts = {4: "four", 9: "nine"}.get(len(others), str(len(others)))
|
||||
raise LaunchError(f"DeepSeek-V4.1-Flash {d.get('label', model)} is in {parts} parts and {len(missing)} of them are not in {folder} "
|
||||
@@ -1726,6 +1743,11 @@ class Runner:
|
||||
parts = [p for p in parts if p["kind"] != "weights"] + [{"kind": "weights", "label": "the ready-made pack", "gb": gap["gb"]}]
|
||||
gb = round(sum(p["gb"] for p in parts), 1)
|
||||
engine, here = True, []
|
||||
if self.multi_file(): # DeepSeek does not use the Strata engine: nothing about it in the plan (its own package is in the placement)
|
||||
return {"gb": gb, "parts": parts, "engine_missing": False, "engine_usable": True, "own_files": st.own_files,
|
||||
"text": ("Nothing to download: your model files are used where they are." if gb < 0.05 and st.own_files else
|
||||
"Nothing to download: the model is already on this computer." if gb < 0.05 else
|
||||
f"About {gb:.1f} GB to download.")}
|
||||
try:
|
||||
report = engine_component.status(Path(self.components_dir) if self.components_dir else None)
|
||||
engine = bool(report.get("usable"))
|
||||
@@ -1859,7 +1881,11 @@ class Runner:
|
||||
found, _known = self.dsv41_engine_found()
|
||||
if found is None:
|
||||
return
|
||||
found = self.upgrade_dsv41_engine(found)
|
||||
old = str(cfg.get("exe") or "")
|
||||
choice = getattr(self, "dsv41_choice", None)
|
||||
if choice and choice[0] == str(found):
|
||||
self.emit("engine", f"DeepSeek engine: {found} ({choice[1]}).", None)
|
||||
if os.path.normcase(os.path.normpath(old)) != os.path.normcase(os.path.normpath(str(found))):
|
||||
cfg["exe"] = str(found)
|
||||
if old:
|
||||
@@ -1867,6 +1893,27 @@ class Runner:
|
||||
libs = [d for d in (cfg.get("lib_dirs") or []) if "engine-dsv41" not in str(d)]
|
||||
cfg["lib_dirs"] = [*libs, str(Path(found).parent)] # its own folder carries the libraries it needs
|
||||
|
||||
def upgrade_dsv41_engine(self, found: Path) -> Path:
|
||||
"""The installed DeepSeek engine package is older than the signed feed's: fetch, check and start the newer one next to it and switch to it
|
||||
(strata_control.dsv41_engine.upgrade). Not for a --dsv41-package or a STRATA_DSV41_EXE (the person chose the engine), not for an engine
|
||||
outside an engine-dsv41 folder, and silent when the feed cannot be reached (a 10 s check; the launch never waits on the network)."""
|
||||
from strata_control import dsv41_engine
|
||||
if (os.environ.get("STRATA_DSV41_EXE") or "").strip() or getattr(self.s, "dsv41_package", None):
|
||||
return found
|
||||
root = Path(found).parent.parent
|
||||
if root.name != dsv41_engine.DIR:
|
||||
return found
|
||||
try:
|
||||
from strata_cli import update_cmd
|
||||
source = update_cmd._feed_source(argparse.Namespace(feed=self.feed), root.parent)
|
||||
if not dsv41_engine.source_reachable(source):
|
||||
return found
|
||||
new = dsv41_engine.upgrade(root, source, installed=found,
|
||||
emit=lambda m: self.emit("engine", m, None, level="warn" if "could not" in m else "info"))
|
||||
except Exception: # noqa: BLE001 - never stop a launch for this
|
||||
return found
|
||||
return new or found
|
||||
|
||||
def dsv41_engine_found(self) -> tuple[Path | None, bool | None]:
|
||||
"""(strata-dsv41 already installed, or None; whether the engine can be told at all). Looks where setup puts DeepSeek's own engine package
|
||||
(engine-dsv41/ beside the engine, in Strata's own folder, and in the data folder), and at STRATA_DSV41_EXE (a build of your own)."""
|
||||
@@ -1874,7 +1921,26 @@ class Runner:
|
||||
own = (os.environ.get("STRATA_DSV41_EXE") or "").strip()
|
||||
if own:
|
||||
return (Path(own) if Path(own).is_file() else None), True
|
||||
folders: list[Path] = [Path(self.catalog.root) / dsv41_engine.DIR] # this install's own package first
|
||||
# The install this Terminal RUNS from (STRATA_INSTALL_DIR, else the folder its strata_cli comes from) and the install the model's settings live in
|
||||
# (catalog.root: the first remembered install, which after an update or on a computer with several installs can be an older one) can both hold a
|
||||
# DeepSeek engine package: the NEWEST version wins, the running Terminal's own on a tie, and the answer is logged.
|
||||
roots: list[Path] = []
|
||||
for r in (os.environ.get("STRATA_INSTALL_DIR"), self.catalog.src, self.catalog.root):
|
||||
if r and not any(os.path.normcase(os.path.normpath(str(r))) == os.path.normcase(os.path.normpath(str(x))) for x in roots):
|
||||
roots.append(Path(r))
|
||||
picks = []
|
||||
for i, r in enumerate(roots):
|
||||
exe = dsv41_engine.find(r / dsv41_engine.DIR)
|
||||
if exe is not None:
|
||||
picks.append((dsv41_engine.version_key(exe.parent.name), -i, exe, r))
|
||||
if picks:
|
||||
best = max(picks, key=lambda t: (t[0], t[1]))
|
||||
why = ("this Terminal's own folder" if best[3] == roots[0] or best[3] == Path(self.catalog.src) else "the install the model's settings came from")
|
||||
if len(picks) > 1:
|
||||
why += f", the newest of {len(picks)} packages"
|
||||
self.dsv41_choice = (str(best[2]), why)
|
||||
return best[2], True
|
||||
folders: list[Path] = [Path(self.catalog.root) / dsv41_engine.DIR] # (nothing there: the older places, as before)
|
||||
try:
|
||||
from strata_control import engine_component as ec
|
||||
rep = ec.status()
|
||||
|
||||
@@ -497,6 +497,8 @@ class ModelScreen(Base):
|
||||
if problem:
|
||||
self.say(f"{problem[0]} {problem[1]}", True)
|
||||
return
|
||||
if self.check_own_deepseek(path):
|
||||
return
|
||||
try:
|
||||
plan = models.plan_import(models.absolute(path))
|
||||
except Exception as e: # noqa: BLE001
|
||||
@@ -518,6 +520,37 @@ class ModelScreen(Base):
|
||||
self.say(f"These files cannot be used as they are ({why}). " + (holds + " " if holds else "") +
|
||||
"Check that every shard is in the folder, or pick a model from the list.", True)
|
||||
|
||||
def check_own_deepseek(self, path: str) -> bool:
|
||||
"""The files are DeepSeek-V4.1-Flash's (the pinned shard names): taken as that model, as `strata launch --model-path` does, instead of
|
||||
the generic file check, which does not know DeepSeek and said "could not be read" for its n-gram shard (#3914). True when it answered."""
|
||||
from strata_cli.launch.runner import Runner
|
||||
try:
|
||||
mod = self.ctx.catalog.strata.setup_module()
|
||||
found = Runner.deepseek_set_in(mod, path, self.ctx.settings.model)
|
||||
except Exception: # noqa: BLE001 - not DeepSeek's: the usual check goes on
|
||||
return False
|
||||
if found is None:
|
||||
return False
|
||||
model, d, folder = found
|
||||
legacy = getattr(mod, "DSV41_LEGACY_NAMES", {})
|
||||
missing = [n for n in d["shards"] if not (folder / n).exists() and not (legacy.get(n) and (folder / legacy[n]).exists())]
|
||||
if missing:
|
||||
self.say(f"These are DeepSeek-V4.1-Flash {d.get('label', model)} files, but {len(missing)} of {len(d['shards'])} parts are not in {folder} "
|
||||
f"(first missing: {missing[0]}). Put every part in the folder, or pick a model from the list.", True)
|
||||
return True
|
||||
s = self.ctx.settings
|
||||
s.model_path, s.family, s.model = str(folder), "deepseek41", model
|
||||
self.ctx.picked_in_list = False
|
||||
self.close_field("own")
|
||||
gb = sum(x.stat().st_size for x in folder.iterdir() if x.is_file() and x.name.endswith(".gguf")) / 1e9
|
||||
self.say(f"These files are DeepSeek-V4.1-Flash {d.get('label', model)} ({gb:.0f} GB in this folder). Press Enter to use them.")
|
||||
try:
|
||||
self.menu().select(("deepseek41", model))
|
||||
except Exception: # noqa: BLE001 - the list may not show it; the choice is already made
|
||||
pass
|
||||
self.paint()
|
||||
return True
|
||||
|
||||
def action_back(self) -> None:
|
||||
if self.input_open():
|
||||
self.close_field("own")
|
||||
|
||||
@@ -20,10 +20,16 @@ def exe_name() -> str:
|
||||
return NAME + (".exe" if os.name == "nt" else "")
|
||||
|
||||
|
||||
def version_key(folder_name: str) -> tuple:
|
||||
"""The version at the front of a package folder's name ("0.1.49-windows-x86_64-cuda-cuda13" -> (0, 1, 49)), for choosing the newest."""
|
||||
head = str(folder_name).split("-")[0]
|
||||
return tuple(int(x) for x in head.split(".") if x.isdigit())
|
||||
|
||||
|
||||
def find(dest: Path) -> Path | None:
|
||||
"""The newest installed package's strata-dsv41 in `dest` (engine-dsv41/), or None."""
|
||||
try:
|
||||
folders = sorted((d for d in Path(dest).iterdir() if d.is_dir() and (d / exe_name()).is_file()), key=lambda d: d.name)
|
||||
folders = sorted((d for d in Path(dest).iterdir() if d.is_dir() and (d / exe_name()).is_file()), key=lambda d: (version_key(d.name), d.name))
|
||||
except OSError:
|
||||
return None
|
||||
return folders[-1] / exe_name() if folders else None
|
||||
@@ -151,3 +157,110 @@ def install(dest: Path, source: str, *, hardware: dict | None = None, emit: Call
|
||||
except (updater.UpdateError, feedmod.FeedError, signing.SignatureError) as e:
|
||||
emit(f"The DeepSeek engine could not be fetched: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------- upgrade of an installed package
|
||||
|
||||
def source_reachable(source: str, timeout: float = 10.0) -> bool:
|
||||
"""Can the feed `source` be reached within `timeout` seconds? A folder or file: it exists. A URL: any HTTP answer counts (a 404 is an answer);
|
||||
no answer, a refused or timed-out connection does not. One quick check instead of minutes of retries."""
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from strata_control import transfer
|
||||
kind = transfer.kind_of(source)
|
||||
if kind in ("path", "file"):
|
||||
try:
|
||||
return transfer._local_path(source).exists()
|
||||
except (OSError, transfer.TransferError):
|
||||
return False
|
||||
parts = urllib.parse.urlsplit(source)
|
||||
try:
|
||||
urllib.request.urlopen(urllib.request.Request(f"{parts.scheme}://{parts.netloc}/", method="HEAD", headers={"User-Agent": "strata-launch"}),
|
||||
timeout=timeout).close()
|
||||
return True
|
||||
except urllib.error.HTTPError:
|
||||
return True
|
||||
except (OSError, urllib.error.URLError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def _test_start(exe: Path, timeout: float = 120.0) -> tuple[bool, str]:
|
||||
"""The new engine starts: `strata-dsv41 --version` runs, loads its libraries and names itself (it needs no graphics card and no model)."""
|
||||
import subprocess
|
||||
try:
|
||||
done = subprocess.run([str(exe), "--version"], capture_output=True, text=True, timeout=timeout, cwd=str(Path(exe).parent))
|
||||
except (OSError, subprocess.SubprocessError) as exc:
|
||||
return False, f"it could not be started ({exc})"
|
||||
first = ((done.stdout or "").strip().splitlines() or [""])[0]
|
||||
if done.returncode != 0 or not first.startswith(NAME):
|
||||
tail = ((done.stderr or "").strip().splitlines() or [first or f"exit {done.returncode}"])[-1]
|
||||
return False, f"it did not start correctly ({tail[:160]})"
|
||||
return True, first
|
||||
|
||||
|
||||
def upgrade(dest: Path, source: str, *, installed: Path | None = None, hardware: dict | None = None,
|
||||
emit: Callable[[str], None] = print, cancel=None) -> Path | None:
|
||||
"""The installed DeepSeek engine package in `dest` (engine-dsv41/) is older than the signed feed's `dsv41` member for this PC: fetch the newer one,
|
||||
check it as `install` does (signature, hashes, every file), put it NEXT TO the old one, start it once (`--version`), and only then remove the older
|
||||
folders (one generation). Returns the new strata-dsv41, or None: nothing newer, the feed cannot be read (silent: a launch never waits on the
|
||||
network), or the newer one could not be installed (one plain line, the installed engine keeps running). Versions compare numerically
|
||||
(0.1.49 < 0.1.49.1). The caller does not call this for a --dsv41-package or a STRATA_DSV41_EXE."""
|
||||
from strata_control import feed as feedmod, signing, transfer, updater
|
||||
|
||||
dest = Path(dest)
|
||||
current = installed or find(dest)
|
||||
if current is None:
|
||||
return None
|
||||
have_name = Path(current).parent.name
|
||||
have = version_key(have_name)
|
||||
shown = have_name.split("-")[0]
|
||||
work = dest.parent / (DIR + ".work")
|
||||
hw = hardware or updater.detect_hardware()
|
||||
try:
|
||||
(work / "state").mkdir(parents=True, exist_ok=True)
|
||||
ctx = updater.Context(components=work, source=source, trusted=signing.load_trusted_keys(), hardware=hw, cancel=cancel,
|
||||
emit=lambda event: None)
|
||||
state = updater.State(work)
|
||||
try:
|
||||
feed = updater.open_feed(ctx, state)
|
||||
except feedmod.FeedError as exc:
|
||||
if "could not be read" in str(exc):
|
||||
return None # no network, no feed here: silent, the launch goes on
|
||||
emit(f"A newer DeepSeek engine may be available but could not be checked: {exc}; still using {shown}.")
|
||||
return None
|
||||
except (updater.UpdateError, OSError):
|
||||
return None # (offline mode and the like: silent)
|
||||
pkg, _reasons = feedmod.select_engine(list(feed.dsv41), hw)
|
||||
if pkg is None or version_key(pkg.version) <= have:
|
||||
return None
|
||||
target = None
|
||||
try:
|
||||
archive = updater._download(ctx, "dsv41", pkg.url, pkg.size, pkg.sha256, "the DeepSeek engine", feed)
|
||||
stage_root, _component = updater.stage_engine(ctx, feed, pkg, archive)
|
||||
try:
|
||||
folder = stage_root / "engine" / updater.folder_name(pkg)
|
||||
target = dest / folder.name
|
||||
shutil.rmtree(target, ignore_errors=True)
|
||||
shutil.move(str(folder), str(target))
|
||||
finally:
|
||||
shutil.rmtree(stage_root, ignore_errors=True)
|
||||
if os.name != "nt":
|
||||
(target / exe_name()).chmod(0o755)
|
||||
ok, why = _test_start(target / exe_name())
|
||||
if not ok:
|
||||
raise updater.UpdateError(why)
|
||||
except (updater.UpdateError, feedmod.FeedError, signing.SignatureError, transfer.TransferError, OSError) as exc:
|
||||
if target is not None and target.name != have_name:
|
||||
shutil.rmtree(target, ignore_errors=True) # (never the installed one)
|
||||
emit(f"A newer DeepSeek engine is available but could not be installed: {str(exc).rstrip('.')}; still using {shown}.")
|
||||
return None
|
||||
for old in dest.iterdir(): # switched: the older generations go
|
||||
if old.is_dir() and old.name != target.name and version_key(old.name) < version_key(target.name):
|
||||
shutil.rmtree(old, ignore_errors=True)
|
||||
emit(f"DeepSeek engine updated from {shown} to {pkg.version}.")
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
return target / exe_name()
|
||||
except Exception as exc: # noqa: BLE001 - an upgrade check must never stop a launch
|
||||
emit(f"A newer DeepSeek engine may be available but could not be checked ({type(exc).__name__}); still using {shown}.")
|
||||
return None
|
||||
@@ -124,6 +124,7 @@ int main(int argc, char** argv) {
|
||||
long mb = 3072;
|
||||
bool exact = false, expert = false;
|
||||
int dtype = -1;
|
||||
std::string dump; // --dump FILE: ggml-cpu's results (nt x rows floats) written raw - compare two builds with cmp
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string a = argv[i];
|
||||
auto val = [&] { return i + 1 < argc ? std::atol(argv[++i]) : 0L; };
|
||||
@@ -135,6 +136,7 @@ int main(int argc, char** argv) {
|
||||
else if (a == "--exact") exact = true;
|
||||
else if (a == "--expert") expert = true;
|
||||
else if (a == "--dtype") dtype = (int) val();
|
||||
else if (a == "--dump" && i + 1 < argc) dump = argv[++i];
|
||||
}
|
||||
if (expert) return run_expert(type, dtype < 0 ? type : dtype, threads, mb, nt, reps);
|
||||
ggml_cpu_init();
|
||||
@@ -204,6 +206,12 @@ int main(int argc, char** argv) {
|
||||
|
||||
const double t_gg = pass(0, p0.data());
|
||||
const double t_own = pass(1, p1.data());
|
||||
if (!dump.empty()) {
|
||||
if (std::FILE* df = std::fopen(dump.c_str(), "wb")) {
|
||||
for (int t = 0; t < nt; ++t) std::fwrite(o0[(size_t) t].data(), 4, (size_t) rows, df);
|
||||
std::fclose(df);
|
||||
}
|
||||
}
|
||||
long bad = 0, differ = 0;
|
||||
double worst = 0, worst_ref = 0, absmax = 0, absdiff = 0;
|
||||
for (int t = 0; t < nt; ++t)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Every tools/*.py that setup.py, strata_cli or strata_control runs BY PATH is in the packaged Terminal (and so in the Control bundle).
|
||||
|
||||
0.2.4 shipped without tools/dsv41/pack_gguf.py: the tests ran from the checkout, where the file exists, and the packaged Terminal failed at
|
||||
"Preparing your model" with "can't open file .../tools/dsv41/pack_gguf.py". This test reads the by-path references out of the source (an
|
||||
AST scan: `ROOT / "tools" / "x" / "y.py"` and the plain string "tools/x/y.py"), stages the bundle the way package_terminal does, and checks that
|
||||
each referenced file is in it. It then runs the DeepSeek scripts from the BUNDLE with --help in a folder that holds nothing else, so an import
|
||||
they need (the sibling smoke.py, strata_tokenizer.py) must come from the bundle too."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "packaging"))
|
||||
import package_terminal as pt # noqa: E402
|
||||
|
||||
#: by-path references that are NOT part of the packaged Terminal on purpose (engine build and install code the bundle never runs). None today.
|
||||
NOT_BUNDLED: set[str] = set()
|
||||
|
||||
|
||||
def _flatten(node):
|
||||
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div):
|
||||
return _flatten(node.left) + _flatten(node.right)
|
||||
return [node]
|
||||
|
||||
|
||||
def referenced_tools(root: Path = ROOT) -> dict[str, set[str]]:
|
||||
"""{'tools/dsv41/pack_gguf.py': {files that name it}} for setup.py, strata_cli/**, strata_control/**."""
|
||||
files = [root / "setup.py"] + sorted((root / "strata_cli").rglob("*.py")) + sorted((root / "strata_control").rglob("*.py"))
|
||||
found: dict[str, set[str]] = {}
|
||||
for f in files:
|
||||
tree = ast.parse(f.read_text(encoding="utf-8-sig"))
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div):
|
||||
vals = [p.value if isinstance(p, ast.Constant) and isinstance(p.value, str) else None for p in _flatten(node)]
|
||||
if "tools" in vals:
|
||||
rest = []
|
||||
for v in vals[vals.index("tools") + 1:]:
|
||||
if v is None:
|
||||
break
|
||||
rest.append(v)
|
||||
if rest and rest[-1].endswith(".py"):
|
||||
found.setdefault("tools/" + "/".join(rest), set()).add(f.relative_to(root).as_posix())
|
||||
elif isinstance(node, ast.Constant) and isinstance(node.value, str):
|
||||
m = re.fullmatch(r"tools[/\\]((?:\w+[/\\])*\w+\.py)", node.value)
|
||||
if m:
|
||||
found.setdefault("tools/" + m.group(1).replace("\\", "/"), set()).add(f.relative_to(root).as_posix())
|
||||
return found
|
||||
|
||||
|
||||
class ByPathToolsAreBundled(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.temp = tempfile.TemporaryDirectory()
|
||||
cls.root = Path(cls.temp.name) / "bundle"
|
||||
cls.root.mkdir()
|
||||
(cls.root / "tools").mkdir()
|
||||
for item in pt.TOOLS_SUBSET:
|
||||
shutil.copy2(ROOT / "tools" / item, cls.root / "tools" / item)
|
||||
pt.add_installer(cls.root, ROOT)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.temp.cleanup()
|
||||
|
||||
def test_the_scan_finds_the_deepseek_scripts(self):
|
||||
# (a scan that finds nothing would make the next test pass for the wrong reason)
|
||||
refs = referenced_tools()
|
||||
for name in ("tools/dsv41/pack_gguf.py", "tools/dsv41/smoke.py", "tools/dsv41/tune.py", "tools/iq_pack.py", "tools/mtp_fetch.py"):
|
||||
self.assertIn(name, refs)
|
||||
self.assertGreaterEqual(len(refs), 12)
|
||||
|
||||
def test_every_file_run_by_path_is_in_the_bundle(self):
|
||||
missing = {p: sorted(by) for p, by in referenced_tools().items() if p not in NOT_BUNDLED and not (self.root / p).is_file()}
|
||||
self.assertEqual(missing, {}, f"run by path, absent from the packaged Terminal: {missing}")
|
||||
|
||||
def test_the_deepseek_scripts_start_from_the_bundle_alone(self):
|
||||
env = {k: v for k, v in os.environ.items() if k not in ("PYTHONPATH",)}
|
||||
for rel in ("tools/dsv41/pack_gguf.py", "tools/dsv41/smoke.py", "tools/dsv41/tune.py"):
|
||||
with tempfile.TemporaryDirectory() as cwd:
|
||||
done = subprocess.run([sys.executable, str(self.root / rel), "--help"], capture_output=True, text=True, cwd=cwd, env=env, timeout=120)
|
||||
self.assertEqual(done.returncode, 0, f"{rel} --help from the bundle: {done.stderr[-400:]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,141 @@
|
||||
"""#3927 / #3917: a Qwen vision projector in the shared models folder must never be taken for DeepSeek's (the pack is made without a vision
|
||||
tower, no error). #3914: DeepSeek files the person already has are recognised by the pinned shard names - in the guided screen too - instead of
|
||||
by the generic GGUF check, which does not know DeepSeek (and called its n-gram shard 'could not be read')."""
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
sys.path.insert(0, str(ROOT / "tools" / "dsv41"))
|
||||
import pack_gguf as PG # noqa: E402
|
||||
import setup # noqa: E402
|
||||
|
||||
|
||||
def gguf(path: Path, **kv) -> Path:
|
||||
"""A GGUF with only metadata (string and uint32 values)."""
|
||||
out = bytearray(b"GGUF" + struct.pack("<IQQ", 3, 0, len(kv)))
|
||||
for k, v in kv.items():
|
||||
k = k.replace("__", ".")
|
||||
out += struct.pack("<Q", len(k)) + k.encode()
|
||||
if isinstance(v, str):
|
||||
out += struct.pack("<I", 8) + struct.pack("<Q", len(v)) + v.encode()
|
||||
else:
|
||||
out += struct.pack("<I", 4) + struct.pack("<I", v)
|
||||
out += b"\0" * (-len(out) % 32)
|
||||
path.write_bytes(bytes(out))
|
||||
return path
|
||||
|
||||
|
||||
class SharedFolderMmproj(unittest.TestCase):
|
||||
def layout(self, d: Path):
|
||||
models = d / "models"
|
||||
model = models / "deepseek-v4.1-flash-IST-DAS-2.25bpw"
|
||||
model.mkdir(parents=True)
|
||||
return models, model
|
||||
|
||||
def test_a_qwen_projector_in_the_shared_folder_is_ignored(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
models, model = self.layout(Path(d))
|
||||
gguf(models / "mmproj-Qwen3.8-Flash-Next-BF16.gguf", general__architecture="clip", clip__projector_type="qwen3vl_merger")
|
||||
gguf(models / "mmproj-Swift-Qwen3.8-Flash-Next-BF16.gguf", general__architecture="clip", clip__projector_type="qwen3vl_merger")
|
||||
self.assertIsNone(PG.find_mmproj(model))
|
||||
|
||||
def test_a_deepseek_projector_is_found_even_after_a_qwen_one(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
models, model = self.layout(Path(d))
|
||||
gguf(models / "mmproj-Qwen3.8-Flash-Next-BF16.gguf", general__architecture="clip", clip__projector_type="qwen3vl_merger")
|
||||
ds = gguf(models / "mmproj-zz-DeepSeek-BF16.gguf", general__architecture="clip", clip__projector_type="deepseek41v")
|
||||
self.assertEqual(PG.find_mmproj(model), ds)
|
||||
|
||||
def test_a_broken_or_not_gguf_mmproj_file_is_skipped(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
models, model = self.layout(Path(d))
|
||||
(models / "mmproj-broken.gguf").write_bytes(b"not a gguf at all")
|
||||
(models / "mmproj-empty.gguf").write_bytes(b"")
|
||||
self.assertIsNone(PG.find_mmproj(model))
|
||||
|
||||
def test_no_mmproj_at_all(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
_models, model = self.layout(Path(d))
|
||||
self.assertIsNone(PG.find_mmproj(model))
|
||||
|
||||
def test_setup_does_not_name_an_mmproj_for_the_official_sets(self):
|
||||
cmd = setup.dsv41_pack_cmd(Path("/m/set"), ["a.gguf"], Path("/s/e.gguf"), Path("/s/m.gguf"), Path("/s/d.gguf"), Path("/p"))
|
||||
self.assertNotIn("--mmproj", cmd) # the folder scan above decides, by the projector type
|
||||
|
||||
|
||||
class OwnDeepSeekFiles(unittest.TestCase):
|
||||
def touch_set(self, folder: Path, names):
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
for n in names:
|
||||
(folder / n).write_bytes(b"x")
|
||||
|
||||
def test_the_pinned_shard_names_are_recognised_in_the_folder_above_and_in_the_first_releases_folder(self):
|
||||
from strata_cli.launch.runner import Runner
|
||||
fam = setup.MULTI_FILE_FAMILIES["deepseek41"]
|
||||
new = fam["models"]["IST-DAS-2.00bpw"]["shards"]
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
d = Path(d)
|
||||
self.touch_set(d / "a", new)
|
||||
self.assertEqual(Runner.deepseek_set_in(setup, d / "a")[0], "IST-DAS-2.00bpw")
|
||||
old = ["model-2.25bpw-%05d-of-00004.gguf" % i for i in range(1, 5)]
|
||||
self.touch_set(d / "b" / "2.25bpw", old)
|
||||
m, _entry, folder = Runner.deepseek_set_in(setup, d / "b")
|
||||
self.assertEqual((m, folder), ("IST-DAS-2.25bpw", d / "b" / "2.25bpw"))
|
||||
ud = [setup.DSV41_SHARD.format(i=i) for i in range(1, 10)]
|
||||
self.touch_set(d / "c" / "UD-Q8_K_XL", ud)
|
||||
self.assertEqual(Runner.deepseek_set_in(setup, d / "c")[0], "UD-Q8_K_XL")
|
||||
|
||||
def test_other_models_files_are_not_deepseeks(self):
|
||||
from strata_cli.launch.runner import Runner
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
self.touch_set(Path(d), ["Qwen3.8-Flash-Next-GSQ-RCO-Q2_0-00001-of-00002.gguf"])
|
||||
self.assertIsNone(Runner.deepseek_set_in(setup, Path(d)))
|
||||
|
||||
def screen(self, settings):
|
||||
from strata_cli.launch.screens import ModelScreen
|
||||
f = mock.MagicMock()
|
||||
f.ctx.settings = settings
|
||||
f.ctx.catalog.strata.setup_module.return_value = setup
|
||||
said = []
|
||||
f.say = lambda text, err=False: said.append(text)
|
||||
return ModelScreen, f, said
|
||||
|
||||
def test_the_guided_screen_takes_the_ud_files_as_deepseek_instead_of_saying_a_shard_could_not_be_read(self):
|
||||
from strata_cli.launch import settings as S
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
self.touch_set(Path(d), [setup.DSV41_SHARD.format(i=i) for i in range(1, 10)])
|
||||
cls, f, said = self.screen(S.LaunchSettings())
|
||||
self.assertTrue(cls.check_own_deepseek(f, d))
|
||||
s = f.ctx.settings
|
||||
self.assertEqual((s.family, s.model, s.model_path), ("deepseek41", "UD-Q8_K_XL", str(Path(d))))
|
||||
self.assertIn("DeepSeek-V4.1-Flash", " ".join(said))
|
||||
self.assertNotIn("cannot be used", " ".join(said))
|
||||
|
||||
def test_a_part_that_is_missing_is_said_plainly(self):
|
||||
from strata_cli.launch import settings as S
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
self.touch_set(Path(d), [setup.DSV41_SHARD.format(i=i) for i in range(1, 9)]) # shard 9 is missing
|
||||
cls, f, said = self.screen(S.LaunchSettings())
|
||||
self.assertTrue(cls.check_own_deepseek(f, d))
|
||||
self.assertIn("1 of 9 parts are not in", " ".join(said))
|
||||
self.assertIsNone(f.ctx.settings.model_path)
|
||||
|
||||
def test_files_of_another_model_go_on_to_the_usual_check(self):
|
||||
from strata_cli.launch import settings as S
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
self.touch_set(Path(d), ["x-00001-of-00002.gguf"])
|
||||
cls, f, said = self.screen(S.LaunchSettings())
|
||||
self.assertFalse(cls.check_own_deepseek(f, d))
|
||||
self.assertEqual(said, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -127,6 +127,7 @@ class OwnEngine(unittest.TestCase):
|
||||
f = mock.MagicMock()
|
||||
f.notices = []
|
||||
f.dsv41_engine_found.return_value = (found, True)
|
||||
f.upgrade_dsv41_engine.side_effect = lambda path: path # (the upgrade has its own tests: tests/terminal/test_dsv41_upgrade.py)
|
||||
return R.Runner.use_own_dsv41_engine, f
|
||||
|
||||
def test_a_stale_path_in_the_config_is_replaced_by_this_installs_package(self):
|
||||
@@ -176,3 +177,129 @@ class MissingEngine(unittest.TestCase):
|
||||
self.assertFalse(self.check({"arch": "deepseek41", "exe": str(exe)}, None))
|
||||
self.assertFalse(self.check({"arch": "deepseek41", "exe": "C:/gone/x.exe"}, exe))
|
||||
self.assertFalse(self.check({"arch": "qwen4exp", "exe": "component:strata-engine"}, None))
|
||||
|
||||
|
||||
class WhichInstallsEngine(unittest.TestCase):
|
||||
"""The DeepSeek engine package that runs: the newest of the running Terminal's own install and the install the model's settings came from."""
|
||||
|
||||
def runner(self, own, root):
|
||||
f = mock.MagicMock()
|
||||
f.catalog.src = Path(own)
|
||||
f.catalog.root = Path(root)
|
||||
f.catalog.strata.setup_module.side_effect = RuntimeError("none")
|
||||
f.catalog.data_dir.return_value = Path(root) / "data"
|
||||
f.s.data_dir = None
|
||||
return f
|
||||
|
||||
def make(self, base, version):
|
||||
d = Path(base) / "engine-dsv41" / f"{version}-windows-x86_64-cuda-cuda13"
|
||||
d.mkdir(parents=True)
|
||||
exe = d / ("strata-dsv41.exe" if sys.platform == "win32" else "strata-dsv41")
|
||||
exe.write_bytes(b"x")
|
||||
return exe
|
||||
|
||||
def found(self, own, root):
|
||||
from strata_cli.launch import runner as R
|
||||
f = self.runner(own, root)
|
||||
with mock.patch.dict("os.environ", {"STRATA_DSV41_EXE": "", "STRATA_INSTALL_DIR": ""}), \
|
||||
mock.patch("strata_control.engine_component.status", return_value={"usable": False}):
|
||||
out = R.Runner.dsv41_engine_found(f)
|
||||
return out[0], getattr(f, "dsv41_choice", None)
|
||||
|
||||
def test_the_running_terminals_newer_package_beats_an_older_first_install(self):
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
old = self.make(Path(d) / "inst5", "0.1.48")
|
||||
new = self.make(Path(d) / "inst8", "0.1.49")
|
||||
exe, choice = self.found(Path(d) / "inst8", Path(d) / "inst5")
|
||||
self.assertEqual(exe, new)
|
||||
self.assertIn("own folder", choice[1])
|
||||
self.assertIn("newest of 2", choice[1])
|
||||
|
||||
def test_an_older_package_in_the_running_terminal_does_not_beat_a_newer_one_in_the_settings_install(self):
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
self.make(Path(d) / "inst8", "0.1.48")
|
||||
new = self.make(Path(d) / "inst5", "0.1.49")
|
||||
exe, _ = self.found(Path(d) / "inst8", Path(d) / "inst5")
|
||||
self.assertEqual(exe, new)
|
||||
|
||||
def test_the_same_version_in_both_means_the_running_terminals_own(self):
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
self.make(Path(d) / "inst5", "0.1.49")
|
||||
own = self.make(Path(d) / "inst8", "0.1.49")
|
||||
exe, choice = self.found(Path(d) / "inst8", Path(d) / "inst5")
|
||||
self.assertEqual(exe, own)
|
||||
self.assertIn("own folder", choice[1])
|
||||
|
||||
def test_only_the_settings_install_has_one(self):
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
only = self.make(Path(d) / "inst5", "0.1.49")
|
||||
(Path(d) / "inst8").mkdir()
|
||||
exe, choice = self.found(Path(d) / "inst8", Path(d) / "inst5")
|
||||
self.assertEqual(exe, only)
|
||||
self.assertIn("settings came from", choice[1])
|
||||
|
||||
def test_versions_compare_as_numbers(self):
|
||||
from strata_control import dsv41_engine
|
||||
self.assertGreater(dsv41_engine.version_key("0.1.49-x"), dsv41_engine.version_key("0.1.9-x"))
|
||||
self.assertGreater(dsv41_engine.version_key("0.1.49.1-x"), dsv41_engine.version_key("0.1.49-x"))
|
||||
|
||||
|
||||
class DiskCountsOnlyTheRest(unittest.TestCase):
|
||||
"""The menu's disk check asks only for what is still to be downloaded: files already in the models folder count as done (P2003)."""
|
||||
|
||||
def setUp(self):
|
||||
import strata_mcp as M
|
||||
import tempfile
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.s = M.Strata(ROOT)
|
||||
self.s.data_dir = lambda: Path(self.tmp.name)
|
||||
self.hw = {"ram_gb": 64.0, "gpus": [CARD], "engine_vendor": "nvidia", "disk_free_gb": 176.0}
|
||||
|
||||
def put_files(self, model="IST-DAS-2.00bpw", legacy=False):
|
||||
S = self.s.setup_module()
|
||||
fam = S.MULTI_FILE_FAMILIES["deepseek41"]
|
||||
md = fam["models"][model]
|
||||
models = Path(self.tmp.name) / "models"
|
||||
sizes = {}
|
||||
for where, names in ((models / (fam["tag"] + model), md["shards"]), (models / (fam["tag"] + "shared"), md["shared"])):
|
||||
where.mkdir(parents=True, exist_ok=True)
|
||||
for n in names:
|
||||
name = S.DSV41_LEGACY_NAMES.get(n, n) if legacy and n in S.DSV41_LEGACY_NAMES else n
|
||||
real = S.DSV41_ALL_FILES[name][0]
|
||||
(where / name).write_bytes(b"x" * 8)
|
||||
sizes[name] = (8, S.DSV41_ALL_FILES[name][1])
|
||||
return sizes
|
||||
|
||||
def test_with_every_file_in_place_a_small_disk_is_not_tight(self):
|
||||
S = self.s.setup_module()
|
||||
with mock.patch.dict(S.DSV41_ALL_FILES, self.put_files()):
|
||||
words = self.s.dsv41_words(self.hw, "IST-DAS-2.00bpw")
|
||||
self.assertNotIn("free disk space", words)
|
||||
|
||||
def test_the_first_releases_names_count_too(self):
|
||||
S = self.s.setup_module()
|
||||
with mock.patch.dict(S.DSV41_ALL_FILES, self.put_files(legacy=True)):
|
||||
self.assertNotIn("free disk space", self.s.dsv41_words(self.hw, "IST-DAS-2.00bpw"))
|
||||
|
||||
def test_with_nothing_in_place_176_gb_is_tight_for_the_257_gb_set(self):
|
||||
words = self.s.dsv41_words(self.hw, "IST-DAS-2.00bpw")
|
||||
self.assertIn("free disk space", words)
|
||||
self.assertTrue(words.startswith("tight"))
|
||||
|
||||
def test_only_part_of_the_files_means_only_the_rest_is_counted(self):
|
||||
S = self.s.setup_module()
|
||||
sizes = self.put_files()
|
||||
first = S.MULTI_FILE_FAMILIES["deepseek41"]["models"]["IST-DAS-2.00bpw"]["shards"][0]
|
||||
part = dict(sizes)
|
||||
part[first] = (S.DSV41_ALL_FILES[first][0], sizes[first][1]) # shard 1 is not here at its real size: the rest of the files are
|
||||
(Path(self.tmp.name) / "models" / ("deepseek-v4.1-flash-IST-DAS-2.00bpw") / first).unlink()
|
||||
with mock.patch.dict(S.DSV41_ALL_FILES, part):
|
||||
hw = {**self.hw, "disk_free_gb": 46.0} # shard 1 is 39.1 GB: 46 GB is enough for it plus the 6 GB of room
|
||||
self.assertNotIn("free disk space", self.s.dsv41_words(hw, "IST-DAS-2.00bpw"))
|
||||
hw = {**self.hw, "disk_free_gb": 44.0}
|
||||
self.assertIn("free disk space", self.s.dsv41_words(hw, "IST-DAS-2.00bpw"))
|
||||
@@ -0,0 +1,163 @@
|
||||
"""An installed DeepSeek engine package is updated by itself when the signed feed's `dsv41` member is newer (0.1.49 -> 0.1.49.1):
|
||||
fetched, checked, started once, switched to, the old folder removed; and left alone, with one plain line, when anything goes wrong."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from strata_control import dsv41_engine # noqa: E402
|
||||
from tests.terminal import update_fixture as fx # noqa: E402
|
||||
from tests.terminal.test_dsv41_engine import Dsv41Engine, build # noqa: E402
|
||||
import feed_tool # noqa: E402
|
||||
|
||||
|
||||
class Upgrade(Dsv41Engine):
|
||||
# (the parent's own tests are not repeated here)
|
||||
test_downloads_verifies_installs_beside_the_engine = None
|
||||
test_a_cut_download_resumes = None
|
||||
test_no_package_for_this_pc_is_a_plain_line_not_a_crash = None
|
||||
test_a_feed_signed_with_another_key_is_refused = None
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.dest = self.root / "data" / "engine-dsv41"
|
||||
self.url = self.server.url + "/strata-feed.json"
|
||||
self.publish("0.1.49")
|
||||
self.old = self.install()
|
||||
self.assertIsNotNone(self.old)
|
||||
|
||||
def upgrade(self, lines=None, start=(True, "strata-dsv41 0.1.49.1 build x")):
|
||||
out = [] if lines is None else lines
|
||||
with mock.patch.object(dsv41_engine, "_test_start", return_value=start):
|
||||
return dsv41_engine.upgrade(self.dest, self.url, installed=self.old, hardware=self.hw, emit=out.append)
|
||||
|
||||
def folders(self):
|
||||
return sorted(p.name.split("-")[0] for p in self.dest.iterdir() if p.is_dir())
|
||||
|
||||
def test_a_newer_package_in_the_feed_is_installed_next_to_the_old_and_the_old_goes(self):
|
||||
self.publish("0.1.49.1")
|
||||
lines = []
|
||||
exe = self.upgrade(lines)
|
||||
self.assertIsNotNone(exe, lines)
|
||||
self.assertEqual(self.folders(), ["0.1.49.1"])
|
||||
self.assertTrue(exe.is_file())
|
||||
self.assertEqual(dsv41_engine.find(self.dest), exe)
|
||||
self.assertIn("updated from 0.1.49 to 0.1.49.1", " ".join(lines))
|
||||
self.assertEqual(self.tree(self.eng), self.before, "the Qwen engine's files must not change")
|
||||
|
||||
def test_the_old_one_is_still_there_until_the_new_one_has_started(self):
|
||||
self.publish("0.1.49.1")
|
||||
seen = {}
|
||||
|
||||
def start(exe):
|
||||
seen["folders"] = self.folders()
|
||||
return True, "strata-dsv41 0.1.49.1"
|
||||
with mock.patch.object(dsv41_engine, "_test_start", side_effect=start):
|
||||
self.assertIsNotNone(dsv41_engine.upgrade(self.dest, self.url, installed=self.old, hardware=self.hw, emit=lambda m: None))
|
||||
self.assertEqual(seen["folders"], ["0.1.49", "0.1.49.1"])
|
||||
self.assertEqual(self.folders(), ["0.1.49.1"])
|
||||
|
||||
def test_the_same_version_in_the_feed_changes_nothing(self):
|
||||
lines = []
|
||||
self.assertIsNone(self.upgrade(lines))
|
||||
self.assertEqual((self.folders(), lines), (["0.1.49"], []))
|
||||
self.assertTrue(self.old.is_file())
|
||||
|
||||
def test_an_older_version_in_the_feed_changes_nothing(self):
|
||||
self.publish("0.1.48")
|
||||
self.assertIsNone(self.upgrade())
|
||||
self.assertEqual(self.folders(), ["0.1.49"])
|
||||
|
||||
def test_versions_compare_as_numbers(self):
|
||||
key = dsv41_engine.version_key
|
||||
self.assertLess(key("0.1.49-x"), key("0.1.49.1-x"))
|
||||
self.assertLess(key("0.1.9-x"), key("0.1.10-x"))
|
||||
self.assertLess(key("0.1.49.1-x"), key("0.1.49.10-x"))
|
||||
self.assertGreater(key("0.1.49.1-x"), key("0.1.49-x"))
|
||||
|
||||
def test_an_unreachable_feed_is_silent_and_the_installed_engine_stays(self):
|
||||
for dead in ("http://127.0.0.1:9/strata-feed.json", str(self.root / "no-such-feed")): # nothing answers / the folder is gone
|
||||
lines = []
|
||||
with mock.patch.object(dsv41_engine, "_test_start", return_value=(True, "x")):
|
||||
self.assertIsNone(dsv41_engine.upgrade(self.dest, dead, installed=self.old, hardware=self.hw, emit=lines.append))
|
||||
self.assertEqual(lines, [], dead)
|
||||
self.assertEqual(self.folders(), ["0.1.49"])
|
||||
self.assertFalse(dsv41_engine.source_reachable("http://127.0.0.1:9/strata-feed.json", timeout=3))
|
||||
|
||||
def test_a_feed_with_a_bad_signature_is_refused_and_the_old_engine_stays(self):
|
||||
self.publish("0.1.49.1")
|
||||
sig = self.release / "strata-feed.json.sig"
|
||||
sig.write_bytes(sig.read_bytes().replace(b"a", b"b", 1) if b"a" in sig.read_bytes() else sig.read_bytes() + b" ")
|
||||
lines = []
|
||||
self.assertIsNone(self.upgrade(lines))
|
||||
self.assertEqual(self.folders(), ["0.1.49"])
|
||||
self.assertIn("still using 0.1.49", " ".join(lines))
|
||||
|
||||
def test_a_package_signed_with_another_key_is_refused_and_the_old_engine_stays(self):
|
||||
shutil.rmtree(self.release, ignore_errors=True)
|
||||
(self.release / "dsv41").mkdir(parents=True)
|
||||
other = fx.make_key()
|
||||
folder = build(self.root / "other", "0.1.49.1", other)
|
||||
feed_tool.pack_engine(folder, self.release / "dsv41", url_prefix="dsv41/")
|
||||
feed_tool.build_feed(self.release, key=self.key, sequence=3, dsv41_dir=self.release / "dsv41") # (the feed is ours, the package is not)
|
||||
lines = []
|
||||
self.assertIsNone(self.upgrade(lines))
|
||||
self.assertEqual(self.folders(), ["0.1.49"])
|
||||
self.assertIn("could not be installed", " ".join(lines))
|
||||
self.assertIn("still using 0.1.49", " ".join(lines))
|
||||
|
||||
def test_a_new_engine_that_does_not_start_is_removed_and_the_old_one_keeps_running(self):
|
||||
self.publish("0.1.49.1")
|
||||
lines = []
|
||||
self.assertIsNone(self.upgrade(lines, start=(False, "it did not start correctly (libcudart.so.13: not found)")))
|
||||
self.assertEqual(self.folders(), ["0.1.49"])
|
||||
self.assertTrue(self.old.is_file())
|
||||
self.assertIn("A newer DeepSeek engine is available but could not be installed", " ".join(lines))
|
||||
self.assertIn("libcudart.so.13", " ".join(lines))
|
||||
self.assertIn("still using 0.1.49", " ".join(lines))
|
||||
|
||||
def test_source_reachable(self):
|
||||
self.assertTrue(dsv41_engine.source_reachable(str(self.release)))
|
||||
self.assertFalse(dsv41_engine.source_reachable(str(self.root / "no-such-feed")))
|
||||
self.assertTrue(dsv41_engine.source_reachable(self.url, timeout=5))
|
||||
|
||||
|
||||
class LaunchChoice(unittest.TestCase):
|
||||
"""The launch does not touch an engine the person chose."""
|
||||
|
||||
def runner(self, package=None):
|
||||
from strata_cli.launch import runner as R
|
||||
r = R.Runner.__new__(R.Runner)
|
||||
r.s = mock.Mock(dsv41_package=package)
|
||||
r.feed = None
|
||||
r.emit = lambda *a, **k: None
|
||||
return r
|
||||
|
||||
def test_a_dsv41_package_or_a_build_of_your_own_is_never_replaced(self):
|
||||
found = Path("/x/engine-dsv41/0.1.49-a/strata-dsv41")
|
||||
with mock.patch.object(dsv41_engine, "upgrade", side_effect=AssertionError("must not run")):
|
||||
self.assertEqual(self.runner(package="/some/file.tar.gz").upgrade_dsv41_engine(found), found)
|
||||
with mock.patch.dict(os.environ, {"STRATA_DSV41_EXE": "/my/build/strata-dsv41"}):
|
||||
self.assertEqual(self.runner().upgrade_dsv41_engine(found), found)
|
||||
self.assertEqual(self.runner().upgrade_dsv41_engine(Path("/x/engine/strata-dsv41")), Path("/x/engine/strata-dsv41")) # not a package folder
|
||||
|
||||
def test_an_unreachable_feed_does_not_start_an_upgrade(self):
|
||||
found = Path("/x/engine-dsv41/0.1.49-a/strata-dsv41")
|
||||
with mock.patch.object(dsv41_engine, "source_reachable", return_value=False), \
|
||||
mock.patch.object(dsv41_engine, "upgrade", side_effect=AssertionError("must not run")):
|
||||
self.assertEqual(self.runner().upgrade_dsv41_engine(found), found)
|
||||
|
||||
def test_a_newer_package_replaces_the_choice(self):
|
||||
found = Path("/x/engine-dsv41/0.1.49-a/strata-dsv41")
|
||||
new = Path("/x/engine-dsv41/0.1.49.1-a/strata-dsv41")
|
||||
with mock.patch.object(dsv41_engine, "source_reachable", return_value=True), mock.patch.object(dsv41_engine, "upgrade", return_value=new):
|
||||
self.assertEqual(self.runner().upgrade_dsv41_engine(found), new)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -12,6 +12,12 @@ SH = ROOT / "packaging" / "install.sh"
|
||||
|
||||
|
||||
class Text(unittest.TestCase):
|
||||
def test_the_readme_one_liner_cannot_succeed_on_a_failed_download(self):
|
||||
for name in ("README.md", "README.de.md", "docs/INSTALL.md", "docs/TERMINAL_LAUNCH.md"):
|
||||
text = (ROOT / name).read_text(encoding="utf-8")
|
||||
self.assertNotIn("install.sh | sh", text, name) # `curl ... | sh` runs nothing and exits 0 when curl fails
|
||||
self.assertIn("install.sh -o install.sh && sh install.sh", text, name)
|
||||
|
||||
def setUp(self):
|
||||
self.ps1 = PS1.read_text(encoding="utf-8")
|
||||
self.sh = SH.read_text(encoding="utf-8")
|
||||
@@ -51,3 +57,41 @@ class Run(unittest.TestCase):
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
@unittest.skipUnless(shutil.which("sh") and os.uname().sysname in ("Linux", "Darwin") if hasattr(os, "uname") else False, "needs sh on Linux or macOS")
|
||||
class InstallShFailsVisibly(unittest.TestCase):
|
||||
"""A broken download of install.sh must not look like a finished install."""
|
||||
|
||||
def run_sh(self, text, **env):
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
f = Path(d) / "install.sh"
|
||||
f.write_text(text, encoding="utf-8", newline="\n")
|
||||
home = Path(d) / "home"
|
||||
home.mkdir()
|
||||
r = subprocess.run(["sh", str(f)], capture_output=True, text=True, timeout=120,
|
||||
env={**os.environ, "HOME": str(home), "STRATA_DATA_DIR": str(home / "data"), "STRATA_BIN_DIR": str(home / "bin"), **env})
|
||||
return r, home
|
||||
|
||||
def test_the_script_is_one_function_called_on_the_last_line(self):
|
||||
text = SH.read_text(encoding="utf-8")
|
||||
self.assertIn("\nmain() {", text)
|
||||
self.assertEqual(text.rstrip().splitlines()[-1], 'main "$@"')
|
||||
|
||||
def test_syntax(self):
|
||||
self.assertEqual(subprocess.run(["sh", "-n", str(SH)], capture_output=True).returncode, 0)
|
||||
|
||||
def test_a_half_downloaded_script_runs_nothing_and_fails(self):
|
||||
lines = SH.read_text(encoding="utf-8").splitlines(True)
|
||||
r, home = self.run_sh("".join(lines[:int(len(lines) * 0.8)]))
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertFalse((home / "data").exists() or (home / "bin").exists())
|
||||
|
||||
def test_an_unreachable_release_folder_stops_with_a_reason_and_a_failing_exit(self):
|
||||
if not shutil.which("curl") and not shutil.which("wget"):
|
||||
self.skipTest("no curl or wget")
|
||||
r, home = self.run_sh(SH.read_text(encoding="utf-8"), STRATA_FEED="http://127.0.0.1:9")
|
||||
self.assertEqual(r.returncode, 1)
|
||||
self.assertIn("could not read", r.stderr)
|
||||
self.assertFalse((home / "bin" / "strata").exists())
|
||||
@@ -970,6 +970,11 @@ class FlagsCommand(unittest.TestCase):
|
||||
self.assertEqual(code, 0) # the preset's own key
|
||||
self.assertTrue(out.strip().startswith("sk-strata-"))
|
||||
|
||||
def test_a_setup_line_joined_after_the_model_could_not_be_installed_starts_with_a_capital(self):
|
||||
from strata_cli.launch.runner import setup_failure
|
||||
e = setup_failure([], "the model does not answer sensibly after the preparation: no answer", "", None, False)
|
||||
self.assertTrue(e.text.startswith("The model could not be installed. The model does not answer sensibly"), e.text)
|
||||
|
||||
def test_a_reason_joined_after_a_sentence_starts_with_a_capital(self):
|
||||
from strata_cli.launch.runner import capital
|
||||
self.assertEqual(capital("the update feed could not be read: no route"), "The update feed could not be read: no route")
|
||||
|
||||
@@ -91,6 +91,12 @@ class LaunchDeepSeek(unittest.TestCase):
|
||||
self.assertIn("drafting DSpark", out)
|
||||
self.assertNotIn("MTP", out)
|
||||
|
||||
def test_the_plan_does_not_promise_a_strata_engine_download_for_deepseek(self):
|
||||
p = self.plan("--model", "ud-q8_k_xl", "--no-tui")
|
||||
text = json.dumps(p.get("download") or {})
|
||||
self.assertNotIn("engine for your graphics card", text)
|
||||
self.assertFalse((p.get("download") or {}).get("engine_missing"))
|
||||
|
||||
def test_no_card_is_said(self):
|
||||
self.hw = {**HW_12, "gpus": []}
|
||||
code, out, err = run_cli(["launch", "--plan", "--model", "ud-q8_k_xl", "--no-tui"])
|
||||
|
||||
@@ -201,7 +201,7 @@ class EngineBeforeModel(unittest.TestCase):
|
||||
def test_a_failed_update_still_reports_the_old_message(self):
|
||||
spawns, err = self.run_ensure([UNUSABLE], [{"event": "error", "error": {"message": "cannot reach the feed"}}], code=6)
|
||||
self.assertEqual(err.code, 6)
|
||||
self.assertIn("cannot reach the feed", err.text)
|
||||
self.assertIn("Cannot reach the feed", err.text) # (the reason is capitalised where it joins the sentence)
|
||||
|
||||
|
||||
class SetupControlStep(unittest.TestCase):
|
||||
|
||||
@@ -68,5 +68,11 @@ class SetupFailureWords(unittest.TestCase):
|
||||
self.assertIsNone(run.keep_setup_output(["x"]))
|
||||
|
||||
|
||||
def test_a_message_that_ends_with_a_full_stop_does_not_get_a_second_one(self):
|
||||
error = setup_failure(TRACEBACK, "Strata can't reach GitHub to download the DeepSeek engine.", "Check the internet connection.", "/tmp/a.setup.log", False)
|
||||
self.assertIn("DeepSeek engine. Check", error.text + " " + error.next)
|
||||
self.assertNotIn("engine..", error.text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -42,7 +42,7 @@ class OldLauncherIsToldTheTruth(unittest.TestCase):
|
||||
for env in ({"STRATA_NO_ACQUIRE": "1", "STRATA_HANDOVER": "1"}, {}):
|
||||
done = self.run_version(tree, **env)
|
||||
self.assertEqual(done.returncode, 0, done.stderr)
|
||||
self.assertTrue(done.stdout.startswith("strata 0.2.4 (build "), done.stdout)
|
||||
self.assertTrue(done.stdout.startswith("strata 0.2.4.2 (build "), done.stdout)
|
||||
elsewhere = self.staged_tree(parent="terminal") # not the updater's staging folder
|
||||
self.assertEqual(self.run_version(elsewhere, STRATA_NO_ACQUIRE="1").returncode, 0)
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ def first(path, pattern):
|
||||
|
||||
|
||||
class VersionBump(unittest.TestCase):
|
||||
def test_the_terminal_is_0_2_4_and_control_is_0_1_49_while_the_app_stays_0_2_3(self):
|
||||
def test_the_terminal_is_0_2_4_2_and_control_is_0_1_49_2_while_the_app_stays_0_2_3(self):
|
||||
# 0.2.4 is a Terminal / Control and engine release (DeepSeek V4.1-Flash): the app stays 0.2.3, the three DeepSeek engine packages are 0.1.49,
|
||||
# the other engine packages stay 0.1.48 (metal/CMakeLists.txt keeps 0.1.48)
|
||||
self.assertEqual(strata_cli.PRODUCT_VERSION, "0.2.4")
|
||||
self.assertEqual(strata_cli.version(), "0.1.49") # the CMake project version: Control
|
||||
self.assertEqual(first("CMakeLists.txt", r"^project\(strata VERSION ([0-9.]+) "), "0.1.49")
|
||||
self.assertEqual(strata_cli.PRODUCT_VERSION, "0.2.4.2")
|
||||
self.assertEqual(strata_cli.version(), "0.1.49.2") # the CMake project version: Control
|
||||
self.assertEqual(first("CMakeLists.txt", r"^project\(strata VERSION ([0-9.]+) "), "0.1.49.2")
|
||||
self.assertIn('STRATA_VERSION="0.1.48-metal"', (ROOT / "metal" / "CMakeLists.txt").read_text(encoding="utf-8"))
|
||||
|
||||
def test_every_place_the_app_version_lives_says_0_2_3(self):
|
||||
@@ -49,7 +49,7 @@ class VersionBump(unittest.TestCase):
|
||||
def test_version_prints_the_product_and_the_build(self):
|
||||
done = subprocess.run([sys.executable, str(ROOT / "strata.py"), "--version"], capture_output=True, text=True, cwd=str(ROOT),
|
||||
env={**os.environ, "STRATA_NO_HANDOVER": "1"})
|
||||
self.assertTrue(done.stdout.startswith("strata 0.2.4 (build 0.1.49;"), done.stdout)
|
||||
self.assertTrue(done.stdout.startswith("strata 0.2.4.2 (build 0.1.49.2;"), done.stdout)
|
||||
|
||||
|
||||
class UpdateFromTheLastRelease(test_updater.UpdaterCase):
|
||||
|
||||
@@ -316,6 +316,20 @@ def short(g: Gguf):
|
||||
return None if have >= need else "%s: %d of %d bytes" % (g.path.name, have, need)
|
||||
|
||||
|
||||
def find_mmproj(model: pathlib.Path):
|
||||
"""The DeepSeek vision tower beside the model (an automatic find, no --mmproj): an mmproj*.gguf in the model's parent folder whose projector IS
|
||||
deepseek41v. The folder is shared with other families' files (Qwen keeps its own mmproj-Qwen3.8-Flash-Next-BF16.gguf there, projector
|
||||
qwen3vl_merger): those are never taken, and never an error - the pack is made without a vision tower. A file named with --mmproj is checked
|
||||
strictly in main()."""
|
||||
for p in sorted(model.parent.glob("mmproj*.gguf")):
|
||||
try:
|
||||
if Gguf(p).kv.get("clip.projector_type") == "deepseek41v":
|
||||
return p
|
||||
except (OSError, ValueError, SystemExit, struct.error):
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def is_engram_file(g) -> bool:
|
||||
"""The Engram table's GGUF: general.architecture 'engram' (the first release) or 'deepseek41-engram' (ISTA-DASLab's final files)."""
|
||||
arch = str(g.kv.get("general.architecture") or "")
|
||||
@@ -404,7 +418,7 @@ def main() -> int:
|
||||
"blocks become mtp.0-2 beside the trunk's tensors (default: the first dspark-*.gguf "
|
||||
"in <model>/.. when there is one)")
|
||||
ap.add_argument("--mmproj", help="unsloth's vision tower GGUF (mmproj-*.gguf, clip / deepseek41v): its tensors become the "
|
||||
"checkpoint's vision.* / aligner.* (default: the first mmproj*.gguf in <model>/..)")
|
||||
"checkpoint's vision.* / aligner.* (default: the first mmproj*.gguf in <model>/.. whose projector is deepseek41v; other families' are ignored)")
|
||||
ap.add_argument("--tq-experts", nargs="+", help="folders tq_convert.py wrote (tq.json + tq-L<layer>.bin): the routed experts of the layers "
|
||||
"listed there are read from it as trellis types (TQK5X ..), the others keep their MXFP4")
|
||||
ap.add_argument("--dense-override", action="append", help="a folder dense_requant.py wrote (dense.json + dense.bin): its tensors replace the same-named "
|
||||
@@ -731,8 +745,7 @@ def main() -> int:
|
||||
print("DSpark drafter from %s: %d blocks" % (ds_path.name, nb))
|
||||
# the vision tower from unsloth's mmproj GGUF (clip, projector deepseek41v): llama.cpp's names -> the checkpoint's;
|
||||
# the F32 norms / biases / marker rows are read as BF16 by the engine; mlp.w1 is the file's gate and up
|
||||
mm_path = pathlib.Path(a.mmproj).expanduser().resolve() if a.mmproj else next(
|
||||
iter(sorted(model.parent.glob("mmproj*.gguf"))), None)
|
||||
mm_path = pathlib.Path(a.mmproj).expanduser().resolve() if a.mmproj else find_mmproj(model)
|
||||
mm_geo = {}
|
||||
if mm_path is not None:
|
||||
if not mm_path.is_file():
|
||||
|
||||
+27
-2
@@ -1277,11 +1277,36 @@ class Strata:
|
||||
if installed or free is None or words.startswith("does not fit"):
|
||||
return words
|
||||
pl = self.dsv41_plan(hw, free, model=model)
|
||||
if pl.get("disk_ok") is False:
|
||||
return (f"tight: it needs about {pl['disk_need_gb']:.0f} GB of free disk space (the download and its preparation) and this computer has "
|
||||
from strata_control import dsv41_fit
|
||||
left = self.dsv41_left_gb(model, pl["download_gb"]) # only what still has to be downloaded (files already here count as done)
|
||||
need = left + dsv41_fit.DISK_HEADROOM_GB # + the pack (MBs) and room to work
|
||||
if left > 0.05 and free < need:
|
||||
return (f"tight: it needs about {need:.0f} GB of free disk space (the rest of the download and its preparation) and this computer has "
|
||||
f"{free:.0f} GB free; free some space or choose another drive when you install it ({words})")
|
||||
return words
|
||||
|
||||
def dsv41_left_gb(self, model, default: float) -> float:
|
||||
"""GB of this DeepSeek set's files still to be downloaded: the files already in Strata's models folder (the right size; the first release's
|
||||
names count too) are done, so the disk check asks only for the rest. Files the person keeps elsewhere (--model-path) are handled by the plan.
|
||||
`default`: what to answer when the tables cannot be read (the whole download)."""
|
||||
S = self.setup_module()
|
||||
try:
|
||||
fam = S.MULTI_FILE_FAMILIES["deepseek41"]
|
||||
md = fam["models"][model]
|
||||
models = self.data_dir() / "models"
|
||||
folder, shared = models / (fam["tag"] + model), models / (fam["tag"] + "shared")
|
||||
left = 0
|
||||
for where, names in ((folder, md["shards"]), (shared, md.get("shared", []))):
|
||||
for n in names:
|
||||
for cand in (n, getattr(S, "DSV41_LEGACY_NAMES", {}).get(n)):
|
||||
if cand and (where / cand).is_file() and (where / cand).stat().st_size == S.DSV41_ALL_FILES[cand][0]:
|
||||
break
|
||||
else:
|
||||
left += S.DSV41_ALL_FILES[n][0]
|
||||
return left / 1e9
|
||||
except Exception: # noqa: BLE001 - a hint for the menu: when unsure, count the whole download
|
||||
return default
|
||||
|
||||
def _dsv41_memory_words(self, hw, model=None) -> str:
|
||||
pl = self.dsv41_plan(hw, model=model)
|
||||
v = pl["verdict"]
|
||||
|
||||
@@ -156,6 +156,7 @@ class Install(unittest.TestCase):
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name)
|
||||
self.calls = []
|
||||
self.github_up = True
|
||||
self.cfg = {}
|
||||
self.exe = self.root / "engine" / ("strata-dsv41.exe" if setup.WIN else "strata-dsv41")
|
||||
eng = self.root / "engine"
|
||||
@@ -172,6 +173,7 @@ class Install(unittest.TestCase):
|
||||
patches = [mock.patch.object(setup, "download", lambda url, dst, what=None: self.calls.append(("get", url, dst))),
|
||||
mock.patch.object(setup, "verify_sha256", lambda s, size, sha, own=False: self.calls.append(("sha", s.name, own))),
|
||||
mock.patch.object(setup, "free_gb", lambda p: 900.0),
|
||||
mock.patch.object(setup, "github_reachable", lambda timeout=10.0: self.github_up),
|
||||
mock.patch.object(setup, "pip_install", lambda *a, **k: None),
|
||||
mock.patch.object(setup, "engine_step", engine_step),
|
||||
mock.patch.object(setup, "run", lambda cmd, **k: self.calls.append(("run", cmd))),
|
||||
@@ -334,6 +336,48 @@ ISTA = "IST-DAS-2.00bpw"
|
||||
RTX5070 = {"index": 0, "name": "NVIDIA GeForce RTX 5070", "vram_gb": 12.0, "vendor": "nvidia"}
|
||||
|
||||
|
||||
class NoNetwork(Install):
|
||||
"""GitHub cannot be reached (a connection that is never answered): one plain line at once, not minutes of retries; and the Strata engine is not used."""
|
||||
|
||||
def test_unreachable_github_is_one_plain_line_before_anything_is_downloaded(self):
|
||||
self.github_up = False
|
||||
rc, out = self.install(engine=False)
|
||||
self.assertEqual(rc, 1, out)
|
||||
self.assertIn("Strata can't reach GitHub to download the DeepSeek engine.", out)
|
||||
self.assertIn("Check the internet connection and run the same command again.", out)
|
||||
self.assertFalse([c for c in self.calls if c[0] == "get"])
|
||||
|
||||
def test_a_package_the_person_has_needs_no_github(self):
|
||||
self.github_up = False
|
||||
self.install(engine=False) # (the first run only creates the folders)
|
||||
pk = self.root / "engine-dsv41"
|
||||
pk.mkdir(exist_ok=True)
|
||||
(pk / "strata-dsv41-0.1.49-x.tar.gz").write_bytes(b"x")
|
||||
with mock.patch.object(setup.dsv41_engine_module if hasattr(setup, "dsv41_engine_module") else __import__("strata_control.dsv41_engine", fromlist=["x"]),
|
||||
"install_package", lambda *a, **k: None):
|
||||
rc, out = self.install(engine=False)
|
||||
self.assertNotIn("can't reach GitHub", out)
|
||||
|
||||
def test_the_strata_engine_is_not_part_of_a_deepseek_install(self):
|
||||
rc, out = self.install()
|
||||
self.assertEqual(rc, 0, out)
|
||||
self.assertFalse([c for c in self.calls if c[0] == "engine"]) # engine_step is not called any more
|
||||
self.assertIn("DeepSeek engine", out)
|
||||
|
||||
|
||||
class GithubCheck(unittest.TestCase):
|
||||
def test_github_reachable_answers_quickly_in_both_directions(self):
|
||||
import urllib.error
|
||||
with mock.patch.object(setup, "_urlopen", side_effect=urllib.error.URLError("timed out")):
|
||||
self.assertFalse(setup.github_reachable(1))
|
||||
with mock.patch.object(setup, "_urlopen", side_effect=OSError("unreachable")):
|
||||
self.assertFalse(setup.github_reachable(1))
|
||||
with mock.patch.object(setup, "_urlopen", side_effect=urllib.error.HTTPError("https://github.com", 403, "no", {}, None)):
|
||||
self.assertTrue(setup.github_reachable(1)) # any answer means it is reachable
|
||||
with mock.patch.object(setup, "_urlopen", return_value=mock.MagicMock()):
|
||||
self.assertTrue(setup.github_reachable(1))
|
||||
|
||||
|
||||
class OfficialSets(unittest.TestCase):
|
||||
"""The two official IST-DAS sets (ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF): the catalog, the pins, the pack recipe, the fit words."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user