setiastrosuitepro 1.8.0.post3__py3-none-any.whl → 1.8.1.post2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/saspro/__main__.py +12 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/cosmicclarity_engines/darkstar_engine.py +22 -2
- setiastro/saspro/cosmicclarity_engines/denoise_engine.py +68 -15
- setiastro/saspro/cosmicclarity_engines/satellite_engine.py +7 -3
- setiastro/saspro/cosmicclarity_engines/sharpen_engine.py +371 -98
- setiastro/saspro/cosmicclarity_engines/superres_engine.py +1 -0
- setiastro/saspro/model_manager.py +65 -0
- setiastro/saspro/model_workers.py +58 -24
- setiastro/saspro/ops/settings.py +45 -8
- setiastro/saspro/planetprojection.py +68 -36
- setiastro/saspro/resources.py +18 -14
- setiastro/saspro/runtime_torch.py +571 -127
- setiastro/saspro/star_alignment.py +262 -210
- setiastro/saspro/widgets/spinboxes.py +5 -7
- {setiastrosuitepro-1.8.0.post3.dist-info → setiastrosuitepro-1.8.1.post2.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.8.0.post3.dist-info → setiastrosuitepro-1.8.1.post2.dist-info}/RECORD +21 -21
- {setiastrosuitepro-1.8.0.post3.dist-info → setiastrosuitepro-1.8.1.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.8.0.post3.dist-info → setiastrosuitepro-1.8.1.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.8.0.post3.dist-info → setiastrosuitepro-1.8.1.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.8.0.post3.dist-info → setiastrosuitepro-1.8.1.post2.dist-info}/licenses/license.txt +0 -0
|
@@ -16,16 +16,13 @@ from contextlib import contextmanager
|
|
|
16
16
|
import platform as _plat
|
|
17
17
|
from pathlib import Path as _Path
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if _plat.system() != "Linux":
|
|
22
|
-
return None
|
|
19
|
+
|
|
20
|
+
def _rt_dbg(msg: str, status_cb=print):
|
|
23
21
|
try:
|
|
24
|
-
|
|
25
|
-
p = base / "bin" / "torch_shm_manager"
|
|
26
|
-
return str(p) if p.exists() else None
|
|
22
|
+
status_cb(f"[RT] {msg}")
|
|
27
23
|
except Exception:
|
|
28
|
-
|
|
24
|
+
print(f"[RT] {msg}", flush=True)
|
|
25
|
+
|
|
29
26
|
|
|
30
27
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
31
28
|
# Paths & runtime selection
|
|
@@ -67,14 +64,25 @@ def _runtime_base_dir() -> Path:
|
|
|
67
64
|
def _current_tag() -> str:
|
|
68
65
|
return f"py{sys.version_info.major}{sys.version_info.minor}"
|
|
69
66
|
|
|
70
|
-
def _discover_existing_runtime_dir() -> Path | None:
|
|
67
|
+
def _discover_existing_runtime_dir(status_cb=print) -> Path | None:
|
|
71
68
|
"""
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
Prefer an existing runtime that MATCHES the current interpreter minor.
|
|
70
|
+
Only if none exists, fall back to the highest available.
|
|
74
71
|
"""
|
|
75
72
|
base = _runtime_base_dir()
|
|
76
73
|
if not base.exists():
|
|
77
74
|
return None
|
|
75
|
+
|
|
76
|
+
cur_tag = _current_tag() # e.g. py311, py312
|
|
77
|
+
cur_dir = base / cur_tag
|
|
78
|
+
cur_vpy = cur_dir / "venv" / ("Scripts/python.exe" if platform.system() == "Windows" else "bin/python")
|
|
79
|
+
|
|
80
|
+
# 1) If matching-current exists and has a venv python, use it.
|
|
81
|
+
if cur_vpy.exists():
|
|
82
|
+
_rt_dbg(f"Found matching runtime for current interpreter: {cur_dir}", status_cb)
|
|
83
|
+
return cur_dir
|
|
84
|
+
|
|
85
|
+
# 2) Otherwise, fall back to "newest existing"
|
|
78
86
|
candidates: list[tuple[int, int, Path]] = []
|
|
79
87
|
for p in base.glob("py*"):
|
|
80
88
|
vpy = p / "venv" / ("Scripts/python.exe" if platform.system() == "Windows" else "bin/python")
|
|
@@ -83,22 +91,21 @@ def _discover_existing_runtime_dir() -> Path | None:
|
|
|
83
91
|
ver = _venv_pyver(vpy)
|
|
84
92
|
if ver:
|
|
85
93
|
candidates.append((ver[0], ver[1], p))
|
|
94
|
+
|
|
86
95
|
if not candidates:
|
|
87
96
|
return None
|
|
88
|
-
candidates.sort() # pick the highest Python (major, minor)
|
|
89
|
-
return candidates[-1][2]
|
|
90
97
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
candidates.sort()
|
|
99
|
+
chosen = candidates[-1][2]
|
|
100
|
+
_rt_dbg(f"No matching runtime; using newest existing: {chosen}", status_cb)
|
|
101
|
+
return chosen
|
|
102
|
+
|
|
103
|
+
def _user_runtime_dir(status_cb=print) -> Path:
|
|
104
|
+
existing = _discover_existing_runtime_dir(status_cb=status_cb)
|
|
105
|
+
chosen = existing or (_runtime_base_dir() / _current_tag())
|
|
106
|
+
_rt_dbg(f"_user_runtime_dir() -> {chosen}", status_cb)
|
|
107
|
+
return chosen
|
|
98
108
|
|
|
99
|
-
# ──────────────────────────────────────────────────────────────────────────────
|
|
100
|
-
# Shadowing & sanity checks
|
|
101
|
-
# ──────────────────────────────────────────────────────────────────────────────
|
|
102
109
|
|
|
103
110
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
104
111
|
# Shadowing & sanity checks
|
|
@@ -172,23 +179,6 @@ def _purge_bad_torch_from_sysmodules(status_cb=print) -> None:
|
|
|
172
179
|
except Exception:
|
|
173
180
|
pass
|
|
174
181
|
|
|
175
|
-
def _torch_stack_sanity_check(status_cb=print) -> None:
|
|
176
|
-
"""
|
|
177
|
-
Ensure torch imports sanely AND torchvision/torchaudio are importable.
|
|
178
|
-
(Satellite engine requires torchvision; we install torchaudio too for safety.)
|
|
179
|
-
"""
|
|
180
|
-
_torch_sanity_check(status_cb=status_cb)
|
|
181
|
-
|
|
182
|
-
try:
|
|
183
|
-
import torchvision # noqa
|
|
184
|
-
except Exception as e:
|
|
185
|
-
raise RuntimeError(f"torchvision import failed: {e}") from e
|
|
186
|
-
|
|
187
|
-
try:
|
|
188
|
-
import torchaudio # noqa
|
|
189
|
-
except Exception as e:
|
|
190
|
-
raise RuntimeError(f"torchaudio import failed: {e}") from e
|
|
191
|
-
|
|
192
182
|
|
|
193
183
|
def _torch_sanity_check(status_cb=print):
|
|
194
184
|
try:
|
|
@@ -631,129 +621,517 @@ def _install_torch(venv_python: Path, prefer_cuda: bool, prefer_xpu: bool, prefe
|
|
|
631
621
|
# Public entry points
|
|
632
622
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
633
623
|
|
|
634
|
-
def
|
|
624
|
+
def _venv_import_probe(venv_python: Path, modname: str) -> tuple[bool, str]:
|
|
635
625
|
"""
|
|
636
|
-
|
|
637
|
-
|
|
626
|
+
Try importing a module INSIDE the runtime venv python.
|
|
627
|
+
Returns (ok, output_or_error_tail).
|
|
638
628
|
"""
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
629
|
+
code = (
|
|
630
|
+
"import importlib, sys\n"
|
|
631
|
+
f"m=importlib.import_module('{modname}')\n"
|
|
632
|
+
"print('OK', getattr(m,'__version__',None), getattr(m,'__file__',None))\n"
|
|
633
|
+
)
|
|
634
|
+
r = subprocess.run([str(venv_python), "-c", code],
|
|
635
|
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
|
636
|
+
out = (r.stdout or "").strip()
|
|
637
|
+
if r.returncode == 0 and out.startswith("OK"):
|
|
638
|
+
return True, out
|
|
639
|
+
return False, out[-4000:] if out else "no output"
|
|
642
640
|
|
|
643
|
-
add_runtime_to_sys_path(status_cb=lambda *_: None)
|
|
644
641
|
|
|
645
|
-
|
|
642
|
+
def _write_torch_marker(marker: Path, status_cb=print) -> None:
|
|
643
|
+
"""
|
|
644
|
+
Create torch_installed.json based on runtime venv imports.
|
|
645
|
+
Safe to call repeatedly.
|
|
646
|
+
"""
|
|
647
|
+
rt = marker.parent
|
|
648
|
+
vp = _venv_paths(rt)["python"]
|
|
649
|
+
|
|
650
|
+
ok_t, out_t = _venv_import_probe(vp, "torch")
|
|
651
|
+
ok_v, out_v = _venv_import_probe(vp, "torchvision")
|
|
652
|
+
ok_a, out_a = _venv_import_probe(vp, "torchaudio")
|
|
653
|
+
|
|
654
|
+
payload = {
|
|
655
|
+
"installed": bool(ok_t),
|
|
656
|
+
"when": int(time.time()),
|
|
657
|
+
"python": None,
|
|
658
|
+
"torch": None,
|
|
659
|
+
"torchvision": None,
|
|
660
|
+
"torchaudio": None,
|
|
661
|
+
"torch_file": None,
|
|
662
|
+
"torchvision_file": None,
|
|
663
|
+
"torchaudio_file": None,
|
|
664
|
+
"probe": {
|
|
665
|
+
"torch": out_t,
|
|
666
|
+
"torchvision": out_v,
|
|
667
|
+
"torchaudio": out_a,
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
# get venv python version
|
|
646
672
|
try:
|
|
647
|
-
import
|
|
648
|
-
|
|
649
|
-
|
|
673
|
+
r = subprocess.run([str(vp), "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"],
|
|
674
|
+
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
|
|
675
|
+
if r.returncode == 0:
|
|
676
|
+
payload["python"] = (r.stdout or "").strip()
|
|
650
677
|
except Exception:
|
|
651
678
|
pass
|
|
652
679
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
680
|
+
# parse "OK ver file" lines
|
|
681
|
+
def _parse_ok(s: str):
|
|
682
|
+
# format: "OK <ver> <file>"
|
|
683
|
+
try:
|
|
684
|
+
parts = s.split(" ", 2)
|
|
685
|
+
ver = parts[1] if len(parts) > 1 else None
|
|
686
|
+
f = parts[2] if len(parts) > 2 else None
|
|
687
|
+
return ver, f
|
|
688
|
+
except Exception:
|
|
689
|
+
return None, None
|
|
690
|
+
|
|
691
|
+
if ok_t:
|
|
692
|
+
payload["torch"], payload["torch_file"] = _parse_ok(out_t)
|
|
693
|
+
if ok_v:
|
|
694
|
+
payload["torchvision"], payload["torchvision_file"] = _parse_ok(out_v)
|
|
695
|
+
if ok_a:
|
|
696
|
+
payload["torchaudio"], payload["torchaudio_file"] = _parse_ok(out_a)
|
|
657
697
|
|
|
658
698
|
try:
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
699
|
+
marker.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
700
|
+
status_cb(f"[RT] Wrote marker: {marker}")
|
|
701
|
+
except Exception as e:
|
|
702
|
+
status_cb(f"[RT] Failed to write marker {marker}: {e!r}")
|
|
703
|
+
|
|
704
|
+
def _venv_has_torch_stack(
|
|
705
|
+
vp: Path,
|
|
706
|
+
status_cb=print,
|
|
707
|
+
*,
|
|
708
|
+
require_torchaudio: bool = True
|
|
709
|
+
) -> tuple[bool, dict]:
|
|
710
|
+
"""
|
|
711
|
+
Definitive check: can the RUNTIME VENV import torch/torchvision/(torchaudio)?
|
|
712
|
+
This does NOT use the frozen app interpreter to decide installation state.
|
|
713
|
+
"""
|
|
714
|
+
ok_t, out_t = _venv_import_probe(vp, "torch")
|
|
715
|
+
ok_v, out_v = _venv_import_probe(vp, "torchvision")
|
|
716
|
+
ok_a, out_a = _venv_import_probe(vp, "torchaudio")
|
|
717
|
+
|
|
718
|
+
info = {
|
|
719
|
+
"torch": (ok_t, out_t),
|
|
720
|
+
"torchvision": (ok_v, out_v),
|
|
721
|
+
"torchaudio": (ok_a, out_a),
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
ok_all = (ok_t and ok_v and ok_a) if require_torchaudio else (ok_t and ok_v)
|
|
725
|
+
return ok_all, info
|
|
726
|
+
|
|
727
|
+
def _marker_says_ready(
|
|
728
|
+
marker: Path,
|
|
729
|
+
site: Path,
|
|
730
|
+
venv_ver: tuple[int, int] | None,
|
|
731
|
+
*,
|
|
732
|
+
require_torchaudio: bool = True,
|
|
733
|
+
max_age_days: int = 180,
|
|
734
|
+
) -> bool:
|
|
735
|
+
"""
|
|
736
|
+
Advisory fast-path gate ONLY.
|
|
737
|
+
|
|
738
|
+
Returns True if the marker looks sane enough that an in-process import attempt
|
|
739
|
+
is worth trying *without* doing the expensive subprocess venv probes.
|
|
740
|
+
|
|
741
|
+
IMPORTANT:
|
|
742
|
+
- This must NOT be used to decide whether to install/uninstall anything.
|
|
743
|
+
- If this returns True and the in-process import fails, we fall back to the
|
|
744
|
+
definitive venv probe (_venv_has_torch_stack).
|
|
745
|
+
"""
|
|
746
|
+
try:
|
|
747
|
+
if not marker.exists():
|
|
748
|
+
return False
|
|
749
|
+
|
|
750
|
+
raw = marker.read_text(encoding="utf-8", errors="replace")
|
|
751
|
+
data = json.loads(raw) if raw else {}
|
|
752
|
+
if not isinstance(data, dict):
|
|
753
|
+
return False
|
|
754
|
+
|
|
755
|
+
if not data.get("installed", False):
|
|
756
|
+
return False
|
|
757
|
+
|
|
758
|
+
# Age gate (advisory only).
|
|
759
|
+
when = data.get("when")
|
|
760
|
+
if isinstance(when, (int, float)):
|
|
761
|
+
age_s = max(0.0, time.time() - float(when))
|
|
762
|
+
if age_s > (max_age_days * 86400):
|
|
763
|
+
return False
|
|
764
|
+
|
|
765
|
+
# Marker python version should match the RUNTIME VENV python (not the app interpreter).
|
|
766
|
+
py = data.get("python")
|
|
767
|
+
if not (isinstance(py, str) and py.strip()):
|
|
768
|
+
return False
|
|
663
769
|
|
|
664
|
-
# If no marker, perform install under a lock
|
|
665
|
-
if not marker.exists():
|
|
666
770
|
try:
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
except Exception as e:
|
|
672
|
-
if _is_access_denied(e):
|
|
673
|
-
raise OSError(_access_denied_msg(rt)) from e
|
|
674
|
-
raise
|
|
771
|
+
maj_s, min_s = py.strip().split(".", 1)
|
|
772
|
+
marker_ver = (int(maj_s), int(min_s))
|
|
773
|
+
except Exception:
|
|
774
|
+
return False
|
|
675
775
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
776
|
+
# If we can't determine venv version, treat marker as unreliable for fast-path.
|
|
777
|
+
if venv_ver is None:
|
|
778
|
+
return False
|
|
779
|
+
|
|
780
|
+
if marker_ver != venv_ver:
|
|
781
|
+
return False
|
|
782
|
+
|
|
783
|
+
# Check that recorded files (if present) live under the computed site-packages path.
|
|
784
|
+
site_s = str(site)
|
|
785
|
+
tf = data.get("torch_file")
|
|
786
|
+
tvf = data.get("torchvision_file")
|
|
787
|
+
taf = data.get("torchaudio_file")
|
|
788
|
+
|
|
789
|
+
def _under_site(p: str | None) -> bool:
|
|
790
|
+
if not p or not isinstance(p, str):
|
|
791
|
+
return False
|
|
792
|
+
return site_s in p
|
|
793
|
+
|
|
794
|
+
if not _under_site(tf):
|
|
795
|
+
return False
|
|
796
|
+
if not _under_site(tvf):
|
|
797
|
+
return False
|
|
798
|
+
if require_torchaudio and not _under_site(taf):
|
|
799
|
+
return False
|
|
800
|
+
|
|
801
|
+
return True
|
|
802
|
+
except Exception:
|
|
803
|
+
return False
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _qt_settings():
|
|
807
|
+
"""
|
|
808
|
+
Create QSettings without importing PyQt6 at module import time.
|
|
809
|
+
We only import it inside the function so runtime_torch stays usable
|
|
810
|
+
in non-GUI contexts.
|
|
811
|
+
"""
|
|
812
|
+
try:
|
|
813
|
+
from PyQt6.QtCore import QSettings
|
|
814
|
+
# Must match what your app sets via QCoreApplication.setOrganizationName / setApplicationName
|
|
815
|
+
return QSettings()
|
|
816
|
+
except Exception:
|
|
817
|
+
return None
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _qcache_get():
|
|
821
|
+
s = _qt_settings()
|
|
822
|
+
if not s:
|
|
823
|
+
return None
|
|
824
|
+
s.beginGroup("runtime_torch")
|
|
825
|
+
data = {
|
|
826
|
+
"tag": s.value("tag", "", str),
|
|
827
|
+
"rt_dir": s.value("rt_dir", "", str),
|
|
828
|
+
"site": s.value("site", "", str),
|
|
829
|
+
"python": s.value("python", "", str),
|
|
830
|
+
"torch": s.value("torch", "", str),
|
|
831
|
+
"torchvision": s.value("torchvision", "", str),
|
|
832
|
+
"torchaudio": s.value("torchaudio", "", str),
|
|
833
|
+
"when": s.value("when", 0, int),
|
|
834
|
+
"require_torchaudio": s.value("require_torchaudio", True, bool),
|
|
835
|
+
}
|
|
836
|
+
s.endGroup()
|
|
837
|
+
return data
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _qcache_set(*, tag: str, rt_dir: Path, site: Path, python_ver: str | None,
|
|
841
|
+
torch_ver: str | None, tv_ver: str | None, ta_ver: str | None,
|
|
842
|
+
require_torchaudio: bool):
|
|
843
|
+
s = _qt_settings()
|
|
844
|
+
if not s:
|
|
845
|
+
return
|
|
846
|
+
s.beginGroup("runtime_torch")
|
|
847
|
+
s.setValue("tag", tag)
|
|
848
|
+
s.setValue("rt_dir", str(rt_dir))
|
|
849
|
+
s.setValue("site", str(site))
|
|
850
|
+
s.setValue("python", python_ver or "")
|
|
851
|
+
s.setValue("torch", torch_ver or "")
|
|
852
|
+
s.setValue("torchvision", tv_ver or "")
|
|
853
|
+
s.setValue("torchaudio", ta_ver or "")
|
|
854
|
+
s.setValue("when", int(time.time()))
|
|
855
|
+
s.setValue("require_torchaudio", bool(require_torchaudio))
|
|
856
|
+
s.endGroup()
|
|
857
|
+
s.sync()
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def _qcache_clear():
|
|
861
|
+
s = _qt_settings()
|
|
862
|
+
if not s:
|
|
863
|
+
return
|
|
864
|
+
s.beginGroup("runtime_torch")
|
|
865
|
+
s.remove("") # remove all keys in group
|
|
866
|
+
s.endGroup()
|
|
867
|
+
s.sync()
|
|
680
868
|
|
|
681
|
-
|
|
682
|
-
|
|
869
|
+
|
|
870
|
+
# module-level cache (optional but recommended)
|
|
871
|
+
# module-level cache (optional but recommended)
|
|
872
|
+
_TORCH_CACHED = None
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def import_torch(
|
|
876
|
+
prefer_cuda: bool = True,
|
|
877
|
+
prefer_xpu: bool = False,
|
|
878
|
+
prefer_dml: bool = False,
|
|
879
|
+
status_cb=print,
|
|
880
|
+
*,
|
|
881
|
+
require_torchaudio: bool = True,
|
|
882
|
+
):
|
|
883
|
+
"""
|
|
884
|
+
Ensure a per-user venv exists with torch installed; return the imported torch module.
|
|
885
|
+
|
|
886
|
+
ULTRA FAST PATH:
|
|
887
|
+
- Use QSettings cached site-packages (no subprocess at all) and attempt in-process import.
|
|
888
|
+
|
|
889
|
+
FAST PATH:
|
|
890
|
+
- If marker looks valid, compute site-packages (1 subprocess) and try in-process imports.
|
|
891
|
+
- If that works, skip expensive subprocess probes.
|
|
892
|
+
|
|
893
|
+
SLOW PATH:
|
|
894
|
+
- Probe runtime venv via subprocess (torch/torchvision/torchaudio).
|
|
895
|
+
- Install only if missing, then re-probe.
|
|
896
|
+
- Finally import in-process from venv site-packages.
|
|
897
|
+
|
|
898
|
+
NEW RULES:
|
|
899
|
+
- Marker/QSettings are advisory only (fast path gates).
|
|
900
|
+
- If torch/torchvision(/torchaudio) exist in the runtime venv, USE THEM. Do nothing else.
|
|
901
|
+
- Only if missing in the runtime venv should we install.
|
|
902
|
+
- NEVER auto-uninstall user torch/torchvision/torchaudio. No automatic repair.
|
|
903
|
+
"""
|
|
904
|
+
global _TORCH_CACHED
|
|
905
|
+
if _TORCH_CACHED is not None:
|
|
906
|
+
return _TORCH_CACHED
|
|
907
|
+
|
|
908
|
+
def _write_qcache_best_effort(rt: Path, site: Path, venv_ver: tuple[int,int] | None):
|
|
909
|
+
"""
|
|
910
|
+
Write QSettings cache only after we have proven imports work in-process.
|
|
911
|
+
"""
|
|
683
912
|
try:
|
|
684
|
-
|
|
913
|
+
import torch as _t # noqa
|
|
914
|
+
import torchvision as _tv # noqa
|
|
915
|
+
_ta = None
|
|
916
|
+
if require_torchaudio:
|
|
917
|
+
import torchaudio as _ta # noqa
|
|
918
|
+
|
|
919
|
+
_qcache_set(
|
|
920
|
+
tag=rt.name, # IMPORTANT: runtime tag, not sys.version_info tag
|
|
921
|
+
rt_dir=rt,
|
|
922
|
+
site=site,
|
|
923
|
+
python_ver=(f"{venv_ver[0]}.{venv_ver[1]}" if venv_ver else ""),
|
|
924
|
+
torch_ver=getattr(_t, "__version__", None),
|
|
925
|
+
tv_ver=getattr(_tv, "__version__", None),
|
|
926
|
+
ta_ver=getattr(_ta, "__version__", None) if _ta else None,
|
|
927
|
+
require_torchaudio=require_torchaudio,
|
|
928
|
+
)
|
|
685
929
|
except Exception:
|
|
686
930
|
pass
|
|
687
931
|
|
|
688
|
-
|
|
932
|
+
_rt_dbg(f"sys.frozen={getattr(sys,'frozen',False)}", status_cb)
|
|
933
|
+
_rt_dbg(f"sys.executable={sys.executable}", status_cb)
|
|
934
|
+
_rt_dbg(f"sys.version={sys.version}", status_cb)
|
|
935
|
+
_rt_dbg(f"current_tag={_current_tag()}", status_cb)
|
|
936
|
+
_rt_dbg(f"SASPRO_RUNTIME_DIR={os.getenv('SASPRO_RUNTIME_DIR')!r}", status_cb)
|
|
937
|
+
|
|
938
|
+
# Remove obvious shadowing paths (repo folders / cwd torch trees)
|
|
939
|
+
_ban_shadow_torch_paths(status_cb=status_cb)
|
|
940
|
+
_purge_bad_torch_from_sysmodules(status_cb=status_cb)
|
|
941
|
+
|
|
942
|
+
# ------------------------------------------------------------
|
|
943
|
+
# Choose runtime + ensure venv exists
|
|
944
|
+
# ------------------------------------------------------------
|
|
945
|
+
rt = _user_runtime_dir(status_cb=status_cb)
|
|
946
|
+
vp = _ensure_venv(rt, status_cb=status_cb)
|
|
947
|
+
|
|
948
|
+
# ------------------------------------------------------------
|
|
949
|
+
# ULTRA FAST PATH (runtime-aware): QSettings cache.
|
|
950
|
+
# Now we can compare the cache tag against the RUNTIME tag, not sys.version_info.
|
|
951
|
+
# This stays correct for "app python != runtime venv python" cases.
|
|
952
|
+
# ------------------------------------------------------------
|
|
953
|
+
try:
|
|
954
|
+
qc = _qcache_get()
|
|
955
|
+
if qc:
|
|
956
|
+
site_s = (qc.get("site") or "").strip()
|
|
957
|
+
rt_s = (qc.get("rt_dir") or "").strip()
|
|
958
|
+
req_ta = bool(qc.get("require_torchaudio", True))
|
|
959
|
+
tag = (qc.get("tag") or "").strip()
|
|
960
|
+
|
|
961
|
+
# Accept cache only if it matches this runtime folder tag
|
|
962
|
+
if (
|
|
963
|
+
tag == rt.name
|
|
964
|
+
and site_s and Path(site_s).exists()
|
|
965
|
+
and rt_s and Path(rt_s).exists()
|
|
966
|
+
and (req_ta == require_torchaudio)
|
|
967
|
+
):
|
|
968
|
+
status_cb("[RT] QSettings cache hit (runtime tag match); attempting zero-subprocess import.")
|
|
969
|
+
|
|
970
|
+
if site_s not in sys.path:
|
|
971
|
+
sys.path.insert(0, site_s)
|
|
972
|
+
|
|
973
|
+
_demote_shadow_torch_paths(status_cb=status_cb)
|
|
974
|
+
_purge_bad_torch_from_sysmodules(status_cb=status_cb)
|
|
975
|
+
|
|
976
|
+
import torch # noqa
|
|
977
|
+
import torchvision # noqa
|
|
978
|
+
if require_torchaudio:
|
|
979
|
+
import torchaudio # noqa
|
|
980
|
+
|
|
981
|
+
_TORCH_CACHED = torch
|
|
982
|
+
|
|
983
|
+
return torch
|
|
984
|
+
|
|
985
|
+
except Exception as e:
|
|
986
|
+
status_cb(f"[RT] QSettings fast-path failed: {type(e).__name__}: {e}. Continuing…")
|
|
689
987
|
try:
|
|
690
|
-
|
|
988
|
+
_qcache_clear()
|
|
691
989
|
except Exception:
|
|
692
990
|
pass
|
|
693
991
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
_install_torch(
|
|
699
|
-
vp,
|
|
700
|
-
prefer_cuda=prefer_cuda,
|
|
701
|
-
prefer_xpu=prefer_xpu,
|
|
702
|
-
prefer_dml=prefer_dml,
|
|
703
|
-
status_cb=status_cb,
|
|
704
|
-
)
|
|
705
|
-
importlib.invalidate_caches()
|
|
706
|
-
_demote_shadow_torch_paths(status_cb=status_cb)
|
|
992
|
+
# site-packages path (subprocess but relatively cheap)
|
|
993
|
+
site = _site_packages(vp)
|
|
994
|
+
marker = rt / "torch_installed.json"
|
|
995
|
+
venv_ver = _venv_pyver(vp)
|
|
707
996
|
|
|
997
|
+
_rt_dbg(f"venv_ver={venv_ver}", status_cb)
|
|
998
|
+
_rt_dbg(f"rt={rt}", status_cb)
|
|
999
|
+
_rt_dbg(f"venv_python={vp}", status_cb)
|
|
1000
|
+
_rt_dbg(f"marker={marker} exists={marker.exists()}", status_cb)
|
|
1001
|
+
_rt_dbg(f"site={site}", status_cb)
|
|
708
1002
|
|
|
1003
|
+
# Best-effort ensure numpy in venv (harmless if already there)
|
|
709
1004
|
try:
|
|
710
1005
|
_ensure_numpy(vp, status_cb=status_cb)
|
|
711
1006
|
except Exception:
|
|
712
1007
|
pass
|
|
713
1008
|
|
|
1009
|
+
# ------------------------------------------------------------
|
|
1010
|
+
# FAST PATH: if marker looks valid, try in-process import NOW.
|
|
1011
|
+
# This avoids the 3 subprocess probes on every launch.
|
|
1012
|
+
# ------------------------------------------------------------
|
|
714
1013
|
try:
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1014
|
+
if _marker_says_ready(marker, site, venv_ver, require_torchaudio=require_torchaudio):
|
|
1015
|
+
status_cb("[RT] Marker valid; attempting fast in-process import (skipping venv probe).")
|
|
1016
|
+
|
|
1017
|
+
sp = str(site)
|
|
1018
|
+
if sp not in sys.path:
|
|
1019
|
+
sys.path.insert(0, sp)
|
|
1020
|
+
|
|
1021
|
+
_demote_shadow_torch_paths(status_cb=status_cb)
|
|
1022
|
+
_purge_bad_torch_from_sysmodules(status_cb=status_cb)
|
|
1023
|
+
|
|
1024
|
+
import torch # noqa
|
|
1025
|
+
import torchvision # noqa
|
|
1026
|
+
if require_torchaudio:
|
|
1027
|
+
import torchaudio # noqa
|
|
1028
|
+
|
|
1029
|
+
# refresh marker (best-effort)
|
|
720
1030
|
try:
|
|
721
|
-
|
|
722
|
-
marker.write_text(json.dumps({
|
|
723
|
-
"installed": True,
|
|
724
|
-
"python": pyver,
|
|
725
|
-
"when": int(time.time()),
|
|
726
|
-
"torch": getattr(torch, "__version__", None),
|
|
727
|
-
"torchvision": getattr(torchvision, "__version__", None),
|
|
728
|
-
"torchaudio": getattr(torchaudio, "__version__", None),
|
|
729
|
-
}), encoding="utf-8")
|
|
1031
|
+
_write_torch_marker(marker, status_cb=status_cb)
|
|
730
1032
|
except Exception:
|
|
731
|
-
|
|
1033
|
+
pass
|
|
732
1034
|
|
|
733
|
-
|
|
1035
|
+
_TORCH_CACHED = torch
|
|
1036
|
+
_write_qcache_best_effort(rt, site, venv_ver)
|
|
1037
|
+
return torch
|
|
1038
|
+
|
|
1039
|
+
except Exception as e:
|
|
1040
|
+
status_cb(f"[RT] Marker fast-path failed: {type(e).__name__}: {e}. Falling back to full probe…")
|
|
1041
|
+
# if marker fast path fails, your cached site-packages may also be stale
|
|
1042
|
+
try:
|
|
1043
|
+
_qcache_clear()
|
|
1044
|
+
except Exception:
|
|
1045
|
+
pass
|
|
1046
|
+
|
|
1047
|
+
# ------------------------------------------------------------
|
|
1048
|
+
# SLOW PATH: Probe the runtime venv definitively.
|
|
1049
|
+
# If it has torch stack, we're DONE (no installs, no repair).
|
|
1050
|
+
# ------------------------------------------------------------
|
|
1051
|
+
ok_all, info = _venv_has_torch_stack(vp, status_cb=status_cb, require_torchaudio=require_torchaudio)
|
|
1052
|
+
status_cb(
|
|
1053
|
+
"[RT] venv probe: "
|
|
1054
|
+
f"torch={info['torch'][0]} "
|
|
1055
|
+
f"torchvision={info['torchvision'][0]} "
|
|
1056
|
+
f"torchaudio={info['torchaudio'][0]}"
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
if not ok_all:
|
|
1060
|
+
missing = []
|
|
1061
|
+
if not info["torch"][0]:
|
|
1062
|
+
missing.append("torch")
|
|
1063
|
+
if not info["torchvision"][0]:
|
|
1064
|
+
missing.append("torchvision")
|
|
1065
|
+
if require_torchaudio and (not info["torchaudio"][0]):
|
|
1066
|
+
missing.append("torchaudio")
|
|
1067
|
+
|
|
1068
|
+
status_cb(f"[RT] Missing in runtime venv: {missing}. Installing…")
|
|
1069
|
+
|
|
1070
|
+
try:
|
|
1071
|
+
with _install_lock(rt):
|
|
1072
|
+
_install_torch(
|
|
1073
|
+
vp,
|
|
1074
|
+
prefer_cuda=prefer_cuda,
|
|
1075
|
+
prefer_xpu=prefer_xpu,
|
|
1076
|
+
prefer_dml=prefer_dml,
|
|
1077
|
+
status_cb=status_cb,
|
|
1078
|
+
)
|
|
1079
|
+
except Exception as e:
|
|
1080
|
+
if _is_access_denied(e):
|
|
1081
|
+
raise OSError(_access_denied_msg(rt)) from e
|
|
1082
|
+
raise
|
|
1083
|
+
|
|
1084
|
+
# Re-probe after install
|
|
1085
|
+
ok_all, info = _venv_has_torch_stack(vp, status_cb=status_cb, require_torchaudio=require_torchaudio)
|
|
1086
|
+
status_cb(
|
|
1087
|
+
"[RT] venv re-probe: "
|
|
1088
|
+
f"torch={info['torch'][0]} "
|
|
1089
|
+
f"torchvision={info['torchvision'][0]} "
|
|
1090
|
+
f"torchaudio={info['torchaudio'][0]}"
|
|
1091
|
+
)
|
|
1092
|
+
if not ok_all:
|
|
1093
|
+
msg = "\n".join([f"{k}: ok={ok} :: {out}" for k, (ok, out) in info.items()])
|
|
1094
|
+
raise RuntimeError("Torch stack still not importable in runtime venv after install:\n" + msg)
|
|
1095
|
+
|
|
1096
|
+
# Always write/update marker for convenience, but never trust it for decisions.
|
|
1097
|
+
try:
|
|
1098
|
+
_write_torch_marker(marker, status_cb=status_cb)
|
|
734
1099
|
except Exception:
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
import torch, torchvision, torchaudio
|
|
744
|
-
marker.write_text(json.dumps({
|
|
745
|
-
"installed": True,
|
|
746
|
-
"python": pyver,
|
|
747
|
-
"when": int(time.time()),
|
|
748
|
-
"torch": getattr(torch, "__version__", None),
|
|
749
|
-
"torchvision": getattr(torchvision, "__version__", None),
|
|
750
|
-
"torchaudio": getattr(torchaudio, "__version__", None),
|
|
751
|
-
}), encoding="utf-8")
|
|
752
|
-
except Exception:
|
|
753
|
-
marker.write_text(json.dumps({"installed": True, "python": pyver, "when": int(time.time())}), encoding="utf-8")
|
|
1100
|
+
pass
|
|
1101
|
+
|
|
1102
|
+
# ------------------------------------------------------------
|
|
1103
|
+
# Now import torch in-process, but ONLY after putting runtime site first.
|
|
1104
|
+
# ------------------------------------------------------------
|
|
1105
|
+
sp = str(site)
|
|
1106
|
+
if sp not in sys.path:
|
|
1107
|
+
sys.path.insert(0, sp)
|
|
754
1108
|
|
|
1109
|
+
_demote_shadow_torch_paths(status_cb=status_cb)
|
|
1110
|
+
_purge_bad_torch_from_sysmodules(status_cb=status_cb)
|
|
1111
|
+
|
|
1112
|
+
try:
|
|
1113
|
+
import torch # noqa
|
|
1114
|
+
|
|
1115
|
+
_TORCH_CACHED = torch
|
|
1116
|
+
_write_qcache_best_effort(rt, site, venv_ver)
|
|
755
1117
|
return torch
|
|
756
1118
|
|
|
1119
|
+
except Exception as e:
|
|
1120
|
+
# prevent repeatedly hitting a bad cached site path on next launch
|
|
1121
|
+
try:
|
|
1122
|
+
_qcache_clear()
|
|
1123
|
+
except Exception:
|
|
1124
|
+
pass
|
|
1125
|
+
|
|
1126
|
+
msg = "\n".join([f"{k}: ok={ok} :: {out}" for k, (ok, out) in info.items()])
|
|
1127
|
+
raise RuntimeError(
|
|
1128
|
+
"Runtime venv probe says torch stack exists, but in-process import failed.\n"
|
|
1129
|
+
"This typically indicates a frozen-stdlib / PyInstaller packaging issue, not a bad torch install.\n\n"
|
|
1130
|
+
f"Original error: {type(e).__name__}: {e}\n\n"
|
|
1131
|
+
"Runtime venv probe:\n" + msg
|
|
1132
|
+
) from e
|
|
1133
|
+
|
|
1134
|
+
|
|
757
1135
|
def _find_system_python_cmd() -> list[str]:
|
|
758
1136
|
import platform as _plat
|
|
759
1137
|
if _plat.system() == "Darwin":
|
|
@@ -814,7 +1192,7 @@ def add_runtime_to_sys_path(status_cb=print) -> None:
|
|
|
814
1192
|
"""
|
|
815
1193
|
Warm up sys.path so a fresh launch can see the runtime immediately.
|
|
816
1194
|
"""
|
|
817
|
-
rt = _user_runtime_dir()
|
|
1195
|
+
rt = _user_runtime_dir(status_cb=status_cb)
|
|
818
1196
|
p = _venv_paths(rt)
|
|
819
1197
|
vpy = p["python"]
|
|
820
1198
|
if not vpy.exists():
|
|
@@ -837,3 +1215,69 @@ def add_runtime_to_sys_path(status_cb=print) -> None:
|
|
|
837
1215
|
_demote_shadow_torch_paths(status_cb=status_cb)
|
|
838
1216
|
except Exception:
|
|
839
1217
|
return
|
|
1218
|
+
|
|
1219
|
+
def prewarm_torch_cache(
|
|
1220
|
+
status_cb=print,
|
|
1221
|
+
*,
|
|
1222
|
+
require_torchaudio: bool = True,
|
|
1223
|
+
ensure_venv: bool = True,
|
|
1224
|
+
ensure_numpy: bool = False,
|
|
1225
|
+
validate_marker: bool = True,
|
|
1226
|
+
) -> None:
|
|
1227
|
+
"""
|
|
1228
|
+
Build and persist the QSettings cache early (startup), so the first real
|
|
1229
|
+
import_torch() call can be zero-subprocess.
|
|
1230
|
+
|
|
1231
|
+
By default this does NOT import torch (keeps startup lighter).
|
|
1232
|
+
It only computes runtime rt/vpy/site and writes QSettings.
|
|
1233
|
+
"""
|
|
1234
|
+
try:
|
|
1235
|
+
_ban_shadow_torch_paths(status_cb=status_cb)
|
|
1236
|
+
_purge_bad_torch_from_sysmodules(status_cb=status_cb)
|
|
1237
|
+
|
|
1238
|
+
rt = _user_runtime_dir(status_cb=status_cb)
|
|
1239
|
+
p = _venv_paths(rt)
|
|
1240
|
+
vp = p["python"]
|
|
1241
|
+
|
|
1242
|
+
if ensure_venv:
|
|
1243
|
+
vp = _ensure_venv(rt, status_cb=status_cb)
|
|
1244
|
+
|
|
1245
|
+
if not vp.exists():
|
|
1246
|
+
return
|
|
1247
|
+
|
|
1248
|
+
if ensure_numpy:
|
|
1249
|
+
try:
|
|
1250
|
+
_ensure_numpy(vp, status_cb=status_cb)
|
|
1251
|
+
except Exception:
|
|
1252
|
+
pass
|
|
1253
|
+
|
|
1254
|
+
site = _site_packages(vp)
|
|
1255
|
+
marker = rt / "torch_installed.json"
|
|
1256
|
+
venv_ver = _venv_pyver(vp)
|
|
1257
|
+
|
|
1258
|
+
# Optionally only cache if marker looks valid (recommended),
|
|
1259
|
+
# otherwise you may cache a site-packages that doesn't actually contain torch yet.
|
|
1260
|
+
if validate_marker:
|
|
1261
|
+
if not _marker_says_ready(marker, site, venv_ver, require_torchaudio=require_torchaudio):
|
|
1262
|
+
status_cb("[RT] prewarm: marker not valid; skipping QSettings cache write.")
|
|
1263
|
+
return
|
|
1264
|
+
|
|
1265
|
+
# IMPORTANT: use runtime tag, not app interpreter tag, for mixed-version scenarios
|
|
1266
|
+
cache_tag = rt.name # e.g. "py312"
|
|
1267
|
+
|
|
1268
|
+
_qcache_set(
|
|
1269
|
+
tag=cache_tag,
|
|
1270
|
+
rt_dir=rt,
|
|
1271
|
+
site=site,
|
|
1272
|
+
python_ver=(f"{venv_ver[0]}.{venv_ver[1]}" if venv_ver else ""),
|
|
1273
|
+
torch_ver=None,
|
|
1274
|
+
tv_ver=None,
|
|
1275
|
+
ta_ver=None,
|
|
1276
|
+
require_torchaudio=require_torchaudio,
|
|
1277
|
+
)
|
|
1278
|
+
status_cb("[RT] prewarm: QSettings cache written.")
|
|
1279
|
+
except Exception as e:
|
|
1280
|
+
try:
|
|
1281
|
+
status_cb(f"[RT] prewarm failed: {type(e).__name__}: {e}")
|
|
1282
|
+
except Exception:
|
|
1283
|
+
pass
|