Give a workload a device
This guide gives a pod a piece of hardware: a GPU for a transcoder, and a USB adapter that one pod holds alone. No step here needs a privileged pod or a host path.
Devices describes what liken publishes and why. The hardware operators publish devices the operating system does not: paired Bluetooth controllers, monitor outputs, and audio outputs.
1. See what a node offers
kubectl get resourceslices
kubectl get resourceslice <node>-liken.sh -o yaml
Each entry is one device you can claim:
- name: pci-0000-00-02-0
allowMultipleAllocations: true
attributes:
address: {string: "0000:00:02.0"}
bus: {string: pci}
class: {string: display}
classCode: {string: "030000"}
driver: {string: i915}
modalias: {string: "pci:v00008086d000046D2..."}
name: {string: Alder Lake-N [UHD Graphics]}
product: {string: 46d2}
renderNode: {bool: true}
subsystem: {string: drm}
vendor: {string: "8086"}
- name: pci-0000-00-02-0-display
attributes:
address: {string: "0000:00:02.0"}
bus: {string: pci}
class: {string: display}
classCode: {string: "030000"}
displayNode: {bool: true}
driver: {string: i915}
modalias: {string: "pci:v00008086d000046D2..."}
name: {string: Alder Lake-N [UHD Graphics]}
product: {string: 46d2}
subsystem: {string: drm}
vendor: {string: "8086"}
- name: pci-0000-00-02-0-i2c-dev
attributes:
address: {string: "0000:00:02.0"}
bus: {string: pci}
class: {string: display}
classCode: {string: "030000"}
driver: {string: i915}
modalias: {string: "pci:v00008086d000046D2..."}
name: {string: Alder Lake-N [UHD Graphics]}
product: {string: 46d2}
subsystem: {string: i2c-dev}
vendor: {string: "8086"}
This machine publishes three devices for one GPU.
pci-0000-00-02-0delivers the render node,/dev/dri/renderD128. More than one claim can allocate it, so several workloads encode, decode, and compute on this GPU at the same time.pci-0000-00-02-0-displaydelivers the card node,/dev/dri/card0, which carries modesetting. One claim allocates it, because one process at a time can drive a display.pci-0000-00-02-0-i2c-devdelivers the i2c monitor-control buses that i915 registers for each display output. One claim allocates it, because those buses are raw wires.
All three carry the same address, because they are one card. A claim
uses that to ask for halves of the same GPU. See
Two requests, one card.
If the hardware you expect is not in the list, no driver is bound to it. Look at the hardware the machine reports that it cannot drive:
kubectl get machine <name> -o jsonpath='{.status.hardware.unclaimed}'
2. Declare the driver
A device becomes available for a claim when a driver binds it. Add the
module to spec.modules
in the machine’s manifest:
spec:
modules:
- i915
Apply the manifest, and the machine loads the module. Most modules
load without a reboot. The Machine’s SpecConverged condition shows
when the change has an effect. The device is in the node’s slice in a
few seconds.
The hardware report names these modules for you, as comments, when you install the machine. See Install a cluster.
3. Say what your workload needs
A DeviceClass is a named set of conditions on hardware. Write one
for each kind of device that your deployments ask for.
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
name: gpu-render
spec:
selectors:
- cel:
expression: |
device.driver == "liken.sh" &&
has(device.attributes["liken.sh"].renderNode)
liken publishes an attribute only when it is true of the hardware, so
has() is the complete test. Guard an attribute a device may lack
with has() before you read it. The Kubernetes API treats an
unguarded read of a missing attribute as an evaluation error, and an
evaluation error aborts the whole allocation.
Do not select on driver alone, and do not select on subsystem
alone. Every device that liken publishes for one GPU carries the same
driver, and the card node carries the same subsystem: drm as the
render node. A class that matches more than one of them can allocate
the monitor buses, or the display, to your transcoder. renderNode
is the fact that names the half you want, and only that half carries
it.
This class matches any GPU with a DRM render node, on any machine in the fleet. A selector that asks for a vendor and a product ID names one model, and it stops matching on the next machine you buy.
4. Claim the GPU for a deployment
The deployment writes a claim against the class, and the claim
allocates the GPU. The claim is also what places the pod: the
scheduler picks a machine whose ResourceSlice offers a matching
device.
apiVersion: resource.k8s.io/v1
kind: ResourceClaim
metadata:
name: transcoder-gpu
namespace: media
spec:
devices:
requests:
- name: gpu
exactly:
deviceClassName: gpu-render
The pod names the claim, and each container that needs the device names the pod’s entry:
spec:
template:
spec:
resourceClaims:
- name: gpu
resourceClaimName: transcoder-gpu
containers:
- name: ffmpeg
image: ...
resources:
claims:
- name: gpu
The container receives every node the published device delivers. For
the GPU above, that is /dev/dri/renderD128 only. The card node and
the /dev/i2c-* monitor-control nodes belong to the companion
devices, and each companion needs its own request. Nothing else
changes: no privilege, and no host mount. Your image supplies the
userspace driver, and the image’s user must be able to open the node.
Check what the container received:
kubectl exec deploy/transcoder -- ls -l /dev/dri
A device that carries allowMultipleAllocations: true serves more
than one claim: a second deployment writes its own ResourceClaim
against the same DeviceClass, and both deployments run. The
integrated GPU above carries it, so more than one transcoder can
allocate the render node at once.
Give a deployment that holds a claim like this the Recreate
strategy. A rolling update runs the old pod and the new pod at once,
both name the same claim, and Kubernetes gives a claim’s device to
every pod that names the claim. Recreate stops the old pod first,
so one pod holds the device at a time.
A workload that drives a display
A player or a kiosk sets the video mode, so it needs the card node as
well. Write a second DeviceClass for the display half:
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
name: gpu-display
spec:
selectors:
- cel:
expression: |
device.driver == "liken.sh" &&
has(device.attributes["liken.sh"].displayNode)
Then put both requests in one claim. The container receives the nodes of both devices:
apiVersion: resource.k8s.io/v1
kind: ResourceClaim
metadata:
name: player-gpu
namespace: media
spec:
devices:
requests:
- name: render
exactly:
deviceClassName: gpu-render
- name: display
exactly:
deviceClassName: gpu-display
The display device allocates to one claim, so a second player waits until the first claim ends. This is the correct behavior: the kernel gives modesetting to one process for each card, and a second player that started would fail when it opened the card node.
Sound is a third device, and a different one: the audio controller has its own PCI address, so a player that plays HDMI audio adds a request for it. That device is shareable, so a second player does not wait for it.
Two requests, one card
On a machine with two GPUs, the two requests above can allocate halves
of different cards. A constraints block pairs them. Each constraint
names the requests it applies to and one attribute, and the scheduler
then allocates devices whose value for that attribute is the same.
The attribute to use is address, which is the device’s address on
its bus:
spec:
devices:
requests:
- name: render
exactly:
deviceClassName: gpu-render
- name: display
exactly:
deviceClassName: gpu-display
constraints:
- requests: [render, display]
matchAttribute: liken.sh/address
Every device that liken publishes for one card carries that card’s address, and two cards in a machine have different addresses. The render node and the card node the claim allocates are now halves of the same GPU.
Leave the audio controller out of the list. It has an address of its own, so a constraint that included its request would never match.
5. Give one pod a device alone
Most devices are not shareable. A USB adapter has one control endpoint, so liken publishes it as a device that allocates one time. Select the device by its identity:
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
name: zigbee-adapter
spec:
selectors:
- cel:
expression: |
device.driver == "liken.sh" &&
device.attributes["liken.sh"].vendor == "10c4" &&
device.attributes["liken.sh"].product == "ea60"
Add device.attributes["liken.sh"].serial == "..." when a machine has
two adapters of the same model and you must select one of them. The
name of the device follows the port, not the unit, so a replacement
adapter in the same port has the same name.
Then give each pod its own claim:
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
name: zigbee
namespace: home
spec:
spec:
devices:
requests:
- name: adapter
exactly:
deviceClassName: zigbee-adapter
# in the pod template:
resourceClaims:
- name: adapter
resourceClaimTemplateName: zigbee
A template makes one claim for each pod, and the device allocates to one claim, so only one pod holds the adapter. A rolling update waits until the old pod releases the adapter. This is correct for hardware that one process must own.
Do not point two pods at one ResourceClaim for a device of this
kind. Both pods receive it. Kubernetes shares a claim with every pod
that names it, by design, and liken does not refuse the second pod. A
device node does not grant exclusive access by itself. The kernel is
what enforces exclusive access, through O_EXCL and the driver’s own
open path. Use a template to give the device to one pod only.
When a claim does not schedule
If a pod stays in Pending with a claim that is not allocated, then
usually no device matched the claim. Do these checks in this order:
-
kubectl get resourceslices -o yamlshows if the device is published. If the device is not there, no driver is bound to it: go back to step 2. -
Compare your selector with the device’s attributes. A selector that reads an attribute the device does not have errors on that device and aborts the allocation, so it never matches. Guard the reference with
has()so an absent attribute means “does not match” instead. -
If the device is published and it matches, another claim can already hold it. Find the holder:
kubectl get resourceclaims -A -o wideA device that is not shareable allocates one time, and the second claim waits until the first claim releases it.