Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht

## [Unreleased]

### Added

- **`QUANTUI_ENABLE_SLURM` site gate** — SLURM batch mode stays hidden and
undispatchable until an operator sets this environment variable. Student CPU
images and JupyterHub profiles can omit it (default off); instructors enable
cluster testing with `QUANTUI_ENABLE_SLURM=1` without any extra student
command.

### Fixed

- **`apptainer/quantui-gpu.def` never installed the `xtb` extra**, so GFN-FF
Expand Down
1 change: 1 addition & 0 deletions apptainer/slurm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ See the [NCShare SLURM batch runbook](https://github.com/The-Schultz-Lab/QuantUI

| Variable | Default | Purpose |
|----------|---------|---------|
| `QUANTUI_ENABLE_SLURM` | *(unset — off)* | Show **SLURM batch (cluster)** in Settings and allow cluster dispatch. Requires `sbatch` on PATH. Leave unset in student CPU images; set on instructor/test profiles when validating NCShare. |
| `QUANTUI_MAX_CONCURRENT_JOBS` | `2` | Active SLURM job cap |
| `QUANTUI_SLURM_SUBMIT_COOLDOWN_S` | `30` | Min seconds between submits (`0` disables) |
| `QUANTUI_SLURM_STALE_NO_ID_S` | `600` | Stale registry rows without SLURM id |
Expand Down
1 change: 1 addition & 0 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ successfully; only the auto-open is best-effort.
| --- | --- |
| `QUANTUI_LOG_DIR` | Override the default `~/.quantui/logs/` location. The dashboard's default output (`~/.quantui/dashboard.html`) follows: it lives one level up from the active `QUANTUI_LOG_DIR`. |
| `QUANTUI_DISABLE_GPU` | Force CPU mode even when gpu4pyscf is installed. `quantui gpu check` reports this as the reason. Accepted truthy values: `1`, `true`, `True`. |
| `QUANTUI_ENABLE_SLURM` | Opt in to **SLURM batch (cluster)** on the System Settings → Execution backend dropdown. Requires `sbatch` on PATH. Off by default so student deployments stay in-kernel until an operator enables cluster mode in the Apptainer image, JupyterHub spawner, or shell. Accepted truthy values: `1`, `true`, `yes`, `on`. |
| `QUANTUI_FREQ_PARALLEL` | Opt in to parallel **CPU** workers for the IR-intensity finite-difference loop in frequency calculations (`6N` displaced SCFs). Same effect as the **Parallelize IR intensity displacements** checkbox on the System Settings tab; when this env var is set it overrides the saved setting. Reference SCF and Hessian still use gpu4pyscf when available. Requires ≥4 cores and ≥2 atoms. Off by default. Accepted truthy values: `1`, `true`, `yes`, `on`. |

---
Expand Down
3 changes: 2 additions & 1 deletion quantui/app_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
sync_textarea_from_table,
)
from quantui.backends.cluster_config import max_concurrent_jobs
from quantui.backends.dispatch import slurm_unavailable_note
from quantui.help_content import HELP_TOPICS
from quantui.live_log import LiveLog
from quantui.orbital_visualization import (
Expand Down Expand Up @@ -349,7 +350,7 @@ def _render_status(gpu_state: Any) -> str:
if not slurm_available:
exec_backend_note = widgets.HTML(
f'<div style="font-size:11px;color:{_theme.css.TEXT_SUBTLE};margin:2px 0 0 0">'
"SLURM unavailable here (sbatch not found).</div>"
f"{slurm_unavailable_note()}</div>"
)
else:
limit = max_concurrent_jobs()
Expand Down
5 changes: 2 additions & 3 deletions quantui/app_slurm.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
calc_type_key_from_app,
is_slurm_available,
slurm_backend_for_app,
slurm_unavailable_user_message,
)
from quantui.backends.registry import JobRecord, JobRegistry
from quantui.backends.slurm_errors import format_error_html
Expand Down Expand Up @@ -140,9 +141,7 @@ def startup_slurm_check(app: Any) -> None:
def submit_slurm_run(app: Any) -> None:
"""Submit the current configuration to SLURM and start the monitor thread."""
if not is_slurm_available():
app.run_status.value = (
"SLURM is not available on this system (sbatch not found)."
)
app.run_status.value = slurm_unavailable_user_message()
return

