feat: dedicated CPU panel with auto-aligned two-column Resources layout - #91
PhilipEriksson wants to merge 10 commits into
Conversation
Add a CPU panel (usage, temperature, power, model/cores with sparklines) as the single home for CPU stats on the device page, and restructure the Resources section into two independent columns: left: GPU + CPU (CPU directly beneath GPU) right: [RAM] + Storage + Network [+ Tailnet] On GB10 devices (DGX Spark / GX10) the CPU and GPU share one package and power envelope, and the CPU is often the part that runs hot first — this makes that visible at the device level. Layout: each column is a flex stack so panels take natural heights (no row-stretch dead space under the GPU panel). The bottom panel of each column gets flex-grow, so whichever side is shorter stretches its lowest panel to match — both columns always end at the same height regardless of content. CPU stats previously lived as a temp row inside the GPU panel (Sparks) and RAM panel (hosts); those rows are removed in favor of the dedicated panel so CPU is shown in one place. NetworkPanel and TailscalePanel gain an optional className prop to support the grow alignment.
|
Hi @MikeGibbsOnyx @MiaAI-Lab — first-time contributor, small nudge. To be clear on scope: CPU temperature on the summary page is already covered upstream (1.8.6) — this PR is about the device page, where there was no CPU section at all. It adds a dedicated CPU panel (usage, temperature, power, model/cores) and reflows Resources into two auto-aligned columns with CPU directly under GPU. The one thing I'd value your take on: I removed the CPU-temp rows from the GPU/RAM panels in favor of the dedicated panel (single home for CPU stats) — on GB10 the CPU is often the hotter half of the package, which motivated it, but happy to keep the rows if you'd rather not change that. Otherwise self-contained frontend, no backend impact. |
|
Hi Philip - Go nuts! I'm a hack at best and you probably know the inner working for these things better than I. I'm not territorial about anything I put in there 🙂
Michael Gibbs
Chief Executive Officer
Mobile: +1 (416) 822-0710
E-mail: ***@***.******@***.***>
[cid:062069a0-e3cc-46dc-876e-54ce8bfb9947]
…________________________________
From: Philip ***@***.***>
Sent: Thursday, September 10, 2026 8:50 AM
To: MiaAI-Lab/sparkDash ***@***.***>
Cc: Mike Gibbs ***@***.***>; Mention ***@***.***>
Subject: Re: [MiaAI-Lab/sparkDash] feat: dedicated CPU panel with auto-aligned two-column Resources layout (PR #91)
[https://avatars.githubusercontent.com/u/35528685?s=20&v=4]PhilipEriksson left a comment (MiaAI-Lab/sparkDash#91)<#91 (comment)>
Hi @MikeGibbsOnyx<https://github.com/MikeGibbsOnyx> @MiaAI-Lab<https://github.com/MiaAI-Lab> — first-time contributor, small nudge. To be clear on scope: CPU temperature on the summary page is already covered upstream (1.8.6) — this PR is about the device page, where there was no CPU section at all. It adds a dedicated CPU panel (usage, temperature, power, model/cores) and reflows Resources into two auto-aligned columns with CPU directly under GPU. The one thing I'd value your take on: I removed the CPU-temp rows from the GPU/RAM panels in favor of the dedicated panel (single home for CPU stats) — on GB10 the CPU is often the hotter half of the package, which motivated it, but happy to keep the rows if you'd rather not change that. Otherwise self-contained frontend, no backend impact.
—
Reply to this email directly, view it on GitHub<#91?email_source=notifications&email_token=B4EL5I3RZFSLIYG5DEBQG235OKPQZA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNRRHA4TINJRGMYKM4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5618945130>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/B4EL5IYDDUTQ4KQUYRBUNS35OKPQZAVCNFSNUABGKJSXA33TNF2G64TZHMYTGMBVHE2DKMBXGU5US43TOVSTWNJUGA2TAMJSGQ2TBILWAI>.
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
|
Appreciate it Mike! I'll leave the merge to you🫱🏻🫲🏼 |
Add a 'Clock Cap' row to the CPU and GPU panels showing the active frequency ceilings, so a capped device is visible at a glance. - CPU: per-frequency-domain max_perf read from sysfs (local) or a single SSH command (remote). Domains are grouped by cpuinfo_max_freq and labelled X925/A725 on GB10; a domain is 'capped' when its max_perf ceiling is below the hardware max. - GPU: nvidia-smi does not expose the active -lgc range, so the lock is read from the gpu-clock-lock.service unit (path configurable via GPU_CLOCK_LOCK_UNIT, defaulting to the common convention). - GPU SM-clock bar now scales to the cap when one is set, so it reflects the headroom that actually exists (2177/2200 reads ~full, not 72%). - Caps are cached 60s with an in-flight guard so the parallel CPU/GPU polls share one read. Verified on a 3x GB10 fleet (2x DGX Spark + 1x GX10): X925 2600/3900 capped, A725 2808/2808, GPU 2200 lock — local and remote-over-SSH.
…dware-bounded slider, live apply + boot-unit persist - server: pure clockControl.js (domain enum, bounds parsing from cpuinfo_min/ max_freq and nvidia-smi -q -d CLOCK Default Applications Clock, clamping, validation, helper argv, exit-code interpretation), SystemCollector applyClockCap with SSH root helper (primary) and local container-root fallback (secondary), volatile override merge so live-only applies are shown truthfully for the cache TTL - API: GET /api/sparks/:id/clocks/bounds, POST /api/sparks/:id/clocks (opt-in per Spark via clockControlEnabled, 403 when off, server-side clamping against re-read hardware bounds, rate-limited) - scripts: sparkdash-set-clock (POSIX sh root helper) + one-time scoped sudoers installer - client: ClockCapControl dialog (slider + number input bounded by discovered range, presets, Apply this-boot vs Save, honest warnings), wired into GPU and CPU panels, Allow clock control toggle in Edit Spark - tests: clockControl.test.js (pure logic), SystemCollector.clockControl (apply paths, override merge, bounds), ClockCapControl.test.tsx (vitest)
…-domain unit preservation, systemd $-safe ExecStart, README - _dropConvergedOverrides: gpu override drops only when a fresh read actually reports the applied value (unit file is blind to a live-only -lgc), so the UI stays honest past the cache TTL - _persistClockUnitLocal: both CPU domains share cpu-clock-cap.service, so a one-domain Save now carries the sibling domain's effective cap (volatile override, else live max_perf) instead of clobbering it; explicit per-core commands (no shell loop variables — systemd expands $VAR in ExecStart); unreadable sibling cores are omitted, not guessed - SparkMonitor.updateConfig calls clearClockCapsOverride (D5 hot config) - helper cpu_unit_body: same systemd $-safe per-core ExecStart - README: Clock control section, TOC/Features/REST API/Scripts rows - tests: gpu override survival/convergence + 5 sibling-persist regressions
…llback warning - parseCpuCoreMaxKhz / expandCpuToken / parseCpuBootUnitDefaults: derive the Boot default preset per CPU domain from what cpu-clock-cap.service actually installs (systemd brace ranges and explicit per-core echoes; unknown cores skipped, never guessed). GPU boot default = the -lgc value parsed from gpu-clock-lock.service. Nothing hardcoded. - _getClockBounds now also returns cpuBootDefaults + gpuBootDefaultMHz (local: read through the host-root bind; remote: the bounds command dumps both boot units as parts[2]/[3]) - bounds route reports the D8 GPU fallback use in warnings; the dialog surfaces bounds warnings next to the range - tests: brace-range expansion, measured-unit parsing (both ExecStart styles), preset exposure; fixture corrected to the real spark-1 topology (cores 0-4 and 10-14 = X925, 5-9 and 15-19 = A725)
…ibling-preserving helper persist Gate 1: SparkMonitor.snapshot() now emits clockControlEnabled (beside comfyMonitoring), and src/App.tsx threads the flag through placeholderSnapshot's roleFields + the WS-down merge block, so the opt-in toggle actually reaches the browser. Pinned by snapshot-contract tests in monitor-lifecycle.test.js. Gate 2: the helper argv chain and the availability probe no longer gate on "sudo -n true" (impossible under the shipped argumentless scoped sudoers grant). The probe exercises exactly the granted command: test -x (missing -> 127/not-installed), then an argumentless "sudo -n <helper>" whose usage print exits 1 = installed-and-allowed; sudo refusals hit the sparkdash-probe-sentinel -> 126/passwordless-sudo-hint. interpretHelperExit anchors on the argv sentinels instead of bare 126/127 digits in the message; the bounds API passes the probe's precise reason through to the domain rows. README + installer verify hint document the encoding. Gate 3: scripts/sparkdash-set-clock cpu_unit_body regenerates BOTH CPU domains (sibling values read from each core's current max_perf, unreadable cores omitted) so a single-domain Save can no longer wipe the sibling's boot cap. The container persist path (_persistClockUnitLocal) now wraps the CPU ExecStart in /bin/sh -c and matches the helper's Description/After/blank-line unit body, so both paths produce byte-identical units for the same request. New clockHelperScript.test.js drives the real script (sh) against a stubbed sysfs tree.
…oks, normalized exec-list shape cpu-big = the >=3 MHz group per the parseCpuClockCaps convention (fixtures had the groups swapped); SPARKDASH_SMI hook shadows the absolute nvidia-smi path; write_core now honors SPARKDASH_CPU_SYS; the helper's per-core command list is built as 'cmd; cmd' with no leading/trailing separators so the generated unit is byte-identical to the container persist path.
…-applied honesty Per the card + orchestrator amendments, measured on the live box: - GPU ceiling is now parsed from "Max Clocks → Graphics" (3003 on this silicon), with the fallback chain Max Clocks → Applications Clocks → Default Applications Clocks → GPU_CLOCK_MAX_MHZ. "Default Applications Clocks" is the boot-default boost point (2418), NOT the hardware limit — using it made 2600/2800/3000 unreachable. The section parser anchors on the exact header line (indent-4, no trailing colon; keys indent-8 with a padded fixed-column colon) and scans only strictly-deeper children, so the live transient "Clocks" sample (2184) can never win. Accepts the singular header spelling some drivers print. The deprecated reader is kept as the reported boot-default datapoint only. - POST /api/sparks/:id/clocks responses carry additive requestedMHz / appliedMHz / snapped (src/api/types.ts mirrors, response shape stays backward compatible). The driver quantises -lgc silently (a requested 2000 lands on 1976), so the apply paths now capture nvidia-smi's own "GPU clocks set to (min, max)" confirmation on BOTH the helper and the container path, and sysfs max_perf writes are read back; the strictest observed value is reported. When no observation is possible the response repeats the request and adds an explicit "could not be read back" warning — it never claims an unobserved confirmation. The D5 volatile override records the APPLIED value, never the requested one. - buildClockCapDomains emits the wire-ready 200 MHz grid (grid.min/max/ step/candidates/bandApplied) per domain: pure arithmetic band [0.30, 0.80]-of-ceiling (align up/down on 200), NO hardware detenting (--query-supported-clocks=graphics prints [N/A] on driver 580.173.02); a degenerate band falls back to the plain hard bounds with no candidate chips instead of crashing. - Tests: the orchestrator-pinned -q -d CLOCK fixture parsed verbatim (ceiling=3003; 2184 and 2418 asserted non-ceiling), the three pinned per-domain candidate lists, band edges, degenerate-band fallback, the captured set-to stdout shapes for both apply paths, and the additive response fields. 90/90 green.
…ion, helper done gate, clean cap removal
…ify button position - Slider (200 MHz grid) and manual-entry field are now two distinct labelled elements (own <label> each), not one combined flex widget. They still share one value state, so moving either updates the other. - Closed state: wrap [Modify][value chip] in one right-aligned flex group and give the chip a fixed width (w-28), so the Modify button sits at the same x-position on every row — the two CPU cluster rows (Big/Little) line up regardless of label or chip-text width. - Tests: +2 (separate labelled controls; Modify+chip one fixed group).
|
I made some new changes that fits here too @MikeGibbsOnyx Basically added a UI/UX for setting MHz Caps on your GPU, and CPU, so that your device can run cooler, while only sacrificing a little throughput in both decode and prefill. For my 2x spark setup, lowering some of my CPU cores was completely free due to the architecture of GLM5.3 Flash, 0% loss in both decode and prefill, whilst saving at least 10-15 degrees celsius worth of heat on my CPU. I don't have write access so I can't merge, but leaving it for you here to have a look, basically these caps are just to simplify the process of capping your GPU and CPU in the UI itself |
|
Yeah that will be really handy 🙂 will take a look today
Michael Gibbs
Chief Executive Officer
Mobile: +1 (416) 822-0710
E-mail: ***@***.******@***.***>
[cid:6571b46e-1717-4e31-a410-1c3b98def935]
…________________________________
From: Philip ***@***.***>
Sent: Sunday, September 13, 2026 3:06 PM
To: MiaAI-Lab/sparkDash ***@***.***>
Cc: Mike Gibbs ***@***.***>; Mention ***@***.***>
Subject: Re: [MiaAI-Lab/sparkDash] feat: dedicated CPU panel with auto-aligned two-column Resources layout (PR #91)
[https://avatars.githubusercontent.com/u/35528685?s=20&v=4]PhilipEriksson left a comment (MiaAI-Lab/sparkDash#91)<#91 (comment)>
I made some new changes that fits here too @MikeGibbsOnyx<https://github.com/MikeGibbsOnyx> Basically added a UI/UX for setting MHz Caps on your GPU, and CPU, so that your device can run cooler, while only sacrificing a little throughput in both decode and prefill.
For my 2x spark setup, lowering some of my CPU cores was completely free due to the architecture of GLM5.3 Flash, 0% loss in both decode and prefill, whilst saving at least 10-15 degrees celsius worth of heat on my CPU.
I don't have write access so I can't merge, but leaving it for you here to have a look, basically these caps are just to simplify the process of capping your GPU and CPU in the UI itself
—
Reply to this email directly, view it on GitHub<#91?email_source=notifications&email_token=B4EL5I76WCSZGZTHJBGOK535O3V3JA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNRVGU2DENJWHA22M4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-5655425685>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/B4EL5IZRL2D3HIVY7Y2R22T5O3V3JAVCNFSNUABGKJSXA33TNF2G64TZHMYTGMBVHE2DKMBXGU5US43TOVSTWNJUGA2TAMJSGQ2TBILWAI>.
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
|
Splitting this for review (same author credit to @PhilipEriksson):
Leaving this PR open so you can close it when A/B look right. No merge from us. |
Summary
Adds a dedicated CPU panel to the device page, restructures the Resources section into two auto-aligned columns, and adds editable clock caps for both the CPU and GPU so you can dial the clocks down to keep the box cool.
On GB10 devices (DGX Spark / GX10) the CPU and GPU share one package and one power envelope, and the CPU is often the part that runs hot first. Previously CPU temperature was only a small row tucked inside the GPU panel (Sparks) or RAM panel (hosts), easy to miss. This makes CPU a first-class section.
CPU panel
Shows, with sparklines where it helps:
Layout
The Resources section is now two independent columns:
flex-grow, so whichever side is shorter stretches its lowest panel (CPU on the left; Network, or Tailnet when it's last, on the right) to match. Both columns always end at the same height regardless of content or which panels are present.Clock caps (editable)
A Clock Cap row on both the CPU and GPU panels opens a dialog to set a frequency ceiling, live and/or persisted to the boot unit. This is the easy way to keep a GB10 box cool: cap the GPU graphics clock and the CPU clusters a few hundred MHz below their hardware max and the temperature drops without meaningfully touching throughput.
Max Clocks → Graphicsceiling, which is 3003 MHz on GB10, not the 2418 "Applications Clocks" default). The driver quantizes the request against its own table (ask 2000, get 1976), so the dialog reports both the requested and the applied value and never snaps the number you typed.scripts/install-clock-helper.sh) provisions a scoped NOPASSWD sudoers grant; the dialog itself never asks for a password.Why bother: cooler with almost no throughput cost
Capping the clocks is the cheapest thermal lever on a GB10 box. In practice the decode and prefill throughput loss is negligible: on the GLM-5.3-Flash-EXL3 box the caps cost 0% on both prefill and decode (the architecture doesn't bottleneck on the clock headroom being given back), while the sustained temperature comes down enough to stop the thermal derating that would otherwise eat more performance than the cap does.
Changes
src/components/SparkPage/CpuPanel.tsx— new panel + per-cluster Clock Cap rowssrc/components/SparkPage/GpuPanel.tsx— Clock Cap row + cap-scaled SM-clock bar; drop the CPU-temp row (now in CpuPanel)src/components/SparkPage/ClockCapControl.tsx— the editable cap dialog (slider + free entry, candidates, presets, requested-vs-applied honesty)src/components/SparkPage/SparkPage.tsx— two-column Resources layout + grow alignmentsrc/components/SparkPage/RamPanel.tsx— drop the CPU-temp row (now in CpuPanel)src/components/SparkPage/NetworkPanel.tsx,TailscalePanel.tsx— optionalclassNameprop for the grow alignmentsrc/components/EditSparkDialog.tsx— per-Spark "Allow clock control" opt-insrc/App.tsx— carryclockControlEnabledthrough the snapshot/WS mergesrc/api/client.ts,src/api/types.ts— bounds + set-cap client,ClockCap*typessrc/components/ui/icons.tsx— newCpuIconsrc/index.css— clock dialog width classserver/collectors/clockControl.js— bounds computation, GPU-lgcreply parser, apply/remove (helper + container paths)server/collectors/SystemCollector.js— clock-cap collection + apply wiringserver/sparks/SparkMonitor.js,SparkRegistry.js—clockControlEnabledsnapshot key + persistenceserver/index.js—POST /api/sparks/:id/clocks(rate-limited) + bounds endpointserver/config.js—GPU_CLOCK_LOCK_UNITenv knobscripts/install-clock-helper.sh,scripts/sparkdash-set-clock— remote helper + scoped sudoers grantREADME.md— clock-control provisioning + usageclockControl.test.js,SystemCollector.clockControl.test.js,SystemCollector.clockCaps.test.js,clockHelperScript.test.js,monitor-lifecycle.test.js,ClockCapControl.test.tsxVerified
Built and running on a live 3x GB10 fleet (2x DGX Spark + 1x GX10). CPU temps render correctly per device, the two columns align at the bottom across boxes with different panel counts, and the clock caps read and apply correctly on all three boxes, local and remote-over-SSH.
Tests: server suite 419/419, frontend 41/41,
tsc --noEmitclean, production build green.