Let's optimize ... DGX Spark

Framework-agnostic DGX Spark setup: less power, more memory, same speed

First off, a huge “thank you” to this community and to the Chinese open-weight labs (DeepSeek, Qwen, GLM, Kimi): you’re the reason the Spark turned into a genuinely useful box, despite everything below. 🙏

There’s already a thread for capping the GPU and one for capping the CPU. So here’s the one that caps both. 🙂

I wish someone had told me all this six months ago. It would’ve saved me a pile of kWh, a few “why is it off again?” mornings, and way too many evenings staring at temperature graphs. So here it is, the short version I wanted back then. Works on any Spark, any engine, one node or ten.

1. Debloat (headless)

Boot to text mode first, so the desktop never starts (undo: graphical.target):

sudo systemctl set-default multi-user.target

Disable: bluetooth cups cups-browsed avahi-daemon ModemManager apport dgx-dashboard dgx-dashboard-admin multipathd rsyslog.
Mask: snapd packagekit nvidia-dgx-telemetry nvidia-dgx-sol udisks2.

  • udisks2 is D-Bus activated, so disable alone doesn’t stop it; it needs mask.
  • PipeWire needs systemctl --user mask on the services and their sockets.
  • Firmware/OTA (over-the-air) updates re-enable some of these, so re-check afterwards.
  • Don’t apt purge --auto-remove the desktop: nvidia-system-station-core depends on ubuntu-desktop, and the cascade takes Spark platform packages (fstab, serial console, GRUB config) with it. A dry-run wanted to remove 1078 packages. Leave the desktop installed; with multi-user.target it never starts anyway.
Leaf packages that are safe to purge (~1–2 GB of disk)

Office, games and desktop apps a headless box never runs. Purge without --auto-remove, and dry-run first (-s):

sudo apt-get -s purge 'libreoffice*' \
  aisleriot gnome-mahjongg gnome-mines gnome-sudoku \
  rhythmbox totem cheese simple-scan transmission-gtk \
  gnome-maps gnome-weather gnome-contacts gnome-music gnome-clocks \
  nvidia-system-station-games nvidia-system-station-apps

Desktop snaps (before masking snapd):

sudo snap remove firefox thunderbird snap-store

If the dry-run lists anything nvidia-spark-*, dgxstation-* or ubuntu-desktop, stop.

2. GPU cap: 1800 MHz

Decode is memory-bound. Above ~1800 MHz you buy only power and heat. Prefill does scale with clock, so going much lower costs prefill speed; 1800 is the top of the energy sweet spot. Credit to @peter.h177, who measured the whole curve at the wall (his thread).

@peter.h177's sweep: 2× Spark, both nodes capped, wall power for both
GPU cap Decode tok/s Wall power Wh per 1M tokens
2400 (uncapped) 51.34 330 W 1,688
2200 51.43 274 W 1,480
1900 50.74 252 W 1,381
1700 49.80 242 W 1,350
1400 47.74 234 W 1,362
800 34.90 211 W 1,679

3. CPU cap: 3000 MHz on the P-cores (performance cores, Cortex-X925) only

Inference engines keep 2–3 threads at 100% on the P-cores, mostly spin-waiting: instead of sleeping until the GPU is done, they poll it in a tight loop to shave microseconds off every step. A spinning core burns full power doing nothing, and power grows much faster than clock (higher clock needs higher voltage), so the top bins are the most expensive way to wait. At 3000 MHz: same throughput, 8–12% less energy, hottest thermal zone 17–18 °C lower.

Don’t go below 2808 MHz, the E-core (efficiency core, Cortex-A725) maximum: the scheduler then moves the hot threads to the E-cores and you start losing speed.

My measurements (2026-11-03)

2× GX10, TP2 (tensor parallel over both nodes), GPU cap 1800 MHz in every run, only the X925 cap changed. Benchmark: TEB (tool-eval-bench) hardmode, 97 agentic scenarios, parallel 3, seed 42. Power and energy from the smart plugs’ own counters; temperature = max of all thermal_zone*.

TensorFold (EXL3 4-bit)

X925 cap TEB score TEB wall Energy, both nodes Avg power head / worker Peak temp head / worker
3900 (stock) 91 661 s 41.9 Wh 116 / 113 W 84.2 / 78.5 °C
3000 91 661 s 38.5 Wh 106 / 104 W 67.5 / 65.1 °C
2600 91 691 s 37.6 Wh 100 / 97 W 62.8 / 60.7 °C

vLLM (W4A16, MTP)

