Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ helm/**/charts
helm/**/Chart.lock
helm/**/requirements.lock

cmd/k8s-device-plugin/k8s-device-plugin
cmd/k8s-device-plugin/k8s-device-plugin

# Build output (Makefile)
bin/
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
FROM golang:1.21-bullseye AS gobuild
# Go module proxy. Defaults to the public proxy; override for faster builds
# behind a slow network, e.g.:
# docker build --build-arg GOPROXY=https://goproxy.cn,direct .
ARG GOPROXY=https://proxy.golang.org,direct
ENV GOPROXY=${GOPROXY}
ADD . /device-plugin
RUN cd /device-plugin && go build -o ./k8s-device-plugin cmd/k8s-device-plugin/main.go

Expand Down
110 changes: 110 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Makefile for HAMi mock-device-plugin
#
# Run `make help` to see the available targets.

# ---- Variables (override on the command line, e.g. `make docker-build IMG=myrepo/mock:dev`) ----
REGISTRY ?= projecthami
IMAGE_NAME ?= mock-device-plugin
TAG ?= latest
IMG ?= $(REGISTRY)/$(IMAGE_NAME):$(TAG)

BINARY ?= k8s-device-plugin
MAIN_PKG ?= ./cmd/k8s-device-plugin
OUTPUT_DIR ?= bin

GIT_DESCRIBE ?= $(shell git describe --always --long --dirty 2>/dev/null || echo "unknown")
LDFLAGS ?= -X main.gitDescribe=$(GIT_DESCRIBE)

GO ?= go
DOCKER ?= docker
# Use `make ... DOCKER=nerdctl` on containerd-only hosts.

# Optional Go module proxy passed into the image build. Set it for faster builds
# behind a slow network, e.g. `make docker-build GOPROXY=https://goproxy.cn,direct`.
GOPROXY ?=
DOCKER_BUILD_ARGS :=
ifneq ($(GOPROXY),)
DOCKER_BUILD_ARGS += --build-arg GOPROXY=$(GOPROXY)
endif

# Detect golangci-lint availability for the lint target.
GOLANGCI_LINT := $(shell command -v golangci-lint 2>/dev/null)

.DEFAULT_GOAL := help

.PHONY: all
all: build ## Alias for `build` (compatibility with standard Makefile tooling).

##@ General

.PHONY: help
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-18s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

##@ Development

.PHONY: fmt
fmt: ## Run go fmt against code.
$(GO) fmt ./...

.PHONY: vet
vet: ## Run go vet against code.
$(GO) vet ./...

.PHONY: tidy
tidy: ## Tidy go.mod / go.sum.
$(GO) mod tidy

.PHONY: lint
lint: ## Run golangci-lint (if installed).
ifeq ($(GOLANGCI_LINT),)
@echo "golangci-lint not found; install: https://golangci-lint.run/usage/install/ -- skipping."
else
golangci-lint run ./...
endif

.PHONY: test
test: ## Run unit tests.
$(GO) test ./...

.PHONY: test-verbose
test-verbose: ## Run unit tests with verbose output.
$(GO) test -v ./...

.PHONY: cover
cover: ## Run unit tests and report coverage.
$(GO) test -coverprofile=coverage.out ./...
$(GO) tool cover -func=coverage.out

##@ Build

.PHONY: build
build: vet ## Build the device-plugin binary into bin/. (run `make fmt` separately to format)
mkdir -p $(OUTPUT_DIR)
CGO_ENABLED=0 $(GO) build -ldflags "$(LDFLAGS)" -o $(OUTPUT_DIR)/$(BINARY) $(MAIN_PKG)

.PHONY: clean
clean: ## Remove build artifacts.
rm -rf $(OUTPUT_DIR) coverage.out

##@ Container

.PHONY: docker-build
docker-build: ## Build the container image ($(IMG)). Set GOPROXY=... for a faster module proxy.
$(DOCKER) build $(DOCKER_BUILD_ARGS) -t $(IMG) .

.PHONY: docker-push
docker-push: ## Push the container image ($(IMG)).
$(DOCKER) push $(IMG)

##@ Deployment (requires kubectl + a valid KUBECONFIG)

.PHONY: deploy
deploy: ## Deploy RBAC + DaemonSet to the current cluster.
kubectl apply -f k8s-mock-rbac.yaml
kubectl apply -f k8s-mock-plugin.yaml

