Skip to content

Commit 9273773

Browse files
fix(gallery): budget variant memory from RAM when a GPU reports no VRAM
Variant selection read its memory budget from VRAM whenever a GPU capability was detected, and from system RAM only when none was. Apple Silicon satisfies the first branch and fails the premise: arm64 macs report the metal capability unconditionally, without probing anything, while TotalAvailableVRAM has no discrete VRAM pool to find and returns zero. The budget therefore came out as zero on every Mac. Zero drops every variant carrying a known size, so the base build was installed on all of them however much memory the machine had. The feature was inert on the platform, and silently: falling back to the base is a legitimate outcome, so nothing looked wrong. Take VRAM only when it is actually a number, and fall back to RAM otherwise. On a unified-memory host RAM is not an approximation of the budget, it is the budget, since the GPU shares it. A discrete GPU whose VRAM could not be read also lands on RAM, which overstates what the card holds but understates nothing the host has; the previous zero understated both. An unreadable RAM figure still yields zero and still installs the base, so a genuinely unknown host is not talked into a larger download. This is what turned tests-apple red: "installs a fitting variant's payload under the entry's own name" asserts on selection, and the runner resolved to the base because its budget was zero. The added specs pin the branch directly rather than relying on a macOS runner to notice again. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 9a43a27 commit 9273773

3 files changed

Lines changed: 86 additions & 4 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package gallery
2+
3+
import (
4+
"os"
5+
6+
"github.com/mudler/LocalAI/pkg/system"
7+
"github.com/mudler/LocalAI/pkg/xsysinfo"
8+
9+
. "github.com/onsi/ginkgo/v2"
10+
. "github.com/onsi/gomega"
11+
)
12+
13+
var _ = Describe("availableModelMemory", func() {
14+
// hostRAM is the figure the fallback resolves to. A host that cannot report
15+
// its own RAM cannot exercise the fallback at all, so those specs skip
16+
// rather than assert on a number that is legitimately unavailable.
17+
hostRAM := func() uint64 {
18+
ram, err := xsysinfo.GetSystemRAMInfo()
19+
if err != nil || ram == nil || ram.Total == 0 {
20+
Skip("this host does not report system RAM; the RAM fallback cannot be exercised here")
21+
}
22+
return ram.Total
23+
}
24+
25+
// stateWithCapability forces DetectedCapability for one spec. The state is
26+
// built fresh so nothing is served from the capability cache.
27+
stateWithCapability := func(capability string, vram uint64) *system.SystemState {
28+
previous, had := os.LookupEnv("LOCALAI_FORCE_META_BACKEND_CAPABILITY")
29+
Expect(os.Setenv("LOCALAI_FORCE_META_BACKEND_CAPABILITY", capability)).To(Succeed())
30+
DeferCleanup(func() {
31+
if had {
32+
Expect(os.Setenv("LOCALAI_FORCE_META_BACKEND_CAPABILITY", previous)).To(Succeed())
33+
return
34+
}
35+
Expect(os.Unsetenv("LOCALAI_FORCE_META_BACKEND_CAPABILITY")).To(Succeed())
36+
})
37+
38+
s := &system.SystemState{}
39+
s.VRAM = vram
40+
Expect(s.DetectedCapability()).To(Equal(capability), "the capability override did not take")
41+
return s
42+
}
43+
44+
// A unified-memory host reports a GPU capability but no discrete VRAM pool.
45+
// Apple Silicon is the case that matters: metal is reported unconditionally
46+
// on arm64 macs while TotalAvailableVRAM finds nothing to add up. Reading
47+
// that zero as the budget drops every sized variant and strands the host on
48+
// the base build however much memory it has, which is what turned the macOS
49+
// runner red.
50+
It("falls back to system RAM when a detected GPU reports no VRAM", func() {
51+
ram := hostRAM()
52+
53+
got := availableModelMemory(stateWithCapability("metal", 0))
54+
55+
Expect(got).ToNot(BeZero(), "a unified-memory host was given a zero budget; every sized variant would be dropped")
56+
Expect(got).To(Equal(ram))
57+
})
58+
59+
// A discrete GPU reports real VRAM, and that stays the budget: the model
60+
// lives on the card, so system RAM is not what bounds it.
61+
It("prefers VRAM when the GPU reports it", func() {
62+
const vram = uint64(24) << 30
63+
64+
Expect(availableModelMemory(stateWithCapability("nvidia-cuda-12", vram))).To(Equal(vram))
65+
})
66+
67+
// With no GPU at all the model lives in system RAM.
68+
It("uses system RAM when no GPU is detected", func() {
69+
ram := hostRAM()
70+
71+
Expect(availableModelMemory(stateWithCapability("default", 0))).To(Equal(ram))
72+
})
73+
})

core/gallery/models.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,16 +287,23 @@ const noGPUDetected = "default"
287287

288288
// availableModelMemory reports how much memory a model may occupy on this host.
289289
//
290-
// With a usable GPU the model lives in VRAM. Without one it lives in system
290+
// With a discrete GPU the model lives in VRAM. Otherwise it lives in system
291291
// RAM, read through xsysinfo because that path is cgroup-aware: under
292292
// Kubernetes the container's limit, not the node's physical RAM, is what the
293293
// model actually gets.
294294
//
295+
// A detected GPU whose VRAM reads as zero falls back to RAM rather than to
296+
// zero. That is the normal shape of a unified-memory host, not an error:
297+
// arm64 macs report the metal capability unconditionally, yet have no discrete
298+
// VRAM pool for TotalAvailableVRAM to find, so the model's real budget is
299+
// system RAM. Returning the zero here would strand every Mac on the base
300+
// build no matter how much memory it has.
301+
//
295302
// An unreadable RAM figure yields 0, which drops every variant with a known
296303
// requirement and installs the base. That is the safe direction: an unknown
297304
// host should not be talked into a larger download.
298305
func availableModelMemory(systemState *system.SystemState) uint64 {
299-
if systemState.DetectedCapability() != noGPUDetected {
306+
if systemState.DetectedCapability() != noGPUDetected && systemState.VRAM > 0 {
300307
return systemState.VRAM
301308
}
302309
ram, err := xsysinfo.GetSystemRAMInfo()

docs/content/features/model-gallery.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,10 @@ quantizations, or the same weights served by a different engine. Such an entry
158158
carries a `variants` list, and installing it normally lets LocalAI choose:
159159

160160
- variants whose backend cannot run on this machine are dropped;
161-
- variants that do not fit the available memory (VRAM on a GPU host, otherwise
162-
system RAM) are dropped;
161+
- variants that do not fit the available memory are dropped. That budget is
162+
VRAM on a discrete-GPU host, and system RAM otherwise — including on
163+
unified-memory machines such as Apple Silicon, where the GPU shares system
164+
RAM and reports no separate VRAM pool;
163165
- the entry's own build is never dropped. It competes with whatever survived
164166
rather than waiting for everything else to fail, so an entry that is itself
165167
the largest build that fits keeps its own payload;

0 commit comments

Comments
 (0)