Related: How to set a clock cap on the GB10 → (Korean) / Why vLLM pegs four CPU cores at 100% on the DGX Spark → / Running DeepSeek V4 Flash on two DGX Sparks → (Korean) / Connecting four DGX Sparks through a switch → (Korean)
While bringing in our third ASUS Ascent GX10 (NVIDIA GB10, part of the DGX Spark family), we walked through the procedure of turning a factory-state machine into an inference server once more, from the beginning. This article is about the ASUS Ascent GX10. The NVIDIA DGX Spark and GB10 machines from other vendors are probably similar, but firmware, base image, and package composition can differ by product, so check on your own hardware before following along. This time we applied in one pass everything we had worked out piece by piece over several weeks on the first two machines, and we also record the problems we hit along the way. The article splits the procedure into four parts: what you must do, what is worth doing, the problems we ran into, and recipes you can run right away on a single node once setup is done. Settings specific to our home network (VLANs, addresses, switches) are left out.
Caution. This article describes system changes that are hard to undo or can cut off remote access: firmware upgrades, driver replacement, removal of desktop packages, firewall configuration. Before running any command, check what it changes on your own hardware and version, and proceed at your own risk. The numbers are measurements from our machines, not guarantees.
The first-boot setup screens (account, keyboard, network) can be completed as guided. There is nothing special to choose. After that it is easier to put the monitor and keyboard away and work over SSH. Everything else in this article was done over SSH. We recommend giving a coding agent (for example Claude Code) SSH access and working through it together. Most of the steps below are repeated checks and comparisons, which suits an agent well.
# On the DGX Spark (once, from a local terminal)
ip -br a # check the wired IP
systemctl status ssh # check the SSH server is running (if missing: sudo apt install openssh-server)
# On your workstation
ssh-copy-id user@<spark-ip> # copy the public key. No password needed from here on
ssh user@<spark-ip>
This is what the factory state looked like. Driver and firmware versions can differ from machine to machine, so check first. The fourth machine came with the timezone already set to Asia/Seoul and a newer OTA metapackage, so it differed from the table in small ways.
| Item | Factory state (delivered 2026-08) | Target in this article |
|---|---|---|
| OS / kernel | Ubuntu 24.04.4 / 6.17.0-1031-nvidia | unchanged |
| Default target | graphical (GNOME, gdm3, 13 snaps, setup wizard app) | multi-user |
| GPU driver | 580.173.02 | 595.84 |
| USB-C PD firmware | 0x1 | 0x516 |
| Clocks | GPU up to 3003MHz, CPU performance cores 3.9GHz (no limit) | (recommended) GPU 2000MHz, performance cores 2.8GHz |
| SSH / firewall | password login allowed / firewall off | key only / default deny |
| Security updates | no automation, 15 pending | applied automatically, no automatic reboot |
Before starting the setup, and once more after finishing, compare the device list the OS sees against the expected values. Run the lines one at a time or bundle them into a script.
lspci | grep -E 'ConnectX|NVIDIA|Non-Volatile|Realtek|MEDIATEK' # PCI devices
ip -br link # network interfaces
ethtool enP7s7 | grep -E 'Speed|Link detected' # 10G RJ45
for d in /sys/class/net/en*np*; do echo $(basename $d) $(cat $d/operstate) $(cat $d/speed); done # 200G
ls /sys/bus/usb/devices | grep -c '^usb'; lsusb # number of USB root hubs, and devices
lsblk -d -o NAME,SIZE,MODEL | grep nvme # NVMe
nproc; free -g | grep Mem; nvidia-smi -L # CPU, memory, GPU
nvidia-smi --query-gpu=driver_version,clocks.sm --format=csv # driver, clock
for z in /sys/class/thermal/thermal_zone*; do printf '%s=%s°C ' $(cat $z/device/path | sed 's/.*\.//') $(( $(cat $z/temp)/1000 )); done; echo # 7 thermal zones
fwupdmgr get-devices | grep 'Current version' # EC, SoC FW, PD, NVMe firmware
sudo dmesg --level=err,crit | grep -v -i tpm | tail # kernel errors
systemctl list-units --state=failed # failed units
| Item | Expected on the GX10 (identical on all four machines) |
|---|---|
| PCI devices | 1 NVIDIA VGA (GB10), 1 NVMe (Phison), 1 Realtek 8127 10G, 1 MediaTek 7925 Wi-Fi, 4 ConnectX-7 (only while a cable is plugged in) |
| Network interfaces | enP7s7 (10G RJ45) UP 10000Mb/s, wlP9s9 (Wi-Fi), and four 200G interfaces: enp1s0f0np0, enp1s0f1np1, enP2p1s0f0np0, enP2p1s0f1np1 (two physical ports, each exposed as two PCIe functions). A port with a cable shows up 200000 |
| USB | 12 root hubs (6 USB 2.0 + 6 USB 3.0), one built-in Wi-Fi/BT combo device |
| Storage | 1 NVMe (1TB or 4TB depending on the shipping configuration) |
| CPU / memory / GPU | 20 cores / MemTotal 121GiB / GPU 0: NVIDIA GB10 |
| Thermal zones | 7: TSOC, TS0E, TS0P, TS1E, TS1P, TGPU, TUNC. Idle 42-50°C |
| Firmware | EC, SoC FW (UEFI), USB-C PD, and NVMe versions are listed. If PD is 0x1, go to 1.3 |
| Kernel errors / failed units | nvidia-fs: warning: error retrieving numa node is harmless. Otherwise 0 errors, 0 failed units |
Check physically too. A device showing up in the OS and a port actually working are two different things. The two 200G QSFP ports in particular do not even appear as PCI devices without a cable, so plug a DAC cable in and confirm that both cages link up (operstate up, speed 200000). Plug a device into each USB-C port and check it appears in lsusb, check the RJ45 link LED and 10000Mb/s in ethtool, and try HDMI with a monitor once. Best done before bolting the machine into its final spot, while it can still be exchanged. We brought in the third machine without cables in the 200G ports and spent a while chasing a phantom "it doesn't show up on PCI" problem (section 3).
The factory image is a GNOME desktop. If the machine is going to be an inference server, it is better to strip the desktop off. The GB10's CPU and GPU share a single 128GiB memory pool, and a running desktop takes several GiB out of that pool. The README of the single-node recipe lists available memory at launch puts available memory at launch at about 114.5GiB on a host with the desktop still running; this node, switched to server mode, had 117.4GiB at launch of the same recipe and 119.0GiB idle. This was not measured before and after on the same machine, so it is not a strict comparison, but a 3-4.5GiB difference goes straight into KV cache headroom when loading a large model.
Figure 1. Available memory (out of MemTotal 121.6GiB). Desktop host (recipe README, at launch) ~114.5GiB; this node in server mode 117.4GiB at launch, 119.0GiB idle.
Why a 128GiB machine reports MemTotal 121GiB, and where the rest goes, is covered in the appendix.
The problem is not the command that removes the desktop but the apt autoremove --purge that follows it. When setting up the second machine, that command took out 14 NVIDIA platform packages in one go. These packages are held only as dependencies of a metapackage called dgx-spark-ota-update-meta, so the moment the metapackage leaves together with snapd, they become "packages nobody needs". The two snippets that supply boot parameters (nvidia-spark-initcall-bl, nvidia-spark-grub-pci) went with them, and we recovered with a reinstall, update-grub, and a reboot.
This time we blocked it in three steps. Mark the platform packages as manually installed before removing anything; run the removal as a simulation first and stop if the removal list contains driver, CUDA, or platform packages; and compare the boot parameters afterwards.
# Commands in sections 1 and 2 assume a root shell (sudo -i)
# 1) Protect: mark the metapackage's dependencies (except snap-related) + installed NVIDIA/DGX/docker/CUDA packages as manually installed
apt-cache depends dgx-spark-ota-update-meta | awk '/Depends:/{print $2}' | grep -v snaps | xargs apt-mark manual
apt-mark manual $(dpkg -l | awk '/^ii/{print $2}' | grep -E '^(nvidia-|dgx-|docker|containerd|cuda|lldpd|mlnx)')
# (if you have a reference machine, compare against its `apt-mark showmanual` list and add what is missing)
# 2) Switch + simulation gate (stops if protected packages appear in the removal list)
systemctl set-default multi-user.target
CAND="ubuntu-desktop ubuntu-desktop-minimal gdm3 gnome-shell xorg xwayland gnome-control-center \
libreoffice-core network-manager-gnome snapd nvidia-desktop-default-snaps nvidia-system-station \
dgxstation-desktop dgx-oobe-desktop dgx-spark-ota-update-meta ubuntu-server-minimal"
PURGE=""; for p in $CAND; do dpkg -s "$p" >/dev/null 2>&1 && PURGE="$PURGE $p"; done # installed ones only
apt-get -s purge $PURGE | awk '/^Remv/{print $2}' | grep -E '^(nvidia-driver|linux-modules-nvidia|cuda|docker|nvidia-spark|network-manager$|netplan|openssh-server$|ufw$)' \
&& { echo "protected package in removal list, aborting"; exit 1; }
apt-get purge -y $PURGE
apt-get -s autoremove --purge | awk '/^Remv/{print $2}' | grep -E '^(nvidia-|dgx-|lldpd|docker|cuda|mlnx)' \
&& { echo "protected package in removal list, aborting"; exit 1; }
apt-get autoremove --purge -y
rm -rf /snap /var/snap /var/lib/snapd /home/*/snap
# 3) Compare (once more after the reboot)
update-grub
grep -o 'initcall_blacklist=[^ ]*\|pci=pcie_bus_safe\|iommu.passthrough=[0-9]' /boot/grub/grub.cfg | sort -u
The gate must judge on the package name field alone, not the whole Remv line. The line carries a version string (Remv network-manager [1.46.0-1]), so a pattern anchored at the end of a name silently fails to match, and dropping the anchors produces false positives on network-manager-gnome, which is a legitimate removal target. On the fourth machine we found the whole-line gate was effectively inert, and fixed it as above.
We do not restore the OTA metapackage. Restoring it brings snapd back through nvidia-desktop-default-snaps and breaks server mode. What the metapackage does is "pull in the packages NVIDIA adds in the next release", so every so often compare its Depends list with what is installed and install only the missing ones individually. Individual platform packages update normally through apt upgrade.
One more thing. Even after the switch to server mode, dgx-dashboard and dgx-dashboard-admin stay behind. They are the desktop web management UI (port 11000) and its root D-Bus backend; a server managed over SSH has no use for them, and the admin process's resident memory grows to hundreds of MB over time (270-360MB measured on our nodes). Turn them off with systemctl disable --now dgx-dashboard dgx-dashboard-admin.
The reason to switch is a defect, not speed. On 580.173.02, a GPU error (Xid 13) reproduced on the path that offloads KV cache to disk, and it disappeared with the same code on 595.84. We put new machines on 595 from the start as well.
apt-get -s install nvidia-driver-595-open linux-modules-nvidia-595-open-nvidia-hwe-24.04 | grep -E '^(Inst|Remv)'
# Check that every removal (Remv) line is a 580-series package. If not, stop
apt-get -s install nvidia-driver-595-open linux-modules-nvidia-595-open-nvidia-hwe-24.04 \
| grep ^Remv | grep -v 580 && { echo "removal other than 580, aborting"; exit 1; }
apt-get install -y nvidia-driver-595-open linux-modules-nvidia-595-open-nvidia-hwe-24.04
linux-modules-nvidia-595-open-nvidia-hwe-24.04 pulls in the prebuilt module package matching the running kernel (here …-6.17.0-1031-nvidia). No DKMS build is needed, and machines with different kernels each get the right one. Right after installation, until reboot, nvidia-smi reports "Driver/library version mismatch"; this is normal. To roll back, check the version with apt-cache policy nvidia-driver-580-open and then apt-get install nvidia-driver-580-open=<version> linux-modules-nvidia-580-open-nvidia-hwe-24.04.
There is a phenomenon reported across the GB10 family (examples: a GX10 user report, a PGX user report; summary in the appendix of the clock cap article (Korean)). Under full load the clock sits at 400-950MHz and performance drops to a third, yet every throttle flag reads Not Active. The understood cause is the USB-C PD controller firmware negotiating power incorrectly and the SoC locks its own clock down; a reboot does not clear it, and you have to unplug the power cable and drain the machine. The prevention is to update the PD firmware to the latest version. This machine's PD firmware was 0x1, and LVFS had 0x516 (urgency High) (source: fwupdmgr get-updates output, LVFS firmware ID com.asus.gx10dgx.usbpd.firmware, LVFS page).
fwupdmgr refresh --force
fwupdmgr get-updates # "GX10 USB-C PD FW Controller Update" 0x516
fwupdmgr update -y --no-reboot-check # the capsule is staged in /boot/efi/EFI/ubuntu/fw/
# Reboot (about 2 minutes with a single capsule) → check 0x00000516 in fwupdmgr get-devices
We handled the driver replacement, the server mode switch, and the firmware update into a single reboot. The UEFI ("GX10 SoC FW") and EC firmware also arrive through the same channel.
When several capsules are pending, watch out for two things. On the fourth machine, the EC, SoC FW, and PD capsules were bundled into one reboot; it took about 9 minutes to get SSH back, and the PD update alone failed (expected 0x516 and got 0x1). Running fwupdmgr refresh --force and then update again re-staged the failed capsule, and the reboot that applied only the PD capsule succeeded. Judge the results with fwupdmgr get-devices --json: in the text output every device is named "UEFI Device Firmware", which makes them hard to tell apart, while the JSON UpdateState field (2=success, 3=failed, 4=needs reboot) is unambiguous.
| Item | Setting | Notes |
|---|---|---|
| sshd | PasswordAuthentication no, KbdInteractiveAuthentication no, PermitRootLogin prohibit-password | After confirming the key is in ~/.ssh/authorized_keys, sshd -t && systemctl reload ssh |
| ufw | incoming deny / outgoing allow / routed deny. SSH only from the management network; service ports one line at a time as needed | If you are working over SSH, add ufw allow from <mgmt-net/24> to any port 22 proto tcp first, then ufw enable, and confirm a new connection from a second terminal while keeping the existing session open. Add inference API ports (8000, 8888, and so on) explicitly when opening them to the LAN |
| unattended-upgrades | automatic package lists and upgrades, Automatic-Reboot "false", Remove-Unused-Kernel-Packages "true" | A reboot in the middle of inference is unacceptable, so automatic reboot is off |
| Timezone / Wi-Fi | timedatectl set-timezone / nmcli radio wifi off | Factory state is UTC (varies by machine). Keep Wi-Fi alive as a second path until the desktop purge, the reboot, and the wired verification are all done, then turn it off last |
| persistenced / journald | enabled / persistent | factory defaults, unchanged |
Everything from here on is optional; the machine works without it. But if you are going to run a GB10 for a long time without a separate cooling fan, power and heat deserve a thought at least once, and the numbers below are material for that decision.
The GB10 has no wattage power limit (nvidia-smi -pl). The only control is the clock cap (-lgc). The two charts below are the result of sweeping from 1400MHz to unlimited in 200MHz steps. Decode is memory-bandwidth bound, so lowering the clock barely reduces token speed, while power and temperature climb steeply with the clock.
Figure 2. LLM decode (DeepSeek V4 Flash Q2, llama.cpp, median of 3 runs of 256 tokens), tok/s (solid) and power under load (dashed) per cap. 2000MHz gives 16.7 tok/s at 22W, unlimited 17.5 tok/s at 45W. At 2600 and above the measured SM clock stays at ~2495MHz, so the results are the same.
Figure 3. Video generation (MiniMax-H3, 20 steps), total time (solid) and power under load (dashed) per cap. 2000MHz: 103s, 50W, 66°C. 2200: 97s, 65W, 73°C. 2400 and above: about 96s, 85W, 80°C or more. For work that is mostly GPU compute, the time gain ends at 2200-2400.
| Cap | Use | LLM decode | Video generation |
|---|---|---|---|
| 2200 | Speed first | 17.1 tok/s, 28W, 57°C | 97s, 65W, 73°C |
| 2000 | Balanced (recommended value) | 16.7 tok/s, 22W, 55°C | 103s, 50W, 66°C |
| 1800 | Midsummer, poor ventilation | 16.2 tok/s, 19W, 53°C | 107s, 41W, 61°C |
| 1400-1600 | Long runs, low heat | 15.0-15.7 tok/s, 15-17W, 49-51°C | 113-125s, 28-33W |
# /etc/systemd/system/gpu-clock-cap.service
# After writing: systemctl daemon-reload && systemctl enable --now gpu-clock-cap (to turn off: systemctl stop)
[Unit]
Description=GPU clock cap 300-2000MHz
After=multi-user.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/nvidia-smi -lgc 300,2000
ExecStop=/usr/bin/nvidia-smi -rgc
[Install]
WantedBy=multi-user.target
Setting 2000 actually gives about 1989MHz, snapped to an internal step. One thing to remember: this cap is a benchmark condition. When recording performance numbers, record the clock condition with them, and check the actual clock under load with nvidia-smi --query-gpu=clocks.sm. All the numbers in the recipes below were taken with this cap in place.
The CPU cap exists for a different reason than the GPU cap. The starting point was "why is the CPU hot during LLM inference?" When we ran decode with vLLM (TP=2) across two connected GB10s, the GPU sat around 70°C while three or four performance cores were pinned at 100% and pushed the SoC temperature to 96°C. The cause was vLLM's default of spinning while its inter-process message queue waits for a new message (2.3 below). That fix lives on the software side, and the problem can come back when the engine or image changes. So we also put a performance-core clock cap on the host as a safety net. A single-node TP=1 setup has none of the spinning in 2.3, so this cap is insurance for when another engine or configuration overdrives the CPU, and it is optional.
The GB10's 20 cores are 10 performance cores (up to 3.9GHz) and 10 efficiency cores (2.8GHz). Looking at the temperature change per zone when the performance cores are capped at 2.8GHz, the same as the efficiency cores, the drop shrinks with distance from the performance cores, which shows they are the heat source.
Figure 4. Temperature change under load (°C) with the performance cores capped 3.9 → 2.8GHz. Performance cores −14, uncore/memory −3.7, GPU −1.7, NVMe 0. SoC overall −9. Single-stream speed was unchanged; 8 concurrent streams −5%.
# /etc/systemd/system/cpu-clock-cap.service: cap only the performance cores (max 3.9GHz) at 2.8GHz
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/bash -c 'for p in /sys/devices/system/cpu/cpufreq/policy*; do \
m=$(cat $p/cpuinfo_max_freq); \
if [ "$m" -gt 2808000 ]; then echo 2808000 > $p/scaling_max_freq; fi; done'
ExecStop=/usr/bin/bash -c 'for p in /sys/devices/system/cpu/cpufreq/policy*; do \
cat $p/cpuinfo_max_freq > $p/scaling_max_freq; done'
[Install]
WantedBy=multi-user.target
/sys/devices/system/cpu/cpufreq/boost defaults to 0 on this platform, so there is nothing to touch there. In short, the GPU cap is a ceiling on power and heat, and the CPU cap is a safety net for when software starts overdriving the CPU. For both, know not just the number but the reason it is there, so you can lift it at the right time later.
mp executor). It does not apply when running TP=1 on one node (the worker lives in the same process, so there is no queue at all). This is about connecting two or more nodes, or explicitly passing --distributed-executor-backend=mp.vLLM's shm_broadcast.py has a default that spins for 1 second (busy_loop_s=1) while waiting on the message queue before it goes to sleep. The interval between decode steps is far shorter than 1 second, so in practice it is always spinning. On an ordinary server with dozens of cores, wasting 3-4 cores goes unnoticed, but on the GB10, where CPU and GPU sit in one package, that heat becomes SoC temperature directly. We captured stacks with py-spy and changed the default to 2ms; the combined CPU of the vLLM processes dropped from 333% to 89% and the SoC temperature fell 11°C. The investigation is in the original article, and the controlled reproduction in the follow-up experiment.
Figure 5. Spin default (1 second) vs the 2ms patch. Left: combined CPU usage of the vLLM processes (%). Right: SoC temperature under load (°C, node without an external fan). In the reproduction, the default reached 85°C after 90 seconds of load, 90°C at 2 minutes 18 seconds, and a peak of 95°C; the patch peaked at 85°C.
# Inside the container/virtualenv, before starting vLLM (adjust the path to your install location)
sed -i 's/busy_loop_s: float = [0-9.]*/busy_loop_s: float = 0.002/' \
$(python -c 'import vllm,os;print(os.path.dirname(vllm.__file__))')/distributed/device_communicators/shm_broadcast.py
grep -n 'busy_loop_s' $(python -c 'import vllm,os;print(os.path.dirname(vllm.__file__))')/distributed/device_communicators/shm_broadcast.py
# The changed line should show up. If the pattern is missing, sed silently does nothing.
# If your build exposes it as an environment variable (VLLM_MQ_SPIN_SECONDS and the like), use that instead.
One honest caveat. In a later re-verification, single-stream decode was about 2-6% slower with the patch: 38.7 tok/s vs 40.9 default (within run-to-run variance, but the direction was consistent; no difference at 4 concurrent streams, and the CPU reduction reproduced as before). So we treat this patch as "a switch to flip when heat is the problem". No external cooling and it is summer? On. Single-stream speed matters most? Off. The CPU cap in 2.2 stays in place regardless of that choice.
Once an inference server is set up there is little reason to sit at it, and most access is SSH from somewhere else. Putting Tailscale on it instead of router port forwarding lets you connect by the same name from anywhere. The later parts of this article were also done from outside the house over the tailnet.
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up # log in at the printed URL (or create an auth key in the admin console and pass it with --authkey)
tailscale ip -4 # this machine's tailnet address
ufw allow in on tailscale0 # if the firewall is on, allow the tailnet interface as a whole
With MagicDNS on, ssh <hostname> is all it takes. The inference API can be opened over the same path, so for services used outside the LAN it is simpler to open them only over the tailnet than to add LAN rules to ufw.
| Symptom | Cause / fix |
|---|---|
docker.service fails after reboot: error creating buildkit instance: invalid database | The buildkit local DB shipped in the factory image (created 2025-09) was corrupt. The two machines we brought in most recently reproduced it identically, so it looks like a defect common to the image. It is only build cache, so systemctl stop docker.socket docker; mv /var/lib/docker/buildkit /var/lib/docker/buildkit.bak; systemctl start docker. Unrelated to images or containers. Then register the nvidia runtime with nvidia-ctk runtime configure --runtime=docker. |
nvidia-smi right after the driver install: Driver/library version mismatch | The old kernel module is still loaded while only the libraries are new. Resolved by a reboot. Do not run GPU jobs before rebooting. |
On 595, nvidia-smi --query-gpu=clocks.applications.graphics and display_mode return strings | "Requested functionality has been deprecated". Health-check scripts that do numeric comparisons will break. Read clocks.sm or handle the string as an exception. |
| GPU clock stays at 400-950MHz under load, power ~10W, all throttle flags Not Active | PD firmware low-clock lock (1.3), reports 1, 2). Neither a reboot nor nvidia-smi -rgc clears it: shut down → unplug the power cable → press the power button to drain → reconnect after 1 minute. Distinguish it from an intentional cap (about 1989MHz). |
Boot parameters disappeared after apt autoremove | Platform packages that were dependencies of the metapackage were removed with it (1.1). After a large purge, compare /proc/cmdline and /etc/default/grub.d/ with a reference machine. |
netplan generate fails with Invalid YAML: aliases are not supported and no netplan configuration takes effect | The factory image can carry a corrupt netplan file whose entire content is null bytes (our fourth machine: 715 bytes, all 0x00). One broken file makes netplan give up on everything, so the symptom shows up far from the cause. Sweep /etc/netplan/* with file, move the broken file away, and run netplan generate again. |
| A static IP reverts to DHCP after a reboot | On this image netplan is the source of truth for the network. On some machines the NM profiles exist only under /run (volatile), so settings made with nmcli con mod can vanish on reboot. Put persistent configuration in /etc/netplan/. An empty /etc/NetworkManager/system-connections/ is not a fault; it is the netplan renderer working as designed. |
200G ConnectX-7 missing from lspci | The NIC is not exposed unless a cable is plugged in (managed by the hotplug package). Not a driver problem. |
Files left in /tmp vanished after a reboot | Ubuntu clears /tmp at boot. Move staging files to home before rebooting. |
Waited for a remote script to finish with pgrep -f scriptname and it stayed "running" forever | The waiting side's ssh command line contains the same string, so it matches itself. Use a bracket pattern like pgrep -f "[d]ownload.sh". pkill -f kills itself for the same reason. |
| When a model server takes more than 90% of memory, a userspace OOM daemon may kill it first | Check whether a daemon like earlyoom is active (systemctl is-active earlyoom). Some recipes that intentionally fill memory with a large model tell you to turn earlyoom off. |
In the single-node recipe community, advice is circulating that "the desktop/display takes 7GiB of UMA, so disable the Display item in UEFI" (the 7GiB figure comes from the MiaAI recipe README). On inspection, the GX10 firmware (GX10DGX.0105) does have items like Display, iGPU, and iGPU Memory Carveout, but they are all hidden (SuppressIf) and do not appear in the setup screen, and they are not in NVIDIA's official UEFI guide either. What the firmware actually takes is fixed at about 3.9GiB (including the 3.0GiB carveout), as shown in the appendix, and the 7GiB in question is userspace GPU allocations by GNOME/Xorg and the like, so it comes back when you switch to server mode. There is nothing to do in the firmware, and editing the hidden variables directly is unverified, so we do not recommend it.
The 200G ports used to join two or more nodes confused us at four stages. The symptoms and fixes for each stage are listed in order. All of it actually happened on our machines.
lspci. The driver (mlx5_core) is loaded. This is normal behavior managed by the hotplug package, and the device appears once a cable is plugged in.enp1s0f0np0 and enP2p1s0f0np0 are the same physical port (a socket-direct configuration exposed on two PCIe roots. NVIDIA's documentation also says each QSFP port appears as two Linux interfaces: ConnectX-7 Networking; the structure is explained by ServeTheHome). cat /sys/class/net/*/phys_switch_id shows the same value for all four interfaces, and only phys_port_name differs (p0 or p1). Do not bond them; give the two interfaces IPs in different subnets (NVIDIA's Connect Two Sparks playbook does the same). That lets NCCL split traffic across both paths so the bandwidth adds up. At this stage the link and RoCE state are ACTIVE, and it only looks like "it won't come up" only because there is no IP.
nmcli con add type ethernet con-name ic200g ifname enp1s0f0np0 ipv4.method manual ipv4.addresses <rail-A>/24 ipv4.never-default yes 802-3-ethernet.mtu 9000
nmcli con add type ethernet con-name ic200g-b ifname enP2p1s0f0np0 ipv4.method manual ipv4.addresses <rail-B>/24 ipv4.never-default yes 802-3-ethernet.mtu 9000
# The peer machine uses the same two subnets. In the firewall, allow all four port names (so it survives moving the cable to the other cage)
for i in enp1s0f0np0 enp1s0f1np1 enP2p1s0f0np0 enP2p1s0f1np1; do ufw allow in on $i; doneib_write_bw and iperf3 stall around 13Gb/s (normal is 98-110Gb/s per path, about 196Gb/s combined; the per-path ceiling is the PCIe Gen5 x4 link, not the cable, and in a later switch-based setup ib_send_bw read a uniform 109Gb/s per path. NVIDIA's playbook benchmarking guide expects about 197Gb/s from ib_write_bw, and the same 13Gb/s ceiling is reported on the NVIDIA forums). If two processes on the same device split it 6.9+6.9, you are hitting a device ceiling, and if two interfaces within one machine sending to each other also give 13, the cable and the peer machine are ruled out. A reboot does not fix it. Shut down both machines, unplug the power cables, press the power button to drain, and reconnect after 1 minute; that cleared it (13.3 → 108Gb/s, 196Gb/s over both paths). It held through several normal reboots afterwards. This looks like the same family of power-side issue as the PD firmware low-clock lock.permanent MAC address doesn't match. Clear the MAC lock with nmcli con mod ic200g 802-3-ethernet.mac-address "" connection.interface-name <new interface> and bind by name again. Firewall rules are also by port name, so open all four as above.Two things to remember when diagnosing. ufw allows ICMP by default, so you can get a state where ping works but TCP does not (ib_write_bw says "Couldn't connect" while ping is fine). And on the GB10 there are reports of ib_write_bw reading low while NCCL is fine (NVIDIA forums), so make the final call with NCCL (nccl-tests). GPU_DIRECT_RDMA_SUPPORTED=False in the NCCL log is normal on the GB10.
All of the above is about connecting two machines directly with a cable. With three or more nodes there are only two ports per machine, so a full mesh of direct links is impossible and a switch becomes necessary. That setup is covered separately in connecting four DGX Sparks through a switch (Korean).
Two recipes you can follow verbatim on a single node once setup is done. Both were measured with the GPU cap above (1989MHz) in place.
The fastest functional check. Install ollama with the official script, download Qwen3.8-27B-Q4_K_M.gguf (19GB) from ggml-org/Qwen3.8-27B-GGUF on Hugging Face, and register it with a Modelfile. One trap: in this environment ollama's default context came out as 262144 (check with ollama ps), so even a small model can take tens of GB of KV cache. Set num_ctx explicitly in the Modelfile.
curl -fsSL https://ollama.com/install.sh | sh
pip install -U huggingface_hub # hf CLI
hf download ggml-org/Qwen3.8-27B-GGUF Qwen3.8-27B-Q4_K_M.gguf --local-dir ~/models/qwen3.8-27b
cat > Modelfile <<EOF
FROM /home/$USER/models/qwen3.8-27b/Qwen3.8-27B-Q4_K_M.gguf
PARAMETER num_ctx 32768
EOF
ollama create qwen3.8-27b-q4km -f Modelfile # about 1 minute (blob copy)
ollama run qwen3.8-27b-q4km --verbose "Introduce yourself in one sentence"
| Measurement | Value | Notes |
|---|---|---|
| Load | 100% GPU, 20GB | ollama ps. First load 43 seconds (cold page cache), resident afterwards |
| Decode | 11.5-11.8 tok/s | For a 27B dense Q4 at 19GB, the ceiling from the GB10's memory bandwidth (273GB/s) is about 14 tok/s, so this is in the normal range |
| Prompt evaluation | 290 tok/s | The first request took 36 seconds as warm-up, 0.3 seconds from the second on |
| GPU during generation | 95%, 1989MHz, 25W, 57°C | GPU cap applied |
The original FP8 (166.9GB) does not fit on one node. MiaAI-Lab/DeepSeek-v4-Flash-One-DGX-Spark serves 0xSero/deepseek-v4-flash-0731-spark with SparkInfer kernels and DSpark speculative decoding. This checkpoint is a 106.9GB build that keeps 216 of the 256 experts (REAP) and is quantized to EXL3 3.0bpw. So be aware that this is a pruned, 3bpw variant, not "the original DeepSeek V4 Flash".
# Prerequisites: docker + compose v2, nvidia runtime registered (nvidia-ctk runtime configure --runtime=docker), user in the docker group
git clone https://github.com/MiaAI-Lab/DeepSeek-v4-Flash-One-DGX-Spark mia-one && cd mia-one
./download.sh # image pull → 107GB from HF → TP4→TP1 coalesce → checksum. ~200GB of disk, 30 minutes to several hours depending on your connection
systemctl stop earlyoom ollama # required by the recipe (the server intentionally takes 94% of memory) and also stops the ollama instance from 4.1. Confirm available ≥114.3GiB in free -h
./start.sh --no-wait # defaults: MAX_MODEL_LEN 384000, MAX_NUM_SEQS 1, KV_RECORD=stock432, DSpark speculative decoding
curl -s -o /dev/null -w '%{http_code}\n' localhost:8888/health # 200 means ready. Took 552 seconds on this machine
This is where server mode paid off. Available memory at launch was 117.4GiB (the recipe README's host: 114.5), so the KV pool came out at 510,778 tokens. That is 16% more than the README's best of 439,622.
Figure 6. Prefill speed by prompt length (tok/s, no prefix cache). ~1,000 tok/s from 32k to 100k, 843 tok/s at 330k. Needle-in-a-haystack tests at the same lengths (finding a hidden passphrase) were all exactly correct at 64k, 200k, and 330k, with 0 preemptions.
| Measurement | Value | README claim |
|---|---|---|
| Launch (/health 200) | 552s | - |
| Decode, single stream (thinking off) | 36-37 tok/s (512 tok ×3), 38-44 tok/s (acceptance-test code generation) | 44-47 (330k context, structured) |
| Prefill 32k / 100k | 1,091 / 1,024 tok/s | ~1,024 |
| Needle 64k / 200k / 330k | all correct | 320k/370k correct |
Recipe's own acceptance test (acceptance_c1.py) | all pass | gate ≥35 tok/s |
Only decode came in at 80-95% of the README, but we had the GPU cap (1989MHz) on and the conditions differed from the README's (330k context, structured output). We suspect EXL3 trellis decode is compute-heavy enough to be sensitive to the clock, but we did not run an A/B with the cap lifted. The first structured-output (JSON schema) request took 28 seconds for 23 tokens, which is a one-time grammar compilation cost; it is normal from the second request on. If you have two or more nodes, there is a separate article (Korean) on running the original FP8 with TP=2.
ip -br a, systemctl status ssh → ssh-copy-id from the workstationtimedatectl set-timezone, apt-get updateapt-mark manual on the platform packages (meta Depends + installed nvidia-spark-* and the like)/proc/cmdline, PD firmware 0x516, 0 failed units, docker OKAll memory numbers in this article are binary GiB (230 bytes). That is the unit free -g and /proc/meminfo use, and the product's "128GB" is really 128GiB (137.4GB decimal). The memory address range DMI reports, 0x80000000-0x207FFFFFFF, is exactly 237 bytes = 128GiB, and it is whittled down from there in the order below (all values read on this machine).
| Stage | Size | What |
|---|---|---|
| Physical LPDDR5X | 128.00GiB | Single pool shared by CPU and GPU (LPDDR5 8533MT/s, dmidecode) |
| Not in the memory map | −0.37GiB | DRAM that is simply absent from the memory map UEFI hands to the kernel (memblock 127.63GiB). Presumably used by firmware; its purpose is not documented |
| In the map but reserved | −3.55GiB | The display/iGPU reservation lives here. NVIDIA's release notes (July 2026) say the "Display Reserved Memory" can be set in the BIOS to 2GB (default) or 4GB (release notes, forum announcement). On our ASUS firmware the two large regions are 2616MiB + 460MiB = 3076MiB, the boot framebuffer (BOOTFB) sits inside them, and that matches the hidden "iGPU Memory Carveout" value 24 from section 3 (times 128MiB = 3072MiB). The unit of that field is not documented, though, and its minimum is also 24, so it cannot be reduced. The remaining ~0.55GiB is UEFI runtime, ACPI, TPM and similar. System RAM left: 124.08GiB |
| Kernel's own fixed allocations | −2.45GiB | page structs for 32,526,926 4KiB pages (64B each = 1.94GiB) + kernel image, initramfs, percpu, and so on, 0.5GiB. After this: MemTotal 121.63GiB, the 121 in free -g. CMA 128MiB is included in here |
| Idle system usage | −2.6GiB | Services, slab (including the GPU driver's UVM), excluding page cache. Server mode idle MemAvailable 119.0GiB. With a desktop running, a few more GiB go here |
So "only 121GB of 128GB shows up" is normal, not a fault, and the number to plan a model against is MemAvailable rather than MemTotal. If you are loading a model where 100MiB matters, there is really one lever left: a 64KiB-page kernel (NVIDIA ships it as linux-image-nvidia-64k-hwe-24.04) shrinks the 1.94GiB of page structs to about one sixteenth and recovers roughly 1.8GiB; the Grace performance tuning guide also recommends 64K pages for large-memory workloads. But the DGX OS kernel that ships on Spark is the non-64K -nvidia kernel, and the 595 driver has a known defect (open-gpu-kernel-modules #1269) on paths that pin 4GiB or more in one mapping (such as disk KV offloading), so validate with your workload first and keep a 4K boot entry. The display reservation cannot be lowered even where the BIOS exposes it (the default, 2GB or the value 24 on our firmware, is the minimum), CMA (128MiB) is already used as movable pages so shrinking it gains nothing (see cma= and crashkernel= in the kernel parameters documentation), and crashkernel is already 0.
Recorded 2026-08-21 to 2026-08-22. Hardware: ASUS Ascent GX10 (NVIDIA GB10, 128GB). Other GB10 products may differ in firmware and image. Firmware GX10DGX.0105, Ubuntu 24.04.4, kernel 6.17.0-1031-nvidia, driver 595.84. The clock cap sweep and spin experiment numbers were measured on the two existing machines of the same model (see the linked articles). Environment-dependent items such as network configuration, addresses, and hostnames are omitted.