@@ -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
0 commit comments