Spark Control Plane
Open source · No software on the nodes

One browser tab shows
all of your Spark computers.

The dashboard shows the GPU, the unified memory, the temperatures, and the tokens for each second on every DGX Spark node. The run planner calculates the memory for a full vLLM recipe. Then it starts that recipe on the node.

Read-only SSH No agent on the nodes Demo mode needs no hardware MIT license

What it shows

Each node has a card and a page.

The metrics come to your browser on a WebSocket, and the server keeps the history. If you refresh the page, the chart continues. If a second person opens the same page, that person sees the same chart.

GPU

The GPU load, the clocks, and the processes

The panel shows the temperature, the power, and the SM clock. It also shows each process that holds GPU memory. The SM share for each process comes from nvidia-smi pmon.

SM status

Why the SMs are at that speed

The panel shows the SM clock and its maximum value. It decodes the NVML clock events: the power limit, the temperature limit, and the hardware slowdown. It also shows the number of degrees before the driver decreases the clock.

Unified memory

The CPU and the GPU share one pool

On the GB10, the CPU and the GPU use one LPDDR5X memory pool. The dashboard gives this memory its correct name. It does not show a separate VRAM value, because the driver does not report one.

Inference

Decode and prefill have different charts

The dashboard identifies vLLM, llama.cpp, SGLang, TGI, and Ollama from their own replies. Decode gives tens of tokens each second. A prefill burst gives thousands of tokens. If they share one axis, the decode data becomes too small to read.

CPU, storage, network

Data for each core, mount, and interface

The server calculates each rate from the difference between two polls. Sometimes a counter goes back to zero, because a host or a model server started again. The rate then shows zero, and not a large incorrect value.

Temperatures and power

Every temperature zone from the kernel

The dashboard also has the three commands that write and do not read: reboot, shutdown, and Wake-on-LAN. You must give the node its MAC address before Wake-on-LAN operates. The dashboard refuses to shut down its own host, because this stops the dashboard.

Containers

Start, stop, and restart

The list shows each container from docker ps -a. After a command, the server polls the node again. The row then shows the true state. If a container does not start, the row shows this.

HuggingFace cache

Each model and dataset, largest first

On a Spark, this cache is usually the largest data on the disk. It was 588 GB in 28 repositories on the test machine. A download continues on the node after you close the page. Before you delete a repository, the panel shows the correct space from hf cache rm --dry-run.

The whole fleet

Ten tabs cost the same as one tab

The server polls the nodes, and the browsers only subscribe. Each node has its own timer. Thus a slow host becomes late alone, and it does not delay the other nodes.

Screens

Demo mode makes all of these screens.

Demo mode gives synthetic metrics for three example nodes. This is why the nodes have the names spark-demo-01, spark-demo-02, and workstation. Two commands start the same dashboard on your computer, and you do not need a GPU.

The overview tab: a fleet summary strip above three node cards.

The top strip adds the values from each node that is online. Below it, each node has one card: a GPU dial, the unified memory, the CPU, a chart of the last ten minutes, and four values.overview.png · dark theme

Model runs

The catalog has complete recipes.

A recipe gives its weights, its image, and each vLLM flag. Select a recipe that fits. Set the context length and the number of concurrent requests. The node then downloads the weights, pulls the image, and starts the container. It waits until the endpoint replies.

The settings are related to each other. DFlash2 needs a target model with an unquantized lm_head. GDN layers operate in only one mamba cache mode. FP8 KV needs calibration scales, and only some exports have them. With independent fields, it is easy to select a combination that fails some minutes after the weight load starts.

The planner calculates the memory fraction. At start, vLLM takes utilization × total memory as one block. It gives all of the remainder to the KV cache, and it does not change that cache again. Thus the planner calculates the smallest fraction for your context length and your request count. This recipe needed 0.92, or 120 GB. It now operates at 0.37, or 48 GB.
  1. Weights — the node downloads them to its HuggingFace cache and shows the bytes
  2. Image — the node pulls it, or builds it when the recipe needs more wheels
  3. Container — the node starts it on the port from the recipe, or on your port
  4. Loading — vLLM reads the weights into memory and makes the CUDA graphs
  5. Serving — the endpoint replied to a real request; the server then examines docker ps at each poll

The run continues on the node. It does not stop if you close the page, start the dashboard again, or lose the SSH connection.

Unified memory on spark-demo-0155.0 GB WOULD STILL BE FREE OF 128.0 GB
Qwen3.8-27B · NVFP4 + DFlash2 fits · 0.35 minimum
Already in use 28.2 GB This run needs 44.3 GB Spare KV cache 527 MB

The weights are 25.7 GB and the overhead is 7.64 GB. The KV pool has the size for your context length and your request count. At the minimum fraction, almost no memory is spare. If you increase the fraction, the pale segment becomes larger, and the panel shows the added prefix cache.

