diff --git a/pkg/engines/busses.go b/pkg/engines/busses.go index 1f2b2869..46f79c80 100644 --- a/pkg/engines/busses.go +++ b/pkg/engines/busses.go @@ -12,6 +12,8 @@ func (device Device) validateBus(extraFields []string) error { return device.validatePci(extraFields) case "usb": return device.validateUsb(extraFields) + case "fastrpc": + return device.validateFastRpc(extraFields) case "": // default to pci bus return device.validatePci(extraFields) default: @@ -49,3 +51,32 @@ func (device Device) validatePci(extraFields []string) error { return nil } + +func (device Device) validateFastRpc(extraFields []string) error { + if device.Type != "" && device.Type != "npu" { + return fmt.Errorf("fastrpc bus only supports npu devices") + } + + validFields := []string{ + "Type", + "Bus", + "NodeGlob", + "SnapConnections", + } + validFields = append(validFields, extraFields...) + + t := reflect.TypeOf(device) + v := reflect.ValueOf(device) + + for i := 0; i < t.NumField(); i++ { + fieldName := t.Field(i).Name + fieldValue := v.FieldByName(fieldName) + if fieldValue.IsValid() && !fieldValue.IsZero() { + if !slices.Contains(validFields, fieldName) { + return fmt.Errorf("fastrpc: invalid field: %s", fieldName) + } + } + } + + return nil +} diff --git a/pkg/engines/busses_test.go b/pkg/engines/busses_test.go index a8abd686..6155aa99 100644 --- a/pkg/engines/busses_test.go +++ b/pkg/engines/busses_test.go @@ -42,4 +42,13 @@ func TestDeviceBus(t *testing.T) { } t.Log(err) }) + + t.Run("FastRPC bus with non-NPU type", func(t *testing.T) { + device.Bus = "fastrpc" + err := device.validate() + if err == nil { + t.Fatal("FastRPC bus should be invalid for GPU type") + } + t.Log(err) + }) } diff --git a/pkg/engines/devices_test.go b/pkg/engines/devices_test.go index 168b3565..5531036b 100644 --- a/pkg/engines/devices_test.go +++ b/pkg/engines/devices_test.go @@ -130,6 +130,29 @@ func TestDeviceNpu(t *testing.T) { } t.Log(err) }) + + t.Run("NPU fastrpc valid fields", func(t *testing.T) { + device = Device{Type: "npu", Bus: "fastrpc"} + nodeGlob := "/dev/fastrpc-cdsp*" + device.NodeGlob = &nodeGlob + + err := device.validate() + if err != nil { + t.Fatalf("NPU fastrpc fields should be valid: %v", err) + } + }) + + t.Run("NPU fastrpc invalid fields", func(t *testing.T) { + device = Device{Type: "npu", Bus: "fastrpc"} + hexValue := types.HexInt(0xAA) + device.VendorId = &hexValue + + err := device.validate() + if err == nil { + t.Fatal("NPU fastrpc fields should be invalid") + } + t.Log(err) + }) } func TestDeviceTypeless(t *testing.T) { diff --git a/pkg/engines/types.go b/pkg/engines/types.go index 803c120a..05c7da14 100644 --- a/pkg/engines/types.go +++ b/pkg/engines/types.go @@ -45,7 +45,7 @@ type Devices struct { type Device struct { // General Type string `yaml:"type,omitempty" json:"type,omitempty"` // cpu, gpu, npu or nil - Bus string `yaml:"bus,omitempty" json:"bus,omitempty"` // pci, usb or nil + Bus string `yaml:"bus,omitempty" json:"bus,omitempty"` // pci, usb, fastrpc or nil // CPUs Architecture *string `yaml:"architecture,omitempty" json:"architecture,omitempty"` @@ -69,7 +69,7 @@ type Device struct { ComputeCapability *string `yaml:"compute-capability,omitempty" json:"compute-capability,omitempty"` // NPU - // no additional properties for now + NodeGlob *string `yaml:"node-glob,omitempty" json:"node-glob,omitempty"` // Drivers SnapConnections []string `yaml:"snap-connections,omitempty" json:"snap-connections,omitempty"` diff --git a/pkg/hardware_info/disk/disk_info.go b/pkg/hardware_info/disk/disk_info.go index 1781be2a..7858efde 100644 --- a/pkg/hardware_info/disk/disk_info.go +++ b/pkg/hardware_info/disk/disk_info.go @@ -2,6 +2,7 @@ package disk import ( "fmt" + "os" "github.com/canonical/inference-snaps-cli/pkg/constants" "github.com/canonical/inference-snaps-cli/pkg/types" @@ -16,6 +17,14 @@ func Info() (map[string]types.DirStats, error) { var info = make(map[string]types.DirStats) for _, dir := range directories { + // Skip directories that don't exist + if _, err := os.Stat(dir); err != nil { + if os.IsNotExist(err) { + continue + } + return nil, fmt.Errorf("checking directory %s: %v", dir, err) + } + dirInfo, err := statFs(dir) if err != nil { return nil, fmt.Errorf("getting directory info for %s: %v", dir, err) diff --git a/pkg/hardware_info/hardware-info.go b/pkg/hardware_info/hardware-info.go index a87bd62c..8f3eea58 100644 --- a/pkg/hardware_info/hardware-info.go +++ b/pkg/hardware_info/hardware-info.go @@ -4,12 +4,14 @@ import ( "encoding/json" "fmt" "os" + "strings" "testing" "github.com/canonical/inference-snaps-cli/pkg/hardware_info/cpu" "github.com/canonical/inference-snaps-cli/pkg/hardware_info/disk" "github.com/canonical/inference-snaps-cli/pkg/hardware_info/memory" "github.com/canonical/inference-snaps-cli/pkg/hardware_info/pci" + "github.com/canonical/inference-snaps-cli/pkg/hardware_info/qualcomm" "github.com/canonical/inference-snaps-cli/pkg/types" ) @@ -40,6 +42,12 @@ func Get(friendlyNames bool) (*types.HwInfo, []string, error) { } hwInfo.PciDevices = pciDevices + devices, err := qualcomm.Info() + if err != nil { + return nil, nil, fmt.Errorf("getting qualcomm devices: %v", err) + } + hwInfo.Devices = devices + return &hwInfo, warnings, nil } @@ -100,6 +108,20 @@ func GetFromRawData(t *testing.T, device string, friendlyNames bool, testDir str } hwInfo.PciDevices = pciDevices + fastrpcNodesFile := devicePath + "fastrpc-nodes.txt" + if _, err := os.Stat(fastrpcNodesFile); err == nil { + nodesData, err := os.ReadFile(fastrpcNodesFile) + if err != nil { + t.Fatal(err) + } + + nodes := strings.Fields(string(nodesData)) + devices := qualcomm.DetectFromNodes(nodes) + hwInfo.Devices = devices + } else if !os.IsNotExist(err) { + t.Fatalf("error checking file '%s': %v\n", fastrpcNodesFile, err) + } + // Additional properties - we append these directly from a file, as we can not run the vendor specific tools on the machine addPropsFile := devicePath + "additional-properties.json" _, err = os.Stat(addPropsFile) diff --git a/pkg/hardware_info/qualcomm/qualcomm.go b/pkg/hardware_info/qualcomm/qualcomm.go new file mode 100644 index 00000000..08d7bb93 --- /dev/null +++ b/pkg/hardware_info/qualcomm/qualcomm.go @@ -0,0 +1,71 @@ +package qualcomm + +import ( + "fmt" + "path/filepath" + "sort" + "strings" + + "github.com/canonical/inference-snaps-cli/pkg/types" +) + +var devRoot = "/dev" + +func Info() ([]types.DetectedDevice, error) { + nodes, err := globSorted(filepath.Join(devRoot, "fastrpc-*")) + if err != nil { + return nil, fmt.Errorf("detecting fastrpc nodes: %v", err) + } + + return DetectFromNodes(nodes), nil +} + +func DetectFromNodes(nodes []string) []types.DetectedDevice { + var devices []types.DetectedDevice + + for _, node := range nodes { + node = strings.TrimSpace(node) + if node == "" { + continue + } + + base := filepath.Base(node) + if !strings.HasPrefix(base, "fastrpc-") { + continue + } + + devices = append(devices, types.DetectedDevice{ + Type: detectNpuType(base), + Bus: "fastrpc", + Metadata: &types.DeviceMetadata{ + VendorName: "qualcomm", + ProductName: node, + }, + }) + } + + sort.Slice(devices, func(i, j int) bool { + return devices[i].Metadata.ProductName < devices[j].Metadata.ProductName + }) + + return devices +} + +func detectNpuType(nodeBaseName string) string { + const prefix = "fastrpc-" + if strings.HasPrefix(nodeBaseName, prefix) { + suffix := strings.TrimPrefix(nodeBaseName, prefix) + return fmt.Sprintf("NPU - %s", suffix) + } + return "NPU" +} + +func globSorted(pattern string) ([]string, error) { + matches, err := filepath.Glob(pattern) + if err != nil { + return nil, err + } + + sort.Strings(matches) + return matches, nil +} diff --git a/pkg/hardware_info/qualcomm/qualcomm_test.go b/pkg/hardware_info/qualcomm/qualcomm_test.go new file mode 100644 index 00000000..d189e526 --- /dev/null +++ b/pkg/hardware_info/qualcomm/qualcomm_test.go @@ -0,0 +1,109 @@ +package qualcomm + +import ( + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/canonical/inference-snaps-cli/pkg/types" +) + +func TestInfo(t *testing.T) { + tmp := t.TempDir() + originalRoot := devRoot + devRoot = tmp + t.Cleanup(func() { + devRoot = originalRoot + }) + + _, err := os.Create(filepath.Join(tmp, "fastrpc-adsp-secure")) + if err != nil { + t.Fatal(err) + } + _, err = os.Create(filepath.Join(tmp, "fastrpc-cdsp")) + if err != nil { + t.Fatal(err) + } + _, err = os.Create(filepath.Join(tmp, "fastrpc-cdsp1")) + if err != nil { + t.Fatal(err) + } + + devices, err := Info() + if err != nil { + t.Fatal(err) + } + + expectedDevices := []types.DetectedDevice{ + { + Type: "NPU - adsp-secure", + Bus: "fastrpc", + Metadata: &types.DeviceMetadata{ + VendorName: "qualcomm", + ProductName: filepath.Join(tmp, "fastrpc-adsp-secure"), + }, + }, + { + Type: "NPU - cdsp", + Bus: "fastrpc", + Metadata: &types.DeviceMetadata{ + VendorName: "qualcomm", + ProductName: filepath.Join(tmp, "fastrpc-cdsp"), + }, + }, + { + Type: "NPU - cdsp1", + Bus: "fastrpc", + Metadata: &types.DeviceMetadata{ + VendorName: "qualcomm", + ProductName: filepath.Join(tmp, "fastrpc-cdsp1"), + }, + }, + } + if !reflect.DeepEqual(devices, expectedDevices) { + t.Fatalf("devices=%+v expected=%+v", devices, expectedDevices) + } +} + +func TestDetectFromNodes(t *testing.T) { + nodes := []string{ + "/dev/fastrpc-cdsp1-secure", + "/dev/fastrpc-adsp", + "/dev/something-else", + "/dev/fastrpc-cdsp", + } + + devices := DetectFromNodes(nodes) + + expectedDevices := []types.DetectedDevice{ + { + Type: "NPU - adsp", + Bus: "fastrpc", + Metadata: &types.DeviceMetadata{ + VendorName: "qualcomm", + ProductName: "/dev/fastrpc-adsp", + }, + }, + { + Type: "NPU - cdsp", + Bus: "fastrpc", + Metadata: &types.DeviceMetadata{ + VendorName: "qualcomm", + ProductName: "/dev/fastrpc-cdsp", + }, + }, + { + Type: "NPU - cdsp1-secure", + Bus: "fastrpc", + Metadata: &types.DeviceMetadata{ + VendorName: "qualcomm", + ProductName: "/dev/fastrpc-cdsp1-secure", + }, + }, + } + + if !reflect.DeepEqual(devices, expectedDevices) { + t.Fatalf("devices=%+v expected=%+v", devices, expectedDevices) + } +} diff --git a/pkg/selector/fastrpc/fastrpc.go b/pkg/selector/fastrpc/fastrpc.go new file mode 100644 index 00000000..ba0111be --- /dev/null +++ b/pkg/selector/fastrpc/fastrpc.go @@ -0,0 +1,79 @@ +package fastrpc + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + + "github.com/canonical/go-snapctl" + "github.com/canonical/inference-snaps-cli/pkg/engines" + "github.com/canonical/inference-snaps-cli/pkg/selector/weights" + "github.com/canonical/inference-snaps-cli/pkg/types" +) + +const defaultNpuNodeGlob = "/dev/fastrpc-cdsp*" + +func Match(manifestDevice engines.Device, hostDevices []types.DetectedDevice) (int, []string) { + nodeGlob := defaultNpuNodeGlob + if manifestDevice.NodeGlob != nil { + nodeGlob = *manifestDevice.NodeGlob + } + + for _, hostDevice := range hostDevices { + if hostDevice.Bus != "fastrpc" { + continue + } + if !deviceTypeMatch(manifestDevice.Type, hostDevice.Type) { + continue + } + if hostDevice.Metadata == nil || hostDevice.Metadata.ProductName == "" { + continue + } + + matched, err := filepath.Match(nodeGlob, hostDevice.Metadata.ProductName) + if err != nil { + return 0, []string{fmt.Sprintf("invalid node-glob %q: %v", nodeGlob, err)} + } + if !matched { + continue + } + + score := weights.PciDevice + weights.PciDeviceType + for _, connection := range manifestDevice.SnapConnections { + if testing.Testing() { + continue + } + connected, err := snapctl.IsConnected(connection).Run() + if err != nil { + return 0, []string{fmt.Sprintf("checking snap connection %q: %v", connection, err)} + } + if !connected { + return 0, []string{fmt.Sprintf("%q is not connected", connection)} + } + } + + return score, nil + } + + return 0, []string{fmt.Sprintf("device node matching %q not found", nodeGlob)} +} + +func deviceTypeMatch(manifestType, hostType string) bool { + if manifestType == "" { + return true + } + + manifest := strings.ToLower(strings.TrimSpace(manifestType)) + host := strings.ToLower(strings.TrimSpace(hostType)) + + if manifest == host { + return true + } + + if manifest == "npu" && strings.HasPrefix(host, "npu") { + return true + } + + return false +} diff --git a/pkg/selector/fastrpc/fastrpc_test.go b/pkg/selector/fastrpc/fastrpc_test.go new file mode 100644 index 00000000..1c849e1d --- /dev/null +++ b/pkg/selector/fastrpc/fastrpc_test.go @@ -0,0 +1,58 @@ +package fastrpc + +import ( + "testing" + + "github.com/canonical/inference-snaps-cli/pkg/engines" + "github.com/canonical/inference-snaps-cli/pkg/types" +) + +func TestMatch(t *testing.T) { + hostDevices := []types.DetectedDevice{ + { + Type: "npu", + Bus: "fastrpc", + Metadata: &types.DeviceMetadata{ + VendorName: "qualcomm", + ProductName: "/dev/fastrpc-cdsp", + }, + }, + { + Type: "npu", + Bus: "fastrpc", + Metadata: &types.DeviceMetadata{ + VendorName: "qualcomm", + ProductName: "/dev/fastrpc-cdsp1-secure", + }, + }, + } + + t.Run("default pattern", func(t *testing.T) { + device := engines.Device{Type: "npu", Bus: "fastrpc"} + score, issues := Match(device, hostDevices) + if score == 0 || len(issues) > 0 { + t.Fatalf("expected a positive score with no issues, score=%d issues=%v", score, issues) + } + }) + + t.Run("custom node glob", func(t *testing.T) { + nodeGlob := "/dev/fastrpc-cdsp1*" + device := engines.Device{Type: "npu", Bus: "fastrpc", NodeGlob: &nodeGlob} + score, issues := Match(device, hostDevices) + if score == 0 || len(issues) > 0 { + t.Fatalf("expected a positive score with no issues, score=%d issues=%v", score, issues) + } + }) + + t.Run("no matching node", func(t *testing.T) { + nodeGlob := "/dev/fastrpc-gdsp*" + device := engines.Device{Type: "npu", Bus: "fastrpc", NodeGlob: &nodeGlob} + score, issues := Match(device, hostDevices) + if score != 0 { + t.Fatalf("expected score=0, got %d", score) + } + if len(issues) == 0 { + t.Fatal("expected compatibility issues") + } + }) +} diff --git a/pkg/selector/fastrpc_test.go b/pkg/selector/fastrpc_test.go new file mode 100644 index 00000000..7f5796c7 --- /dev/null +++ b/pkg/selector/fastrpc_test.go @@ -0,0 +1,53 @@ +package selector + +import ( + "testing" + + "github.com/canonical/inference-snaps-cli/pkg/engines" + "github.com/canonical/inference-snaps-cli/pkg/types" +) + +func TestFastRpcNpuSelection(t *testing.T) { + hwInfo := &types.HwInfo{ + Memory: types.MemoryInfo{TotalRam: 8 * 1024 * 1024 * 1024}, + Disk: map[string]types.DirStats{ + "/var/lib/snapd/snaps": { + Avail: 20 * 1024 * 1024 * 1024, + }, + }, + Devices: []types.DetectedDevice{ + { + Type: "npu", + Bus: "fastrpc", + Metadata: &types.DeviceMetadata{ + VendorName: "qualcomm", + ProductName: "/dev/fastrpc-cdsp", + }, + }, + }, + } + + manifest := engines.Manifest{ + Name: "qualcomm-npu", + Description: "qualcomm dragonwing npu", + Vendor: "qualcomm", + Grade: "stable", + Devices: engines.Devices{ + Anyof: []engines.Device{{ + Type: "npu", + Bus: "fastrpc", + }}, + }, + } + + score, report, err := checkEngine(hwInfo, manifest) + if err != nil { + t.Fatal(err) + } + if !report.EngineCompatible() { + t.Fatalf("expected engine to be compatible: %+v", report) + } + if score <= 0 { + t.Fatalf("expected positive score, got %d", score) + } +} diff --git a/pkg/selector/select_stack.go b/pkg/selector/select_stack.go index 671f52c1..bd84a5db 100644 --- a/pkg/selector/select_stack.go +++ b/pkg/selector/select_stack.go @@ -8,6 +8,7 @@ import ( "github.com/canonical/inference-snaps-cli/pkg/constants" "github.com/canonical/inference-snaps-cli/pkg/engines" "github.com/canonical/inference-snaps-cli/pkg/selector/cpu" + "github.com/canonical/inference-snaps-cli/pkg/selector/fastrpc" "github.com/canonical/inference-snaps-cli/pkg/selector/pci" "github.com/canonical/inference-snaps-cli/pkg/types" "github.com/canonical/inference-snaps-cli/pkg/utils" @@ -165,6 +166,15 @@ func scoreDevicesAll(hardwareInfo *types.HwInfo, devices []engines.Device) int { compatible = false devices[i].CompatibilityIssues = append(devices[i].CompatibilityIssues, "usb device matching not implemented") + } else if devices[i].Bus == "fastrpc" { + fastRpcScore, fastRpcIssues := fastrpc.Match(devices[i], hardwareInfo.Devices) + if len(fastRpcIssues) > 0 { + compatible = false + devices[i].CompatibilityIssues = append(devices[i].CompatibilityIssues, fastRpcIssues...) + } else { + compatibilityScore += fastRpcScore + } + } else if devices[i].Bus == "" || devices[i].Bus == "pci" { // Fallback to PCI as default bus pciScore, pciIssues := pci.Match(devices[i], hardwareInfo.PciDevices) @@ -204,6 +214,15 @@ func scoreDevicesAny(hardwareInfo *types.HwInfo, devices []engines.Device) int { compatible = false device.CompatibilityIssues = append(device.CompatibilityIssues, "usb device matching not implemented") + } else if device.Bus == "fastrpc" { + fastRpcScore, fastRpcIssues := fastrpc.Match(device, hardwareInfo.Devices) + if len(fastRpcIssues) > 0 { + devices[i].CompatibilityIssues = append(device.CompatibilityIssues, fastRpcIssues...) + } else { + devicesFound++ + compatibilityScore += fastRpcScore + } + } else if device.Bus == "" || device.Bus == "pci" { // Fallback to PCI as default bus pciScore, pciIssues := pci.Match(device, hardwareInfo.PciDevices) diff --git a/pkg/types/detected_devices.go b/pkg/types/detected_devices.go new file mode 100644 index 00000000..6595cadc --- /dev/null +++ b/pkg/types/detected_devices.go @@ -0,0 +1,12 @@ +package types + +type DeviceMetadata struct { + VendorName string `json:"vendor_name" yaml:"vendor_name"` + ProductName string `json:"product_name,omitempty" yaml:"product_name,omitempty"` +} + +type DetectedDevice struct { + Type string `json:"type" yaml:"type"` + Bus string `json:"bus" yaml:"bus"` + Metadata *DeviceMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} diff --git a/pkg/types/hw_info.go b/pkg/types/hw_info.go index 9548c730..a8acc817 100644 --- a/pkg/types/hw_info.go +++ b/pkg/types/hw_info.go @@ -5,4 +5,5 @@ type HwInfo struct { Memory MemoryInfo `json:"memory,omitempty" yaml:"memory,omitempty"` Disk map[string]DirStats `json:"disk,omitempty" yaml:"disk,omitempty"` PciDevices []PciDevice `json:"pci,omitempty" yaml:"pci"` + Devices []DetectedDevice `json:"devices,omitempty" yaml:"devices,omitempty"` }