Files
LocalAI/pkg/xsysinfo/sysfsgpu.go
mudler's LocalAI [bot] c2704dba5b fix(gpu): detect GPUs via sysfs when no pci.ids database is present (#10966)
* fix(gpu): detect GPUs via sysfs when no pci.ids database is present

ghw.GPU() calls pci.New() before it reads /sys/class/drm and fails
outright when it cannot find a pci.ids database file. jaypipes/pcidb
embeds no database and has network fetch disabled by default, so on an
image that ships no pci.ids, GPU enumeration returns an error and every
detection path downstream goes dark.

The Dockerfile installs pciutils only in the vulkan and cublas branches,
so the Intel image had no pci.ids. A correctly passed-through Arc A310
was reported as "No GPU detected" with zero VRAM even though clinfo and
sycl-ls both enumerated it inside the same container. NVIDIA and AMD
images were shielded by their nvidia-smi / rocm-smi binary fallbacks;
Intel has no equivalent, leaving it fully exposed.

Read PCI vendor IDs directly from /sys/class/drm/card*/device/vendor,
which needs no database, and consult that from DetectGPUVendor. The
same scan replaces the ghw-only guard in getIntelGPUMemory, which is
what had been blocking the working clinfo path and keeping VRAM at
zero. Install hwdata in the base image stage as well, so ghw stops
failing for every image variant rather than only Intel.

Also apply the documented NVIDIA > AMD > Intel priority to the ghw
path, which previously returned whichever card DRM enumerated first
and so reported "intel" on a machine with an Intel iGPU at card0 and
an NVIDIA dGPU at card1.

HasGPU() carried the same blindness plus one of its own: it matched
the requested vendor against ghw's card description with a
case-sensitive Contains, so "nvidia" never matched the pci.ids
spelling "NVIDIA Corporation". It only worked because that same
description embeds the lowercase kernel driver name ("nvidia",
"amdgpu"), and it returned false outright whenever ghw errored. Route
it through the shared vendor lookup so it matches case-insensitively
and falls back to sysfs. It feeds the GPU option and NGPULayers
defaults in core/config/gguf.go.

Fixes #10941

Assisted-by: Claude:claude-opus-4-8 golangci-lint
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactor(gpu): key vendor detection off the numeric PCI ID in both paths

The ghw and sysfs legs were identifying vendors by different means: ghw
by substring-matching the pci.ids vendor name, sysfs by the numeric PCI
vendor ID. ghw already exposes that same numeric ID via
DeviceInfo.Vendor.ID, read from the kernel's modalias rather than from
the database, so the name matching was both a duplicate mechanism and
the weaker of the two.

It is weaker because a card absent from an outdated pci.ids gets
Name: "unknown" while its ID is still correct. Detection then failed
even though ghw had enumerated the card successfully. Verified in a
container with a vendor-less pci.ids and an Arc's modalias: before,
DetectGPUVendor returned ""; after, "intel".

Both legs now resolve through the same pciVendorIDs table and share the
hex parsing, with the vendor name kept only as a fallback for devices
exposing no parseable ID.

ghwHasVendor is deliberately not a priority pick, unlike vendorFromGHW:
HasGPU("intel") must stay true on a hybrid-graphics host whose discrete
NVIDIA card outranks the integrated Intel one.

Assisted-by: Claude:claude-opus-4-8 golangci-lint
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(gpu): silence the gosec G304 on the sysfs attribute read

gosec flags os.ReadFile with a non-literal path. The path here is the
DRM root (a package constant in production, a temp dir under test)
joined with a ReadDir entry name and a fixed attribute filename, so no
external input reaches it.

gosec's suggested autofix, os.Root, cannot be used: /sys/class/drm/cardN
is a symlink into the PCI device tree, and os.Root refuses to traverse
it ("path escapes from parent"), which would disable the whole scan.

Assisted-by: Claude:claude-opus-4-8 gosec golangci-lint
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-07-20 00:37:06 +02:00

203 lines
6.2 KiB
Go

