Help us shape the future of Go! Take the 2026 Go Developer Survey

allocwatch

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package allocwatch turns Kubernetes GPU allocation into mock NVML memory readings.

Issue #506 asks that `memory.used_bytes` respond to allocation: a pod holding an nvidia.com/gpu claim should move the number, and the number should return when the pod goes away. Nothing in the deployment observed allocation, so every profile reported a constant zero and any dashboard built on the mock was visibly fake.

The write path already existed. The engine hot-reloads <driver_root>/config/overrides.yaml within one TTL, and that file sits beside the config the DaemonSet stages into every injected container. This package supplies the missing producer: it reads the node's current allocation from the kubelet pod-resources API and renders the memory half of that override document.

The design is deliberately level-triggered. Every poll recomputes all readings from the full current allocation set rather than applying deltas, so a missed or duplicated event cannot accumulate drift, and "returns when the pod is removed" needs no separate teardown path — it is what an empty claim list already computes.

What this does NOT claim: the byte count is synthetic. The mock runs no kernel and allocates no device memory, so the number reflects that a claim EXISTS, not what a workload actually touched. See the Metric Fidelity section of docs/configuration.md.

Index

Constants

View Source
const DefaultInterval = 2 * time.Second

DefaultInterval is how often the node's allocation is re-read. The engine re-reads the override file at most once per its own TTL (1s by default), so polling much faster only burns kubelet CPU without being observable.

View Source
const DefaultSocketPath = "/var/lib/kubelet/pod-resources/kubelet.sock"

DefaultSocketPath is where the kubelet serves the pod-resources API. The socket is node-local and read-only, so it needs a hostPath mount rather than any API-server RBAC.

View Source
const GPUResourceName = "nvidia.com/gpu"

GPUResourceName is the extended resource the NVIDIA device plugin advertises. pod-resources reports device IDs per resource name and a node commonly serves others (SR-IOV NICs, hugepages, other vendors' accelerators), so this name is the filter that keeps the watcher to GPUs.

Variables

This section is empty.

Functions

func OverrideDoc

func OverrideDoc(readings []Reading) map[string]any

OverrideDoc renders readings as the `devices` map of a config override document, matching what `nvml-mock-ctl set --gpu N memory.used_bytes=...` writes.

Only used_bytes and free_bytes are emitted. total_bytes is a hardware fact that allocation does not change, and emitting any non-memory key would clobber a concurrent nvml-mock-ctl override of temperature, utilization or failure injection when the two documents merge.

func Publish

func Publish(path string, readings []Reading) error

Publish merges readings into the override document at path.

Read-modify-write under the same lock nvml-mock-ctl takes, so a human running `nvml-mock-ctl temp` while the watcher runs does not lose their edit and the watcher does not lose its reading. Only the memory block of each device is touched; everything else in the document is carried through untouched.

Types

type Claim

type Claim struct {
	DeviceUUID string

	// Pod, Namespace and Container identify the holder. They are unused by the
	// memory reconciliation and carried for logging and for the `processes`
	// surface tracked separately in #506 item 2.
	Namespace string
	Pod       string
	Container string
}

Claim is a single container's hold on one device. The same DeviceUUID appears once per holding container, so time-sliced GPUs shared by several containers produce several claims.

type Device

type Device struct {
	Index         int
	UUID          string
	TotalBytes    uint64
	ReservedBytes uint64
}

Device is one GPU the mock exposes on this node, as resolved from the loaded profile. UUID is the identifier the device plugin advertises and therefore the key pod-resources reports claims against.

func DevicesFromConfig

func DevicesFromConfig(cfg *engine.Config) []Device

DevicesFromConfig resolves the GPU set the mock exposes from the loaded profile, keyed by the UUID the device plugin advertises — which is what pod-resources reports claims against.

type Lister

type Lister interface {
	List(ctx context.Context) ([]Claim, error)
	Close() error
}

Lister reads the node's current GPU allocation.

func NewPodResourcesLister

func NewPodResourcesLister(ctx context.Context, socketPath string) (Lister, error)

NewPodResourcesLister dials the kubelet pod-resources socket.

Chosen over the NRI plugin because this API is level-triggered: one call returns the node's complete current allocation, so a reader that misses a window recovers on the next poll rather than carrying a stale delta forever. It also reports device-plugin, DRA and NRI allocations alike, and keeps working with nri.enabled=false.

The dial blocks so a missing or unmountable socket fails at startup. The alternative — a lazy connection — would leave the watcher reporting an idle node indefinitely, which is indistinguishable from a genuinely idle node.

type Policy

type Policy struct {
	// UsedFractionPerClaim is the share of the usable aperture
	// (total - reserved) attributed to each claim. Claims on one device add up
	// and clamp at the aperture.
	UsedFractionPerClaim float64
}

Policy tunes how much memory a single claim is reported to consume.

func DefaultPolicy

func DefaultPolicy() Policy

DefaultPolicy attributes half the usable aperture per claim, so a single exclusive claim reads as a substantial but not saturated card, and two time-sliced containers fill it. The value is arbitrary in the sense that no real allocation backs it; it is chosen to look plausible on a dashboard and to leave headroom that a second claim visibly consumes.

type Reading

type Reading struct {
	Index     int
	UsedBytes uint64
	FreeBytes uint64
}

Reading is the reconciled memory state of one device.

func Reconcile

func Reconcile(devices []Device, claims []Claim, policy Policy) []Reading

Reconcile computes a Reading for every device from the full current claim set. Devices with no claim are included and report the idle values, so the emitted override always covers the whole node: writing only the busy devices would leave a previously busy GPU pinned at its last value once its pod went away.

type Watcher

type Watcher struct {
	Lister       Lister
	Devices      []Device
	Policy       Policy
	OverridePath string
	Interval     time.Duration
}

Watcher polls the node's GPU allocation and republishes the memory readings.

func (*Watcher) Run

func (w *Watcher) Run(ctx context.Context) error

Run polls until ctx is cancelled. Cancellation is a clean shutdown, not an error.

A failed poll is logged and skipped rather than fatal, and publishes nothing. Both halves of that matter: exiting would freeze every GPU at its last reading for the life of the pod, and publishing zeros on a failed read would report the whole node idle every time the kubelet is briefly unreachable.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL