Strata's families and sizes known: checked when a model is added, its memory reserved from them

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5.5 committed 2026-10-30 10:17:42 +01:00
1 parent 91f668fe7e
commit 7465c24830
7 files changed
+231 -16

No files matched your search

+4 -2
View File
@@ -1876,12 +1876,14 @@ served only from a definition written by hand. In order:
- N2. Its engine runs in a session of its own and survived a stop, holding the card and the RAM: the supervisor
now finds what left the group before signalling it. Done 2026-10-30.
- N3. Host memory: the experts live in RAM (40 GB for Qwen3.8-Flash-Next IQ2_XS, 72–136 GB for Unsloth's) and
nothing reserved it. A Strata profile added now must say `min_memory_gb`. Done 2026-10-30; derived from a table: B.
nothing reserved it. A Strata profile added now reserves what its size takes, from the table (N5), unless it says
`min_memory_gb`. Done 2026-10-30.
- N4. Pascal and Volta: installed (CUDA 12.9 build) and then refused (7.0 floor). Floor now 6.0. Done 2026-10-30.
**B. Offered where models are chosen.**
- N5. A table of Strata's families and sizes (qwen, coder, swift, unsloth, gyro, nvfp4, qwen36, deepseek41) with
RAM+VRAM, disk and measured speed; the catalog refuses an unknown family/size (and requires `--size`).
download and RAM, from Strata 0.2.7's setup (internal/strata/models.go); the catalog refuses an unknown family or
size, and requires `--size`. Done 2026-10-30. Speeds measured: Qwen3.8-Flash-Next IQ2_XS 70–87 tok/s on an RTX 6000.
- N6. `modelplan`, `ctl model plan` and `ctl model serve` offer "Strata on one card + N GB RAM" for a model it runs,
with its speed; a large MoE that fits no card is not "fits nowhere".
- N7. The web serve page, the explorer's fit, the console's serve form: the same choice.
+6 -1
View File
@@ -22,6 +22,7 @@ import (
"gitea.mynodes.xyz/ilker/llmgw/internal/control/facility"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/placement"
"gitea.mynodes.xyz/ilker/llmgw/internal/guard"
"gitea.mynodes.xyz/ilker/llmgw/internal/strata"
)
// ErrExists is a model added only if absent that was there already.
@@ -1201,10 +1202,14 @@ func (m Model) validate() error {
if p.Nodes > 1 || p.TP > 1 || p.PP > 1 || p.GPUsPerNode > 1 {
return fmt.Errorf("model %q profile %q: strata runs a replica on one GPU of one node", m.Name, p.Name)
}
if !slices.ContainsFunc(p.Args, func(a string) bool { return a == "--family" || strings.HasPrefix(a, "--family=") }) {
family, size := strata.ArgValue(p.Args, "--family"), strata.ArgValue(p.Args, "--size")
if family == "" || size == "" {
return fmt.Errorf("model %q profile %q: strata needs the model's family and size in args "+
"(--family qwen --size IQ2_XS)", m.Name, p.Name)
}
if _, err := strata.SizeOf(family, size); err != nil {
return fmt.Errorf("model %q profile %q: %w", m.Name, p.Name, err)
}
if strings.HasSuffix(strings.ToLower(strings.TrimSuffix(m.SourceOf(p), "/")), ".gguf") {
return fmt.Errorf("model %q profile %q: strata prepares a model from all of its GGUF files; "+
"give the directory they are in as the source", m.Name, p.Name)
+7 -1
View File
@@ -20,11 +20,17 @@ func TestAStrataProfileNamesItsFamilyAndRunsOnOneCard(t *testing.T) {
"family and size": {Name: "x", Source: "http://mirror:8081/x/1/", Profiles: []Profile{{
Name: "p", Engine: "strata", GPUClass: "6000-24g", Args: []string{"--size", "IQ2_XS"}}}},
"all of its GGUF files": {Name: "x", Source: "http://mirror:8081/x/1/x-00001-of-00002.gguf", Profiles: []Profile{{
Name: "p", Engine: "strata", GPUClass: "6000-24g", Args: []string{"--family", "qwen", "--size", "IQ2_XS"}}}},
"no family \"llama\"": {Name: "x", Source: "http://mirror:8081/x/1/", Profiles: []Profile{{
Name: "p", Engine: "strata", GPUClass: "6000-24g", Args: []string{"--family", "llama", "--size", "IQ2_XS"}}}},
"comes in IQ1_M": {Name: "x", Source: "http://mirror:8081/x/1/", Profiles: []Profile{{
Name: "p", Engine: "strata", GPUClass: "6000-24g", Args: []string{"--family", "coder", "--size", "IQ2_XS"}}}},
"family and size ": {Name: "x", Source: "http://mirror:8081/x/1/", Profiles: []Profile{{
Name: "p", Engine: "strata", GPUClass: "6000-24g", Args: []string{"--family", "qwen"}}}},
"--gguf-dir": {Name: "x", Source: "http://mirror:8081/x/1/", Profiles: []Profile{{
Name: "p", Engine: "strata", GPUClass: "6000-24g", Args: []string{"--family", "qwen", "--gguf-dir", "/etc"}}}},
} {
if _, err := New([]Model{bad}); err == nil || !strings.Contains(err.Error(), want) {
if _, err := New([]Model{bad}); err == nil || !strings.Contains(err.Error(), strings.TrimSpace(want)) {
t.Errorf("expected %q, got %v", want, err)
}
}
+13 -6
View File
@@ -27,6 +27,7 @@ import (
"gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog"
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
"gitea.mynodes.xyz/ilker/llmgw/internal/strata"
)
// Store is the persistence this needs.
@@ -129,15 +130,21 @@ func (s *Service) add(ctx context.Context, m catalog.Model, by string, onlyNew b
// fits everywhere as far as placement can tell — however many of it
// there already are. Required of what is added now; models stored before
// this still load, and say what they are missing.
for _, p := range m.Profiles {
m.Profiles = slices.Clone(m.Profiles)
for i := range m.Profiles {
// Strata keeps a model's experts in the host's memory, tens of
// gigabytes of it: placed as if it took none, two replicas, or one
// beside a CPU model, filled a node until one was killed.
if p.Engine == "strata" && p.MinMemoryGB <= 0 {
return fmt.Errorf("model %q profile %q runs on Strata, which keeps the experts in host memory, and declares "+
"none (min_memory_gb): give what Strata's model table says the size takes, e.g. 40 for Qwen3.8-Flash-Next IQ2_XS",
m.Name, p.Name)
// beside a CPU model, filled a node until one was killed. What its
// size takes, from Strata's table, unless the model says.
if p := &m.Profiles[i]; p.Engine == "strata" && p.MinMemoryGB <= 0 {
size, err := strata.SizeOf(strata.ArgValue(p.Args, "--family"), strata.ArgValue(p.Args, "--size"))
if err != nil {
return fmt.Errorf("model %q profile %q: %w", m.Name, p.Name, err)
}
p.MinMemoryGB = size.MemoryGB()
}
}
for _, p := range m.Profiles {
if p.CPUOnly() && p.MinMemoryGB <= 0 {
return fmt.Errorf("model %q profile %q runs without a GPU and declares no memory (min_memory_gb): "+
"placement could not tell when a node is full", m.Name, p.Name)
+11 -6
View File
@@ -439,17 +439,22 @@ func TestAStoppedModelRunsNoneAndStartsAsItWas(t *testing.T) {
}
}
func TestAStrataModelMustSayTheMemoryItsExpertsTake(t *testing.T) {
s, _, _ := newService(t)
func TestAStrataModelReservesTheMemoryItsExpertsTake(t *testing.T) {
s, _, rec := newService(t)
m := model("big")
m.Source = "http://mirror:8081/big/v1/"
m.Profiles = []catalog.Profile{{Name: "gpu", Engine: "strata", GPUClass: "6000-24g", GPUsPerNode: 1,
Args: []string{"--family", "qwen", "--size", "IQ2_XS"}}}
if err := s.Add(t.Context(), m, "ilker"); err == nil || !strings.Contains(err.Error(), "min_memory_gb") {
t.Fatalf("added without its memory: %v", err)
}
m.Profiles[0].MinMemoryGB = 40
if err := s.Add(t.Context(), m, "ilker"); err != nil {
t.Fatal(err)
}
got, _ := rec.last.Resolve("big")
if got.Profiles[0].MinMemoryGB != 52 {
t.Errorf("IQ2_XS reserves %d GB, want Strata's 48 and 4 for the engine", got.Profiles[0].MinMemoryGB)
}
m.Name = "big2"
m.Profiles[0].Args = []string{"--family", "qwen", "--size", "IQ9"}
if err := s.Add(t.Context(), m, "ilker"); err == nil || !strings.Contains(err.Error(), "comes in") {
t.Errorf("an unknown size: %v", err)
}
}
+161
View File
@@ -0,0 +1,161 @@
package strata
import (
"fmt"
"slices"
"strings"
)
// What Strata runs, as its setup (0.2.7, the version llmgw installs) lists
// it: a few model families, each in a few sizes, with what a size takes.
// Written down here so a model is checked when it is added rather than when
// a node's setup refuses it minutes later, and so the plan can offer Strata
// for a model it runs, with the memory to reserve.
// Family is a model Strata runs, from one publisher's files.
type Family struct {
// Name is setup's --family.
Name string
Title string
// Repo is the Hugging Face repository its files are in; empty when
// Strata fetches them itself from several.
Repo string
// Subdir is whether each size's files are in a directory of its own,
// named as the size, in the repository.
Subdir bool
// Engine is a family that runs on an engine path of its own, with a
// download of its own (DeepSeek V4.1).
Engine bool
}
// Size is one of a family's sizes: setup's --model, which llmgw's profile
// says as --size.
type Size struct {
Name string
Families []string
// DownloadGB is what is fetched; DiskGB what it takes on the node once
// prepared, when more than the download.
DownloadGB, DiskGB float64
// RAMGB is the host memory that keeps every expert in RAM: what a
// replica reserves. Strata can read the rest from the disk with less,
// much slower.
RAMGB float64
// NVIDIAOnly is a size whose kernels only NVIDIA cards have.
NVIDIAOnly bool
}
// 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},
}
// Sizes are the sizes Strata 0.2.7 runs, and which families have each.
var Sizes = []Size{
{Name: "Q2_0", Families: []string{"qwen"}, DownloadGB: 66.4, RAMGB: 48},
{Name: "IQ2_XS", Families: []string{"qwen", "swift"}, DownloadGB: 68.0, RAMGB: 48},
{Name: "IQ3_XXS", Families: []string{"qwen", "swift"}, DownloadGB: 75.8, RAMGB: 60},
{Name: "IQ3_S", Families: []string{"qwen", "swift"}, DownloadGB: 83.6, RAMGB: 62},
{Name: "IQ1_M", Families: []string{"coder"}, DownloadGB: 58.4, RAMGB: 32},
{Name: "UD-IQ4_XS", Families: []string{"unsloth"}, DownloadGB: 93.7, RAMGB: 80},
{Name: "UD-Q4_K_XL", Families: []string{"unsloth", "qwen36"}, DownloadGB: 111.3, RAMGB: 94},
{Name: "UD-Q5_K_XL", Families: []string{"unsloth"}, DownloadGB: 158.3, RAMGB: 108},
{Name: "UD-Q6_K_XL", Families: []string{"unsloth", "qwen36"}, DownloadGB: 169.2, RAMGB: 118},
{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: "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},
{Name: "UD-Q8_K_XL", Families: []string{"deepseek41"}, DownloadGB: 520, RAMGB: 306},
}
// qwen36 is smaller than the Flash-Next sizes of the same names: its own
// figures.
var qwen36RAM = map[string]float64{"UD-Q4_K_XL": 32, "UD-Q6_K_XL": 44, "Q8_0": 52}
var qwen36Download = map[string]float64{"UD-Q4_K_XL": 22.9, "UD-Q6_K_XL": 32.6, "Q8_0": 37.8}
// FamilyNamed is a family by its --family.
func FamilyNamed(name string) (Family, bool) {
i := slices.IndexFunc(Families, func(f Family) bool { return f.Name == name })
if i < 0 {
return Family{}, false
}
return Families[i], true
}
// SizeOf is a family's size by its --size, with that family's figures.
func SizeOf(family, size string) (Size, error) {
if _, ok := FamilyNamed(family); !ok {
return Size{}, fmt.Errorf("strata runs no family %q; it runs %s", family, strings.Join(familyNames(), ", "))
}
var names []string
for _, s := range Sizes {
if !slices.Contains(s.Families, family) {
continue
}
names = append(names, s.Name)
if s.Name == size {
if family == "qwen36" {
s.RAMGB, s.DownloadGB = qwen36RAM[size], qwen36Download[size]
}
return s, nil
}
}
return Size{}, fmt.Errorf("strata's %s comes in %s, not %q", family, strings.Join(names, ", "), size)
}
// ForRepo is the family whose files a Hugging Face repository holds.
func ForRepo(repo string) (Family, bool) {
i := slices.IndexFunc(Families, func(f Family) bool { return f.Repo != "" && strings.EqualFold(f.Repo, repo) })
if i < 0 {
return Family{}, false
}
return Families[i], true
}
// SizesOf are a family's sizes, with its figures.
func SizesOf(family string) []Size {
var out []Size
for _, s := range Sizes {
if slices.Contains(s.Families, family) {
got, _ := SizeOf(family, s.Name)
out = append(out, got)
}
}
return out
}
// MemoryGB is what a replica of a size reserves of the host: its experts,
// and the engine and server beside them.
func (s Size) MemoryGB() int { return int(s.RAMGB+0.999) + 4 }
func familyNames() []string {
out := make([]string, len(Families))
for i, f := range Families {
out[i] = f.Name
}
return out
}
// ArgValue is a flag's value in a profile's arguments, "" when absent.
func ArgValue(args []string, flag string) string {
for i, a := range args {
if a == flag && i+1 < len(args) {
return args[i+1]
}
if v, ok := strings.CutPrefix(a, flag+"="); ok {
return v
}
}
return ""
}
+29
View File
@@ -0,0 +1,29 @@
package strata
import (
"strings"
"testing"
)
func TestASizeIsKnownForTheFamiliesThatHaveIt(t *testing.T) {
if s, err := SizeOf("qwen", "IQ2_XS"); err != nil || s.RAMGB != 48 || s.MemoryGB() != 52 {
t.Errorf("%+v %v", s, err)
}
if s, err := SizeOf("qwen36", "Q8_0"); err != nil || s.RAMGB != 52 {
t.Errorf("qwen36's own figures: %+v %v", s, err)
}
if _, err := SizeOf("coder", "IQ2_XS"); err == nil || !strings.Contains(err.Error(), "IQ1_M") {
t.Errorf("coder in IQ2_XS: %v", err)
}
if _, err := SizeOf("llama", "Q8_0"); err == nil || !strings.Contains(err.Error(), "deepseek41") {
t.Errorf("an unknown family: %v", err)
}
if f, ok := ForRepo("istA-daslab/qwen3.8-flash-next-gsq-rco-gguf"); !ok || f.Name != "qwen" {
t.Errorf("by repository: %+v", f)
}
for _, f := range Families {
if len(SizesOf(f.Name)) == 0 {
t.Errorf("%s has no sizes", f.Name)
}
}
}