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 device 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}
    resource.kubernetes.io/pciBusID: {string: "0000:00:02.0"}
    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}
    resource.kubernetes.io/pciBusID: {string: "0000:00:02.0"}
    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}
    resource.kubernetes.io/pciBusID: {string: "0000:00:02.0"}
    subsystem: {string: i2c-dev}
    vendor: {string: "8086"}

This machine publishes three devices for one GPU.

All three have 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, usually 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 .

The uhid module is for a whole stack. Declare it on a machine whose Bluetooth adapter serves BLE input devices, such as a remote or a keyboard. The adapter’s claim then delivers /dev/uhid. The device reference states why.

A USB-CEC adapter needs a spec.serio entry beside its modules, because its driver binds only after the machine attaches the adapter’s serial line. Attach a USB-CEC adapter gives the steps, and the device reference describes the two devices the adapter publishes.

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 has the same driver, and the card node has 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 attribute that names the half you want, and only that half has 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 has allowMultipleAllocations: true allocates to more than one claim: a second deployment writes its own ResourceClaim against the same DeviceClass, and both deployments run. The integrated GPU above has 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, and both name the same claim. 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. 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. 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 has 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.

liken.sh/address pairs only the devices of liken. To pair the render node with a device that another driver publishes for the same card, such as media-operator’s statement of what a GPU decodes, use matchAttribute: resource.kubernetes.io/pciBusID. Kubernetes defines that attribute, and both drivers publish it with the card’s PCI address.

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 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:

  1. kubectl get resourceslices -o yaml shows if the device is published. If the device is not there, usually no driver is bound to it: go back to step 2. The device reference lists every reason a device stays out of the slice.

  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.

  3. If the device is published and it matches, another claim can already hold it. Find the holder:

    kubectl get resourceclaims -A -o wide
    

    A device that is not shareable allocates one time, and the second claim waits until the first claim releases it.