mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-14 11:49:33 -04:00
fix(xsysinfo): make reported system RAM total cgroup/lxcfs-aware (#8059) GetSystemRAMInfo derived Total from memory.TotalMemory(), which on Linux uses syscall.Sysinfo().Totalram - the HOST kernel total. lxcfs/LXD does NOT virtualize that value, while MemAvailable (used for Free/Available) IS virtualized. Inside an LXD/container with a 128Gi host but a ~10Gi container view this produced Total=128Gi, Available=10Gi => Used=118Gi, reporting ~92% RAM usage on an idle container. Derive Total instead from the minimum of all non-zero, non-unlimited candidates: cgroup v2 memory.max, cgroup v1 memory.limit_in_bytes (the kernel unlimited sentinel is ignored), /proc/meminfo MemTotal (which lxcfs virtualizes), and the syscall.Sysinfo total as the bare-metal fallback. On bare metal every candidate is unlimited or equals the host total, so behavior is unchanged. The selection/parsing lives in a pure function chooseTotalMemory(...) taking file CONTENTS, unit-tested without a real LXD host; OS file reads stay in a thin wrapper. Assisted-by: claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
121 lines
3.3 KiB
Go
121 lines
3.3 KiB
Go
package xsysinfo
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// cgroupV1UnlimitedSentinel is the value the kernel writes to
|
|
// memory.limit_in_bytes when no limit is set. It is PAGE_COUNTER_MAX
|
|
// (LONG_MAX rounded down to a page boundary), i.e. 0x7FFFFFFFFFFFF000 on
|
|
// 4 KiB-page systems. Any value at or above this is treated as "no limit".
|
|
const cgroupV1UnlimitedSentinel = uint64(0x7FFFFFFFFFFFF000)
|
|
|
|
// parseUintField parses a trimmed unsigned integer from raw file contents.
|
|
// It returns (0, false) when the content is empty or not a number.
|
|
func parseUintField(raw string) (uint64, bool) {
|
|
s := strings.TrimSpace(raw)
|
|
if s == "" {
|
|
return 0, false
|
|
}
|
|
v, err := strconv.ParseUint(s, 10, 64)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
return v, true
|
|
}
|
|
|
|
// parseCgroupV2Max interprets the contents of cgroup v2 memory.max.
|
|
// The literal "max" means unlimited, returning 0.
|
|
func parseCgroupV2Max(raw string) uint64 {
|
|
if strings.TrimSpace(raw) == "max" {
|
|
return 0
|
|
}
|
|
v, ok := parseUintField(raw)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
return v
|
|
}
|
|
|
|
// parseCgroupV1Limit interprets the contents of cgroup v1
|
|
// memory.limit_in_bytes. The kernel's "unlimited" sentinel (a value at or
|
|
// above PAGE_COUNTER_MAX) is treated as no limit, returning 0.
|
|
func parseCgroupV1Limit(raw string) uint64 {
|
|
v, ok := parseUintField(raw)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
if v >= cgroupV1UnlimitedSentinel {
|
|
return 0
|
|
}
|
|
return v
|
|
}
|
|
|
|
// parseMemTotal extracts the MemTotal value (in bytes) from raw
|
|
// /proc/meminfo contents. MemTotal is reported in kibibytes, so the parsed
|
|
// value is multiplied by 1024. Returns 0 when the field is missing.
|
|
func parseMemTotal(raw string) uint64 {
|
|
for _, line := range strings.Split(raw, "\n") {
|
|
if !strings.HasPrefix(line, "MemTotal:") {
|
|
continue
|
|
}
|
|
fields := strings.Fields(line)
|
|
// Expected: ["MemTotal:", "<value>", "kB"]
|
|
if len(fields) < 2 {
|
|
return 0
|
|
}
|
|
v, err := strconv.ParseUint(fields[1], 10, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
if len(fields) >= 3 {
|
|
switch strings.ToLower(fields[2]) {
|
|
case "kb":
|
|
return v * 1024
|
|
case "mb":
|
|
return v * 1024 * 1024
|
|
case "gb":
|
|
return v * 1024 * 1024 * 1024
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// chooseTotalMemory selects the most accurate system RAM total in bytes.
|
|
//
|
|
// On Linux the host kernel total (sysinfoTotal, from syscall.Sysinfo) is NOT
|
|
// virtualized by lxcfs/LXD, so inside a container it over-reports physical
|
|
// RAM. The cgroup limits and /proc/meminfo MemTotal, by contrast, do reflect
|
|
// the container's view. We therefore take the MINIMUM of all non-zero,
|
|
// non-unlimited candidates:
|
|
//
|
|
// - cgroup v2 memory.max ("max" => unlimited, skipped)
|
|
// - cgroup v1 memory.limit_in_bytes (kernel sentinel => unlimited, skipped)
|
|
// - /proc/meminfo MemTotal (lxcfs/LXD virtualizes this)
|
|
// - sysinfoTotal (bare-metal fallback)
|
|
//
|
|
// On bare metal the cgroup limits are unlimited and MemTotal == sysinfoTotal,
|
|
// so the result equals the host total exactly as before.
|
|
func chooseTotalMemory(cgroupV2Max, cgroupV1Limit, procMemInfo string, sysinfoTotal uint64) uint64 {
|
|
candidates := []uint64{
|
|
parseCgroupV2Max(cgroupV2Max),
|
|
parseCgroupV1Limit(cgroupV1Limit),
|
|
parseMemTotal(procMemInfo),
|
|
sysinfoTotal,
|
|
}
|
|
|
|
var best uint64
|
|
for _, c := range candidates {
|
|
if c == 0 {
|
|
continue
|
|
}
|
|
if best == 0 || c < best {
|
|
best = c
|
|
}
|
|
}
|
|
return best
|
|
}
|