X925 cap TEB score TEB wall Energy, both nodes Avg power head / worker Peak temp head / worker
3900 (stock) 90 962 s 60.6 Wh 123 / 104 W 89.7 / 78.0 °C
3000 88 932 s 53.2 Wh 108 / 97 W 71.9 / 63.7 °C
2600 94 932 s 49.9 Wh 99 / 95 W 64.0 / 59.9 °C
  • TensorFold: all 97 outputs byte-identical across caps (same score), so the time difference is pure speed. At 2600 the hot threads moved to the E-cores: +4.5%.
  • vLLM is not deterministic here, and it’s not batching: two identical runs with one request at a time still gave identical outputs in only 5 of 48 scenarios. Texts match for a few hundred characters, then a near-tie token flips (“Condition:” vs “Conditions:”) and MTP drafting carries the difference forward. So its ±3% time and 88–94 score spread are run-to-run noise; no cap made it slower.

4. Firmware OTA2610+: +1.5 GiB

The UEFI (firmware) display reservation is now configurable: 2 GB by default, Auto = 512 MiB when headless. The freed memory is ordinary OS memory, so every framework gets it. It can be set from the OS (NVIDIA docs), applied on the next boot.

Check that Auto (512 MiB) is active:

od -An -tx1 /sys/firmware/efi/efivars/NvidiaDispFrmStatus-284e19b9-235f-4aab-ae4c-190456d1bd76
# 06 00 00 00  00  00 02 00 00  ->  Auto, 0x200 = 512 MiB

5. 64K-page kernel: memory, not speed

  • With 4K pages Linux spends ~2 GiB of the 128 GB on page bookkeeping. A 64K kernel cuts that to ~128 MiB.
  • But NVIDIA’s UVM (Unified Virtual Memory) driver then backs every 256-byte GPU page table with a whole 64 KiB page: ~3.25 GiB instead of ~0.2 GiB. A stock 64K kernel leaves you less free memory. That’s not Linux and not your engine, it’s NVIDIA’s UVM driver: fine on GPUs with their own VRAM, wasteful on the Spark’s VRAM-less GB10 with 64K pages, a combo NVIDIA doesn’t validate. And a community patch fixes it by simply packing 16 tables per page. NVIDIA should be especially embarrassed by this one: their own unified-memory box, losing gigabytes to their own driver.
  • The memory-saver patch packs those tables: net +1.8 GiB over 4K. It’s a patched nvidia-uvm DKMS (Dynamic Kernel Module Support) module, so with Secure Boot on it needs your own enrolled key.
  • No speed gain: prefill and decode stay level.

6. Idle is the real waste

A Spark with a model loaded and the CX7 (ConnectX-7 NIC) link up idles at ~46 W at the wall, ~18–20 W of it the CX7 alone: ASPM (Active State Power Management) is off in its firmware, and EEE (Energy-Efficient Ethernet) doesn’t exist at 200G. Wake-on-LAN doesn’t work. The only real idle fix is poweroff plus a smart plug. A Mac Studio or a laptop idles at ~10 W.

7. Useful command-line tools

  • dgxtop: top for the Spark: GPU, CPU, unified memory, per process.

  • sparkview: GPU monitor that understands the GB10’s unified memory (memory pressure instead of a fake “VRAM” number).

  • nvtop: classic GPU usage and per-process view.

  • btop: per-core load, so you can see which cores spin.

  • s-tui: CPU frequency, load, temperature and power on one screen, handy to see the P-core cap work.

  • ttyplot: plots any number stream, e.g. the hottest thermal zone.

    ttyplot: hottest thermal zone, live
    while :; do sort -n /sys/class/thermal/thermal_zone*/temp | tail -1 | awk '{print $1/1000}'; sleep 1; done \
      | ttyplot -t 'max thermal zone' -u C
    
  • plugtop: btop-style monitor for Shelly/Tasmota smart plugs, i.e. real wall power.

  • vllm-monitor: live vLLM dashboard: tok/s, KV cache, queue.

  • EngineMetrics: single-file TUI (terminal UI) for vLLM and TensorFold metrics.

  • tool-eval-bench: agentic tool-calling benchmark, the one behind the tables above.

The units

jetspark-gpu-clock-cap.service + /etc/default/gpu-clock-cap
# /etc/default/gpu-clock-cap
GPU_MAX_MHZ=1800
[Unit]
Description=GB10 fixed MHz GPU clock cap
Wants=nvidia-persistenced.service
After=nvidia-persistenced.service
StartLimitIntervalSec=0

