feat(gallery): drop the redundant variant min_memory field

Variant.MinMemory was an authored override for when the live probe misreads
a variant's footprint. It duplicated an existing field: probeEntryMemory
already passes the entry's declared size: into EstimateModelMultiContext,
whose cascade prefers that declared size over its own guesswork. Correcting
size: on the referenced entry fixes the figure for every consumer rather
than only for variant selection, so min_memory shadowed the right answer.

A variant is now nothing but a name. Its effective size is exactly the probe
result, and an unknown stays unknown: it survives the filter and ranks last.

EffectiveMemory loses its error return along with the field. The authored
string was the only thing that could fail to parse, so the error had no
remaining source and was propagating dead nil-checks through SelectVariant,
DescribeVariants and the pin warning.

Selection behaviour is unchanged. The specs covering probe-derived sizing,
ranking, filtering, the unknown-size path, pin recall, entry/variant
metadata split and deep-copy isolation all survive; the three install specs
that needed a definite size now declare it through the referenced entry's
own size:, which exercises the documented escape hatch directly.

gallery/index.yaml is untouched: no entry ever carried the key.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto
2026-07-18 23:18:12 +00:00
parent 77d01de6ff
commit 84de7a68a8
11 changed files with 126 additions and 277 deletions

View File

@@ -119,10 +119,11 @@ Rules:
and installs the largest survivor, falling back to the declaring entry's own
build. Sizes are measured live from the weights and cached, so nothing has to
be written down.
- `min_memory` on a single variant (e.g. `min_memory: 20GiB`) overrides the
measured size and suppresses the measurement for that variant. Use it only
when you have measured a real load and know the estimate is wrong; most
variants should not carry it.
- A variant is nothing but a name; there is no per-variant memory field. When
the measured size for a build is wrong, correct it on the referenced entry by
setting that entry's own `size:` (e.g. `size: "20GiB"`). The estimator prefers
a declared size over its own guesswork, so the fix applies everywhere the size
is shown or compared rather than only to variant selection.
Users can override the automatic choice with `variant` on `POST /models/apply`,
`local-ai models install --variant`, or the `install_model` MCP tool. See

View File