package xsysinfo
import (
"github.com/jaypipes/ghw/pkg/gpu"
"os"
"path/filepath"
"strconv"
"strings"
)
// defaultSysfsDRMPath is where the kernel exposes the DRM subsystem.
const defaultSysfsDRMPath = "/sys/class/drm"
// pciVendorIDs maps PCI vendor IDs to the vendor constants used
// throughout this package. AMD graphics devices carry the ATI ID
// (0x1002), not AMD's host-bridge ID.
var pciVendorIDs = map[uint64]string{
0x10de: VendorNVIDIA,
0x1002: VendorAMD,
0x8086: VendorIntel,
}
// vendorPriority is the order DetectGPUVendor reports a vendor in when
// a host has cards from more than one: a discrete accelerator always
// outranks an integrated display adapter.
var vendorPriority = []string{VendorNVIDIA, VendorAMD, VendorIntel}
// sysfsGPU is a GPU discovered by walking the kernel's DRM sysfs tree.
type sysfsGPU struct {
Card string
Vendor string
}
// scanSysfsGPUs enumerates GPUs straight from the kernel's DRM sysfs
// tree, identifying each by its raw PCI vendor ID.
//
// This is deliberately dependency-free. ghw, our richer source of GPU
// information, needs a pci.ids database file on disk to translate
// vendor IDs into names, and ghw.GPU() fails outright when it can't
// find one. Container images that ship no pci.ids (the Intel oneAPI
// image among them) therefore get no GPU detection at all, which is
// what issue #10941 reported: a correctly passed-through Arc A310
// reported as "No GPU detected" with zero VRAM. Reading the vendor ID
// ourselves needs nothing but /sys.
func scanSysfsGPUs(root string) []sysfsGPU {
entries, err := os.ReadDir(root)
if err != nil {
return nil
}
var gpus []sysfsGPU
for _, entry := range entries {
name := entry.Name()
if !strings.HasPrefix(name, "card") {
continue
}
// cardN-<INTERFACE>-<ID> entries are display connectors hanging
// off a card, not cards in their own right.
if strings.ContainsRune(name, '-') {
continue
}
deviceDir := filepath.Join(root, name, "device")
vendorID, ok := readSysfsHexUint(filepath.Join(deviceDir, "vendor"))
if !ok {
continue
}
vendor, known := pciVendorIDs[vendorID]
if !known {
// Server BMC display adapters (ASPEED, Matrox) and virtual
// VGA devices land here. They're not usable accelerators.
continue
}
gpus = append(gpus, sysfsGPU{Card: name, Vendor: vendor})
}
return gpus
}
// sysfsVendorPriority returns the highest-priority GPU vendor found in
// the DRM tree, or "" when the host has no recognised GPU.
func sysfsVendorPriority(root string) string {
gpus := scanSysfsGPUs(root)
for _, want := range vendorPriority {
for _, g := range gpus {
if g.Vendor == want {
return want
}
}
}
return ""
}
func readSysfsHexUint(path string) (uint64, bool) {
// #nosec G304 -- path is the DRM root (a package constant in production,
// a temp dir under test) joined with a ReadDir entry name and a fixed
// attribute filename; no external input reaches it. gosec suggests
// os.Root, which cannot work here: /sys/class/drm/cardN symlinks out to
// the PCI device tree, and os.Root rejects that as "path escapes from
// parent".
raw, err := os.ReadFile(path)
if err != nil {
return 0, false
}
return parseHexUint(string(raw))
}
// parseHexUint reads a hex PCI ID as written by either source: sysfs
// spells it "0x8086\n", ghw's modalias parse yields a bare "8086".
func parseHexUint(raw string) (uint64, bool) {
text := strings.TrimPrefix(strings.ToLower(strings.TrimSpace(raw)), "0x")
value, err := strconv.ParseUint(text, 16, 64)
if err != nil {
return 0, false
}
return value, true
}
// vendorFromNames maps a set of PCI vendor name strings (as ghw
// resolves them from pci.ids) onto a vendor constant, applying
// vendorPriority so enumeration order can't decide the answer.
func vendorFromNames(names []string) string {
for _, want := range vendorPriority {
for _, name := range names {
if nameMatchesVendor(name, want) {
return want
}
}
}
return ""
}
// nameMatchesVendor reports whether a PCI vendor name belongs to the
// requested vendor. The comparison is case-insensitive: pci.ids spells
// the vendor "NVIDIA Corporation" while callers pass the lowercase
// vendor constants.
func nameMatchesVendor(name, vendor string) bool {
return strings.Contains(strings.ToUpper(name), strings.ToUpper(vendor))
}
// vendorFromGHW resolves the vendor of the highest-priority card ghw
// enumerated.
//
// It prefers the numeric PCI vendor ID over the pci.ids vendor name:
// ghw reads the ID from the kernel's modalias and only the name from
// the database, so a card absent from an outdated pci.ids still carries
// a usable ID while its name reads "unknown". The name is kept as a
// fallback for devices that expose no parseable ID.
func vendorFromGHW(cards []*gpu.GraphicsCard) string {
var ids, names []string
for _, c := range cards {
if c == nil || c.DeviceInfo == nil || c.DeviceInfo.Vendor == nil {
continue
}
ids = append(ids, c.DeviceInfo.Vendor.ID)
names = append(names, c.DeviceInfo.Vendor.Name)
}
if vendor := vendorFromPCIIDs(ids); vendor != "" {
return vendor
}
return vendorFromNames(names)
}
// vendorFromPCIIDs maps hex PCI vendor IDs onto a vendor constant,
// applying vendorPriority so enumeration order can't decide the answer.
func vendorFromPCIIDs(ids []string) string {
found := map[string]bool{}
for _, raw := range ids {
if id, ok := parseHexUint(raw); ok {
if vendor, known := pciVendorIDs[id]; known {
found[vendor] = true
}
}
}
for _, want := range vendorPriority {
if found[want] {
return want
}
}
return ""
}
// ghwHasVendor reports whether any card ghw enumerated belongs to the
// given vendor. Unlike vendorFromGHW this is not a priority pick: a
// hybrid-graphics host must answer yes for both its integrated and its
// discrete GPU.
func ghwHasVendor(cards []*gpu.GraphicsCard, vendor string) bool {
for _, c := range cards {
if c == nil || c.DeviceInfo == nil || c.DeviceInfo.Vendor == nil {
continue
}
if id, ok := parseHexUint(c.DeviceInfo.Vendor.ID); ok {
if pciVendorIDs[id] == vendor {
return true
}
// A parseable ID that maps elsewhere is authoritative; don't
// let the name fall through and contradict it.
continue
}
if nameMatchesVendor(c.DeviceInfo.Vendor.Name, vendor) {
return true
}
}
return false
}