.PHONY: undeploy
undeploy: ## Remove the DaemonSet + RBAC from the current cluster.
kubectl delete -f k8s-mock-plugin.yaml --ignore-not-found
kubectl delete -f k8s-mock-rbac.yaml --ignore-not-found
171 changes: 145 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,175 @@
# Mock device plugin for HAMi
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FProject-HAMi%2Fmock-device-plugin.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FProject-HAMi%2Fmock-device-plugin?ref=badge_shield)


## Introduction

This is a Kubernetes device plugin implementation that enables the registration of virtual-devices which would normally be ignored by scheduler (i.e gpu-memory, gpu-cores, etc..)on each node. After deployment, these resources will be available on `node.status.allocatable` and `node.status.capacity`
This is a Kubernetes device plugin that registers **virtual** device resources (e.g. `gpu-memory`, `gpu-cores`) -- the resources HAMi tracks but the kubelet would normally ignore -- onto a node, **without requiring real hardware**. It lets you test the HAMi scheduler (scheduling policies, quotas, webhooks) on machines that have no GPU/NPU.

After deployment these resources show up under `node.status.allocatable` and `node.status.capacity`.

## How it works (read this first)

The mock plugin **does not detect hardware**. It does the following every ~30s:

```text
node annotation: hami.io/node-<vendor>-register = [ {devmem, devcore, ...}, ... ] (1)
node capacity: <count resource> (e.g. nvidia.com/gpu) > 0 (2) <- health gate
| mock reads
v
registers into allocatable: <vendor>/...mem , <vendor>/...cores , ...
```

On a **real** cluster, (1) and (2) are produced by the real device plugin. In a **mock-only** (no hardware) environment you provide them yourself:

- **(1) the `node-<vendor>-register` annotation** describing the fake cards -- `kubectl annotate`.
- **(2) the count extended resource** (e.g. `nvidia.com/gpu`) -- patched onto the node `status`.

> There is **no auto-labeller** in this repo, so (1) and (2) are manual today. Forgetting them is the usual cause of `device xxx is unhealthy` / `no allocation` -- see issues #14 / #16.

## Prerequisites