@@ -63,10 +63,7 @@ func DescribeVariants(models []*GalleryModel, entry *GalleryModel, env ResolveEn
views := make([]VariantView, 0, len(options))
for _, o := range options {
memory, known, err := o.EffectiveMemory()
if err != nil {
return nil, err
}
memory, known := o.EffectiveMemory()
view := VariantView{
Model: o.Variant.Model,
Backend: o.Backend,

View File

@@ -124,17 +124,6 @@ var _ = Describe("DescribeVariants", func() {
Expect(byName(view, "qwen3-8b-gguf-q8").MemoryBytes).To(Equal(gib(9)))
})
It("lets an authored min_memory win over the probe and skips that probe", func() {
base.Variants = []gallery.Variant{{Model: "qwen3-8b-vllm-awq", MinMemory: "20GiB"}}
view, err := gallery.DescribeVariants(models, base, probing(gib(24), map[string]uint64{
"qwen3-8b-vllm-awq": gib(2),
}))
Expect(err).ToNot(HaveOccurred())
Expect(byName(view, "qwen3-8b-vllm-awq").MemoryBytes).To(Equal(gib(20)))
Expect(probed).ToNot(ContainElement("qwen3-8b-vllm-awq"))
})
It("reports a size it could not determine as unknown rather than as free", func() {
// Zero on the wire means unknown. Rendering it as a zero-byte model
// would advertise the largest download on offer as costless.

View File

@@ -105,9 +105,7 @@ func variantOptions(models []*GalleryModel, entry *GalleryModel, env ResolveEnv)
}
option := VariantOption{Variant: v, Backend: target.Backend}
// An authored figure short circuits the probe: the author already
// answered the question the probe would spend a round trip asking.
if v.MinMemory == "" && env.ProbeMemory != nil {
if env.ProbeMemory != nil {
option.ProbedMemory = env.ProbeMemory(target)
}
options = append(options, option)
@@ -206,7 +204,7 @@ func ResolveVariant(models []*GalleryModel, entry *GalleryModel, env ResolveEnv,
// checks, but a silent bypass makes a later out-of-memory failure
// impossible to trace back to the pin, so it is recorded loudly here.
if pin != "" {
if need, known, verr := selected.EffectiveMemory(); verr == nil && known && env.AvailableMemory < need {
if need, known := selected.EffectiveMemory(); known && env.AvailableMemory < need {
xlog.Warn("Pinned model variant declares more memory than this system reports; installing anyway because the pin overrides hardware resolution",
"model", entry.Name, "variant", selected.Variant.Model, "required_memory", need, "available_memory", env.AvailableMemory)
}

View File

@@ -31,9 +31,10 @@ type VariantOption struct {
// must stay installable on every host and for every client.
IsBase bool
// ProbedMemory is the footprint measured live from the referenced entry's
// weights, in bytes. It is only consulted when the variant declares no
// min_memory of its own, because a human who measured a real load knows
// more than a pre-download estimate does.
// weights, in bytes. It is the only source of a variant's size. An author
// who needs to correct a bad estimate sets `size:` on the referenced entry,
// which the estimator behind the probe already prefers over its own
// guesswork, so the correction lands everywhere the size is used.
//
// Zero means the probe could not determine a size. That is an unknown, not
// a zero requirement: a probe that cannot reach the network must never be
@@ -42,17 +43,12 @@ type VariantOption struct {
}
// EffectiveMemory returns this option's memory requirement in bytes and whether
// one is known at all: the authored figure when there is one, else the live
// probe result, else nothing.
func (o VariantOption) EffectiveMemory() (uint64, bool, error) {
size, known, err := o.Variant.AuthoredMinMemory()
if err != nil || known {
return size, known, err
}
// one is known at all.
func (o VariantOption) EffectiveMemory() (uint64, bool) {
if o.ProbedMemory > 0 {
return o.ProbedMemory, true, nil
return o.ProbedMemory, true
}
return 0, false, nil
return 0, false
}
// ResolveEnv describes the host a variant is selected for.
@@ -69,13 +65,12 @@ type ResolveEnv struct {
// caller with no view of the hardware.
BackendCompatible func(backend string) bool
// ProbeMemory measures how much memory a referenced gallery entry needs,
// without downloading it. It is consulted only for variants that declare no
// min_memory, and a zero result means "could not tell", never "needs
// nothing".
// without downloading it. A zero result means "could not tell", never
// "needs nothing".
//
// It is a func field rather than a live network handle so specs can pin an
// exact size, or an exact failure, without reaching the internet. A nil func
// leaves every unauthored variant unknown, which selection already handles.
// leaves every variant unknown, which selection already handles.
//
// SelectVariant never calls this: the install layer resolves every size into
// VariantOption.ProbedMemory first, so the selector stays pure.
@@ -149,10 +144,7 @@ func SelectVariant(options []VariantOption, env ResolveEnv, pin string) (Variant
continue
}
memory, known, err := o.EffectiveMemory()
if err != nil {
return VariantSelection{}, err
}
memory, known := o.EffectiveMemory()
if known && memory > env.AvailableMemory {
reasons = append(reasons, fmt.Sprintf("%s needs %s of memory", o.Variant.Model, humanBytes(memory)))
continue

View File

@@ -8,69 +8,49 @@ import (
)
var _ = Describe("VariantOption.EffectiveMemory", func() {
It("reports no requirement when nothing is authored and nothing was probed", func() {
It("reports no requirement when nothing was probed", func() {
o := gallery.VariantOption{Variant: gallery.Variant{Model: "x"}}
size, known, err := o.EffectiveMemory()
Expect(err).ToNot(HaveOccurred())
size, known := o.EffectiveMemory()
Expect(known).To(BeFalse())
Expect(size).To(Equal(uint64(0)))
})
It("uses the probed size when nothing is authored", func() {
It("uses the probed size", func() {
o := gallery.VariantOption{Variant: gallery.Variant{Model: "x"}, ProbedMemory: 6 * 1024 * 1024 * 1024}
size, known, err := o.EffectiveMemory()
Expect(err).ToNot(HaveOccurred())
size, known := o.EffectiveMemory()
Expect(known).To(BeTrue())
Expect(size).To(Equal(uint64(6 * 1024 * 1024 * 1024)))
})
It("lets an authored figure win over a probed one", func() {
// A human who measured a real load knows more than a pre-download
// estimate does, which is the entire reason min_memory survives.
o := gallery.VariantOption{
Variant: gallery.Variant{Model: "x", MinMemory: "20GiB"},
ProbedMemory: 6 * 1024 * 1024 * 1024,
}
size, known, err := o.EffectiveMemory()
Expect(err).ToNot(HaveOccurred())
Expect(known).To(BeTrue())
Expect(size).To(Equal(uint64(20 * 1024 * 1024 * 1024)))
})
It("treats a failed probe as unknown rather than as a zero requirement", func() {
// A probe that could not reach the network reports 0. Reading that as
// "needs nothing" would make an unreachable host look like the perfect
// fit and hand the user the largest download on offer.
o := gallery.VariantOption{Variant: gallery.Variant{Model: "x"}, ProbedMemory: 0}
_, known, err := o.EffectiveMemory()
Expect(err).ToNot(HaveOccurred())
_, known := o.EffectiveMemory()
Expect(known).To(BeFalse())
})
It("errors on an unparseable authored figure rather than silently ignoring it", func() {
o := gallery.VariantOption{Variant: gallery.Variant{Model: "x", MinMemory: "twenty gigs"}}
_, _, err := o.EffectiveMemory()
Expect(err).To(HaveOccurred())
})
})
var _ = Describe("SelectVariant", func() {
gib := func(n uint64) uint64 { return n * 1024 * 1024 * 1024 }
option := func(model, backend, minMemory string) gallery.VariantOption {
// Every size here is a probed one, because the probe is now the only source
// a variant's footprint can come from. A zero stands for the probe having
// been unable to tell, which is an unknown rather than a zero requirement.
option := func(model, backend string, probed uint64) gallery.VariantOption {
return gallery.VariantOption{
Variant: gallery.Variant{Model: model, MinMemory: minMemory},
Backend: backend,
Variant: gallery.Variant{Model: model},
Backend: backend,
ProbedMemory: probed,
}
}
// The base entry declares no memory requirement of its own, so a base
// option's size can only ever come from a probe. It is exempt from every
// filter regardless, which the fallback specs below pin down.
// The base is exempt from every filter, which the fallback specs below pin
// down; its size is carried only so ranking has nothing special to do.
base := func(model string, probed uint64) gallery.VariantOption {
o := option(model, "llama-cpp", "")
o := option(model, "llama-cpp", probed)
o.IsBase = true
o.ProbedMemory = probed
return o
}
@@ -85,8 +65,8 @@ var _ = Describe("SelectVariant", func() {
// The MLX build is both the largest and the only thing that would
// otherwise win, so nothing but the backend gate can reject it.
options := []gallery.VariantOption{
option("m-mlx-8bit", "mlx", "24GiB"),
option("m-gguf-q8", "llama-cpp", "12GiB"),
option("m-mlx-8bit", "mlx", gib(24)),
option("m-gguf-q8", "llama-cpp", gib(12)),
base("m-gguf-q4", gib(6)),
}
@@ -102,8 +82,8 @@ var _ = Describe("SelectVariant", func() {
// The mirror image of the spec above, so the rejection is proven to
// come from the host and not from something intrinsic to the entry.
options := []gallery.VariantOption{
option("m-mlx-8bit", "mlx", "24GiB"),
option("m-gguf-q8", "llama-cpp", "12GiB"),
option("m-mlx-8bit", "mlx", gib(24)),
option("m-gguf-q8", "llama-cpp", gib(12)),
base("m-gguf-q4", gib(6)),
}
@@ -116,7 +96,7 @@ var _ = Describe("SelectVariant", func() {
})
It("treats every backend as runnable when the host cannot be inspected", func() {
options := []gallery.VariantOption{option("m-mlx-8bit", "mlx", "24GiB"), base("m-gguf-q4", gib(6))}
options := []gallery.VariantOption{option("m-mlx-8bit", "mlx", gib(24)), base("m-gguf-q4", gib(6))}
selection, err := gallery.SelectVariant(options, gallery.ResolveEnv{AvailableMemory: gib(80)}, "")
Expect(err).ToNot(HaveOccurred())
@@ -129,9 +109,9 @@ var _ = Describe("SelectVariant", func() {
// Authored smallest-first, so first-match would take m-q4 and any
// ranking that ignores size would too.
options := []gallery.VariantOption{
option("m-q4", "llama-cpp", "6GiB"),
option("m-q8", "llama-cpp", "12GiB"),
option("m-f16", "llama-cpp", "24GiB"),
option("m-q4", "llama-cpp", gib(6)),
option("m-q8", "llama-cpp", gib(12)),
option("m-f16", "llama-cpp", gib(24)),
base("m-base", gib(2)),
}
@@ -145,9 +125,9 @@ var _ = Describe("SelectVariant", func() {
// Same set, authored largest-first. Order must make no difference at
// all, which is the entire point of dropping ordered first-match.
options := []gallery.VariantOption{
option("m-f16", "llama-cpp", "24GiB"),
option("m-q8", "llama-cpp", "12GiB"),
option("m-q4", "llama-cpp", "6GiB"),
option("m-f16", "llama-cpp", gib(24)),
option("m-q8", "llama-cpp", gib(12)),
option("m-q4", "llama-cpp", gib(6)),
base("m-base", gib(2)),
}
@@ -161,8 +141,8 @@ var _ = Describe("SelectVariant", func() {
// nothing proves it does not fit, but it must never displace a
// variant that is known to fit.
options := []gallery.VariantOption{
option("m-unknown", "llama-cpp", ""),
option("m-q8", "llama-cpp", "12GiB"),
option("m-unknown", "llama-cpp", 0),
option("m-q8", "llama-cpp", gib(12)),
base("m-base", gib(2)),
}
@@ -173,7 +153,7 @@ var _ = Describe("SelectVariant", func() {
It("keeps a variant of unknown size rather than dropping it", func() {
options := []gallery.VariantOption{
option("m-unknown", "llama-cpp", ""),
option("m-unknown", "llama-cpp", 0),
base("m-base", gib(2)),
}
@@ -183,19 +163,13 @@ var _ = Describe("SelectVariant", func() {
Expect(selection.FellBackToBase).To(BeFalse())
})
It("ranks and filters a probed size exactly as an authored one", func() {
// Nothing here declares min_memory, so every figure came from the
// live probe. The probe is the primary source now and authoring the
// exception, so it has to drive both the filter and the ranking.
probed := func(model string, size uint64) gallery.VariantOption {
o := option(model, "llama-cpp", "")
o.ProbedMemory = size
return o
}
It("reports why a probed size that does not fit was rejected", func() {
// Ranking and filtering both run off the probe, so a rejection has to
// be traceable back to the figure the probe returned.
options := []gallery.VariantOption{
probed("m-q4", gib(6)),
probed("m-f16", gib(24)),
probed("m-q8", gib(12)),
option("m-q4", "llama-cpp", gib(6)),
option("m-f16", "llama-cpp", gib(24)),
option("m-q8", "llama-cpp", gib(12)),
base("m-base", gib(2)),
}
@@ -206,7 +180,7 @@ var _ = Describe("SelectVariant", func() {
})
It("admits a variant needing exactly the memory available", func() {
options := []gallery.VariantOption{option("m-q8", "llama-cpp", "12GiB"), base("m-base", gib(2))}
options := []gallery.VariantOption{option("m-q8", "llama-cpp", gib(12)), base("m-base", gib(2))}
selection, err := gallery.SelectVariant(options, gallery.ResolveEnv{AvailableMemory: gib(12)}, "")
Expect(err).ToNot(HaveOccurred())
@@ -217,8 +191,8 @@ var _ = Describe("SelectVariant", func() {
Describe("falling back to the base", func() {
It("selects the base when nothing else fits", func() {
options := []gallery.VariantOption{
option("m-q8", "llama-cpp", "12GiB"),
option("m-f16", "llama-cpp", "24GiB"),
option("m-q8", "llama-cpp", gib(12)),
option("m-f16", "llama-cpp", gib(24)),
base("m-base", gib(2)),
}
@@ -234,7 +208,7 @@ var _ = Describe("SelectVariant", func() {
// The base is exempt from the memory filter, not merely favoured by
// it: there is nothing below it, so refusing here would make an entry
// every older client installs fine uninstallable on newer ones.
options := []gallery.VariantOption{option("m-q8", "llama-cpp", "12GiB"), base("m-base", gib(2))}
options := []gallery.VariantOption{option("m-q8", "llama-cpp", gib(12)), base("m-base", gib(2))}
selection, err := gallery.SelectVariant(options, gallery.ResolveEnv{AvailableMemory: 0}, "")
Expect(err).ToNot(HaveOccurred())
@@ -243,8 +217,8 @@ var _ = Describe("SelectVariant", func() {
It("explains why each variant was rejected", func() {
options := []gallery.VariantOption{
option("m-mlx", "mlx", "8GiB"),
option("m-f16", "llama-cpp", "24GiB"),
option("m-mlx", "mlx", gib(8)),
option("m-f16", "llama-cpp", gib(24)),
base("m-base", gib(2)),
}
@@ -258,7 +232,7 @@ var _ = Describe("SelectVariant", func() {
})
It("errors when the caller supplies no base at all", func() {
options := []gallery.VariantOption{option("m-f16", "llama-cpp", "24GiB")}
options := []gallery.VariantOption{option("m-f16", "llama-cpp", gib(24))}
_, err := gallery.SelectVariant(options, gallery.ResolveEnv{AvailableMemory: gib(4)}, "")
Expect(err).To(MatchError(gallery.ErrNoVariantMatch))
@@ -268,7 +242,7 @@ var _ = Describe("SelectVariant", func() {
Describe("explicit selection", func() {
It("honors a pin the hardware would never have chosen", func() {
options := []gallery.VariantOption{
option("m-mlx", "mlx", "64GiB"),
option("m-mlx", "mlx", gib(64)),
base("m-base", gib(2)),
}
@@ -281,7 +255,7 @@ var _ = Describe("SelectVariant", func() {
})
It("honors a pin naming the base, declining an upgrade that fits", func() {
options := []gallery.VariantOption{option("m-f16", "llama-cpp", "8GiB"), base("m-base", gib(2))}
options := []gallery.VariantOption{option("m-f16", "llama-cpp", gib(8)), base("m-base", gib(2))}
selection, err := gallery.SelectVariant(options, gallery.ResolveEnv{AvailableMemory: gib(64)}, "m-base")
Expect(err).ToNot(HaveOccurred())
@@ -289,7 +263,7 @@ var _ = Describe("SelectVariant", func() {
})
It("matches a pin case-insensitively", func() {
options := []gallery.VariantOption{option("m-f16", "llama-cpp", "8GiB"), base("m-base", gib(2))}
options := []gallery.VariantOption{option("m-f16", "llama-cpp", gib(8)), base("m-base", gib(2))}
selection, err := gallery.SelectVariant(options, gallery.ResolveEnv{AvailableMemory: gib(64)}, "M-F16")
Expect(err).ToNot(HaveOccurred())
@@ -297,19 +271,11 @@ var _ = Describe("SelectVariant", func() {
})
It("fails loudly when the pin names nothing in the list", func() {
options := []gallery.VariantOption{option("m-f16", "llama-cpp", "8GiB"), base("m-base", gib(2))}
options := []gallery.VariantOption{option("m-f16", "llama-cpp", gib(8)), base("m-base", gib(2))}
_, err := gallery.SelectVariant(options, gallery.ResolveEnv{AvailableMemory: gib(64)}, "m-gone")
Expect(err).To(MatchError(gallery.ErrPinNotFound))
Expect(err.Error()).To(ContainSubstring("m-gone"))
})
})
It("propagates an unparseable figure instead of treating it as unconstrained", func() {
options := []gallery.VariantOption{option("bad", "llama-cpp", "lots"), base("m-base", gib(2))}
_, err := gallery.SelectVariant(options, gallery.ResolveEnv{AvailableMemory: gib(8)}, "")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("bad"))
})
})

View File

@@ -1,44 +1,18 @@
package gallery
import (
"fmt"
"github.com/mudler/LocalAI/pkg/vram"
)
// Variant is one option in a gallery entry's variant list. It references an
// existing gallery entry by name, and that is all an author has to write.
//
// Authored order carries no meaning. Selection filters out what this host
// cannot run and then ranks what is left, so hardware knowledge lives in the
// selector rather than being pushed onto whoever edits the gallery.
//
// There is deliberately no authored memory figure here. When the live probe
// misreads a variant's footprint, the fix is the referenced entry's own `size:`
// field: the estimator already prefers a declared size over its own guesswork,
// so correcting it there fixes the figure for every consumer rather than only
// for this one selection pass.
type Variant struct {
// Model is the name of a gallery entry that declares no variants of its own.
Model string `json:"model" yaml:"model"`
// MinMemory is the authored memory requirement (e.g. "20GiB"). It exists to
// override the size the installer probes live from the model's weights when
// that figure is known to be wrong; most variants should not need it.
//
// One number covers both VRAM and system RAM. A model's footprint is
// roughly the same wherever it lives, and the selector compares this against
// whichever of the two this host will actually use, so splitting it in two
// would only invite the pair to disagree.
MinMemory string `json:"min_memory,omitempty" yaml:"min_memory,omitempty"`
}
// AuthoredMinMemory returns the hand-written memory requirement in bytes and
// whether one was written at all.
//
// An absent requirement is not a zero requirement. Callers must not read the
// returned 0 as "fits anywhere"; it means the author said nothing, and the
// figure has to come from a live probe instead.
func (v Variant) AuthoredMinMemory() (uint64, bool, error) {
if v.MinMemory == "" {
return 0, false, nil
}
size, err := vram.ParseSizeString(v.MinMemory)
if err != nil {
return 0, false, fmt.Errorf("variant %q has an unparseable min_memory %q: %w", v.Model, v.MinMemory, err)
}
return size, true, nil
}

View File

@@ -36,19 +36,16 @@ url: "github:example/repo/qwen3.6-27b.yaml@master"
variants:
- model: qwen3.6-27b-mlx-8bit
- model: qwen3.6-27b-gguf-q8
min_memory: 28GiB
`), &m)
Expect(err).ToNot(HaveOccurred())
Expect(m.Name).To(Equal("qwen3.6-27b"))
Expect(m.URL).To(Equal("github:example/repo/qwen3.6-27b.yaml@master"))
Expect(m.HasVariants()).To(BeTrue())
Expect(m.Variants).To(HaveLen(2))
// The first variant is nothing but a name, which is the shape authoring
// is meant to reach for.
// A variant is nothing but a name, which is the whole of the authoring
// surface.
Expect(m.Variants[0].Model).To(Equal("qwen3.6-27b-mlx-8bit"))
Expect(m.Variants[0].MinMemory).To(BeEmpty())
Expect(m.Variants[1].Model).To(Equal("qwen3.6-27b-gguf-q8"))
Expect(m.Variants[1].MinMemory).To(Equal("28GiB"))
})
})
@@ -79,12 +76,24 @@ var _ = Describe("ResolveVariant", func() {
base = newModel("qwen3-8b-gguf-q4", "file://gguf.yaml", "Qwen3 8B Q4", "qwen.png")
base.Backend = "llama-cpp"
base.Tags = []string{"llm"}
base.Variants = []gallery.Variant{{Model: "qwen3-8b-vllm-awq", MinMemory: "20GiB"}}
base.Variants = []gallery.Variant{{Model: "qwen3-8b-vllm-awq"}}
models = []*gallery.GalleryModel{upgrade, base}
})
// The AWQ build measures 20GiB, so the specs below vary only the host and
// read off which payload resolution lands on. The probe is injected rather
// than performed, so nothing here reaches the network.
env := func(memory uint64) gallery.ResolveEnv {
return gallery.ResolveEnv{AvailableMemory: memory, BackendCompatible: runsEverything}
return gallery.ResolveEnv{
AvailableMemory: memory,
BackendCompatible: runsEverything,
ProbeMemory: func(target *gallery.GalleryModel) uint64 {
if target.Name == "qwen3-8b-vllm-awq" {
return 20 * 1024 * 1024 * 1024
}
return 0
},
}
}
It("installs a fitting variant's payload under the entry's name", func() {
@@ -141,12 +150,6 @@ var _ = Describe("ResolveVariant", func() {
return e
}
BeforeEach(func() {
// No authored figure anywhere, so every size below comes from the
// probe. This is the shape authoring is meant to reach for.
base.Variants = []gallery.Variant{{Model: "qwen3-8b-vllm-awq"}}
})
It("selects a variant the probe shows to fit", func() {
resolved, variant, err := gallery.ResolveVariant(models, base,
probing(gib(24), map[string]uint64{"qwen3-8b-vllm-awq": gib(20)}), "")
@@ -188,23 +191,6 @@ var _ = Describe("ResolveVariant", func() {
Expect(variant.Model).To(Equal("qwen3-8b-gguf-q4"))
Expect(resolved.URL).To(Equal("file://gguf.yaml"))
})
It("does not probe a variant that declares its own min_memory", func() {
// Probing costs a network round trip, so an authored figure has to
// suppress it outright rather than merely outrank it.
base.Variants = []gallery.Variant{{Model: "qwen3-8b-vllm-awq", MinMemory: "20GiB"}}
probed := []string{}
e := env(gib(24))
e.ProbeMemory = func(target *gallery.GalleryModel) uint64 {
probed = append(probed, target.Name)
return gib(80)
}
_, variant, err := gallery.ResolveVariant(models, base, e, "")
Expect(err).ToNot(HaveOccurred())
Expect(probed).To(BeEmpty())
Expect(variant.Model).To(Equal("qwen3-8b-vllm-awq"))
})
})
It("presents the entry's metadata, not the variant's", func() {
@@ -350,6 +336,16 @@ var _ = Describe("InstallModelFromGallery with variant entries", func() {
return m
}
// sizedEntry declares its footprint through the entry's own size:, which is
// the only way an author influences a variant's measured size. The estimator
// prefers a declared size over its own guesswork and parses it locally, so
// these specs pin an exact figure without touching the network.
sizedEntry := func(name, backend, size string) gallery.GalleryModel {
m := entry(name, backend)
m.Size = size
return m
}
// urlEntry describes an entry through a url rather than an inline
// config_file. The distinction matters for the recorded name: the url branch
// reads a name out of the fetched config, and that name is the referenced
@@ -375,9 +371,10 @@ var _ = Describe("InstallModelFromGallery with variant entries", func() {
}
// withVariants attaches alternative builds to an otherwise ordinary entry.
// The figures are absolute rather than relative to the host: "0GiB" always
// fits and "10000GiB" never does, so these specs assert on selection rather
// than on whatever memory the machine running them happens to have.
// Where a spec needs a definite size it declares it on the referenced entry
// with sizedEntry, in absolute terms rather than relative to the host, so
// these specs assert on selection rather than on whatever memory the machine
// running them happens to have.
withVariants := func(m gallery.GalleryModel, variants ...gallery.Variant) gallery.GalleryModel {
m.Variants = variants
return m
@@ -410,8 +407,8 @@ var _ = Describe("InstallModelFromGallery with variant entries", func() {
It("installs the entry's own payload when no variant fits the host", func() {
newGallery(
withVariants(entry("qwen3-8b-q4", "base-backend"),
gallery.Variant{Model: "qwen3-8b-q8", MinMemory: "10000GiB"}),
entry("qwen3-8b-q8", "upgrade-backend"),
gallery.Variant{Model: "qwen3-8b-q8"}),
sizedEntry("qwen3-8b-q8", "upgrade-backend", "10000GiB"),
)
// No machine clears 10000GiB, so this asserts the base is the last
@@ -423,8 +420,8 @@ var _ = Describe("InstallModelFromGallery with variant entries", func() {
It("installs a fitting variant's payload under the entry's own name", func() {
newGallery(
withVariants(entry("qwen3-8b-q4", "base-backend"),
gallery.Variant{Model: "qwen3-8b-q8", MinMemory: "0GiB"}),
entry("qwen3-8b-q8", "upgrade-backend"),
gallery.Variant{Model: "qwen3-8b-q8"}),
sizedEntry("qwen3-8b-q8", "upgrade-backend", "16MiB"),
)
Expect(install("qwen3-8b-q4", gallery.GalleryModel{})).To(Succeed())
@@ -454,11 +451,11 @@ var _ = Describe("InstallModelFromGallery with variant entries", func() {
// would take the small one.
newGallery(
withVariants(entry("qwen3-8b-q4", "base-backend"),
gallery.Variant{Model: "qwen3-8b-small", MinMemory: "0GiB"},
gallery.Variant{Model: "qwen3-8b-large", MinMemory: "1KiB"},
gallery.Variant{Model: "qwen3-8b-small"},
gallery.Variant{Model: "qwen3-8b-large"},
),
entry("qwen3-8b-small", "small-backend"),
entry("qwen3-8b-large", "large-backend"),
sizedEntry("qwen3-8b-small", "small-backend", "16MiB"),
sizedEntry("qwen3-8b-large", "large-backend", "256MiB"),
)
Expect(install("qwen3-8b-q4", gallery.GalleryModel{})).To(Succeed())
@@ -474,7 +471,7 @@ var _ = Describe("InstallModelFromGallery with variant entries", func() {
// the only thing that can send this to the base.
newGallery(
withVariants(entry("qwen3-8b-q4", "base-backend"),
gallery.Variant{Model: "qwen3-8b-mlx", MinMemory: "0GiB"}),
gallery.Variant{Model: "qwen3-8b-mlx"}),
entry("qwen3-8b-mlx", "mlx"),
)
@@ -489,7 +486,7 @@ var _ = Describe("InstallModelFromGallery with variant entries", func() {
// anything reads the record back.
newGallery(
withVariants(urlEntry("qwen3-8b-q4", "base-backend"),
gallery.Variant{Model: "qwen3-8b-q8", MinMemory: "0GiB"}),
gallery.Variant{Model: "qwen3-8b-q8"}),
urlEntry("qwen3-8b-q8", "upgrade-backend"),
)
@@ -508,7 +505,7 @@ var _ = Describe("InstallModelFromGallery with variant entries", func() {
It("refuses a variant name the entry does not declare, naming what was asked for", func() {
newGallery(
withVariants(entry("qwen3-8b-q4", "base-backend"),
gallery.Variant{Model: "qwen3-8b-q8", MinMemory: "0GiB"}),
gallery.Variant{Model: "qwen3-8b-q8"}),
entry("qwen3-8b-q8", "upgrade-backend"),
)
@@ -538,7 +535,7 @@ var _ = Describe("InstallModelFromGallery with variant entries", func() {
It("records a pin and honors it on a plain reinstall", func() {
newGallery(
withVariants(entry("qwen3-8b-q4", "base-backend"),
gallery.Variant{Model: "qwen3-8b-q8", MinMemory: "0GiB"}),
gallery.Variant{Model: "qwen3-8b-q8"}),
entry("qwen3-8b-q8", "upgrade-backend"),
)
@@ -563,7 +560,7 @@ var _ = Describe("InstallModelFromGallery with variant entries", func() {
It("honors a pin recorded under a custom install name", func() {
newGallery(
withVariants(entry("qwen3-8b-q4", "base-backend"),
gallery.Variant{Model: "qwen3-8b-q8", MinMemory: "0GiB"}),
gallery.Variant{Model: "qwen3-8b-q8"}),
entry("qwen3-8b-q8", "upgrade-backend"),
)
@@ -586,7 +583,7 @@ var _ = Describe("InstallModelFromGallery with variant entries", func() {
It("writes each declared url only once into the persisted gallery file", func() {
base := withVariants(entry("qwen3-8b-q4", "base-backend"),
gallery.Variant{Model: "qwen3-8b-q8", MinMemory: "0GiB"})
gallery.Variant{Model: "qwen3-8b-q8"})
base.URLs = []string{"https://example.invalid/qwen3"}
newGallery(base, entry("qwen3-8b-q8", "upgrade-backend"))

View File

@@ -75,28 +75,6 @@ func checkVariantReferences(entries []gallery.GalleryModel) []variantViolation {
return violations
}
// checkVariantMemory verifies every authored memory figure parses.
//
// Absence is fine and deliberately not flagged: a variant that declares nothing
// gets its size from a live probe, and even a probe that fails leaves a
// legitimate unknown that selection handles by ranking the variant last. An
// UNPARSEABLE figure is different, because it makes selection fail outright for
// the whole entry rather than degrade.
func checkVariantMemory(entries []gallery.GalleryModel) []variantViolation {
var violations []variantViolation
for _, e := range entries {
if !e.HasVariants() {
continue
}
for _, v := range e.Variants {
if _, _, err := v.AuthoredMinMemory(); err != nil {
violations = append(violations, variantViolation{Entry: e.Name, Variant: v.Model, Detail: "has a bad min_memory: " + err.Error()})
}
}
}
return violations
}
// loadGalleryIndex parses gallery/index.yaml once for the whole suite. The
// index carries well over a thousand entries, so re-parsing it per spec is
// pure overhead.
@@ -145,11 +123,13 @@ var _ = Describe("gallery variant lint helpers", func() {
})
It("passes every invariant on a valid entry", func() {
// Authoring is nothing but a list of names, so a bare list of them must
// be entirely valid.
entries := variantFixture(
entryWithVariants("base", "u://base",
gallery.Variant{Model: "big", MinMemory: "24GiB"},
gallery.Variant{Model: "mid", MinMemory: "12GiB"},
gallery.Variant{Model: "metal-big", MinMemory: "32GiB"},
gallery.Variant{Model: "big"},
gallery.Variant{Model: "mid"},
gallery.Variant{Model: "metal-big"},
gallery.Variant{Model: "small"},
),
plainEntry("big", "u://big"),
@@ -159,19 +139,6 @@ var _ = Describe("gallery variant lint helpers", func() {
)
Expect(checkVariantReferences(entries)).To(BeEmpty())
Expect(checkVariantMemory(entries)).To(BeEmpty())
})
It("passes every invariant on an entry that declares no memory at all", func() {
// Authoring is meant to be nothing but a list of names, so an entry
// whose variants carry no figures must be entirely valid.
entries := variantFixture(
entryWithVariants("base", "u://base", gallery.Variant{Model: "big"}),
plainEntry("big", "u://big"),
)
Expect(checkVariantReferences(entries)).To(BeEmpty())
Expect(checkVariantMemory(entries)).To(BeEmpty())
})
Describe("checkVariantReferences", func() {
@@ -212,31 +179,6 @@ var _ = Describe("gallery variant lint helpers", func() {
})
})
Describe("checkVariantMemory", func() {
It("flags a variant whose memory figure cannot be parsed", func() {
entries := variantFixture(
entryWithVariants("base", "u://base", gallery.Variant{Model: "a", MinMemory: "lots"}),
plainEntry("a", "u://a"),
)
violations := checkVariantMemory(entries)
Expect(violations).To(HaveLen(1))
Expect(violations[0].Variant).To(Equal("a"))
Expect(violations[0].Detail).To(ContainSubstring("bad min_memory"))
})
It("accepts a variant that declares no figure at all", func() {
// The overwhelmingly common shape: a bare name, whose size the
// installer probes live. Flagging it would make the rule reject the
// authoring style the design exists to enable.
entries := variantFixture(
entryWithVariants("base", "u://base", gallery.Variant{Model: "a"}),
plainEntry("a", "u://a"),
)
Expect(checkVariantMemory(entries)).To(BeEmpty())
})
})
})
var _ = Describe("gallery/index.yaml variant invariants", Ordered, func() {
@@ -255,9 +197,4 @@ var _ = Describe("gallery/index.yaml variant invariants", Ordered, func() {
v := checkVariantReferences(entries)
Expect(v).To(BeEmpty(), formatViolations(v))
})
It("declares only parseable memory figures", func() {
v := checkVariantMemory(entries)
Expect(v).To(BeEmpty(), formatViolations(v))
})
})

View File

@@ -67,10 +67,6 @@
"model": {
"type": "string",
"description": "Name of a gallery entry that declares no variants of its own"
},
"min_memory": {
"type": "string",
"description": "Authored memory requirement, for example 20GiB. Optional, and only needed to override the size the installer probes live from the model's weights when that figure is wrong. Authoritative: an authored figure suppresses the probe entirely."
}
},
"additionalProperties": false

View File

@@ -62,10 +62,12 @@ var _ = Describe("LocalModelManager variant selection", func() {
Expect(err).ToNot(HaveOccurred())
DeferCleanup(func() { Expect(os.RemoveAll(modelsDir)).To(Succeed()) })
// "0GiB" always fits, so auto-selection would take the upgrade on any
// machine. Only an honored pin can land the install on the base.
// The upgrade declares no size and carries no weight files, so the probe
// reports an unknown, the variant survives the filter and auto-selection
// would take it on any machine. Only an honored pin can land the install
// on the base.
entries := []gallery.GalleryModel{
entry("qwen3-8b-q4", "base-backend", gallery.Variant{Model: "qwen3-8b-q8", MinMemory: "0GiB"}),
entry("qwen3-8b-q4", "base-backend", gallery.Variant{Model: "qwen3-8b-q8"}),
entry("qwen3-8b-q8", "upgrade-backend"),
}
out, err := yaml.Marshal(entries)