Skip to content

Architecture

The main idea of Mokka is to simulate very low-level system contracts, so that higher layer components work meaningfully without any modifications.

This is achieved via simulation of device status, PCI trees, driver footprints for GPU and networking. The lower level we operate, the better here as we expand the number of real workflows that are executed (vs. being completely disabled or mocked away).

Mokka General Architecture

Higher level applications are our consumers: - platform components such as nvidia-smi, K8s DRA driver, NFD, GPU Operator, Network Operator, Topograph, etc. - applications like Slurm, NVSentinel, etc.

We don't try to mock a specific higher layer component or use case, but rather focus on the simulation of contracts between lower and higher layers. This should help to support a wide range of higher layer applications that we don't know or have access to (for example, neocloud's proprietary AI infrastructure services).

Mokka provides a mock driver that looks like the real one to management tooling. Same libraries and file footprints, no real kernel module, no GPU data path. That is why nvidia-smi can run unmodified, and why module surfaces such as what lsmod reads belong in the same story. GPU identity and behavior come from config profiles (different chips, counts, topology), with tools to change health and fault state at runtime.

Contract surfaces

Mocking a single interface (for example NVML alone) can prove a GPU can be allocated. Many K8s control-plane consumers read a broader evidence surface than allocation alone: NVML / nvidia-smi, PCI and sysfs trees, and kernel or driver footprints under /proc and /sys (including module state such as lsmod).

Not every surface is covered yet. See what is not simulated yet in the FAQ.

Independent failure and attribution

Layers form a dependency stack, but each layer must be able to fail on its own while others stay healthy. Which layer broke decides the owner, the remediation path, and the urgency.

For the mock, that means each simulated surface has to be controllable independently. A GPU can show up in the PCI tree with no driver footprint; that is a normal failure mode, and the mock needs to reproduce it. If surfaces can only be turned on together, the layers collapse into one failure and attribution cannot be tested.

Delivery

Simulated file surfaces also have to be visible at the paths consumers already use. Pointing a consumer at a substitute path with a flag only works when that flag exists, and it no longer tests that the consumer reads the real path.

LD_PRELOAD can rewrite libc calls for C tools such as lspci and ibv_devinfo. It does not work for Go binaries: they make syscalls directly and never go through the preloaded library. Most of the Kubernetes control plane is Go, so file surfaces need to be mounted into the container instead of intercepted.

The moving parts

Component Runs as Responsibility
Node daemon DaemonSet container, one per node Stages every simulated surface onto the host and supervises the long-lived ones
Simulators Packages inside the node daemon One per surface: GPU driver, PCI bus, CDI, IMEX, NVLink, fabricmanager, InfiniBand
Mock NVML library Shared object loaded by each consumer process Answers NVML calls from the profile instead of a driver
Shims LD_PRELOAD libraries and an execve wrapper Make C tools read the staged tree at the real paths
NRI plugin Optional DaemonSet Injects the mock into containers that never requested a GPU
Allocation watcher Sidecar next to the node daemon Reads the kubelet pod-resources socket to see which GPUs are claimed
nvml-mock-ctl CLI, run against a node Changes simulated state at runtime without a redeploy
Control plane Deployment, disabled by default Health probes only today; see MEP-0001 for the intent

The GPU profile is the single input. It names the model, count, topology and health, and every component above derives its behaviour from it. Swapping profiles changes what the whole node appears to be.

How they connect

flowchart LR
    profile[GPU profile]
    topo[Topology overlay]

    subgraph node [On every targeted node]
        daemon[Node daemon<br/>seven simulators]
        surfaces[(Staged surfaces<br/>driver files · device nodes<br/>PCI tree · CDI specs · IB devices)]
        daemon --> surfaces
    end

    subgraph delivery [Delivery into a container]
        mounts[hostPath mounts]
        shims[LD_PRELOAD shims]
        nri[NRI injection]
    end

    consumers[nvidia-smi · device plugin<br/>DRA driver · GPU Operator · DCGM]

    profile --> daemon
    topo --> daemon
    surfaces --> mounts --> consumers
    surfaces --> shims --> consumers
    surfaces --> nri --> consumers

Three ways a surface reaches a consumer

A staged file is useless if the consumer looks somewhere else, so Mokka has three delivery paths and uses whichever the consumer's runtime allows:

Mechanism Works for Used because
hostPath mounts anything, including Go binaries the only approach that survives direct syscalls
LD_PRELOAD shims C tools — lspci, ibv_devinfo rewrites libc path calls, so tools read the mock tree at real paths
NRI injection pods with no GPU request adds devices and mounts at container-create time, with no pod spec change

How the system behaves

A node comes up

The daemon reads the profile, stages all seven simulators in parallel, waits for that wave to finish, then starts the long-lived processes. If any simulator fails to stage, no daemon starts: a half-built node is worse than an obviously broken one. Teardown reverses it on a timeout that outlives context cancellation, so a deleted pod still cleans up after itself.

A consumer reads a GPU

nvidia-smi and the device plugin load the mock library in their own process and get values from the profile. lspci and ibv_devinfo are C tools, so a shim rewrites their paths into the staged tree. Go consumers bypass shims entirely and read the hostPath mounts. All three paths describe the same node.

State changes at runtime

There is no daemon to send a command to — the library lives inside each consumer process. nvml-mock-ctl instead writes an override file next to the profile, and every loaded copy of the library re-reads it on a short TTL and merges it over the base. Running and newly started processes converge on the new state within one interval, and the base profile is never mutated. See Runtime Control.

A GPU fails

Failure is a configuration state, not a special path: a profile or an override marks a device as ECC-faulted, lost, or fallen off the bus, and every surface reports it consistently. That is what makes the failure legible to a consumer — the device plugin, DCGM and nvidia-smi agree, exactly as they would on real hardware.

The fleet is heterogeneous

Each release targets a node set, so different nodes can run different profiles at once. A cluster can present A100 and T4 workers side by side without either being real.

Where to go next

To understand Read
How a node gets its simulated surfaces Node Daemon
How consumers are made to see fake hardware Libraries and Shims
How a pod gets GPUs without asking NRI Plugin
Every knob in the profile Configuration
Deploying and shaping a cluster Installation
Changing state on a running node Runtime Control
Adding functions or profiles Contributing