diff --git a/BACKLOG.md b/BACKLOG.md index 85e03a0..f096a31 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -1889,6 +1889,10 @@ served only from a definition written by hand. In order: - N7. The web serve page, the explorer's fit, the console's serve form: the same choice. Done 2026-10-30. - N8. `--context`/`--parallel` tied to `context_length`/`max_output`; `ctl model context` takes Strata. +- N7b. The mirror page's Strata tab: every family and size, fetch (GGUF) or serve (packs Strata fetches itself: + NVFP4, DeepSeek V4.1). Done 2026-10-30. Community `strata-ple` packs Strata 0.2.7 does not list stay unservable; + running them would mean starting the engine past Strata's setup (`--pack`, `--native`, `--ple-gguf`). + **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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aadf34..af53a35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,16 @@ is only released once its **exit criterion** is met, not when its code lands. Until v1.0.0 the minor version is the breaking one: configuration formats and Go APIs change between minors without a deprecation period. +## Unreleased + +### Added + +- **The mirror page's Strata tab.** Every family and size Strata runs, with its download, the RAM it keeps and where + it would run here; a GGUF size fetched into the mirror in one press (checked against Hugging Face's checksums), + and the packs Strata fetches itself — NVFP4, DeepSeek V4.1 — served at once, their repository the source. + `ctl model plan` and `ctl model serve` take those repositories too, and the explorer names a `strata-ple` + repository for what it is: part of a pack Strata knows, or one Strata 0.2.7 does not. + ## v0.16.0 — 2026-10-30 Weights pinned to the commit they were added at and checked, by the nodes and the mirror, against the sha256 diff --git a/docs/install.md b/docs/install.md index d2c84fe..9d16227 100644 --- a/docs/install.md +++ b/docs/install.md @@ -761,7 +761,24 @@ llmgw ctl model serve ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF \ ``` The Hugging Face explorer shows the same ("1× … with Strata, N GB of RAM"), -and the mirror's serve form serves such a version on Strata. A Strata profile +and the mirror's serve form serves such a version on Strata. + +The mirror page's **Strata** tab lists every family and size Strata runs, with +what each downloads and keeps in RAM and where it would run here: + +- **Fetch into the mirror**, for the GGUF families: the size's files, checked + against Hugging Face's checksums; the version's serve form then serves it on + Strata. +- **Serve**, for the packs Strata fetches itself — the NVFP4 packs + (`Maximilian228/Qwen3.8-Flash-Next-NVFP4-GPTQ-Strata` and its NVIDIA + sibling, NVIDIA's FP4-Local) and DeepSeek V4.1: the model is added with the + repository as its source, and its first start fetches tens of gigabytes onto + the node, each file pinned and checked by Strata, before preparing it. + `ctl model serve` does the same for these repositories. + +Repositories of architecture `strata-ple` that Strata 0.2.7 does not list — +community packs such as `…-Q8_0-Strata` — are named as such in the explorer: +Strata's setup refuses them, and llama.cpp cannot read them. A Strata profile says its family and size (`--family qwen --size IQ2_XS`), which the catalog checks against what Strata runs, and reserves the RAM the size keeps its experts in (48 GB and the engine's 4 for IQ2_XS) unless it says `min_memory_gb`. Its diff --git a/internal/control/modelserve/modelserve.go b/internal/control/modelserve/modelserve.go index 5ac2793..a9bc963 100644 --- a/internal/control/modelserve/modelserve.go +++ b/internal/control/modelserve/modelserve.go @@ -35,6 +35,7 @@ import ( "gitea.mynodes.xyz/ilker/llmgw/internal/modelplan" "gitea.mynodes.xyz/ilker/llmgw/internal/notify" "gitea.mynodes.xyz/ilker/llmgw/internal/store" + "gitea.mynodes.xyz/ilker/llmgw/internal/strata" ) // Registry reads a repository. @@ -412,6 +413,40 @@ func (s *Server) run(j *Job) { if req.TP > 0 && req.GPUsPerNode == 0 { req.GPUsPerNode = req.TP } + // A pack Strata fetches itself: no mirror; its repository is the source. + if _, size, ok := strata.ForFetchedRepo(repo.ID); ok { + plan, err := s.plan(ctx, repo, PlanRequest{Repo: repo.ID}) + if err != nil { + s.fail(j, "registry", err) + return + } + set := hf.FileSet{Name: size.Name} + layout, err := chooseLayout(Request{Format: "gguf", Engine: "strata", GPUClass: req.GPUClass}, plan, true, set) + if err != nil { + s.fail(j, "registry", err) + return + } + name := req.Name + if name == "" { + name = hf.ModelName(repo.ID) + } + s.mu.Lock() + j.Model = name + s.mu.Unlock() + if !req.sized { + req.ContextLength = strataContext + } + s.step(j, "registry", Done, fmt.Sprintf("Strata's ready-made %s pack, revision %s; runs on %s", + size.Name, repo.ShortRevision(), layout[0].Label())) + s.step(j, "mirror", Done, fmt.Sprintf("not through the mirror: Strata fetches about %.0f GB onto the node itself", size.DownloadGB)) + profiles := profilesFor(layout, req, 0, 0) + profiles[0].StartupTimeout = 4 * time.Hour + s.mu.Lock() + j.Request = req + s.mu.Unlock() + s.addAndStart(ctx, j, repo, set, name, repo.ID, profiles, hardwareExists(layout)) + return + } // The plan: which file suits the hardware, and how it spreads over it. planCtx := req.CtxPool if req.Format == "safetensors" { diff --git a/internal/control/modelserve/plan.go b/internal/control/modelserve/plan.go index aa1f3fc..58e48f6 100644 --- a/internal/control/modelserve/plan.go +++ b/internal/control/modelserve/plan.go @@ -98,6 +98,19 @@ func (s *Server) Plan(ctx context.Context, req PlanRequest) (Plan, error) { } func (s *Server) plan(ctx context.Context, repo hf.Repo, req PlanRequest) (Plan, error) { + // A pack Strata fetches itself: one way, Strata's, whatever its files. + if fam, size, ok := strata.ForFetchedRepo(repo.ID); ok { + art := modelplan.Artifact{Name: size.Name, Format: "strata", Bytes: int64(size.DownloadGB * 1e9), Quant: size.Name} + p := Plan{Repo: repo.ID, Revision: repo.Revision, Format: "strata", Arch: repo.Architecture, License: repo.License, + Context: strataContext, Servable: true, Note: "Strata fetches this pack onto the node itself on its first start"} + p.Options = modelplan.StrataOptions(fam.Name, []modelplan.Artifact{art}, s.deps.Pool()) + pa := PlannedArtifact{Artifact: art} + if len(p.Options) > 0 { + pa.Best = &p.Options[0] + } + p.Artifacts = []PlannedArtifact{pa} + return p, nil + } p := Plan{Repo: repo.ID, Revision: repo.Revision, Format: req.Format, Arch: repo.Architecture, License: repo.License, Params: repo.Params, Context: req.Context, Gated: repo.Gated.Is(), Servable: true, Projectors: Projectors(repo)} diff --git a/internal/control/modelserve/strata_test.go b/internal/control/modelserve/strata_test.go index 61dbfb0..4e10291 100644 --- a/internal/control/modelserve/strata_test.go +++ b/internal/control/modelserve/strata_test.go @@ -35,3 +35,18 @@ func TestAStrataWayIsChosenAndMadeAProfile(t *testing.T) { t.Errorf("%+v", p) } } + +func TestAReadyMadePackIsPlannedOnStrataOnly(t *testing.T) { + s := New(Deps{Pool: func() []modelplan.PoolNode { + return []modelplan.PoolNode{{ID: "cuda01", Class: "6000-24g", GPUMemGB: 24, GPUs: 1, FreeGPUs: 1, + HostMemGB: 251, HostFreeGB: 200, Engines: []string{"strata"}}} + }}) + p, err := s.plan(t.Context(), hf.Repo{ID: "Maximilian228/Qwen3.8-Flash-Next-NVIDIA-NVFP4-Strata", Revision: "abc", + Architecture: "strata-ple"}, PlanRequest{}) + if err != nil { + t.Fatal(err) + } + if !p.Servable || len(p.Options) != 1 || p.Options[0].StrataFamily != "nvfp4" || p.Options[0].StrataSize != "NVFP4-NVIDIA" { + t.Errorf("%+v", p) + } +} diff --git a/internal/strata/models.go b/internal/strata/models.go index 8ea4059..9e50392 100644 --- a/internal/strata/models.go +++ b/internal/strata/models.go @@ -14,9 +14,9 @@ import ( // Family is a model Strata runs, from one publisher's files. type Family struct { - // Name is setup's --family. - Name string - Title string + // Name is setup's --family; Model the name Strata gives the model. + Name, Model string + Title string // Repo is the Hugging Face repository its files are in; empty when // Strata fetches them itself from several. Repo string @@ -42,18 +42,21 @@ type Size struct { RAMGB float64 // NVIDIAOnly is a size whose kernels only NVIDIA cards have. NVIDIAOnly bool + // Repo is a size's own repository, for a ready-made pack Strata + // fetches itself. + Repo string } // Families are the families Strata 0.2.7 runs. var Families = []Family{ - {Name: "qwen", Title: "Qwen3.8-Flash-Next", Repo: "ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF", Subdir: true}, - {Name: "swift", Title: "Swift 1.5 (Qwen3.8-Flash-Next, shorter thinking)", Repo: "ukisai/Swift-1.5-Qwen3.8-Flash-Next-GSQ-RCO-GGUF"}, - {Name: "coder", Title: "Qwen3.8-Flash-Next Coder", Repo: "ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF", Subdir: true}, - {Name: "unsloth", Title: "Qwen3.8-Flash-Next (Unsloth)", Repo: "unsloth/Qwen3.8-Flash-Next-GGUF", Subdir: true}, - {Name: "gyro", Title: "Qwen3.8-Flash-Next (Gyro)", Repo: "agentionai/Qwen3.8-Flash-Next-Gyro-GGUF"}, - {Name: "nvfp4", Title: "Qwen3.8-Flash-Next (NVFP4)"}, - {Name: "qwen36", Title: "Qwen3.6-35B-A3B", Repo: "unsloth/Qwen3.6-35B-A3B-MTP-GGUF"}, - {Name: "deepseek41", Title: "DeepSeek V4.1-Flash", Repo: "ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF", Engine: true}, + {Name: "qwen", Model: "qwen3.8-flash-next", Title: "Qwen3.8-Flash-Next", Repo: "ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF", Subdir: true}, + {Name: "swift", Model: "swift-1.5", Title: "Swift 1.5 (Qwen3.8-Flash-Next, shorter thinking)", Repo: "ukisai/Swift-1.5-Qwen3.8-Flash-Next-GSQ-RCO-GGUF"}, + {Name: "coder", Model: "qwen3.8-flash-next-coder", Title: "Qwen3.8-Flash-Next Coder", Repo: "ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-Coder-GGUF", Subdir: true}, + {Name: "unsloth", Model: "qwen3.8-flash-next-unsloth", Title: "Qwen3.8-Flash-Next (Unsloth)", Repo: "unsloth/Qwen3.8-Flash-Next-GGUF", Subdir: true}, + {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}, } // Sizes are the sizes Strata 0.2.7 runs, and which families have each. @@ -70,9 +73,12 @@ var Sizes = []Size{ {Name: "Q8_0", Families: []string{"unsloth", "qwen36"}, DownloadGB: 188.2, RAMGB: 136}, {Name: "Gyro-S", Families: []string{"gyro"}, DownloadGB: 58.5, RAMGB: 27.6}, {Name: "Gyro-M", Families: []string{"gyro"}, DownloadGB: 92.0, RAMGB: 35}, - {Name: "NVFP4", Families: []string{"nvfp4"}, DownloadGB: 128, RAMGB: 80, NVIDIAOnly: true}, - {Name: "NVFP4-NVIDIA", Families: []string{"nvfp4"}, DownloadGB: 128, RAMGB: 80, NVIDIAOnly: true}, - {Name: "NVFP4-QAD", Families: []string{"nvfp4"}, DownloadGB: 102.2, DiskGB: 212.2, RAMGB: 80, NVIDIAOnly: true}, + {Name: "NVFP4", Families: []string{"nvfp4"}, DownloadGB: 128, RAMGB: 80, NVIDIAOnly: true, + Repo: "Maximilian228/Qwen3.8-Flash-Next-NVFP4-GPTQ-Strata"}, + {Name: "NVFP4-NVIDIA", Families: []string{"nvfp4"}, DownloadGB: 128, RAMGB: 80, NVIDIAOnly: true, + Repo: "Maximilian228/Qwen3.8-Flash-Next-NVIDIA-NVFP4-Strata"}, + {Name: "NVFP4-QAD", Families: []string{"nvfp4"}, DownloadGB: 102.2, DiskGB: 212.2, RAMGB: 80, NVIDIAOnly: true, + Repo: "nvidia/Qwen3.8-Flash-Next-FP4-Local"}, {Name: "IST-DAS-1.75bpw", Families: []string{"deepseek41"}, DownloadGB: 239, RAMGB: 34}, {Name: "IST-DAS-2.00bpw", Families: []string{"deepseek41"}, DownloadGB: 257, RAMGB: 40}, {Name: "IST-DAS-2.25bpw", Families: []string{"deepseek41"}, DownloadGB: 274, RAMGB: 44}, @@ -172,3 +178,51 @@ func SizeForFile(family, file string) (Size, bool) { } return best, best.Name != "" } + +// FetchesItself is a family whose files Strata's setup fetches itself, each +// pinned to a commit and checked: ready-made packs, and DeepSeek's sets with +// an engine of their own. A model of it names its repository as the source. +func (f Family) FetchesItself() bool { return f.Name == "nvfp4" || f.Engine } + +// RepoOf is the repository a size of a family is in. +func RepoOf(f Family, s Size) string { + if s.Repo != "" { + return s.Repo + } + return f.Repo +} + +// ForFetchedRepo is the family and size a repository is, for one Strata +// fetches itself: a ready-made pack's own, or DeepSeek's (whose size the +// repository does not say, so the smallest). +func ForFetchedRepo(repo string) (Family, Size, bool) { + for _, f := range Families { + if !f.FetchesItself() { + continue + } + for _, s := range SizesOf(f.Name) { + if strings.EqualFold(RepoOf(f, s), repo) { + return f, s, true + } + } + } + return Family{}, Size{}, false +} + +// Fetchable says whether a size's files can be fetched by llmgw from the +// family's repository: not where Strata fetches them itself (NVFP4's packs, +// DeepSeek's sets with an engine of their own). +func (f Family) Fetchable() bool { return f.Repo != "" && !f.Engine } + +// FilesOf are the weights of a size among a repository's files: its GGUF +// shards, not the picture reader. +func FilesOf(size string, paths []string) []string { + var out []string + for _, p := range paths { + u := strings.ToUpper(p) + if strings.HasSuffix(u, ".GGUF") && !strings.Contains(u, "MMPROJ") && strings.Contains(u, strings.ToUpper(size)) { + out = append(out, p) + } + } + return out +} diff --git a/internal/web/admin_mirror.go b/internal/web/admin_mirror.go index 1c9875c..3cc5e9f 100644 --- a/internal/web/admin_mirror.go +++ b/internal/web/admin_mirror.go @@ -24,6 +24,7 @@ import ( "gitea.mynodes.xyz/ilker/llmgw/internal/safedial" "gitea.mynodes.xyz/ilker/llmgw/internal/sourcecheck" "gitea.mynodes.xyz/ilker/llmgw/internal/store" + "gitea.mynodes.xyz/ilker/llmgw/internal/strata" ) // MirrorAdmin is the mirror's management API as this page needs it. @@ -169,6 +170,10 @@ type adminMirrorData struct { // the serve action is then not offered rather than offered and refused. HasCatalog bool + // Strata is the Strata tab: the models it runs, and where they would + // run here. + Strata *strataView + // The explorer. HasRegistry bool Query string @@ -256,6 +261,14 @@ func (p *AccountPages) RegisterAdminMirror(mux *http.ServeMux, d MirrorDeps) { render(w, r, "mirror_repo", d) })) + mux.HandleFunc("POST /admin/mirror/strata", p.mirrorAction(m, + func(ctx context.Context, admin store.User, r *http.Request) (string, error) { + if r.FormValue("action") == "serve" { + return p.serveStrata(ctx, langOf(r), admin.Email, r.FormValue("family"), r.FormValue("size")) + } + return p.fetchStrata(ctx, langOf(r), m, r.FormValue("family"), r.FormValue("size")) + })) + mux.HandleFunc("POST /admin/mirror/fetch", p.mirrorAction(m, func(ctx context.Context, admin store.User, r *http.Request) (string, error) { // One form posts one source or fifteen. A sharded model is one @@ -462,6 +475,9 @@ func (p *AccountPages) renderMirror(w http.ResponseWriter, r *http.Request, if d.Tab == tabExplore { p.explore(r, &d) } + if d.Tab == tabStrata { + d.Strata = p.strataView(l) + } got, err := m.List(r.Context()) if err != nil { @@ -618,6 +634,7 @@ const ( tabHeld = "held" tabExplore = "explore" tabFetch = "fetch" + tabStrata = "strata" ) // mirrorTab is one of the page's tabs. @@ -636,7 +653,7 @@ type mirrorTab struct { // shown, where its effect is, unless it was refused: then the form it came // from, to try again. func mirrorTabFor(r *http.Request, errMsg string) string { - valid := func(t string) bool { return t == tabHeld || t == tabExplore || t == tabFetch } + valid := func(t string) bool { return t == tabHeld || t == tabExplore || t == tabFetch || t == tabStrata } if r.Method == http.MethodPost { if t := r.PostFormValue("tab"); errMsg != "" && valid(t) { return t @@ -673,6 +690,10 @@ func mirrorTabs(l *Lang, d adminMirrorData) []mirrorTab { tabs = append(tabs, mirrorTab{Label: l.T("mirror.tab.explore"), Href: "/admin/mirror?tab=explore", Active: d.Tab == tabExplore}) } + if d.HasRegistry { + tabs = append(tabs, mirrorTab{Label: l.T("mirror.tab.strata"), + Href: "/admin/mirror?tab=strata", Active: d.Tab == tabStrata}) + } return append(tabs, mirrorTab{Label: l.T("mirror.tab.fetch"), Href: "/admin/mirror?tab=fetch", Active: d.Tab == tabFetch}) } @@ -823,9 +844,15 @@ func (p *AccountPages) openRepo(r *http.Request, d *adminMirrorData, repo string Version: got.ShortRevision(), } rs.Verdict = p.verdictFor(langOf(r), got.Architecture, set.Name) - // A family Strata runs: one card and the experts in RAM, whether - // or not llama.cpp could load it. - if layout, ok := fit.strataLayout(langOf(r), got.ID, set.Name, set.Bytes); ok { + // A pack made for Strata: served whole, from the Strata tab, when + // Strata knows it; named for what it is when it does not. + if fam, size, ok := strata.ForFetchedRepo(got.ID); ok { + rs.Verdict = gguf.Verdict{Checked: true, Reason: langOf(r).T("strata.pack_files", fam.Title, size.Name)} + } else if got.Architecture == "strata-ple" { + rs.Verdict = gguf.Verdict{Checked: true, Reason: langOf(r).T("strata.unknown_pack")} + } else if layout, ok := fit.strataLayout(langOf(r), got.ID, set.Name, set.Bytes); ok { + // A family Strata runs: one card and the experts in RAM, + // whether or not llama.cpp could load it. rs.Verdict, rs.GPULayout = gguf.Verdict{Checked: true, Servable: true}, layout } else if fit != nil && rs.Verdict.Servable { if v, ok := fit.holds(langOf(r), set.Name, set.Bytes, got.Params); !ok { diff --git a/internal/web/admin_strata.go b/internal/web/admin_strata.go new file mode 100644 index 0000000..5cf485d --- /dev/null +++ b/internal/web/admin_strata.go @@ -0,0 +1,153 @@ +package web + +import ( + "context" + "errors" + "slices" + "strconv" + "strings" + "time" + + "gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog" + "gitea.mynodes.xyz/ilker/llmgw/internal/mirror" + "gitea.mynodes.xyz/ilker/llmgw/internal/modelplan" + "gitea.mynodes.xyz/ilker/llmgw/internal/strata" +) + +// The mirror page's Strata tab: the models Strata runs, each size with what +// it downloads and keeps in RAM and where it would run here, and a button +// that fetches a size's files into the mirror, where the version's serve +// form then serves it on Strata. + +type strataView struct { + // Nodes are the nodes that have Strata installed. + Nodes []string + Families []strataFamilyRow +} + +type strataFamilyRow struct { + Name, Title, Repo string + // Fetchable is a family whose files llmgw fetches into the mirror; + // FetchesItself one whose files Strata fetches onto the node, served + // from here at once. + Fetchable, FetchesItself bool + Sizes []strataSizeRow +} + +type strataSizeRow struct { + Size string + DownloadGB, RAMGB string + Runs string + Fits, NVIDIAOnly bool +} + +func (p *AccountPages) strataView(l *Lang) *strataView { + v := &strataView{} + var pool []modelplan.PoolNode + if fit := p.fitOf(); fit != nil { + pool = fit.pool + } + for _, n := range pool { + if slices.Contains(n.Engines, "strata") { + v.Nodes = append(v.Nodes, n.ID) + } + } + for _, f := range strata.Families { + row := strataFamilyRow{Name: f.Name, Title: f.Title, Repo: f.Repo, Fetchable: f.Fetchable(), + FetchesItself: f.FetchesItself()} + for _, s := range strata.SizesOf(f.Name) { + sr := strataSizeRow{Size: s.Name, DownloadGB: l.Num(s.DownloadGB, 0), RAMGB: l.Int(int64(s.MemoryGB())), + NVIDIAOnly: s.NVIDIAOnly} + opts := modelplan.StrataOptions(f.Name, []modelplan.Artifact{{Name: s.Name + ".gguf", Format: "gguf", + Bytes: int64(s.DownloadGB * 1e9)}}, pool) + switch { + case len(opts) > 0: + 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") + default: + sr.Runs = l.T("strata.no_room", s.MemoryGB()) + } + row.Sizes = append(row.Sizes, sr) + } + v.Families = append(v.Families, row) + } + return v +} + +// 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) { + fam, ok := strata.FamilyNamed(family) + s, err := strata.SizeOf(family, size) + if !ok || err != nil { + return "", errors.New(l.T("strata.err.unknown", family, size)) + } + if !fam.FetchesItself() || p.catalog == nil { + return "", errors.New(l.T("strata.err.not_servable", fam.Title)) + } + var pool []modelplan.PoolNode + if fit := p.fitOf(); fit != nil { + pool = fit.pool + } + opts := modelplan.StrataOptions(fam.Name, []modelplan.Artifact{{Name: s.Name + ".gguf", Format: "gguf"}}, pool) + if len(opts) == 0 { + return "", errors.New(l.T("strata.no_room", s.MemoryGB())) + } + name := fam.Model + "-" + strings.ToLower(s.Name) + disk := s.DownloadGB + if s.DiskGB > disk { + disk = s.DiskGB + } + m := catalog.Model{Name: name, Source: strata.RepoOf(fam, s), RoutingProfile: "chat", ContextLength: strataContext, + Scaling: catalog.Scaling{Min: 1, Max: 1}, + Profiles: []catalog.Profile{{Name: "strata-" + opts[0].Class, Engine: "strata", GPUClass: opts[0].Class, + GPUsPerNode: 1, Weight: 1, Priority: 100, MinMemoryGB: s.MemoryGB(), MinDiskGB: int(disk) + 10, + Args: []string{"--family", fam.Name, "--size", s.Name, "--context", strconv.Itoa(strataContext), "--vision", "no"}, + // Its files come onto the node on its first start, tens of + // gigabytes from the internet, before it is prepared. + StartupTimeout: 4 * time.Hour}}} + if err := p.catalog.Add(ctx, m, by); err != nil { + return "", err + } + return l.T("strata.msg.served", name, l.Num(disk, 0)), nil +} + +// strataContext is the context a Strata model is served with from here. +const strataContext = 32768 + +// fetchStrata fetches a Strata size's files from its family's repository +// into the mirror, each checked against the sha256 the registry published. +func (p *AccountPages) fetchStrata(ctx context.Context, l *Lang, m MirrorAdmin, family, size string) (string, error) { + fam, ok := strata.FamilyNamed(family) + if _, err := strata.SizeOf(family, size); !ok || err != nil { + return "", errors.New(l.T("strata.err.unknown", family, size)) + } + if !fam.Fetchable() || p.registry == nil { + return "", errors.New(l.T("strata.err.not_fetchable", fam.Title)) + } + repo, err := p.registry.Repo(ctx, fam.Repo) + if err != nil { + return "", err + } + var paths []string + sums := map[string]string{} + for _, f := range repo.Files { + paths = append(paths, f.Path) + sums[f.Path] = f.SHA256 + } + files := strata.FilesOf(size, paths) + if len(files) == 0 { + return "", errors.New(l.T("strata.err.no_files", fam.Repo, size)) + } + model := fam.Model + "-" + strings.ToLower(size) + version := repo.ShortRevision() + for i, path := range files { + req := mirror.FetchRequest{Source: repo.DownloadURL("", path), Model: model, Version: version, + File: mirror.LastPathElement(path), SHA256: sums[path]} + if _, err := m.Fetch(ctx, req); err != nil && !strings.Contains(err.Error(), "already") { + return "", errors.New(l.T("mirror.err.partly_started", i, len(files), req.File, l.Err(err))) + } + } + return l.T("mirror.msg.started", model, version, l.N("mirror.files", int64(len(files)))), nil +} diff --git a/internal/web/admin_strata_test.go b/internal/web/admin_strata_test.go new file mode 100644 index 0000000..99331d2 --- /dev/null +++ b/internal/web/admin_strata_test.go @@ -0,0 +1,88 @@ +package web + +import ( + "net/url" + "strings" + "testing" + "time" + + "gitea.mynodes.xyz/ilker/llmgw/internal/control/adminapi" + "gitea.mynodes.xyz/ilker/llmgw/internal/hf" +) + +// strataFleet is one node with an RTX 6000, 251 GB and Strata installed. +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"}}} +} + +func TestTheStrataTabListsWhatRunsHereAndFetchesASize(t *testing.T) { + reg := &fakeRegistry{repo: hf.Repo{ID: "ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF", + Revision: "0123456789abcdef0123456789abcdef01234567", Files: []hf.File{ + {Path: "IQ2_XS/Qwen3.8-Flash-Next-GSQ-RCO-IQ2_XS-00001-of-00002.gguf", SHA256: "aa"}, + {Path: "IQ2_XS/Qwen3.8-Flash-Next-GSQ-RCO-IQ2_XS-00002-of-00002.gguf", SHA256: "bb"}, + {Path: "IQ3_S/Qwen3.8-Flash-Next-GSQ-RCO-IQ3_S-00001-of-00002.gguf"}, + {Path: "mmproj-Qwen3.8-Flash-Next-BF16.gguf"}, + }}} + m := &fakeMirror{} + mux := mirrorPageDeps(t, MirrorDeps{Mirror: m, Registry: reg, Fleet: strataFleet{}}, true) + + 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"} { + 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"`) { + t.Error("offered DeepSeek's 306 GB size, which no node holds, and which Strata fetches itself") + } + + mirrorPost(t, mux, "/admin/mirror/strata", url.Values{"csrf": {"tok"}, "family": {"qwen"}, "size": {"IQ2_XS"}}) + if len(m.all) != 2 { + t.Fatalf("fetched %+v", m.all) + } + f := m.all[0] + if f.Model != "qwen3.8-flash-next-iq2_xs" || f.Version != "01234567" || f.SHA256 != "aa" || + !strings.Contains(f.Source, "/resolve/0123456789abcdef0123456789abcdef01234567/IQ2_XS/") { + t.Errorf("%+v", f) + } +} + +// A ready-made pack is served at once, its repository the source, Strata +// fetching it onto the node. +func TestAReadyMadeStrataPackIsServedFromTheTab(t *testing.T) { + cat := &fakeCatalog{} + mux := mirrorPageDeps(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: &fakeRegistry{}, Catalog: cat, Fleet: strataFleet{}}, true) + if body := mirrorGet(t, mux, "/admin/mirror?tab=strata").Body.String(); !strings.Contains(body, `name="action" value="serve"`) { + t.Fatal("no serve button for a pack") + } + mirrorPost(t, mux, "/admin/mirror/strata", url.Values{"csrf": {"tok"}, "action": {"serve"}, "family": {"nvfp4"}, "size": {"NVFP4"}}) + if len(cat.added) != 1 { + t.Fatalf("added %+v", cat.added) + } + m := cat.added[0] + p := m.Profiles[0] + if m.Name != "qwen3.8-flash-next-nvfp4-nvfp4" || m.Source != "Maximilian228/Qwen3.8-Flash-Next-NVFP4-GPTQ-Strata" || + p.Engine != "strata" || p.GPUClass != "6000-24g" || p.MinMemoryGB != 84 || p.StartupTimeout < time.Hour || + strings.Join(p.Args, " ") != "--family nvfp4 --size NVFP4 --context 32768 --vision no" { + t.Errorf("%+v", m) + } +} + +func TestAStrataPacksFilesAreNamedForWhatTheyAre(t *testing.T) { + for repo, want := range map[string]string{ + "Maximilian228/Qwen3.8-Flash-Next-NVFP4-GPTQ-Strata": "ready-made Qwen3.8-Flash-Next (NVFP4) NVFP4 pack", + "trymirai/Qwen3.8-Flash-Next-S-strata-experimental": "Strata 0.2.7 does not know", + } { + reg := &fakeRegistry{repo: hf.Repo{ID: repo, Architecture: "strata-ple", Revision: "abc", + Files: []hf.File{{Path: "ple-fp8.gguf", Bytes: 51e9}}}} + mux := mirrorPageDeps(t, MirrorDeps{Mirror: &fakeMirror{}, Registry: reg, Fleet: strataFleet{}}, true) + if body := mirrorGet(t, mux, "/admin/mirror?repo="+repo).Body.String(); !strings.Contains(body, want) { + t.Errorf("%s: no %q", repo, want) + } + } +} diff --git a/internal/web/locales/en/mirror.json b/internal/web/locales/en/mirror.json index 1b08e7e..85ab8ef 100644 --- a/internal/web/locales/en/mirror.json +++ b/internal/web/locales/en/mirror.json @@ -257,5 +257,26 @@ "mirror.err.wrong_arch": "that adapter is for %s models and %s is %s", "mirror.msg.adapter_added": "%[1]s: adapter %[2]s added; ask for it by that name once the replicas are restarted (llmgw ctl restart --model %[1]s)", "mirror.err.adapter_op": "add or remove an adapter", - "explore.strata": "1× %s with Strata, %s GB of RAM" + "explore.strata": "1× %s with Strata, %s GB of RAM", + "mirror.tab.strata": "Strata", + "strata.intro": "Strata runs these mixture-of-experts models with the experts in a node's RAM and the rest on one card of 12 GB or more: a model that fits no card, served from one. Fetch a size into the mirror; its serve form on the mirror's tab then serves it on Strata. The first start prepares it on the node, which takes minutes.", + "strata.nodes": "Strata is installed on: %s", + "strata.none": "No node has Strata installed. Name it among the cluster's engines (llmgw ctl config set engines.strata 0.2.7) and the nodes with an NVIDIA card install it.", + "strata.self": "Strata fetches these onto the node itself, each file pinned and checked, on the model's first start: served from here, they do not go through the mirror.", + "strata.col.size": "Size", + "strata.col.download": "Download", + "strata.col.ram": "RAM it keeps", + "strata.col.runs": "Here", + "strata.fetch": "Fetch into the mirror", + "strata.nvidia": "NVIDIA only", + "strata.no_node": "no node has Strata", + "strata.no_room": "no node with Strata has %d GB of RAM and a card of 12 GB", + "strata.err.unknown": "Strata runs no %s %s", + "strata.err.not_fetchable": "Strata fetches %s itself; serve it with a definition that names its family and size.", + "strata.err.no_files": "%s has no files of size %s", + "strata.serve": "Serve", + "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" } diff --git a/internal/web/locales/tr/mirror.json b/internal/web/locales/tr/mirror.json index 534491f..18f8a87 100644 --- a/internal/web/locales/tr/mirror.json +++ b/internal/web/locales/tr/mirror.json @@ -250,5 +250,26 @@ "mirror.err.wrong_arch": "bu adaptör %s modelleri için; %s ise %s", "mirror.msg.adapter_added": "%[1]s: %[2]s adaptörü eklendi; replikalar yeniden başlatılınca bu adla isteyin (llmgw ctl restart --model %[1]s)", "mirror.err.adapter_op": "bir adaptör ekleyin ya da kaldırın", - "explore.strata": "Strata ile 1× %s, %s GB RAM" + "explore.strata": "Strata ile 1× %s, %s GB RAM", + "mirror.tab.strata": "Strata", + "strata.intro": "Strata bu uzman karışımı (MoE) modelleri, uzmanları node'un RAM'inde, gerisini 12 GB ya da daha büyük tek bir kartta tutarak çalıştırır: hiçbir karta sığmayan bir model tek karttan sunulur. Bir boyutu aynaya indirin; aynadakiler sekmesindeki sunma formu onu Strata ile sunar. İlk açılışta model node üzerinde hazırlanır; bu birkaç dakika sürer.", + "strata.nodes": "Strata kurulu node'lar: %s", + "strata.none": "Hiçbir node'da Strata kurulu değil. Kümenin motorları arasında adını verin (llmgw ctl config set engines.strata 0.2.7); NVIDIA kartı olan node'lar onu kurar.", + "strata.self": "Bunları Strata, modelin ilk açılışında her dosyayı sabitleyip denetleyerek node'a kendisi indirir: buradan sunulur, aynadan geçmez.", + "strata.col.size": "Boyut", + "strata.col.download": "İndirme", + "strata.col.ram": "Ayırdığı RAM", + "strata.col.runs": "Burada", + "strata.fetch": "Aynaya indir", + "strata.nvidia": "Yalnız NVIDIA", + "strata.no_node": "Strata hiçbir node'da yok", + "strata.no_room": "Strata kurulu hiçbir node'da %d GB RAM ve 12 GB'lık kart yok", + "strata.err.unknown": "Strata %s %s çalıştırmıyor", + "strata.err.not_fetchable": "%s dosyalarını Strata kendisi indirir; ailesini ve boyutunu veren bir tanımla sunun.", + "strata.err.no_files": "%s içinde %s boyutunda dosya yok", + "strata.serve": "Sun", + "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" } diff --git a/internal/web/templates/admin_mirror.html b/internal/web/templates/admin_mirror.html index 8d7a1af..ee09abe 100644 --- a/internal/web/templates/admin_mirror.html +++ b/internal/web/templates/admin_mirror.html @@ -424,6 +424,40 @@ document.querySelector("table.explore").addEventListener("submit", async functio {{if .Repo}}{{template "mirror_repo" .}}{{end}} {{end}} +{{if and (eq .Tab "strata") .Strata}} +

{{t "strata.intro"}}

+{{if .Strata.Nodes}}

{{t "strata.nodes" (join .Strata.Nodes)}}

+{{else}}

{{th "strata.none"}}

{{end}} +{{range .Strata.Families}} +

{{.Title}} --family {{.Name}}{{if .Repo}} · {{.Repo}}{{end}}

+{{if .FetchesItself}}

{{t "strata.self"}}

{{end}} + + + {{$fam := .}} + {{range .Sizes}} + + + + + + + + {{end}} +
{{t "strata.col.size"}}{{t "strata.col.download"}}{{t "strata.col.ram"}}{{t "strata.col.runs"}}
{{.Size}}{{if .NVIDIAOnly}} {{t "strata.nvidia"}}{{end}}{{.DownloadGB}} GB{{.RAMGB}} GB{{if .Fits}}{{.Runs}}{{else}}{{.Runs}}{{end}}{{if and $fam.FetchesItself .Fits}}
+ + + + + +
{{end}}{{if and $fam.Fetchable .Fits}}
+ + + + +
{{end}}
+{{end}} +{{end}} + {{if eq .Tab "fetch"}}

{{t "mirror.fetch.intro"}}