diff --git a/README.md b/README.md index 8b3b6d16..bad7c86e 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ hardware: * [`ghw.Network()`](#network) * [`ghw.PCI()`](#pci) * [`ghw.GPU()`](#gpu) (graphical processing unit) +* [`ghw.Accelerator()`](#accelerator) (processing accelerators, AI) * [`ghw.Chassis()`](#chassis) * [`ghw.BIOS()`](#bios) * [`ghw.Baseboard()`](#baseboard) @@ -893,7 +894,7 @@ information about the host computer's graphics hardware. The `ghw.GPUInfo` struct contains one field: * `ghw.GPUInfo.GraphicCards` is an array of pointers to `ghw.GraphicsCard` - structs, one for each graphics card found for the systen + structs, one for each graphics card found for the system Each `ghw.GraphicsCard` struct contains the following fields: @@ -945,6 +946,60 @@ information `ghw.TopologyNode` struct if you'd like to dig deeper into the NUMA/topology subsystem +### Accelerator + +The `ghw.Accelerator()` function returns a `ghw.AcceleratorInfo` struct that contains +information about the host computer's processing accelerator hardware. In this category +we can find used hardware for AI. The hardware detected in this category will be +processing accelerators (PCI class `1200`), 3D controllers (`0302`) and Display +controllers (`0380`). + +The `ghw.AcceleratorInfo` struct contains one field: + +* `ghw.AcceleratorInfo.Devices` is an array of pointers to `ghw.AcceleratorDevice` + structs, one for each processing accelerator card found for the system. + +Each `ghw.AcceleratorDevice` struct contains the following fields: + +* `ghw.AcceleratorDevice.Address` is the PCI address for the processing accelerator card. +* `ghw.AcceleratorDevice.PCIDevice` is a pointer to a `ghw.PCIDevice` struct. + describing the processing accelerator card. This may be `nil` if no PCI device + information could be determined for the card. + +```go +package main + +import ( + "fmt" + + "github.com/jaypipes/ghw" +) + +func main() { + accel, err := ghw.Accelerator() + if err != nil { + fmt.Printf("Error getting processing accelerator info: %v", err) + } + + fmt.Printf("%v\n", accel) + + for _, card := range accel.Devices { + fmt.Printf(" %v\n", device) + } +} +``` + +Example output from a testing machine: + +``` +processing accelerators (1 device) + device @0000:00:04.0 -> driver: 'fake_pci_driver' class: 'Processing accelerators' vendor: 'Red Hat, Inc.' product: 'QEMU PCI Test Device' +``` + +**NOTE**: You can [read more](#pci) about the fields of the `ghw.PCIDevice` +struct if you'd like to dig deeper into PCI subsystem and programming interface +information + ### Chassis The `ghw.Chassis()` function returns a `ghw.ChassisInfo` struct that contains diff --git a/alias.go b/alias.go index ccd5e808..9c403d96 100644 --- a/alias.go +++ b/alias.go @@ -7,6 +7,7 @@ package ghw import ( + "github.com/jaypipes/ghw/pkg/accelerator" "github.com/jaypipes/ghw/pkg/baseboard" "github.com/jaypipes/ghw/pkg/bios" "github.com/jaypipes/ghw/pkg/block" @@ -183,3 +184,10 @@ type GraphicsCard = gpu.GraphicsCard var ( GPU = gpu.New ) + +type AcceleratorInfo = accelerator.Info +type AcceleratorDevice = accelerator.AcceleratorDevice + +var ( + Accelerator = accelerator.New +) diff --git a/cmd/ghwc/commands/accelerator.go b/cmd/ghwc/commands/accelerator.go new file mode 100644 index 00000000..24cf28dc --- /dev/null +++ b/cmd/ghwc/commands/accelerator.go @@ -0,0 +1,48 @@ +// +// Use and distribution licensed under the Apache license version 2. +// +// See the COPYING file in the root project directory for full text. +// + +package commands + +import ( + "fmt" + + "github.com/jaypipes/ghw" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +// acceleratorCmd represents the install command +var acceleratorCmd = &cobra.Command{ + Use: "accelerator", + Short: "Show processing accelerators information for the host system", + RunE: showGPU, +} + +// showAccelerator show processing accelerators information for the host system. +func showAccelerator(cmd *cobra.Command, args []string) error { + accel, err := ghw.Accelerator() + if err != nil { + return errors.Wrap(err, "error getting Accelerator info") + } + + switch outputFormat { + case outputFormatHuman: + fmt.Printf("%v\n", accel) + + for _, card := range accel.Devices { + fmt.Printf(" %v\n", card) + } + case outputFormatJSON: + fmt.Printf("%s\n", accel.JSONString(pretty)) + case outputFormatYAML: + fmt.Printf("%s", accel.YAMLString()) + } + return nil +} + +func init() { + rootCmd.AddCommand(acceleratorCmd) +} diff --git a/cmd/ghwc/commands/root.go b/cmd/ghwc/commands/root.go index 0a2dfc45..3bf54461 100644 --- a/cmd/ghwc/commands/root.go +++ b/cmd/ghwc/commands/root.go @@ -90,6 +90,9 @@ func showAll(cmd *cobra.Command, args []string) error { if err := showProduct(cmd, args); err != nil { return err } + if err := showAccelerator(cmd, args); err != nil { + return err + } case outputFormatJSON: host, err := ghw.Host() if err != nil { diff --git a/go.mod b/go.mod index f52637a5..d2a60e4a 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,9 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/samber/lo v1.47.0 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/sys v0.1.0 // indirect + golang.org/x/text v0.16.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index cc122227..06fb0874 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -26,6 +28,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/host.go b/host.go index 5d82a53a..89b1ad27 100644 --- a/host.go +++ b/host.go @@ -11,6 +11,7 @@ import ( "github.com/jaypipes/ghw/pkg/context" + "github.com/jaypipes/ghw/pkg/accelerator" "github.com/jaypipes/ghw/pkg/baseboard" "github.com/jaypipes/ghw/pkg/bios" "github.com/jaypipes/ghw/pkg/block" @@ -28,18 +29,19 @@ import ( // HostInfo is a wrapper struct containing information about the host system's // memory, block storage, CPU, etc type HostInfo struct { - ctx *context.Context - Memory *memory.Info `json:"memory"` - Block *block.Info `json:"block"` - CPU *cpu.Info `json:"cpu"` - Topology *topology.Info `json:"topology"` - Network *net.Info `json:"network"` - GPU *gpu.Info `json:"gpu"` - Chassis *chassis.Info `json:"chassis"` - BIOS *bios.Info `json:"bios"` - Baseboard *baseboard.Info `json:"baseboard"` - Product *product.Info `json:"product"` - PCI *pci.Info `json:"pci"` + ctx *context.Context + Memory *memory.Info `json:"memory"` + Block *block.Info `json:"block"` + CPU *cpu.Info `json:"cpu"` + Topology *topology.Info `json:"topology"` + Network *net.Info `json:"network"` + GPU *gpu.Info `json:"gpu"` + Accelerator *accelerator.Info `json:"accelerator"` + Chassis *chassis.Info `json:"chassis"` + BIOS *bios.Info `json:"bios"` + Baseboard *baseboard.Info `json:"baseboard"` + Product *product.Info `json:"product"` + PCI *pci.Info `json:"pci"` } // Host returns a pointer to a HostInfo struct that contains fields with @@ -71,6 +73,10 @@ func Host(opts ...*WithOption) (*HostInfo, error) { if err != nil { return nil, err } + acceleratorInfo, err := accelerator.New(opts...) + if err != nil { + return nil, err + } chassisInfo, err := chassis.New(opts...) if err != nil { return nil, err @@ -92,18 +98,19 @@ func Host(opts ...*WithOption) (*HostInfo, error) { return nil, err } return &HostInfo{ - ctx: ctx, - CPU: cpuInfo, - Memory: memInfo, - Block: blockInfo, - Topology: topologyInfo, - Network: netInfo, - GPU: gpuInfo, - Chassis: chassisInfo, - BIOS: biosInfo, - Baseboard: baseboardInfo, - Product: productInfo, - PCI: pciInfo, + ctx: ctx, + CPU: cpuInfo, + Memory: memInfo, + Block: blockInfo, + Topology: topologyInfo, + Network: netInfo, + GPU: gpuInfo, + Accelerator: acceleratorInfo, + Chassis: chassisInfo, + BIOS: biosInfo, + Baseboard: baseboardInfo, + Product: productInfo, + PCI: pciInfo, }, nil } @@ -111,10 +118,11 @@ func Host(opts ...*WithOption) (*HostInfo, error) { // structs' String-ified output func (info *HostInfo) String() string { return fmt.Sprintf( - "%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n", + "%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n", info.Block.String(), info.CPU.String(), info.GPU.String(), + info.Accelerator.String(), info.Memory.String(), info.Network.String(), info.Topology.String(), diff --git a/host_test.go b/host_test.go index 0a8abcca..2c1eb209 100644 --- a/host_test.go +++ b/host_test.go @@ -79,4 +79,11 @@ func TestHost(t *testing.T) { if gpu == nil { t.Fatalf("Expected non-nil GPU but got nil.") } + + // Processing accelerator cards are not common nowadays. + // You may not have one in your machine, so this check displays a message but does not interrupt the test. + accel := host.Accelerator + if accel == nil { + t.Logf("WARNING: Processing accelerator cards not detected.") + } } diff --git a/pkg/accelerator/accelerator.go b/pkg/accelerator/accelerator.go new file mode 100644 index 00000000..b51ef2e2 --- /dev/null +++ b/pkg/accelerator/accelerator.go @@ -0,0 +1,84 @@ +// +// Use and distribution licensed under the Apache license version 2. +// +// See the COPYING file in the root project directory for full text. +// + +package accelerator + +import ( + "fmt" + + "github.com/jaypipes/ghw/pkg/context" + "github.com/jaypipes/ghw/pkg/marshal" + "github.com/jaypipes/ghw/pkg/option" + "github.com/jaypipes/ghw/pkg/pci" +) + +type AcceleratorDevice struct { + // the PCI address where the accelerator device can be found + Address string `json:"address"` + // pointer to a PCIDevice struct that describes the vendor and product + // model, etc + PCIDevice *pci.Device `json:"pci_device"` +} + +func (dev *AcceleratorDevice) String() string { + deviceStr := dev.Address + if dev.PCIDevice != nil { + deviceStr = dev.PCIDevice.String() + } + nodeStr := "" + return fmt.Sprintf( + "device %s@%s", + nodeStr, + deviceStr, + ) +} + +type Info struct { + ctx *context.Context + Devices []*AcceleratorDevice `json:"devices"` +} + +// New returns a pointer to an Info struct that contains information about the +// accelerator devices on the host system +func New(opts ...*option.Option) (*Info, error) { + ctx := context.New(opts...) + info := &Info{ctx: ctx} + + if err := ctx.Do(info.load); err != nil { + return nil, err + } + return info, nil +} + +func (i *Info) String() string { + numDevsStr := "devices" + if len(i.Devices) == 1 { + numDevsStr = "device" + } + return fmt.Sprintf( + "processing accelerators (%d %s)", + len(i.Devices), + numDevsStr, + ) +} + +// simple private struct used to encapsulate processing accelerators information in a top-level +// "accelerator" YAML/JSON map/object key +type acceleratorPrinter struct { + Info *Info `json:"accelerator"` +} + +// YAMLString returns a string with the processing accelerators information formatted as YAML +// under a top-level "accelerator:" key +func (i *Info) YAMLString() string { + return marshal.SafeYAML(i.ctx, acceleratorPrinter{i}) +} + +// JSONString returns a string with the processing accelerators information formatted as JSON +// under a top-level "accelerator:" key +func (i *Info) JSONString(indent bool) string { + return marshal.SafeJSON(i.ctx, acceleratorPrinter{i}, indent) +} diff --git a/pkg/accelerator/accelerator_linux.go b/pkg/accelerator/accelerator_linux.go new file mode 100644 index 00000000..67b9aa3a --- /dev/null +++ b/pkg/accelerator/accelerator_linux.go @@ -0,0 +1,72 @@ +// Use and distribution licensed under the Apache license version 2. +// +// See the COPYING file in the root project directory for full text. +// + +package accelerator + +import ( + "github.com/samber/lo" + + "github.com/jaypipes/ghw/pkg/context" + "github.com/jaypipes/ghw/pkg/pci" +) + +// PCI IDs list available at https://admin.pci-ids.ucw.cz/read/PD +const ( + pciClassProcessingAccelerator = "12" + pciSubclassProcessingAccelerator = "00" + pciClassController = "03" + pciSubclass3DController = "02" + pciSubclassDisplayController = "80" +) + +var ( + acceleratorPCIClasses = map[string][]string{ + pciClassProcessingAccelerator: []string{ + pciSubclassProcessingAccelerator, + }, + pciClassController: []string{ + pciSubclass3DController, + pciSubclassDisplayController, + }, + } +) + +func (i *Info) load() error { + accelDevices := make([]*AcceleratorDevice, 0) + + // get PCI devices + pciInfo, err := pci.New(context.WithContext(i.ctx)) + if err != nil { + i.ctx.Warn("error loading PCI information: %s", err) + return nil + } + + // Prepare hardware filter based in the PCI Class + Subclass + isAccelerator := func(dev *pci.Device) bool { + class := dev.Class.ID + subclass := dev.Subclass.ID + if subclasses, ok := acceleratorPCIClasses[class]; ok { + if lo.Contains(subclasses, subclass) { + return true + } + } + return false + } + + // This loop iterates over the list of PCI devices and filters them based on discovery criteria + for _, device := range pciInfo.Devices { + if !isAccelerator(device) { + continue + } + accelDev := &AcceleratorDevice{ + Address: device.Address, + PCIDevice: device, + } + accelDevices = append(accelDevices, accelDev) + } + + i.Devices = accelDevices + return nil +} diff --git a/pkg/accelerator/accelerator_linux_test.go b/pkg/accelerator/accelerator_linux_test.go new file mode 100644 index 00000000..6a42c487 --- /dev/null +++ b/pkg/accelerator/accelerator_linux_test.go @@ -0,0 +1,73 @@ +// +// Use and distribution licensed under the Apache license version 2. +// +// See the COPYING file in the root project directory for full text. + +package accelerator_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/jaypipes/ghw/pkg/accelerator" + "github.com/jaypipes/ghw/pkg/option" + "github.com/jaypipes/ghw/pkg/snapshot" + + "github.com/jaypipes/ghw/testdata" +) + +func testScenario(t *testing.T, filename string, expectedDevs int) { + testdataPath, err := testdata.SnapshotsDirectory() + if err != nil { + t.Fatalf("Expected nil err, but got %v", err) + } + + t.Setenv("PCIDB_PATH", testdata.PCIDBChroot()) + + workstationSnapshot := filepath.Join(testdataPath, filename) + + tmpRoot, err := os.MkdirTemp("", "ghw-accelerator-testing-*") + if err != nil { + t.Fatalf("Unable to create temporary directory: %v", err) + } + + _, err = snapshot.UnpackInto(workstationSnapshot, tmpRoot, 0) + if err != nil { + t.Fatalf("Unable to unpack %q into %q: %v", workstationSnapshot, tmpRoot, err) + } + + defer func() { + _ = snapshot.Cleanup(tmpRoot) + }() + + info, err := accelerator.New(option.WithChroot(tmpRoot)) + if err != nil { + t.Fatalf("Expected nil err, but got %v", err) + } + if info == nil { + t.Fatalf("Expected non-nil AcceleratorInfo, but got nil") + } + if len(info.Devices) != expectedDevs { + t.Fatalf("Expected %d processing accelerator devices, but found %d.", expectedDevs, len(info.Devices)) + } +} + +func TestAcceleratorDefault(t *testing.T) { + if _, ok := os.LookupEnv("GHW_TESTING_SKIP_ACCELERATOR"); ok { + t.Skip("Skipping PCI tests.") + } + + // In this scenario we have 1 processing accelerator device + testScenario(t, "linux-amd64-accel.tar.gz", 1) + +} + +func TestAcceleratorNvidia(t *testing.T) { + if _, ok := os.LookupEnv("GHW_TESTING_SKIP_ACCELERATOR"); ok { + t.Skip("Skipping PCI tests.") + } + + // In this scenario we have 1 Nvidia 3D controller device + testScenario(t, "linux-amd64-accel-nvidia.tar.gz", 1) +} diff --git a/pkg/accelerator/accelerator_stub.go b/pkg/accelerator/accelerator_stub.go new file mode 100644 index 00000000..7e0b9fd4 --- /dev/null +++ b/pkg/accelerator/accelerator_stub.go @@ -0,0 +1,19 @@ +//go:build !linux +// +build !linux + +// Use and distribution licensed under the Apache license version 2. +// +// See the COPYING file in the root project directory for full text. +// + +package accelerator + +import ( + "runtime" + + "github.com/pkg/errors" +) + +func (i *Info) load() error { + return errors.New("accelerator.Info.load not implemented on " + runtime.GOOS) +} diff --git a/testdata/snapshots/linux-amd64-accel-nvidia.tar.gz b/testdata/snapshots/linux-amd64-accel-nvidia.tar.gz new file mode 100644 index 00000000..b3685ff5 Binary files /dev/null and b/testdata/snapshots/linux-amd64-accel-nvidia.tar.gz differ diff --git a/testdata/snapshots/linux-amd64-accel.tar.gz b/testdata/snapshots/linux-amd64-accel.tar.gz new file mode 100644 index 00000000..6ae2de94 Binary files /dev/null and b/testdata/snapshots/linux-amd64-accel.tar.gz differ