Skip to content

Commit c2704db

Browse files
localai-botmudler
andauthored
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>
1 parent 92dc326 commit c2704db

6 files changed

Lines changed: 453 additions & 42 deletions

File tree

Dockerfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ ARG APT_MIRROR
1212
ARG APT_PORTS_MIRROR
1313
ENV DEBIAN_FRONTEND=noninteractive
1414

15+
# hwdata ships /usr/share/hwdata/pci.ids. Without it, the ghw library we use
16+
# for hardware detection cannot resolve PCI vendor IDs and fails to enumerate
17+
# GPUs at all, so the image reports "No GPU detected" (see issue #10941).
1518
RUN --mount=type=bind,source=.docker/apt-mirror.sh,target=/usr/local/sbin/apt-mirror \
1619
APT_MIRROR="${APT_MIRROR}" APT_PORTS_MIRROR="${APT_PORTS_MIRROR}" sh /usr/local/sbin/apt-mirror && \
1720
apt-get update && \
1821
apt-get install -y --no-install-recommends \
1922
ca-certificates curl wget espeak-ng libgomp1 \
20-
ffmpeg libopenblas0 libopenblas-dev libopus0 sox && \
23+
ffmpeg libopenblas0 libopenblas-dev libopus0 sox \
24+
hwdata && \
2125
apt-get clean && \
2226
rm -rf /var/lib/apt/lists/*
2327

pkg/xsysinfo/gpu.go

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -213,20 +213,21 @@ func minNonZeroVRAM(infos []GPUMemoryInfo) uint64 {
213213
return min
214214
}
215215

216+
// HasGPU reports whether a GPU of the given vendor is present. An empty
217+
// vendor asks whether the host has any GPU at all.
218+
//
219+
// Both legs consult sysfs as well as ghw: ghw enumeration fails outright
220+
// on images with no pci.ids database, and returning "no GPU" there is
221+
// what made a passed-through Intel Arc invisible in #10941.
216222
func HasGPU(vendor string) bool {
217-
gpus, err := GPUs()
218-
if err != nil {
219-
return false
220-
}
221223
if vendor == "" {
222-
return len(gpus) > 0
223-
}
224-
for _, gpu := range gpus {
225-
if strings.Contains(gpu.String(), vendor) {
224+
gpus, err := GPUs()
225+
if err == nil && len(gpus) > 0 {
226226
return true
227227
}
228+
return len(scanSysfsGPUs(defaultSysfsDRMPath)) > 0
228229
}
229-
return false
230+
return hasGPUVendor(vendor)
230231
}
231232

232233
// DetectGPUVendor detects the GPU vendor using multiple methods with fallbacks.
@@ -235,25 +236,18 @@ func HasGPU(vendor string) bool {
235236
// Priority order: NVIDIA > AMD > Intel > Vulkan
236237
func DetectGPUVendor() (string, error) {
237238
// First, try ghw library detection
238-
gpus, err := GPUs()
239-
if err == nil && len(gpus) > 0 {
240-
for _, gpu := range gpus {
241-
if gpu.DeviceInfo != nil && gpu.DeviceInfo.Vendor != nil {
242-
vendorName := strings.ToUpper(gpu.DeviceInfo.Vendor.Name)
243-
if strings.Contains(vendorName, strings.ToUpper(VendorNVIDIA)) {
244-
xlog.Debug("GPU vendor detected via ghw", "vendor", VendorNVIDIA)
245-
return VendorNVIDIA, nil
246-
}
247-
if strings.Contains(vendorName, strings.ToUpper(VendorAMD)) {
248-
xlog.Debug("GPU vendor detected via ghw", "vendor", VendorAMD)
249-
return VendorAMD, nil
250-
}
251-
if strings.Contains(vendorName, strings.ToUpper(VendorIntel)) {
252-
xlog.Debug("GPU vendor detected via ghw", "vendor", VendorIntel)
253-
return VendorIntel, nil
254-
}
255-
}
256-
}
239+
if vendor := vendorFromGHW(ghwCards()); vendor != "" {
240+
xlog.Debug("GPU vendor detected via ghw", "vendor", vendor)
241+
return vendor, nil
242+
}
243+
244+
// Then read PCI vendor IDs straight from sysfs. ghw needs a pci.ids
245+
// database file to resolve vendor names and errors out entirely when
246+
// the image doesn't ship one, which is how a passed-through Intel Arc
247+
// ended up undetected in #10941.
248+
if vendor := sysfsVendorPriority(defaultSysfsDRMPath); vendor != "" {
249+
xlog.Debug("GPU vendor detected via sysfs", "vendor", vendor)
250+
return vendor, nil
257251
}
258252

259253
// Fallback to binary detection (priority: NVIDIA > AMD > Intel > Vulkan)
@@ -707,9 +701,9 @@ func getIntelGPUMemory() []GPUMemoryInfo {
707701
return gpus
708702
}
709703
// clinfo enumerates every OpenCL platform, so guard the
710-
// subprocess with the cached ghw GPU list: non-Intel hosts skip
704+
// subprocess with the detected GPU list: non-Intel hosts skip
711705
// it entirely.
712-
if !hasGHWVendor(VendorIntel) {
706+
if !hasGPUVendor(VendorIntel) {
713707
return nil
714708
}
715709
var out []GPUMemoryInfo
@@ -721,17 +715,28 @@ func getIntelGPUMemory() []GPUMemoryInfo {
721715
return out
722716
}
723717

724-
// hasGHWVendor reports whether ghw observed any GPU whose vendor name
725-
// matches (case-insensitive substring). Uses the package-level cache
726-
// in GPUs() so the call is free after the first invocation.
727-
func hasGHWVendor(vendor string) bool {
728-
gpus, _ := GPUs()
729-
target := strings.ToUpper(vendor)
730-
for _, g := range gpus {
731-
if g.DeviceInfo == nil || g.DeviceInfo.Vendor == nil {
732-
continue
733-
}
734-
if strings.Contains(strings.ToUpper(g.DeviceInfo.Vendor.Name), target) {
718+
// ghwCards returns the GPUs ghw observed, or nil when it could not
719+
// enumerate at all, which it does whenever no pci.ids database is
720+
// present on the host. Uses the package-level cache in GPUs() so the
721+
// call is free after the first invocation.
722+
func ghwCards() []*gpu.GraphicsCard {
723+
cards, err := GPUs()
724+
if err != nil {
725+
return nil
726+
}
727+
return cards
728+
}
729+
730+
// hasGPUVendor reports whether a GPU of the given vendor is present,
731+
// consulting ghw first and then sysfs. The sysfs leg matters because it
732+
// is the only one that works on images with no pci.ids database, where
733+
// ghw enumeration fails outright.
734+
func hasGPUVendor(vendor string) bool {
735+
if ghwHasVendor(ghwCards(), vendor) {
736+
return true
737+
}
738+
for _, g := range scanSysfsGPUs(defaultSysfsDRMPath) {
739+
if g.Vendor == vendor {
735740
return true
736741
}
737742
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package xsysinfo
2+
3+
import (
4+
"github.com/jaypipes/ghw/pkg/gpu"
5+
"github.com/jaypipes/ghw/pkg/pci"
6+
"github.com/jaypipes/pcidb"
7+
. "github.com/onsi/ginkgo/v2"
8+
. "github.com/onsi/gomega"
9+
)
10+
11+
func card(vendorID, vendorName string) *gpu.GraphicsCard {
12+
return &gpu.GraphicsCard{
13+
DeviceInfo: &pci.Device{
14+
Vendor: &pcidb.Vendor{ID: vendorID, Name: vendorName},
15+
},
16+
}
17+
}
18+
19+
var _ = Describe("vendorFromGHW", func() {
20+
It("identifies the vendor from the numeric PCI ID", func() {
21+
Expect(vendorFromGHW([]*gpu.GraphicsCard{card("10de", "NVIDIA Corporation")})).To(Equal(VendorNVIDIA))
22+
Expect(vendorFromGHW([]*gpu.GraphicsCard{card("1002", "Advanced Micro Devices, Inc. [AMD/ATI]")})).To(Equal(VendorAMD))
23+
Expect(vendorFromGHW([]*gpu.GraphicsCard{card("8086", "Intel Corporation")})).To(Equal(VendorIntel))
24+
})
25+
26+
It("identifies the vendor even when it is missing from the pci.ids database", func() {
27+
// ghw reads the vendor ID from the kernel modalias and only the
28+
// name from pci.ids, so a card absent from an outdated database
29+
// still carries a usable ID.
30+
Expect(vendorFromGHW([]*gpu.GraphicsCard{card("8086", "unknown")})).To(Equal(VendorIntel))
31+
})
32+
33+
It("falls back to the vendor name when no usable ID is present", func() {
34+
Expect(vendorFromGHW([]*gpu.GraphicsCard{card("", "NVIDIA Corporation")})).To(Equal(VendorNVIDIA))
35+
})
36+
37+
It("prefers a discrete NVIDIA GPU over an integrated Intel one regardless of enumeration order", func() {
38+
cards := []*gpu.GraphicsCard{card("8086", "Intel Corporation"), card("10de", "NVIDIA Corporation")}
39+
Expect(vendorFromGHW(cards)).To(Equal(VendorNVIDIA))
40+
})
41+
42+
It("prefers AMD over Intel", func() {
43+
cards := []*gpu.GraphicsCard{card("8086", "Intel Corporation"), card("1002", "AMD/ATI")}
44+
Expect(vendorFromGHW(cards)).To(Equal(VendorAMD))
45+
})
46+
47+
It("returns empty for a device that is not a known GPU vendor", func() {
48+
// QEMU virtual VGA (1234:1111) and ASPEED BMC adapters land here.
49+
Expect(vendorFromGHW([]*gpu.GraphicsCard{card("1234", "unknown")})).To(BeEmpty())
50+
})
51+
52+
It("tolerates cards with no device information at all", func() {
53+
Expect(vendorFromGHW([]*gpu.GraphicsCard{{}, nil})).To(BeEmpty())
54+
Expect(vendorFromGHW(nil)).To(BeEmpty())
55+
})
56+
})
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package xsysinfo
2+
3+
import (
4+
"github.com/jaypipes/ghw/pkg/gpu"
5+
. "github.com/onsi/ginkgo/v2"
6+
. "github.com/onsi/gomega"
7+
)
8+
9+
var _ = Describe("ghwHasVendor", func() {
10+
It("matches on the numeric PCI vendor ID", func() {
11+
cards := []*gpu.GraphicsCard{card("8086", "Intel Corporation")}
12+
Expect(ghwHasVendor(cards, VendorIntel)).To(BeTrue())
13+
Expect(ghwHasVendor(cards, VendorNVIDIA)).To(BeFalse())
14+
})
15+
16+
It("reports every vendor present, not only the highest-priority one", func() {
17+
// A hybrid-graphics host has both. Asking "is there an Intel GPU"
18+
// must not be answered by the discrete NVIDIA card outranking it.
19+
cards := []*gpu.GraphicsCard{card("8086", "Intel Corporation"), card("10de", "NVIDIA Corporation")}
20+
Expect(ghwHasVendor(cards, VendorNVIDIA)).To(BeTrue())
21+
Expect(ghwHasVendor(cards, VendorIntel)).To(BeTrue())
22+
Expect(ghwHasVendor(cards, VendorAMD)).To(BeFalse())
23+
})
24+
25+
It("matches the vendor name case-insensitively when no ID is available", func() {
26+
// pci.ids spells it "NVIDIA Corporation"; callers pass the
27+
// lowercase vendor constant.
28+
Expect(ghwHasVendor([]*gpu.GraphicsCard{card("", "NVIDIA Corporation")}, VendorNVIDIA)).To(BeTrue())
29+
})
30+
31+
It("does not match a device that is not a known GPU vendor", func() {
32+
Expect(ghwHasVendor([]*gpu.GraphicsCard{card("1234", "unknown")}, VendorIntel)).To(BeFalse())
33+
})
34+
35+
It("tolerates missing device information", func() {
36+
Expect(ghwHasVendor([]*gpu.GraphicsCard{{}, nil}, VendorIntel)).To(BeFalse())
37+
Expect(ghwHasVendor(nil, VendorIntel)).To(BeFalse())
38+
})
39+
})

0 commit comments

Comments
 (0)