calc_type = calc_type_key_from_app(app)
Expand Down
46 changes: 44 additions & 2 deletions quantui/backends/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from __future__ import annotations

import os
import shutil
from pathlib import Path
from typing import Any
Expand All @@ -17,12 +18,53 @@
# Calc types that may run a DFT geometry optimization before the main step.
_PREOPT_CALC_TYPES = frozenset({"frequency", "tddft"})

_TRUTHY_ENV = frozenset({"1", "true", "yes", "on"})

def is_slurm_available() -> bool:
"""Return True when ``sbatch`` is on PATH."""

def _env_truthy(name: str) -> bool:
"""Return True when ``name`` is set to a truthy value in the environment."""
return os.environ.get(name, "").strip().lower() in _TRUTHY_ENV


def is_slurm_cli_present() -> bool:
"""Return True when the SLURM CLI (``sbatch``) is on PATH."""
return shutil.which("sbatch") is not None


def is_slurm_site_enabled() -> bool:
"""Return True when the operator has opted in via ``QUANTUI_ENABLE_SLURM``."""
return _env_truthy("QUANTUI_ENABLE_SLURM")


def is_slurm_available() -> bool:
"""Return True when SLURM batch mode is available for the UI and dispatch."""
return is_slurm_cli_present() and is_slurm_site_enabled()


def slurm_unavailable_note() -> str:
"""Return a short Settings-tab note when SLURM batch mode is not offered."""
if is_slurm_available():
return ""
if not is_slurm_cli_present():
return "SLURM unavailable here (sbatch not found)."
return (
"SLURM batch mode is off for this deployment "
"(operator: set QUANTUI_ENABLE_SLURM=1 to enable)."
)


def slurm_unavailable_user_message() -> str:
"""Return a user-facing run-status message when SLURM dispatch is blocked."""
if is_slurm_available():
return ""
if not is_slurm_cli_present():
return "SLURM is not available on this system (sbatch not found)."
return (
"SLURM batch mode is disabled on this deployment. "
"Ask your instructor to set QUANTUI_ENABLE_SLURM=1."
)


def calc_type_key_from_app(app: Any) -> str:
from quantui.app_runflow import calc_type_key

Expand Down
30 changes: 29 additions & 1 deletion tests/test_app_slurm.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
build_calculation_request,
calc_type_key_from_app,
is_slurm_available,
is_slurm_cli_present,
is_slurm_site_enabled,
slurm_unavailable_note,
slurm_unavailable_user_message,
)
from quantui.backends.registry import JobRegistry
from quantui.security import SecurityError
Expand Down Expand Up @@ -60,12 +64,36 @@ def test_calc_type_key_from_app(self):
assert calc_type_key_from_app(app) == "frequency"

@patch("quantui.backends.dispatch.shutil.which")
def test_is_slurm_available(self, mock_which):
def test_is_slurm_available(self, mock_which, monkeypatch):
mock_which.return_value = "/usr/bin/sbatch"
monkeypatch.delenv("QUANTUI_ENABLE_SLURM", raising=False)
assert is_slurm_cli_present() is True
assert is_slurm_site_enabled() is False
assert is_slurm_available() is False

monkeypatch.setenv("QUANTUI_ENABLE_SLURM", "1")
assert is_slurm_site_enabled() is True
assert is_slurm_available() is True

mock_which.return_value = None
assert is_slurm_cli_present() is False
assert is_slurm_available() is False

@patch("quantui.backends.dispatch.shutil.which")
def test_slurm_unavailable_messages(self, mock_which, monkeypatch):
mock_which.return_value = None
monkeypatch.delenv("QUANTUI_ENABLE_SLURM", raising=False)
assert "sbatch not found" in slurm_unavailable_note()
assert "sbatch not found" in slurm_unavailable_user_message()

mock_which.return_value = "/usr/bin/sbatch"
assert "QUANTUI_ENABLE_SLURM=1" in slurm_unavailable_note()
assert "QUANTUI_ENABLE_SLURM=1" in slurm_unavailable_user_message()

monkeypatch.setenv("QUANTUI_ENABLE_SLURM", "yes")
assert slurm_unavailable_note() == ""
assert slurm_unavailable_user_message() == ""


class TestUseSlurmExecution:
@patch("quantui.app_slurm.is_slurm_available", return_value=True)
Expand Down