diff --git a/BACKLOG.md b/BACKLOG.md index f096a31..62ff492 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -1896,6 +1896,8 @@ served only from a definition written by hand. In order: **C. Operating it.** - N9. `ctl engine-check strata`: the flags of setup.py and server.py, which drift between versions. - N10. `ctl engine-build NODE strata cuda12|cuda13`, as vLLM has. +- N11a. Strata's per-replica state directories (engine-home/strata/) are left behind when a replica stops; + remove them with it. - N11. Disk for the prepared pack and draft layer (and DeepSeek V4.1's 240–280 GB), cleaned with the weights; a first start's preparation within the startup timeout. - N12. docs/install.md: Strata in the engines table, a section with a profile, its memory and first-run time; diff --git a/CHANGELOG.md b/CHANGELOG.md index 285eeb4..c9685ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,9 @@ APIs change between minors without a deprecation period. - **A repository that keeps each quantisation in a directory of its own** (ISTA-DASLab's `IQ2_XS/`, `IQ3_S/`) listed no weights: its file tree is now read recursively, by the explorer, the plan and `ctl model serve`. +- **DeepSeek V4.1 offered on a card its engine was not built for.** Strata's DeepSeek engine runs on RTX 30 (compute + 8.6) and newer; on a Turing card the tab and the plan offered it, and Strata's setup refused it once placed. Now + neither offers it there, the tab says why, and the agent refuses such a profile before starting it. ## v0.16.0 — 2026-10-30 diff --git a/internal/agent/engines/strata.go b/internal/agent/engines/strata.go index b424d1d..a4e1f83 100644 --- a/internal/agent/engines/strata.go +++ b/internal/agent/engines/strata.go @@ -9,6 +9,8 @@ import ( "regexp" "strconv" "sync/atomic" + + "gitea.mynodes.xyz/ilker/llmgw/internal/strata" ) // StrataConfig locates a Strata installation. @@ -133,6 +135,13 @@ func (*Strata) Validate(p Profile, gpus []GPUInfo) error { if err := requireComputeCapability(p, gpus, 6.0, "strata"); err != nil { return err } + // A family whose engine needs more: said now, rather than by Strata's + // setup after the replica was placed here. + if fam, ok := strata.FamilyNamed(strata.ArgValue(p.Args, "--family")); ok && fam.MinCC > 0 { + if err := requireComputeCapability(p, gpus, fam.MinCC, "strata's "+fam.Title+" engine"); err != nil { + return err + } + } return validateCommon(p, gpus) } diff --git a/internal/agent/engines/strata_test.go b/internal/agent/engines/strata_test.go index 5bc045c..416dcff 100644 --- a/internal/agent/engines/strata_test.go +++ b/internal/agent/engines/strata_test.go @@ -98,3 +98,16 @@ func TestAStrataNamedByHandIsReportedWithItsVersion(t *testing.T) { t.Error("reported without its engine") } } + +func TestStrataRefusesDeepSeekOnACardItsEngineWasNotBuiltFor(t *testing.T) { + s := &Strata{} + p := Profile{Name: "p", Engine: "strata", ModelPath: "/m", TP: 1, GPUsPerNode: 1, Args: []string{"--family", "deepseek41", "--size", "IST-DAS-1.75bpw"}} + gpus := []GPUInfo{{Index: 0, Name: "Quadro RTX 6000", ComputeCapability: "7.5"}} + if err := s.Validate(p, gpus); err == nil || !strings.Contains(err.Error(), "8.6") { + t.Errorf("DeepSeek on 7.5: %v", err) + } + p.Args = []string{"--family", "qwen", "--size", "IQ2_XS"} + if err := s.Validate(p, gpus); err != nil { + t.Errorf("Qwen on 7.5: %v", err) + } +} diff --git a/internal/control/adminapi/api.go b/internal/control/adminapi/api.go index 3757069..8af29fb 100644 --- a/internal/control/adminapi/api.go +++ b/internal/control/adminapi/api.go @@ -1954,6 +1954,11 @@ func PoolNodesOf(nodes []NodeView) []modelplan.PoolNode { if len(n.Fabrics) > 0 { p.Fabric = n.Fabrics[0] } + for _, c := range n.ComputeCapabilities { + if v, err := strconv.ParseFloat(c, 64); err == nil && v > p.CC { + p.CC = v + } + } if l := n.Load; l != nil && l.MemTotal > 0 { p.HostFreeGB = int((l.MemTotal - l.MemUsed) >> 30) } diff --git a/internal/modelplan/options.go b/internal/modelplan/options.go index 20ffb2b..bf84953 100644 --- a/internal/modelplan/options.go +++ b/internal/modelplan/options.go @@ -55,6 +55,8 @@ type PoolNode struct { Kinds []PoolKind // Engines are the engines the node has installed. Engines []string + // CC is its newest card's compute capability, 0 when not known. + CC float64 } // PoolKind is one class of a node's devices. diff --git a/internal/modelplan/strata.go b/internal/modelplan/strata.go index 4ab1535..f1e256b 100644 --- a/internal/modelplan/strata.go +++ b/internal/modelplan/strata.go @@ -24,12 +24,18 @@ func StrataOptions(family string, arts []Artifact, nodes []PoolNode) []Option { continue } need := float64(size.MemoryGB()) + fam, _ := strata.FamilyNamed(family) byClass := map[string]*Option{} var order []string for _, n := range nodes { if !slices.Contains(n.Engines, "strata") || n.HostMemGB <= 0 { continue } + // A family whose engine needs a newer card: refused by Strata on + // this one, after the node was chosen. + if fam.MinCC > 0 && n.CC > 0 && n.CC < fam.MinCC { + continue + } have := float64(n.HostMemGB) * hostShare if need > have { continue diff --git a/internal/modelplan/strata_test.go b/internal/modelplan/strata_test.go index 16ca096..005fa3c 100644 --- a/internal/modelplan/strata_test.go +++ b/internal/modelplan/strata_test.go @@ -30,3 +30,22 @@ func TestStrataIsOfferedOnANodeWithItTheCardAndTheRAM(t *testing.T) { t.Errorf("%q %q", o.Label(), o.MemoryText()) } } + +// DeepSeek's engine is built for RTX 30 and newer: not offered on a Turing +// card, which Strata's setup refuses after the replica is placed. +func TestDeepSeekOnStrataNeedsANewerCard(t *testing.T) { + arts := []Artifact{{Name: "IST-DAS-1.75bpw.gguf", Format: "gguf"}} + node := func(cc float64) []PoolNode { + return []PoolNode{{ID: "n", Class: "6000-24g", GPUMemGB: 24, GPUs: 1, FreeGPUs: 1, HostMemGB: 251, + Engines: []string{"strata"}, CC: cc}} + } + if got := StrataOptions("deepseek41", arts, node(7.5)); len(got) != 0 { + t.Errorf("offered on 7.5: %+v", got) + } + if got := StrataOptions("deepseek41", arts, node(8.9)); len(got) != 1 { + t.Errorf("not offered on 8.9: %+v", got) + } + if got := StrataOptions("qwen", []Artifact{{Name: "IQ2_XS.gguf"}}, node(7.5)); len(got) != 1 { + t.Errorf("Qwen not offered on 7.5: %+v", got) + } +} diff --git a/internal/strata/models.go b/internal/strata/models.go index 9e50392..a5feb73 100644 --- a/internal/strata/models.go +++ b/internal/strata/models.go @@ -26,6 +26,10 @@ type Family struct { // Engine is a family that runs on an engine path of its own, with a // download of its own (DeepSeek V4.1). Engine bool + // MinCC is the compute capability its engine needs of an NVIDIA card, + // above Strata's own 6.0; 0 for none. DeepSeek's engine is built for + // RTX 30 (8.6) and newer only. + MinCC float64 } // Size is one of a family's sizes: setup's --model, which llmgw's profile @@ -56,7 +60,7 @@ var Families = []Family{ {Name: "gyro", Model: "qwen3.8-flash-next-gyro", Title: "Qwen3.8-Flash-Next (Gyro)", Repo: "agentionai/Qwen3.8-Flash-Next-Gyro-GGUF"}, {Name: "nvfp4", Model: "qwen3.8-flash-next-nvfp4", Title: "Qwen3.8-Flash-Next (NVFP4)"}, {Name: "qwen36", Model: "qwen3.6-35b-a3b", Title: "Qwen3.6-35B-A3B", Repo: "unsloth/Qwen3.6-35B-A3B-MTP-GGUF"}, - {Name: "deepseek41", Model: "deepseek-v4.1-flash", Title: "DeepSeek V4.1-Flash", Repo: "ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF", Engine: true}, + {Name: "deepseek41", Model: "deepseek-v4.1-flash", Title: "DeepSeek V4.1-Flash", Repo: "ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF", Engine: true, MinCC: 8.6}, } // Sizes are the sizes Strata 0.2.7 runs, and which families have each. diff --git a/internal/web/admin_strata.go b/internal/web/admin_strata.go index 5cf485d..c71dfd8 100644 --- a/internal/web/admin_strata.go +++ b/internal/web/admin_strata.go @@ -65,6 +65,8 @@ func (p *AccountPages) strataView(l *Lang) *strataView { sr.Fits, sr.Runs = true, l.T("explore.strata", opts[0].Class, l.Num(opts[0].NeedGB, 0)) case len(v.Nodes) == 0: sr.Runs = l.T("strata.no_node") + case f.MinCC > 0 && cardsOlderThan(pool, f.MinCC): + sr.Runs = l.T("strata.old_card", l.Num(f.MinCC, 1)) default: sr.Runs = l.T("strata.no_room", s.MemoryGB()) } @@ -75,6 +77,22 @@ func (p *AccountPages) strataView(l *Lang) *strataView { return v } +// cardsOlderThan says whether every node with Strata has a card of a +// compute capability below cc, as far as they say. +func cardsOlderThan(pool []modelplan.PoolNode, cc float64) bool { + older := false + for _, n := range pool { + if !slices.Contains(n.Engines, "strata") { + continue + } + if n.CC == 0 || n.CC >= cc { + return false + } + older = true + } + return older +} + // serveStrata adds a model of a family Strata fetches itself: its source // the repository, its replica on a card of the class the plan found. func (p *AccountPages) serveStrata(ctx context.Context, l *Lang, by, family, size string) (string, error) { diff --git a/internal/web/admin_strata_test.go b/internal/web/admin_strata_test.go index 99331d2..23fc4f4 100644 --- a/internal/web/admin_strata_test.go +++ b/internal/web/admin_strata_test.go @@ -15,7 +15,7 @@ type strataFleet struct{ fakeFleet } func (strataFleet) Nodes() []adminapi.NodeView { return []adminapi.NodeView{{ID: "cuda01", State: "ready", GPUTotal: 1, GPUFree: 1, MemoryGB: 251, - Classes: []string{"6000-24g"}, Engines: []string{"llamacpp", "strata"}}} + Classes: []string{"6000-24g"}, Engines: []string{"llamacpp", "strata"}, ComputeCapabilities: []string{"7.5"}}} } func TestTheStrataTabListsWhatRunsHereAndFetchesASize(t *testing.T) { @@ -31,13 +31,14 @@ func TestTheStrataTabListsWhatRunsHereAndFetchesASize(t *testing.T) { body := mirrorGet(t, mux, "/admin/mirror?tab=strata").Body.String() for _, want := range []string{"Strata is installed on: cuda01", "Qwen3.8-Flash-Next", "IQ2_XS", - "1× 6000-24g with Strata, 52 GB of RAM", `name="size" value="IQ2_XS"`, "Strata fetches these onto the node itself"} { + "1× 6000-24g with Strata, 52 GB of RAM", `name="size" value="IQ2_XS"`, "Strata fetches these onto the node itself", + "compute capability 8.6 or newer"} { if !strings.Contains(body, want) { t.Errorf("the tab lacks %q", want) } } // Unsloth's Q8_0 keeps 140 GB: it runs here; nothing past the node's RAM does. - if strings.Contains(body, `value="UD-Q8_K_XL"`) { + if strings.Contains(body, `value="UD-Q8_K_XL"`) || strings.Contains(body, `value="IST-DAS-1.75bpw"`) { t.Error("offered DeepSeek's 306 GB size, which no node holds, and which Strata fetches itself") } diff --git a/internal/web/locales/en/mirror.json b/internal/web/locales/en/mirror.json index 85ab8ef..18145dc 100644 --- a/internal/web/locales/en/mirror.json +++ b/internal/web/locales/en/mirror.json @@ -278,5 +278,6 @@ "strata.err.not_servable": "%s is fetched into the mirror first, then served from its version", "strata.msg.served": "%s added: its first start fetches about %s GB onto the node and prepares it, which can take hours on a slow link; Replicas shows how far it is.", "strata.pack_files": "part of Strata's ready-made %s %s pack: served whole from the mirror's Strata tab, Strata fetching it onto the node", - "strata.unknown_pack": "a pack made for Strata that Strata 0.2.7 does not know: its setup refuses it, and llama.cpp cannot read it" + "strata.unknown_pack": "a pack made for Strata that Strata 0.2.7 does not know: its setup refuses it, and llama.cpp cannot read it", + "strata.old_card": "its engine needs an NVIDIA card of compute capability %s or newer (RTX 30 on); the nodes with Strata have older ones" } diff --git a/internal/web/locales/tr/mirror.json b/internal/web/locales/tr/mirror.json index 18f8a87..90d2ce2 100644 --- a/internal/web/locales/tr/mirror.json +++ b/internal/web/locales/tr/mirror.json @@ -271,5 +271,6 @@ "strata.err.not_servable": "%s önce aynaya indirilir, sonra sürümünden sunulur", "strata.msg.served": "%s eklendi: ilk açılışında node'a yaklaşık %s GB indirip hazırlar; yavaş bağlantıda saatler sürebilir. İlerleyişi Replikalar gösterir.", "strata.pack_files": "Strata'nın hazır %s %s paketinin parçası: aynanın Strata sekmesinden bütün olarak sunulur, Strata onu node'a kendisi indirir", - "strata.unknown_pack": "Strata için hazırlanmış ama Strata 0.2.7'nin tanımadığı bir paket: kurulumu onu reddeder, llama.cpp de okuyamaz" + "strata.unknown_pack": "Strata için hazırlanmış ama Strata 0.2.7'nin tanımadığı bir paket: kurulumu onu reddeder, llama.cpp de okuyamaz", + "strata.old_card": "motoru compute capability %s ya da daha yeni bir NVIDIA kartı ister (RTX 30 ve sonrası); Strata kurulu node'ların kartları daha eski" }