[Service]
Type=oneshot
EnvironmentFile=/etc/default/gpu-clock-cap
ExecStart=/usr/bin/nvidia-smi -pm 1
ExecStart=/usr/bin/nvidia-smi -lgc 0,${GPU_MAX_MHZ}
ExecStop=/usr/bin/nvidia-smi -rgc
RemainAfterExit=yes
Restart=on-failure
RestartSec=5
TimeoutStartSec=60
TimeoutStopSec=30

[Install]
WantedBy=multi-user.target
jetspark-cpu-clock-cap.service + /etc/default/cpu-clock-cap
# /etc/default/cpu-clock-cap  (GB10 P-cores are cpu5-9 and cpu15-19)
CPU_X925_CORES=5-9,15-19
CPU_X925_MAX_MHZ=3000
[Unit]
Description=GB10 fixed MHz CPU clock cap (P-cores, Cortex-X925)
After=nv-cpu-governor.service
StartLimitIntervalSec=0

[Service]
Type=oneshot
EnvironmentFile=/etc/default/cpu-clock-cap
ExecStart=/usr/bin/cpupower -c ${CPU_X925_CORES} frequency-set -u ${CPU_X925_MAX_MHZ}MHz
ExecStop=/usr/bin/cpupower -c ${CPU_X925_CORES} frequency-set -u 3900MHz
RemainAfterExit=yes
Restart=on-failure
RestartSec=5
TimeoutStartSec=60
TimeoutStopSec=30

[Install]
WantedBy=multi-user.target

cpupower comes with linux-tools for your kernel; DGX OS already needs it for nv-cpu-governor.service. Verify under load with cat /sys/devices/system/cpu/cpu17/cpufreq/scaling_cur_freq, not scaling_max_freq.


Bottom line: skip the fancy fan curves and thermal-hysteresis scripts. Pareto applies, ~80% of the benefit for ~20% of the effort, and here it’s just two tiny oneshot units.

It’s a shame to cap a machine this powerful, but burning extra watts for zero extra throughput is just pointless. And it’s not just the power bill: at stock clocks the hottest zone spikes to ~90 °C, and a Spark that hits a heat spike simply switches itself off, mid-job, no logs. So here’s the million-dollar question: is it NVIDIA’s engineering culture, or greedy stakeholders who want people buying new “better” and “optimized” hardware over and over? Probably a bit of both.

This is great, thank you!

On my MSI sparks, I’m getting:

od: /sys/firmware/efi/efivars/NvidiaDispFrmStatus-284e19b9-235f-4aab-ae4c-190456d1bd: No such file or directory

Does that mean I’m on old firmware?

Gigabyte has quietly reduced the options for allocating graphics memory within the BIOS for its AI TOP series.
The “AUTO” setting and the 1GB minimum option have been removed, leaving only 4GB and 2GB as the available choices.
Because of this, my impression of Gigabyte has become extremely negative.

On a separate note, I have found the “Automatic Boot on Power-Up” feature—used in conjunction with a smart plug—to be genuinely useful.

@Dickson , no such file → no such firmware :) It’s pretty obvious. My Asus has the file.

@evehour, the more users contact support, the better the chances of getting another 1.5 GB of free RAM. Or just ask them for a 2% refund on the box. RAM isn’t cheap these days! Money is the only language they understand.

That’s the frustrating thing about the Sparks. All of the OEMs gets the same board from Nvidia yet the board firmware and how often it gets updated are handled by the OEMs. Why even bother doing that? All the OEMS do is change the desktop background and tweak some text in the BIOS.

Are you new to this planet? Nobody cares about your frustration. There are plenty of very important people busy negotiating very important business contracts. The only thing that might actually work is posting on X with hashtags like #MSI #RefundMyStolenRAM. Bad PR is a dialect they understand pretty well too.

Oh you sweet sweet summer child.

@evehour That’s incredibly frustrating. The gb10 boards are all the same, why would Gigabyte do that? Doesn’t make sense at all. The amount reports of failed updates related to firmware seems to be increasing in the past few weeks.

I’ve always been diligent about keeping enough memory available so that I could properly use the recipes shared on this forum.
This included steps like disabling X11 to avoid using a GUI, turning off wireless functions, and shutting down unused ports (such as the CX7 port).
Given that I’m already in a position where I can’t even use those fantastic dashboards, this recent development is truly disappointing.

I plan to send an email to Gigabyte soon; in the meantime, I’ll have to revert to an older version of the firmware—available on their website—to restore the 1GB memory option so I can continue using the device.

Thanks. This is helpful.

@Dickson On my ASUS I had to use “fwupdmgr enable-remote lvfs-testing” to get the pre-release firmware or OTA2610 but maybe that has changed now. Maybe Gigabyte has something like that?

