Skip to content

Commit 7431999

Browse files
Merge pull request #31 from The-Schultz-Lab/gpu-diagnostics-and-toggle
Fix misleading GPU probe diagnostics and add a GPU offload toggle (GPU.7, GPU.8)
2 parents 400d78c + d3f296a commit 7431999

8 files changed

Lines changed: 594 additions & 98 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,31 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
3636
avoids the base image's implicit `defaults` channel. No change to the shipped
3737
package set or runtime behavior.
3838

39+
### Added
40+
41+
- **GPU offload can now be switched off from the UI** — Status tab → Settings →
42+
"Use GPU when available". The preference persists across launches. This exists
43+
because GPU offload is not always faster: quantum-chemistry SCF is
44+
double-precision throughout, and consumer/workstation GPUs gate FP64 to roughly
45+
1/32–1/64 of their FP32 rate, so offload on such a card can be *slower* than a
46+
many-core CPU. `QUANTUI_DISABLE_GPU=1` still overrides the setting for scripted
47+
runs.
48+
- **A warning when a detected GPU is unlikely to help.** `quantui gpu check` and
49+
the Status tab now flag consumer-class devices with a note that double
50+
precision is weak on them and offload may be slower than CPU, instead of
51+
presenting any detected CUDA device as free speed.
52+
3953
### Fixed
4054

55+
- **`quantui gpu check` no longer reports a broken CUDA install as "gpu4pyscf not
56+
installed".** `ModuleNotFoundError` is a subclass of `ImportError`, so catching
57+
the latter conflated "the package is absent" with "the package is present but
58+
its CUDA libraries are missing" — the second case was reported as the first,
59+
sending users back to an install step they had already completed. The two are
60+
now distinguished, the underlying exception is included in the message (e.g.
61+
the missing `libnvJitLink.so`), and both cases are logged. The reason string
62+
now comes from the detection probe itself rather than being re-derived by the
63+
CLI, so the message can no longer contradict what the run dispatcher decided.
4164
- **Exit is now a two-stage confirmation, so one click can no longer tear down an
4265
HPC allocation.** Exit shuts the server down by sending `SIGTERM` to the parent
4366
process. On a laptop that parent is just Voilà, but on a cluster interactive

quantui/app.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,7 @@ class QuantUIApp:
896896
_files_root_dd: Any
897897
_files_status_html: Any
898898
_files_up_btn: Any
899+
gpu_enabled_cb: Any
899900
help_content_html: Any
900901
help_tab_panel: Any
901902
help_topic_dd: Any
@@ -1207,11 +1208,14 @@ def _detect_gpu() -> None:
12071208
except Exception: # noqa: BLE001 — warm-up is best-effort
12081209
pass
12091210
try:
1210-
from quantui.gpu_offload import is_gpu_available
1211+
from quantui.gpu_offload import probe_gpu
12111212

1212-
state = is_gpu_available()
1213+
# probe_gpu (not is_gpu_available) so the badge can show the
1214+
# actual reason offload isn't active instead of a generic
1215+
# "not installed or no CUDA device".
1216+
state = probe_gpu()
12131217
except Exception: # noqa: BLE001 — treat any failure as "no GPU"
1214-
state = (False, None)
1218+
state = (False, None, "")
12151219
render = getattr(self, "_render_status_html", None)
12161220
html_widget = getattr(self, "_status_html", None)
12171221
if render is None or html_widget is None:
@@ -1404,6 +1408,7 @@ def _build_status_panel(self) -> None:
14041408
visualization_available=VISUALIZATION_AVAILABLE,
14051409
viz_default_backend=self._user_settings.viz.default_backend,
14061410
vib_framerate_fps=self._user_settings.viz.vib_framerate_fps,
1411+
gpu_enabled=self._user_settings.compute.gpu_enabled,
14071412
)
14081413