CONTEXT 256KREQUESTS 1RESERVES 44.8 GB
recipes.yaml
- id: qwen38-27b-nvfp4-dflash2
  name: Qwen3.8-27B · NVFP4 + DFlash2
  model:
    repo: RadixArk/Qwen3.8-27B-NVFP4-BF16-LMHead
    sizeGB: 23.8
    measured: true        # false, and the panel says "estimate"
  image:
    ref: vllm/vllm-openai:v0.28.0-aarch64
  port: 8000
  overheadGB: 8.2         # non-torch + activation + graphs
  kvBytesPerToken: 44827  # gives the pool its size
  args:
    --max-model-len: 262144
    --kv-cache-dtype: fp8

The server checks each recipe when it reads the file. It checks the ids, the image references, the container names, and each flag. If one recipe is bad, the server refuses the full catalog and gives the reason. The dashboard continues to show the node data.

Install

You install nothing on the nodes.

The server sends read-only commands on SSH: /proc, /sys, and nvidia-smi. It puts these commands in one script, thus each poll uses one connection. There is no agent and no configuration file to write. You do not restart the server after you add a node.

One command

The installer clones the repository into ~/.spark-control-plane, installs the dependencies, builds the interface, and puts a spark-control-plane command on your PATH. Nothing needs sudo. If /usr/local/bin is not writable, the command goes to ~/.local/bin, and if you do not have Node.js 20 or later, the installer fetches its own copy into the prefix instead of touching the system.

Start with no hardware

Demo mode gives synthetic metrics for three example nodes. This is sufficient to see all of the dashboard, and each screenshot on this page comes from it. You need no GPU and no Spark.

Then add your nodes

Open the dashboard and click Add node. Give the host, the SSH user, and the path to the key. Then click Test. It tells you if the connection operates, if nvidia-smi found a GPU, and if each inference port replied. Do this before you save the node.

bash — install
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/garykww/spark-control-plane/main/install.sh)"

Run this on the machine that will host the dashboard, not on the nodes. Re-run it to update: it fast-forwards the checkout, rebuilds, and leaves your configuration and any local edits alone. It offers to start on boot under systemd or launchd; --service accepts that without the prompt, and --no-service declines it.

bash — run it
$ spark-control-plane
# synthetic nodes, no hardware needed
$ DEMO_MODE=1 spark-control-plane

The server listens at 127.0.0.1:5555. Port and bind address live in ~/.spark-control-plane/spark-control-plane.env, which updates never overwrite; to reach it from your LAN, set SPARK_BIND_HOST=0.0.0.0 there or pass --bind to the installer.

bash — docker
$ docker compose up --build -d

Docker mounts ./config for the node list. It mounts ~/.ssh read-only for your keys. The Spark uses arm64, thus you must build on the Spark or add --platform linux/arm64.

bash — from source
$ npm install
$ npm run build
$ npm start

The clone, if you would rather manage it yourself. DEMO_MODE=1 npm run dev gives the demo on the Vite dev server at localhost:5173, with a DEMO DATA badge in the header.

Limits

The data that the dashboard cannot get.

A dashboard must not show a value that it cannot measure. These are the values that this dashboard does not have.

There is no data for each SM

NVML and DCGM do not give data for each streaming multiprocessor. utilization.gpu is the fraction of the time when one kernel or more was on the GPU. It is an average for the full GPU. Only a profiler gives data for each SM, and a profiler attaches to one process. Thus the occupancy grid is a proportional view and not a hardware map. The panel gives this information on the screen.

Memory bandwidth is a specification

The dashboard shows 273 GB/s for the GB10 as a platform specification, and not as a measurement. To measure the true bandwidth, you need the DCGM profiling fields. DCGM is a different NVIDIA package, and this dashboard does not use it.

A dash means "not reported"

A dash does not mean zero. The driver does not report the fan speed on a GB10, because the GB10 has no fan. A model server without a /metrics endpoint is a second example. The dashboard shows its model list, but the counters show a dash. "Not reported" and "nothing occurred" are different.

The prompt total includes the cached tokens

vLLM does not process the cached blocks. Thus the prefill rate includes work that the GPU did not do. In one long agent workload with a warm prefix cache, 94% of the prompt tokens were cache hits. The server reported 12.99M prompt tokens, and 12.32M of them were free. This is why the panel shows the share next to the total.

Do not connect the dashboard to the internet. The API has no authentication. It assumes a trusted network, the same as the SSH keys that it uses. Control of the Docker daemon on a node is equal to root access on that machine. Put the dashboard behind Tailscale, a VPN, or a reverse proxy with authentication.

Open source

All of the code is in one repository.

The repository has an Express server with a poll loop and a React interface. The tests are pure functions, and they use recorded output from /proc and nvidia-smi. There are about 11,000 lines, and the MIT license applies to all of them. You do not need hardware to run the tests.