From 0b78d0bf22fc772900d972752edd30caa23a3adb Mon Sep 17 00:00:00 2001 From: 16601 <16601@example.com> Date: Mon, 13 Jul 2026 16:02:34 +0800 Subject: [PATCH] feat: detect Houmo XH2A NPUs --- internal/hal/npu.go | 2 + internal/hal/npu_linux.go | 43 +++++++++++++++++- internal/hal/npu_linux_test.go | 40 +++++++++++++++++ internal/onboarding/status.go | 8 ++++ internal/onboarding/status_test.go | 18 ++++++++ internal/onboarding/types.go | 9 ++++ internal/ui/static/index.html | 72 ++++++++++++++++++++++++++++-- 7 files changed, 186 insertions(+), 6 deletions(-) create mode 100644 internal/hal/npu_linux_test.go diff --git a/internal/hal/npu.go b/internal/hal/npu.go index c10a8ec9..85b39a8c 100644 --- a/internal/hal/npu.go +++ b/internal/hal/npu.go @@ -28,6 +28,8 @@ func npuVendorFromDriver(driver string) string { return "intel" case strings.HasPrefix(driver, "qcom"): return "qualcomm" + case strings.HasPrefix(driver, "houmo,"): + return "houmo" default: return driver } diff --git a/internal/hal/npu_linux.go b/internal/hal/npu_linux.go index fc84dca0..a72ca3ae 100644 --- a/internal/hal/npu_linux.go +++ b/internal/hal/npu_linux.go @@ -10,8 +10,17 @@ import ( const accelSysfsDir = "/sys/class/accel" +const xh2aDriver = "houmo,xh2a" + func detectNPU() *NPUInfo { - entries, err := os.ReadDir(accelSysfsDir) + if npu := detectAccelNPU(accelSysfsDir); npu != nil { + return npu + } + return detectXH2ANPU("/sys/bus/pci/devices") +} + +func detectAccelNPU(sysfsDir string) *NPUInfo { + entries, err := os.ReadDir(sysfsDir) if err != nil { return nil } @@ -22,7 +31,7 @@ func detectNPU() *NPUInfo { if !strings.HasPrefix(entry.Name(), "accel") { continue } - devDir := filepath.Join(accelSysfsDir, entry.Name(), "device") + devDir := filepath.Join(sysfsDir, entry.Name(), "device") uevent, err := os.ReadFile(filepath.Join(devDir, "uevent")) if err != nil { @@ -53,6 +62,36 @@ func detectNPU() *NPUInfo { return npu } +// detectXH2ANPU detects Houmo XH2A IPUs. The vendor kernel driver exposes the +// accelerator as a PCI memory controller instead of the generic /sys/class/accel +// interface, so the standard NPU probe cannot see it. +func detectXH2ANPU(pciDevicesDir string) *NPUInfo { + entries, err := os.ReadDir(pciDevicesDir) + if err != nil { + return nil + } + + count := 0 + for _, entry := range entries { + driverLink := filepath.Join(pciDevicesDir, entry.Name(), "driver") + target, err := os.Readlink(driverLink) + if err != nil || filepath.Base(target) != xh2aDriver { + continue + } + count++ + } + if count == 0 { + return nil + } + + return &NPUInfo{ + Vendor: "houmo", + Name: "Houmo XH2A IPU", + Driver: xh2aDriver, + Count: count, + } +} + func readTrimmedFile(path string) string { data, err := os.ReadFile(path) if err != nil { diff --git a/internal/hal/npu_linux_test.go b/internal/hal/npu_linux_test.go new file mode 100644 index 00000000..a925f761 --- /dev/null +++ b/internal/hal/npu_linux_test.go @@ -0,0 +1,40 @@ +//go:build linux + +package hal + +import ( + "os" + "path/filepath" + "testing" +) + +func TestDetectXH2ANPU(t *testing.T) { + root := t.TempDir() + for name, driver := range map[string]string{ + "0000:01:00.0": xh2aDriver, + "0000:02:00.0": xh2aDriver, + "0000:03:00.0": "unrelated-driver", + } { + deviceDir := filepath.Join(root, name) + if err := os.Mkdir(deviceDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.Symlink(filepath.Join("..", "drivers", driver), filepath.Join(deviceDir, "driver")); err != nil { + t.Fatal(err) + } + } + + npu := detectXH2ANPU(root) + if npu == nil { + t.Fatal("expected XH2A NPU") + } + if npu.Vendor != "houmo" || npu.Name != "Houmo XH2A IPU" || npu.Driver != xh2aDriver || npu.Count != 2 { + t.Fatalf("unexpected NPU: %+v", npu) + } +} + +func TestDetectXH2ANPUAbsent(t *testing.T) { + if npu := detectXH2ANPU(t.TempDir()); npu != nil { + t.Fatalf("expected no XH2A NPU, got %+v", npu) + } +} diff --git a/internal/onboarding/status.go b/internal/onboarding/status.go index 3678c17a..2afb3b57 100644 --- a/internal/onboarding/status.go +++ b/internal/onboarding/status.go @@ -118,6 +118,14 @@ func buildHardware(ctx context.Context, deps *Deps, hw *hal.HardwareInfo) Hardwa }} result.UnifiedMemory = hw.GPU.UnifiedMemory } + if hw.NPU != nil { + result.NPU = &NPU{ + Vendor: hw.NPU.Vendor, + Name: hw.NPU.Name, + Driver: hw.NPU.Driver, + Count: hw.NPU.Count, + } + } result.CPU = CPU{ Model: hw.CPU.Model, diff --git a/internal/onboarding/status_test.go b/internal/onboarding/status_test.go index 23382c93..c8ff920d 100644 --- a/internal/onboarding/status_test.go +++ b/internal/onboarding/status_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/jguan/aima/internal/hal" "github.com/jguan/aima/internal/mcp" ) @@ -41,6 +42,23 @@ func TestBuildStatus_NoConfig(t *testing.T) { } } +func TestBuildHardwareIncludesNPU(t *testing.T) { + hardware := buildHardware(context.Background(), nil, &hal.HardwareInfo{ + NPU: &hal.NPUInfo{ + Vendor: "houmo", + Name: "Houmo XH2A IPU", + Driver: "houmo,xh2a", + Count: 1, + }, + }) + if hardware.NPU == nil { + t.Fatal("expected NPU in onboarding hardware") + } + if hardware.NPU.Name != "Houmo XH2A IPU" || hardware.NPU.Driver != "houmo,xh2a" || hardware.NPU.Count != 1 { + t.Fatalf("unexpected NPU: %+v", hardware.NPU) + } +} + func TestBuildVersion_CachesFailedLookup(t *testing.T) { orig := FetchLatestRelease defer func() { FetchLatestRelease = orig }() diff --git a/internal/onboarding/types.go b/internal/onboarding/types.go index 86a591cf..45ee9d58 100644 --- a/internal/onboarding/types.go +++ b/internal/onboarding/types.go @@ -29,6 +29,14 @@ type GPU struct { UnifiedMemory bool `json:"unified_memory,omitempty"` } +// NPU is a single neural processing unit entry in the onboarding status response. +type NPU struct { + Vendor string `json:"vendor"` + Name string `json:"name"` + Driver string `json:"driver"` + Count int `json:"count"` +} + // CPU describes the host CPU in the onboarding status response. type CPU struct { Model string `json:"model"` @@ -38,6 +46,7 @@ type CPU struct { // Hardware aggregates hardware info for the onboarding status response. type Hardware struct { GPU []GPU `json:"gpu"` + NPU *NPU `json:"npu,omitempty"` CPU CPU `json:"cpu"` RAMMiB int `json:"ram_mib"` OS string `json:"os"` diff --git a/internal/ui/static/index.html b/internal/ui/static/index.html index 7e321cb1..333f9a12 100644 --- a/internal/ui/static/index.html +++ b/internal/ui/static/index.html @@ -4312,6 +4312,11 @@
+ +