- Kubernetes version >= v1.18
- Kubernetes >= v1.18
- The `hami-scheduler-device` ConfigMap (the device config). If HAMi is installed it already exists; otherwise create it from [device-configmap.yaml](https://github.com/Project-HAMi/HAMi/blob/master/charts/hami/templates/scheduler/device-configmap.yaml).

## Deployment

If the `hami-scheduler-device` ConfigMap is not deployed, it needs to be deployed first. refer [device-configmap.yaml](https://github.com/Project-HAMi/HAMi/blob/master/charts/hami/templates/scheduler/device-configmap.yaml)

```bash
make deploy # = kubectl apply -f k8s-mock-rbac.yaml && kubectl apply -f k8s-mock-plugin.yaml
# or manually:
kubectl apply -f k8s-mock-rbac.yaml
kubectl apply -f k8s-mock-plugin.yaml
```
$ kubectl apply -f k8s-mock-rbac.yaml
$ kubectl apply -f k8s-mock-plugin.yaml

## Understanding the values

The mock **derives the registered resources from the annotation, not from the count resource.** This trips people up, so to be explicit:

- **Number of fake cards = the number of entries in the annotation array** (not the count value).
- Registered `...-memory` = **sum of `devmem`** over all entries.
- Registered `...-cores` / `...-core` = **sum of `devcore`** over all entries.
- The **count extended resource is only a health gate**: its value just needs to be `> 0`. It does **not** affect the registered memory/cores. By convention it is set to `cards x splits-per-card` (e.g. Ascend `2 x VDeviceCount(4) = 8`), but `1` would work equally well for the memory/cores to appear.

Annotation entry fields:

| field | meaning |
| :-- | :-- |
| `id` | unique device UUID (any string) |
| `devmem` | per-card memory in MB -- **summed** into `...-memory` |
| `devcore` | per-card cores. **NVIDIA/Hygon:** summed into `...-cores` (NVIDIA: percentage, 100 = a whole card). **Ascend:** ignored -- `huawei.com/<chip>-core` is percentage-based, registered as **100 per card**. |
| `count` | per-card split count (informational for the mock) |
| `type` | device model string |
| `health` | must be `true` to be counted |
| `index` | card index `0,1,2,...` (`0` may be omitted) |
| `numa`, `mode` | optional |

> **Worked example (Ascend, below):** the annotation has **2 entries**, each `devmem=32768`. So `...-memory = 2x32768 = 65536` and `...-core = 2x100 = 200` (Ascend `...-core` is **percentage-based**: a whole card is always **100**). The count resource `=8` is a separate health-gate value and is unrelated to these numbers.

## Usage by vendor

To mock one card you always need the **three pieces**: the vendor **config block** (in the `hami-scheduler-device` ConfigMap, gives the resource names), the **count extended resource** (passes the health gate), and the **node-register annotation** (describes the fake cards). Replace `<node>` below with your target node. Resource names follow the ConfigMap (HAMi defaults shown). See [Understanding the values](#understanding-the-values) for how the numbers are derived.

### NVIDIA (e.g. A100-80GB)

- config block: `nvidia:` | annotation: `hami.io/node-nvidia-register` (JSON) | count: `nvidia.com/gpu`
- mock registers: `nvidia.com/gpumem`, `nvidia.com/gpucores`, `nvidia.com/gpumem-percentage`

```bash
# (2) count resource: splitCount=10 -> one card advertises 10
kubectl patch node <node> --subresource=status --type=json \
-p '[{"op":"add","path":"/status/capacity/nvidia.com~1gpu","value":"10"}]'
# (1) annotation: 1 x A100-80G (devmem=81920 MB, devcore=100)
kubectl annotate node <node> \
'hami.io/node-nvidia-register=[{"id":"GPU-MOCK-0","count":10,"devmem":81920,"devcore":100,"type":"NVIDIA-A100-SXM4-80GB","health":true,"numa":0,"mode":"hami-core"}]'
# verify (~30s later)
kubectl get node <node> -o json | jq '.status.allocatable|with_entries(select(.key|test("nvidia.com")))'
# expect: nvidia.com/gpu=10, nvidia.com/gpumem=81920, nvidia.com/gpucores=100, nvidia.com/gpumem-percentage=100
```

## Build
### Ascend NPU (e.g. 910B4)

- config block: `vnpus.configs` (entry for `910B4`) | annotation: `hami.io/node-register-Ascend910B4` (JSON) | count: `huawei.com/Ascend910B4`
- mock registers: `huawei.com/Ascend910B4-memory`, and -- **only when the node runs in `hami-vnpu-core` soft mode** (see below) -- `huawei.com/Ascend910B4-core`

```bash
# (2) count resource: 2 cards x VDeviceCount(4) = 8
kubectl patch node <node> --subresource=status --type=json \
-p '[{"op":"add","path":"/status/capacity/huawei.com~1Ascend910B4","value":"8"}]'
# (1) annotation: 2 x 910B4 (devmem=32768 MB, devcore=20), matching the real ascend-device-plugin report
kubectl annotate node <node> \
'hami.io/node-register-Ascend910B4=[{"id":"MOCK-0","count":4,"devmem":32768,"devcore":20,"type":"Ascend910B4","health":true},{"id":"MOCK-1","index":1,"count":4,"devmem":32768,"devcore":20,"type":"Ascend910B4","health":true}]'
# (1b) soft mode: mark the node as hami-vnpu-core so the -core resource is reported.
# Either set vnpus.hamiVnpuCore: true globally in the ConfigMap, or annotate this node
# (the node annotation overrides the global setting, same precedence as the real plugin):
kubectl annotate node <node> hami-vnpu-core=true
# verify
kubectl get node <node> -o json | jq '.status.allocatable|with_entries(select(.key|test("Ascend910B4")))'
# expect: huawei.com/Ascend910B4-memory=65536, huawei.com/Ascend910B4-core=200 (2 cards x 100)
```
docker build .

The Ascend `-core` resource is **percentage-based**: each physical card contributes **100** (a whole card), independent of the annotation's `devcore`. HAMi caps a core request at 100 and, in `hami-vnpu-core` soft mode, treats a card's total core as 100.

**When is `-core` reported?** Only on a node that runs in `hami-vnpu-core` (soft-partition) mode, mirroring the real ascend-device-plugin and the HAMi scheduler. Mode is decided by: the node's `hami-vnpu-core` annotation if present, otherwise the global `vnpus.hamiVnpuCore` in the ConfigMap (`true`/`false`, default `false`). On a hard/template node (`hami-vnpu-core` off) the HAMi scheduler filters out any pod that requests `-core` (`ModeNotFit`), so the mock does not register `-core` there. There is **no ascend-device-plugin** in a mock environment, so nothing writes the `hami-vnpu-core` node annotation automatically -- set it yourself (like the `node-register` annotation and the count resource).

### Hygon DCU

- config block: `hygon:` | annotation: `hami.io/node-dcu-register` (**CSV**, not JSON) | count: `hygon.com/dcunum`
- mock registers: `hygon.com/dcumem`, and -- when the config sets `resourceCoreName` -- `hygon.com/dcucores` (sum of per-card `devcore`)

```bash
# (2) count resource: 2 DCUs
kubectl patch node <node> --subresource=status --type=json \
-p '[{"op":"add","path":"/status/capacity/hygon.com~1dcunum","value":"2"}]'
# (1) annotation: CSV form is "id,count,devmem,devcore,type,numa,health,index,mode:" per card
kubectl annotate node <node> \
'hami.io/node-dcu-register=DCU-MOCK-0,2,16384,100,Z100L,0,true,0,hami-core:DCU-MOCK-1,2,16384,100,Z100L,1,true,1,hami-core:'
# verify
kubectl get node <node> -o json | jq '.status.allocatable|with_entries(select(.key|test("hygon.com")))'
# expect: hygon.com/dcumem=32768, hygon.com/dcucores=200 (2 cards x devcore 100)
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

## Examples
## Ascend config compatibility (new vs legacy)

The Ascend `vnpus` config has two layouts and the plugin accepts **both**:

```yaml
# New (HAMi >= v2.9.0): nested object
vnpus:
hamiVnpuCore: true # node runs in soft mode -> the -core resource is reported
configs:
- chipName: 910B4
resourceCoreName: huawei.com/Ascend910B4-core # name of the -core resource
...
```
Allocatable:
...
memory: 769189866507
nvidia.com/gpu: 20
nvidia.com/gpucores: 200
nvidia.com/gpumem: 65536
nvidia.com/gpumem-percentage: 200
pods: 110
...
```yaml
# Legacy (older / downstream): a bare list
vnpus:
- chipName: 910B4
...
```
The new nested format is tried first; if that fails it falls back to the legacy flat list; if neither matches an error is returned. This lets clusters that have not upgraded their ConfigMap keep working.

## ManagedResources

| Devices | Mocking Resources |
| :--- | :----: |
| Nvidia GPU | nvidia.com/gpumem, nvidia.com/gpumem-percentage, nvidia.com/gpucores |
| Hygon DCU | hygon.com/dcumem |
| Ascend | huawei.com/Ascend{chip-name}-memory |
| Devices | Mocking Resources |
| :--- | :---- |
| Nvidia GPU | `nvidia.com/gpumem`, `nvidia.com/gpumem-percentage`, `nvidia.com/gpucores` |
| Hygon DCU | `hygon.com/dcumem`, `hygon.com/dcucores` (when `resourceCoreName` is set) |
| Ascend | `huawei.com/Ascend{chip}-memory`, `huawei.com/Ascend{chip}-core` (when `resourceCoreName` is set **and** the node is in `hami-vnpu-core` mode) |

**Note:** If the counted memory is too large, for example exceeding 120GB, it will display as 0. In this case, you can set the `memoryFactor` in `hami-scheduler-device` ConfigMap. The default value of `memoryFactor` is 1.
**Note:** If the counted memory is too large (e.g. > 120GB) it may display as 0. Set `memoryFactor` in the `hami-scheduler-device` ConfigMap (default 1).

## Build

```bash
make build # build the binary into bin/
make test # run unit tests
make docker-build # build the image (override: IMG=myrepo/mock:tag)
make help # list all targets
```

## Maintainer

limengxuan@4paradigm.com

## License
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FProject-HAMi%2Fmock-device-plugin.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FProject-HAMi%2Fmock-device-plugin?ref=badge_large)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FProject-HAMi%2Fmock-device-plugin.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FProject-HAMi%2Fmock-device-plugin?ref=badge_large)
Loading
Loading