A little LLM lab on the DGX Spark

For a while I wanted to run a serious model at home — one I could actually point my tools at, watch it work, and not think about. Not a cloud endpoint I pay per token, and not a laptop that chokes. Just: a model, a clean way to talk to it, and a dashboard I could trust.

That’s what ended up living on my DGX Spark. A single desktop box, quiet enough to sit on a desk, now runs a 27B-class model and serves it the way my tools expect — and the whole thing is described in one config file.

Everything in this post — the CLI, the model recipes, the dashboards, the docs — lives in the spark-lab repo.

The stack, in plain terms

NVIDIA DGX Spark (GB10, unified memory) SGLang :30000 · /metrics LiteLLM gateway :4000 · keys + spend Postgres · Redis Prometheus :9090 Grafana :3000 Tailscale private mesh Cloudflare Tunnel optional · public your client OpenAI SDK

The short version: SGLang runs the model and speaks an OpenAI-compatible API. LiteLLM sits in front of it as the thing I actually talk to — it hands out API keys, tracks spend, and gives the model a stable name that survives me swapping models underneath. Prometheus scrapes metrics from the model, the GPU, and the host, and Grafana turns that into dashboards I can actually read. Tailscale lets me reach the gateway from any of my machines over a private mesh; a Cloudflare Tunnel is there if I ever want to share it with a friend.

Nothing here is exotic. The interesting part is how little I have to manage.

One config, one command

Instead of a pile of copy-pasted scripts, everything is generated from a single config.yaml. Pick the model, the ports, which dashboards you want, whether you want the tunnel on. Then:

$ spark-lab init
$ spark-lab apply

apply is the part I like. It’s declarative: it renders the whole stack from your config, works out what actually changed since the last time you ran it, and only touches that. Change the model, add a dashboard, bump a port — run apply again and the node converges to the new state. No more “which of the three files did I edit, and which service do I need to restart?”

Here it is actually running — SGLang up, and the gateway plus its supporting services healthy:

$ sparkrun status
Job: recipes/qwen38-27b-dspark-nvfp4.yaml  [62cafcefd78a4fcf]  (1 container)
  solo       127.0.0.1                                 Up 27 hours   lmsysorg/sglang:qwen38-27b
  logs: sparkrun logs 62cafcefd78a4fcf
  stop: sparkrun stop 62cafcefd78a4fcf

Total: 1 container(s) across 1 host(s)

$ docker compose ps
NAME                     SERVICE          STATUS
litellm-litellm-1        litellm          Up 7 hours        0.0.0.0:4000->4000/tcp
litellm-db-1             db               Up 43 hours (healthy)
litellm-redis-1          redis            Up 43 hours (healthy)  0.0.0.0:6379->6379/tcp
litellm-prometheus-1     prometheus       Up 42 hours       0.0.0.0:9090->9090/tcp
litellm-grafana-1        grafana          Up 43 hours       0.0.0.0:3000->3000/tcp
litellm-node_exporter-1  node_exporter    Up 31 hours       0.0.0.0:9100->9100/tcp
litellm-dcgm_exporter-1  dcgm_exporter    Up 42 hours       0.0.0.0:9835->9835/tcp

And the GPU is doing work — two processes, the model and its scheduler:

$ nvidia-smi
|   0  NVIDIA GB10                    On  |  0000000F:01:00.0 Off |       N/A |
| N/A   66C    P0             43W /  N/A  |  Not Supported        |    96%    |
+-----------------------------------------------------------------------------------------+
| Processes:                                                                              |
|    0   N/A  N/A         925214      C   /usr/bin/python3                        6316MiB |
|    0   N/A  N/A         926002      C   sglang::scheduler                       10223MiB |

(That Not Supported for memory is the GB10 being honest with you: the GPU shares unified memory with the host, so it doesn’t report a fixed VRAM number the way a discrete card would. The monitoring stack accounts for that.)

What you get

A Grafana you can actually read is the payoff. There’s an SGLang dashboard — request latency, time-to-first-token, throughput, queue depth, cache-hit rate — and a host-overview one tuned for the GB10.

Grafana SGLang dashboard
The SGLang dashboard: live throughput, latency, and cache metrics.
Grafana host overview dashboard
The host overview: CPU, memory, and the GPU, in one place.

Reaching the model is just “point any OpenAI-compatible client at the gateway with your key”:

curl http://<your-spark>:4000/v1/models -H "Authorization: Bearer $LITELLM_MASTER_KEY"

Over Tailscale it’s even simpler — the Spark shows up on the mesh like any other device, so any of my laptops can use the model without a single open port:

$ tailscale status
100.71.216.115   my-spark     linux
100.86.52.50     pop-os       linux    active; direct
100.77.2.33      startos      linux

If I did want to share it publicly, the Cloudflare Tunnel is the escape hatch — front the gateway with a token and hand out a LiteLLM key. I keep that off by default, because “I can share a model with a friend” and “the model is public” are very different sentences.

Run it on your own

If you’ve got a DGX Spark — or a rack of them, because it scales across nodes the same way — clone the repo, spark-lab init, point config.yaml at your model, and spark-lab apply.

It’s MIT-licensed, and it’s just me tidying up a setup I actually use — so expect it to read more like a well-organized toolbox than a product. The docs cover the architecture, day-2 operations, model recipes, and networking.