14091414
# ── Welcome header ────────────────────────────────────────────────────
@@ -1699,6 +1704,10 @@ def _wire_callbacks(self) -> None:
16991704
self.vib_framerate_si.observe(
17001705
self._safe_cb(self._on_vib_framerate_changed), names="value"
17011706
)
1707+
# Settings → GPU offload on/off (Status tab; persisted).
1708+
self.gpu_enabled_cb.observe(
1709+
self._safe_cb(self._on_gpu_enabled_changed), names="value"
1710+
)
17021711
# 3D viewer style and lighting controls
17031712
if VISUALIZATION_AVAILABLE:
17041713
self.viz_style_dd.observe(
@@ -2824,6 +2833,38 @@ def _on_viz_default_backend_changed(self, change) -> None:
28242833
return
28252834
self._set_viz_preference(change["new"], persist=True)
28262835

2836+
def _on_gpu_enabled_changed(self, change) -> None:
2837+
"""Persist the GPU-offload preference and re-probe immediately.
2838+
2839+
The detection probe is cached for the process lifetime, so flipping this
2840+
without clearing the cache would leave the next run using the old
2841+
decision — the toggle would appear to do nothing until restart.
2842+
"""
2843+
new_val = bool(change["new"])
2844+
if new_val == self._user_settings.compute.gpu_enabled:
2845+
return
2846+
self._user_settings.compute.gpu_enabled = new_val
2847+
self._user_settings.save()
2848+
try:
2849+
from quantui.gpu_offload import is_gpu_available, probe_gpu
2850+
2851+
is_gpu_available.cache_clear()
2852+
state = probe_gpu()
2853+
except Exception: # noqa: BLE001 — a probe failure must not break the UI
2854+
state = (False, None, "")
2855+
# Refresh the Status badge so it reflects the new decision right away.
2856+
render = getattr(self, "_render_status_html", None)
2857+
html_widget = getattr(self, "_status_html", None)
2858+
if render is not None and html_widget is not None:
2859+
try:
2860+
html_widget.value = render(state)
2861+
except Exception: # noqa: BLE001 — best-effort badge refresh
2862+
pass
2863+
try:
2864+
_calc_log.log_event("gpu_enabled_changed", f"gpu_enabled={new_val}")
2865+
except OSError:
2866+
pass
2867+
28272868
def _on_vib_framerate_changed(self, change) -> None:
28282869
"""Persist the vibrational-animation framerate and re-render the
28292870
current mode so the new fps applies immediately. Re-rendering also

quantui/app_builders.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def build_status_panel(
6565
visualization_available: bool,
6666
viz_default_backend: str = "auto",
6767
vib_framerate_fps: int = 10,
68+
gpu_enabled: bool = True,
6869
) -> None:
6970
"""Build the Status tab panel."""
7071
cores, mem_gb = get_session_resources_fn()
@@ -106,12 +107,29 @@ def _ok(flag: bool, extra: str = "") -> str:
106107
def _gpu_cell(gpu_state: Any) -> str:
107108
if gpu_state is None:
108109
return '<span style="color:#94a3b8">&#8987; checking&hellip;</span>'
109-
avail, name = gpu_state
110+
# Accepts the 2-tuple from is_gpu_available() or the 3-tuple from
111+
# probe_gpu() — the app passes the latter so the real reason can be
112+
# shown instead of a guess.
113+
avail, name = gpu_state[0], gpu_state[1]
114+
reason = gpu_state[2] if len(gpu_state) > 2 else ""
110115
if avail:
111-
return _ok(True, f"&mdash; <code>{name}</code>")
112-
return _ok(
113-
False, "&mdash; <code>gpu4pyscf</code> not installed or no CUDA device"
114-
)
116+
cell = _ok(True, f"&mdash; <code>{name}</code>")
117+
from quantui.gpu_offload import is_low_fp64_device
118+
119+
if is_low_fp64_device(name):
120+
# Detected is not the same as beneficial. Consumer cards gate
121+
# FP64 to ~1/32–1/64 of FP32 and PySCF SCF is FP64 throughout,
122+
# so offload can be slower than the CPU. Warn in place rather
123+
# than let it look like free speed.
124+
cell += (
125+
'<div style="color:#b45309;font-size:11px;margin-top:2px">'
126+
"&#9888; consumer-class GPU &mdash; weak double precision; "
127+
"may run <b>slower</b> than CPU. Benchmark before relying "
128+
"on it.</div>"
129+
)
130+
return cell
131+
detail = reason if reason else "not installed or no CUDA device"
132+
return _ok(False, f'&mdash; <span style="font-size:12px">{detail}</span>')
115133

116134
def _render_status(gpu_state: Any) -> str:
117135
items = [
@@ -203,12 +221,31 @@ def _render_status(gpu_state: Any) -> str:
203221
"(persists across launches)</span></div>"
204222
)
205223

224+
# GPU offload toggle (persists across launches). Off is a legitimate choice
225+
# on consumer cards, where FP64 offload can be slower than the CPU — see
226+
# gpu_offload.is_low_fp64_device. QUANTUI_DISABLE_GPU=1 overrides this.
227+
gpu_toggle_label = widgets.HTML(
228+
'<div style="font-size:12px;color:#475569;margin-top:12px;'
229+
'margin-bottom:0px">GPU offload '
230+
'<span style="color:#94a3b8;font-size:11px">'
231+
"(persists across launches; only applies when a CUDA device is "
232+
"detected)</span></div>"
233+
)
234+
app.gpu_enabled_cb = widgets.Checkbox(
235+
value=gpu_enabled,
236+
description="Use GPU when available",
237+
indent=False,
238+
layout=layout_fn(width="320px", margin="2px 0 0 0"),
239+
)
240+
206241
settings_box = widgets.VBox(
207242
[
208243
settings_html,
209244
app.viz_default_backend_dd,
210245
vib_fps_label,
211246
app.vib_framerate_si,
247+
gpu_toggle_label,
248+
app.gpu_enabled_cb,
212249
],
213250
layout=layout_fn(margin="0 0 8px 0"),
214251
)

quantui/cli.py

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -89,52 +89,35 @@ def _cmd_gpu_check(args: argparse.Namespace) -> int:
8989
Returns exit code 0 when GPU offload is available, 1 when it's not —
9090
so ``if quantui gpu check; then ...; fi`` works in shell scripts.
9191
"""
92-
from quantui.gpu_offload import is_gpu_available
92+
from quantui.gpu_offload import is_gpu_available, is_low_fp64_device, probe_gpu
9393

9494
# The detection probe is cached; clear so each CLI invocation is
9595
# fresh (the user may have just installed gpu4pyscf and wants to
9696
# confirm without restarting their shell).
9797
is_gpu_available.cache_clear()
98-
available, name = is_gpu_available()
98+
available, name, reason = probe_gpu()
9999
if available:
100100
print(f"GPU offload available: {name}")
101-
return 0
102-
print("GPU offload not available", file=sys.stderr)
103-
# Surface the most common reasons so a user knows where to look next.
104-
import os as _os
105-
106-
if _os.environ.get("QUANTUI_DISABLE_GPU", "").strip() in ("1", "true", "True"):
107-
print(
108-
" reason: QUANTUI_DISABLE_GPU is set in the environment",
109-
file=sys.stderr,
110-
)
111-
else:
112-
try:
113-
import gpu4pyscf # noqa: F401
114-
except ImportError:
101+
if is_low_fp64_device(name):
102+
# Available is not the same as worth using: PySCF is FP64
103+
# throughout, and consumer cards gate double precision to a small
104+
# fraction of single. Say so here rather than let the user discover
105+
# it as an unexplained slowdown.
115106
print(
116-
" reason: gpu4pyscf not installed "
117-
"(see README → 'Optional: GPU acceleration')",
107+
f" note: {name} looks like a consumer/workstation GPU, whose "
108+
"double-precision throughput is typically 1/32–1/64 of its "
109+
"single-precision. Quantum-chemistry SCF is double-precision "
110+
"throughout, so offload here may be SLOWER than your CPU. "
111+
"Benchmark before relying on it; switch it off in the Status "
112+
"tab or with QUANTUI_DISABLE_GPU=1.",
118113
file=sys.stderr,
119114
)
120-
return 1
121-
try:
122-
import cupy as _cupy
123-
124-
n = int(_cupy.cuda.runtime.getDeviceCount())
125-
if n < 1:
126-
print(" reason: cupy reports 0 CUDA devices", file=sys.stderr)
127-
else:
128-
print(
129-
" reason: cupy/gpu4pyscf import succeeded but probe "
130-
"raised — run "
131-
'`python -c "import cupy; cupy.show_config()"` to inspect',
132-
file=sys.stderr,
133-
)
134-
except ImportError:
135-
print(" reason: cupy not installed", file=sys.stderr)
136-
except Exception as exc:
137-
print(f" reason: cupy raised: {exc}", file=sys.stderr)
115+
return 0
116+
# The reason comes straight from the probe, so this can never contradict
117+
# what the dispatcher actually decided.
118+
print("GPU offload not available", file=sys.stderr)
119+
if reason:
120+
print(f" reason: {reason}", file=sys.stderr)
138121
return 1
139122

140123

0 commit comments

Comments
 (0)