In addition to all the debloating ideas that @jetspark gives above I am tempted to try building my own kernels to remove more crud. See, for example, GitHub - kindlingai/kindling-spark-os: Minimal read-only OS image for GB10 (DGX Spark / ASUS GX10) boxes · GitHub . Then it doesn’t matter if your bios doesn’t support reducing the display memory reservation option, I don’t believe. I wonder about switching to 64K page sizes, however. I am not convinced that will not break some user level programs that depend on the page size being 4K.

‼️ Breaking news ‼️ 😄

I just tested a different way for the CPU to wait for the GPU (LDXR + WFET) instead of the usual spin-wait, and the results are stunning: same wake-up latency as spinning, ~5 W less per waiting core at the wall. Need to change a couple of lines in Tensorfold (python) and vLLM. Details soon.

Scope: one of two hot Tensorfold threads

TensorFold keeps two CPU cores busy per node while serving. This patch fixes only one of them: the RoCE proxy thread (roce_proxy.c, a small C helper compiled at startup and started with pthread_create). It used to poll the GPU’s doorbell in a tight loop. Now it waits with LDXR + WFET, so the core sleeps until the GPU’s write wakes it, with the same latency as spinning.

The second hot thread is not patched. That’s TensorFold’s main thread waiting for the GPU in torch.cuda.synchronize(). The spin there happens inside NVIDIA’s closed-source CUDA driver (libcuda), so there’s no source to patch. Fixing it would mean replacing those sync points with a “GPU writes a flag, CPU waits on it with WFE” scheme. That’s a bigger change and isn’t covered here.

The measured saving (~3.5 W per node, −3% energy for the same work, identical outputs) comes from one core. The other core still spins.

The patched Tensorfold (gist, works with COMM=roce only), TEB --hardmode-only (28 scenarios), TensorFold TP2, no caps (stock clocks), thermal guard at 92 °C:

┌─────┬────────────────────────────────────────┬───────┬───────┬─────────────┬───────────────┬──────────┬──────────────┐
│ Arm │ RoCE proxy                             │ Wall  │ Score │ Outputs = A │ W 01 / 02     │ Wh 01+02 │ Peak 01 / 02 │
├─────┼────────────────────────────────────────┼───────┼───────┼─────────────┼───────────────┼──────────┼──────────────┤
│ A   │ stock (spin)                           │ 249 s │ 84    │ 28/28       │ 109.1 / 142.2 │ 17.34    │ 79 / 79 °C   │
├─────┼────────────────────────────────────────┼───────┼───────┼─────────────┼───────────────┼──────────┼──────────────┤
│ B   │ WFET (patch)                           │ 249 s │ 84    │ 28/28       │ 104.4 / 138.1 │ 16.76    │ 68 / 78 °C   │
├─────┼────────────────────────────────────────┼───────┼───────┼─────────────┼───────────────┼──────────┼──────────────┤
│ C   │ patched, TF_ROCE_WFE=0 (spin, control) │ 249 s │ 84    │ 28/28       │ 107.1 / 141.1 │ 17.15    │ 75 / 85 °C   │
├─────┼────────────────────────────────────────┼───────┼───────┼─────────────┼───────────────┼──────────┼──────────────┤
│ B2  │ WFET (patch)                           │ 249 s │ 84    │ 28/28       │ 104.2 / 138.3 │ 16.62    │ 67 / 75 °C   │
└─────┴────────────────────────────────────────┴───────┴───────┴─────────────┴───────────────┴──────────┴──────────────┘
  • All 28 outputs are byte-identical across arms, wall time matches to the second, score 84.
  • Energy. The spin arms used 17.34 and 17.15 Wh, the WFE arms 16.76 and 16.62 Wh. That’s −0.55 Wh (−3.2%), 3–4× the repeat spread. Average power fell by 3.8 W on 01 and 3.4 W on 02.
  • Temperature. Head node (01) peak went from 79 °C to 67–68 °C (−11 °C). On 02 the peak was 75–85 °C in every arm, and the WFE vs spin difference there is within noise. I think, it’s a success story 😄
  • The control works. Arm C (same image, spin path) matches stock A, so the saving comes from the WFET wait itself.

Next test - vLLM, but that guy would require more attention…


Folks writing their own inference engines for the Spark: you’re awesome. But it’d be even more awesome if the time your engine spends waiting for the GPU were spent wisely. Right now most engines spin a core at 100% while they wait. On GB10 a core can sleep in WFE and still wake up as fast as a spin. Same latency, ~5 W less per waiting core. Short how-to with measurements and copy-paste code: gist - Stop spinning, wait with WFE