From 5768f71bf94546f32c8f587cd6d8be3c845735c1 Mon Sep 17 00:00:00 2001
From: Kenny Ho <y2kenny@gmail.com>
Date: Thu, 11 Feb 2021 16:57:26 -0500
Subject: [PATCH] Add topology support

The Topology Manager is a Kubelet component that allows resources to be
co-ordinated in a Topology aligned manner.  In order to do this, the
Device Plugin API was extended to include a TopologyInfo struct since
Kubernetes v1.17.

https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/#device-plugin-integration-with-the-topology-manager

This commit add topology support for AMD GPU using hwloc.
---
 Dockerfile                    |  2 +
 README.md                     |  3 +-
 cmd/k8s-device-plugin/main.go | 63 +++++++++++++++++++----
 internal/pkg/hwloc/hwloc.go   | 97 +++++++++++++++++++++++++++++++++++
 4 files changed, 152 insertions(+), 13 deletions(-)
 create mode 100644 internal/pkg/hwloc/hwloc.go

diff --git a/Dockerfile b/Dockerfile
index 52fc863a..e3400eae 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,6 +13,7 @@
 #  limitations under the License.
 FROM golang:1.15.7-alpine3.13
 RUN apk --no-cache add git pkgconfig build-base libdrm-dev
+RUN apk --no-cache add hwloc-dev --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community
 RUN mkdir -p /go/src/github.com/RadeonOpenCompute/k8s-device-plugin
 ADD . /go/src/github.com/RadeonOpenCompute/k8s-device-plugin
 RUN go install \
@@ -22,6 +23,7 @@ RUN go install \
 FROM alpine:3.13
 MAINTAINER Kenny Ho <Kenny.Ho@amd.com>
 RUN apk --no-cache add ca-certificates libdrm
+RUN apk --no-cache add hwloc --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community
 WORKDIR /root/
 COPY --from=0 /go/bin/k8s-device-plugin .
 CMD ["./k8s-device-plugin", "-logtostderr=true", "-stderrthreshold=INFO", "-v=5"]
diff --git a/README.md b/README.md
index 2921a3e0..b45cfe3c 100644
--- a/README.md
+++ b/README.md
@@ -16,8 +16,7 @@ More information about [RadeonOpenCompute (ROCm)][rocm]
 
 
 ## Limitations
-* This is an early prototype/alpha.
-* This plugin currently support device plugin API v1 only.  This means it will only work with k8s v1.16+.
+* This plugin targets Kubernetes v1.18+.
 
 ## Deployment
 The device plugin needs to be run on all the nodes that are equipped with AMD GPU.  The simplist way of doing so is to create a Kubernetes [DaemonSet][ds], which run a copy of a pod on all (or some) Nodes in the cluster.  We have a pre-built Docker image on [DockerHub][dhk8samdgpudp] that you can use for with your DaemonSet.  This repository also have a pre-defined yaml file named `k8s-ds-amdgpu-dp.yaml`.  You can create a DaemonSet in your Kubernetes cluster by running this command:
diff --git a/cmd/k8s-device-plugin/main.go b/cmd/k8s-device-plugin/main.go
index 076d4870..f2a014ef 100644
--- a/cmd/k8s-device-plugin/main.go
+++ b/cmd/k8s-device-plugin/main.go
@@ -28,6 +28,7 @@ import (
 	"time"
 
 	"github.com/RadeonOpenCompute/k8s-device-plugin/internal/pkg/amdgpu"
+	"github.com/RadeonOpenCompute/k8s-device-plugin/internal/pkg/hwloc"
 	"github.com/golang/glog"
 	"github.com/kubevirt/device-plugin-manager/pkg/dpm"
 	"golang.org/x/net/context"
@@ -129,14 +130,46 @@ func (p *Plugin) ListAndWatch(e *pluginapi.Empty, s pluginapi.DevicePlugin_ListA
 
 	devs := make([]*pluginapi.Device, len(p.AMDGPUs))
 
-	i := 0
-	for id := range p.AMDGPUs {
-		devs[i] = &pluginapi.Device{
-			ID:     id,
-			Health: pluginapi.Healthy,
+	// limit scope for hwloc
+	func() {
+		var hw hwloc.Hwloc
+		hw.Init()
+		defer hw.Destroy()
+
+		i := 0
+		for id := range p.AMDGPUs {
+			dev := &pluginapi.Device{
+				ID:     id,
+				Health: pluginapi.Healthy,
+			}
+			devs[i] = dev
+			i++
+
+			numas, err := hw.GetNUMANodes(id)
+			glog.Infof("Watching GPU with bus ID: %s NUMA Node: %+v", id, numas)
+			if err != nil {
+				glog.Error(err)
+				continue
+			}
+
+			if len(numas) == 0 {
+				glog.Errorf("No NUMA for GPU ID: %s", id)
+				continue
+			}
+
+			numaNodes := make([]*pluginapi.NUMANode, len(numas))
+			for j, v := range numas {
+				numaNodes[j] = &pluginapi.NUMANode{
+					ID: int64(v),
+				}
+			}
+
+			dev.Topology = &pluginapi.TopologyInfo{
+				Nodes: numaNodes,
+			}
 		}
-		i++
-	}
+	}()
+
 	s.Send(&pluginapi.ListAndWatchResponse{Devices: devs})
 
 	for {
@@ -241,9 +274,16 @@ func (l *Lister) NewPlugin(resourceLastName string) dpm.PluginInterface {
 var gitDescribe string
 
 func main() {
+	versions := [...]string{
+		"AMD GPU device plugin for Kubernetes",
+		fmt.Sprintf("%s version %s", os.Args[0], gitDescribe),
+		fmt.Sprintf("%s", hwloc.GetVersions()),
+	}
+
 	flag.Usage = func() {
-		fmt.Fprintf(os.Stderr, "AMD GPU device plugin for Kubernetes\n")
-		fmt.Fprintf(os.Stderr, "%s version %s\n", os.Args[0], gitDescribe)
+		for _, v := range versions {
+			fmt.Fprintf(os.Stderr, "%s\n", v)
+		}
 		fmt.Fprintln(os.Stderr, "Usage:")
 		flag.PrintDefaults()
 	}
@@ -252,8 +292,9 @@ func main() {
 	// this is also needed to enable glog usage in dpm
 	flag.Parse()
 
-	glog.Infof("AMD GPU device plugin for Kubernetes")
-	glog.Infof("%s version %s\n", os.Args[0], gitDescribe)
+	for _, v := range versions {
+		glog.Infof("%s", v)
+	}
 
 	l := Lister{
 		ResUpdateChan: make(chan dpm.PluginNameList),
diff --git a/internal/pkg/hwloc/hwloc.go b/internal/pkg/hwloc/hwloc.go
new file mode 100644
index 00000000..f12c989f
--- /dev/null
+++ b/internal/pkg/hwloc/hwloc.go
@@ -0,0 +1,97 @@
+/**
+ * Copyright 2021 Advanced Micro Devices, Inc.  All rights reserved.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+**/
+
+// Package hwloc is a collection of utility functions to get NUMA membership
+// of AMD GPU via the hwloc library
+package hwloc
+
+// #cgo pkg-config: hwloc
+// #include <stdint.h>
+// #include <hwloc.h>
+import "C"
+import (
+	"fmt"
+	"unsafe"
+)
+
+func GetVersions() string {
+	return fmt.Sprintf("hwloc: _VERSION: %s, _API_VERSION: %#08x, _COMPONENT_ABI: %d, Runtime: %#08x",
+		C.HWLOC_VERSION,
+		C.HWLOC_API_VERSION,
+		C.HWLOC_COMPONENT_ABI,
+		uint(C.hwloc_get_api_version()))
+}
+
+type Hwloc struct {
+	topology C.hwloc_topology_t
+}
+
+func (h *Hwloc) Init() error {
+	rc := C.hwloc_topology_init(&h.topology)
+	if rc != 0 {
+		return fmt.Errorf("Problem initializing hwloc topology rc: %d", rc)
+	}
+
+	rc = C.hwloc_topology_set_type_filter(h.topology,
+		C.HWLOC_OBJ_PCI_DEVICE,
+		C.HWLOC_TYPE_FILTER_KEEP_IMPORTANT)
+	if rc != 0 {
+		C.hwloc_topology_destroy(h.topology)
+		return fmt.Errorf("Problem setting type filter rc: %d", rc)
+	}
+
+	rc = C.hwloc_topology_load(h.topology)
+	if rc != 0 {
+		C.hwloc_topology_destroy(h.topology)
+		return fmt.Errorf("Problem loading topology rc: %d", rc)
+	}
+
+	return nil
+}
+
+func (h *Hwloc) Destroy() {
+	C.hwloc_topology_destroy(h.topology)
+}
+
+func (h *Hwloc) GetNUMANodes(busid string) ([]uint64, error) {
+	var gpu C.hwloc_obj_t
+	var ancestor C.hwloc_obj_t
+
+	busidstr := C.CString(busid)
+	defer C.free(unsafe.Pointer(busidstr))
+
+	gpu = C.hwloc_get_pcidev_by_busidstring(h.topology, busidstr)
+	if gpu == nil {
+		return []uint64{},
+			fmt.Errorf("Fail to find GPU with bus ID: %s", busid)
+	}
+	ancestor = C.hwloc_get_non_io_ancestor_obj(h.topology, gpu)
+
+	if ancestor == nil || ancestor.memory_arity <= 0 {
+		return []uint64{},
+			fmt.Errorf("No NUMA node found with bus ID: %s", busid)
+	}
+
+	var results []uint64
+	nn := ancestor.memory_first_child
+
+	for nn != nil {
+		results = append(results, uint64(nn.logical_index))
+		nn = nn.next_sibling
+	}
+
+	return results, nil
+}
-- 
GitLab