computefence 0.2.11__tar.gz → 0.2.13__tar.gz
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.
- {computefence-0.2.11 → computefence-0.2.13}/PKG-INFO +1 -1
- computefence-0.2.13/computefence/__init__.py +1 -0
- {computefence-0.2.11 → computefence-0.2.13}/computefence/cli.py +36 -1
- computefence-0.2.13/computefence/drift.py +204 -0
- {computefence-0.2.11 → computefence-0.2.13}/computefence/telemetry.py +56 -2
- {computefence-0.2.11 → computefence-0.2.13}/computefence.egg-info/PKG-INFO +1 -1
- {computefence-0.2.11 → computefence-0.2.13}/computefence.egg-info/SOURCES.txt +1 -0
- computefence-0.2.11/computefence/__init__.py +0 -1
- {computefence-0.2.11 → computefence-0.2.13}/LICENSE +0 -0
- {computefence-0.2.11 → computefence-0.2.13}/README.md +0 -0
- {computefence-0.2.11 → computefence-0.2.13}/computefence/checks/__init__.py +0 -0
- {computefence-0.2.11 → computefence-0.2.13}/computefence/checks/dataset.py +0 -0
- {computefence-0.2.11 → computefence-0.2.13}/computefence/checks/environment.py +0 -0
- {computefence-0.2.11 → computefence-0.2.13}/computefence/checks/storage.py +0 -0
- {computefence-0.2.11 → computefence-0.2.13}/computefence.egg-info/dependency_links.txt +0 -0
- {computefence-0.2.11 → computefence-0.2.13}/computefence.egg-info/entry_points.txt +0 -0
- {computefence-0.2.11 → computefence-0.2.13}/computefence.egg-info/requires.txt +0 -0
- {computefence-0.2.11 → computefence-0.2.13}/computefence.egg-info/top_level.txt +0 -0
- {computefence-0.2.11 → computefence-0.2.13}/pyproject.toml +0 -0
- {computefence-0.2.11 → computefence-0.2.13}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: computefence
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.13
|
|
4
4
|
Summary: Don't lose another RunPod checkpoint to ephemeral disk before your job bills.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Keywords: runpod,vast.ai,gpu training,hf_home,cuda,preflight,machine learning,mlops,checkpoint
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.13"
|
|
@@ -5,6 +5,7 @@ from computefence.checks.environment import check_environment
|
|
|
5
5
|
from computefence.checks.storage import check_disk_headroom, check_output_dir, check_storage
|
|
6
6
|
from computefence.checks.dataset import check_dataset
|
|
7
7
|
from computefence.telemetry import record_run
|
|
8
|
+
from computefence.drift import save_env, check_drift
|
|
8
9
|
|
|
9
10
|
console = Console()
|
|
10
11
|
|
|
@@ -28,6 +29,10 @@ def doctor():
|
|
|
28
29
|
input_column = None
|
|
29
30
|
label_column = None
|
|
30
31
|
output_dir = None
|
|
32
|
+
save_env_flag = False
|
|
33
|
+
check_drift_flag = False
|
|
34
|
+
path_override = None
|
|
35
|
+
|
|
31
36
|
args = sys.argv[1:]
|
|
32
37
|
i = 0
|
|
33
38
|
while i < len(args):
|
|
@@ -43,6 +48,15 @@ def doctor():
|
|
|
43
48
|
elif args[i] == "--output-dir" and i + 1 < len(args):
|
|
44
49
|
output_dir = args[i + 1]
|
|
45
50
|
i += 2
|
|
51
|
+
elif args[i] == "--save-env":
|
|
52
|
+
save_env_flag = True
|
|
53
|
+
i += 1
|
|
54
|
+
elif args[i] == "--check-drift":
|
|
55
|
+
check_drift_flag = True
|
|
56
|
+
i += 1
|
|
57
|
+
elif args[i] == "--path" and i + 1 < len(args):
|
|
58
|
+
path_override = args[i + 1]
|
|
59
|
+
i += 2
|
|
46
60
|
else:
|
|
47
61
|
i += 1
|
|
48
62
|
|
|
@@ -82,6 +96,15 @@ def doctor():
|
|
|
82
96
|
console.print("[bold blue]Dataset[/bold blue]")
|
|
83
97
|
for result in dataset_results:
|
|
84
98
|
print_result(result)
|
|
99
|
+
|
|
100
|
+
if check_drift_flag:
|
|
101
|
+
drift_results = check_drift(output_dir=output_dir, path_override=path_override)
|
|
102
|
+
console.print()
|
|
103
|
+
console.print("[bold blue]Drift[/bold blue]")
|
|
104
|
+
for result in drift_results:
|
|
105
|
+
print_result(result)
|
|
106
|
+
all_results = all_results + drift_results
|
|
107
|
+
|
|
85
108
|
console.print()
|
|
86
109
|
console.print("━" * 50)
|
|
87
110
|
if failures:
|
|
@@ -97,6 +120,18 @@ def doctor():
|
|
|
97
120
|
)
|
|
98
121
|
console.print()
|
|
99
122
|
|
|
123
|
+
if save_env_flag:
|
|
124
|
+
if failures:
|
|
125
|
+
console.print("[yellow]⚠ --save-env skipped: blockers present. Fix errors first.[/yellow]")
|
|
126
|
+
console.print()
|
|
127
|
+
else:
|
|
128
|
+
success, msg = save_env(output_dir=output_dir, path_override=path_override)
|
|
129
|
+
if success:
|
|
130
|
+
console.print(f"[green]✓ Environment fingerprint saved to {msg}[/green]")
|
|
131
|
+
else:
|
|
132
|
+
console.print(f"[yellow]⚠ Could not save fingerprint: {msg}[/yellow]")
|
|
133
|
+
console.print()
|
|
134
|
+
|
|
100
135
|
record_run(all_results)
|
|
101
136
|
|
|
102
137
|
def main():
|
|
@@ -108,4 +143,4 @@ def main():
|
|
|
108
143
|
console.print("Usage: computefence doctor")
|
|
109
144
|
|
|
110
145
|
if __name__ == "__main__":
|
|
111
|
-
main()
|
|
146
|
+
main()
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Drift detection for ComputeFence v0.2.13.
|
|
3
|
+
|
|
4
|
+
--save-env : capture a 4-vector environment fingerprint after a clean run
|
|
5
|
+
and write it to .computefence-lock.json in the persistent volume.
|
|
6
|
+
--check-drift : compare the current environment against a saved fingerprint
|
|
7
|
+
and report differences as warnings (never blockers).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
LOCK_FILENAME = ".computefence-lock.json"
|
|
16
|
+
|
|
17
|
+
PERSISTENT_VOLUME_CANDIDATES = [
|
|
18
|
+
"/workspace",
|
|
19
|
+
"/mnt/storage",
|
|
20
|
+
"/runpod-volume",
|
|
21
|
+
"/vast",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _find_persistent_volume(path_override=None):
|
|
26
|
+
if path_override:
|
|
27
|
+
p = Path(path_override)
|
|
28
|
+
if p.exists():
|
|
29
|
+
return p
|
|
30
|
+
return None
|
|
31
|
+
for candidate in PERSISTENT_VOLUME_CANDIDATES:
|
|
32
|
+
p = Path(candidate)
|
|
33
|
+
if p.exists() and os.access(candidate, os.W_OK):
|
|
34
|
+
return p
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_lock_path(path_override=None):
|
|
39
|
+
vol = _find_persistent_volume(path_override)
|
|
40
|
+
if vol is None:
|
|
41
|
+
return None
|
|
42
|
+
return vol / LOCK_FILENAME
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _vector_torch():
|
|
46
|
+
try:
|
|
47
|
+
import torch
|
|
48
|
+
return {
|
|
49
|
+
"torch_version": torch.__version__,
|
|
50
|
+
"cuda_version": torch.version.cuda or "none",
|
|
51
|
+
}
|
|
52
|
+
except ImportError:
|
|
53
|
+
return {"torch_version": "not_installed", "cuda_version": "none"}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _vector_storage(output_dir=None):
|
|
57
|
+
result = {}
|
|
58
|
+
hf_home = os.environ.get("HF_HOME", "")
|
|
59
|
+
result["hf_home"] = hf_home if hf_home else "unset"
|
|
60
|
+
result["hf_home_persistent"] = any(
|
|
61
|
+
hf_home.startswith(c) for c in PERSISTENT_VOLUME_CANDIDATES
|
|
62
|
+
) if hf_home else False
|
|
63
|
+
result["output_dir"] = output_dir or "not_specified"
|
|
64
|
+
result["output_dir_persistent"] = any(
|
|
65
|
+
(output_dir or "").startswith(c) for c in PERSISTENT_VOLUME_CANDIDATES
|
|
66
|
+
)
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _vector_gpu():
|
|
71
|
+
try:
|
|
72
|
+
import torch
|
|
73
|
+
count = torch.cuda.device_count()
|
|
74
|
+
caps = []
|
|
75
|
+
for i in range(count):
|
|
76
|
+
props = torch.cuda.get_device_properties(i)
|
|
77
|
+
caps.append(f"{props.major}.{props.minor}")
|
|
78
|
+
return {"gpu_count": count, "compute_capabilities": caps}
|
|
79
|
+
except Exception:
|
|
80
|
+
return {"gpu_count": 0, "compute_capabilities": []}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _vector_libraries():
|
|
84
|
+
libs = {}
|
|
85
|
+
for lib in ["flash_attn", "transformers", "accelerate", "bitsandbytes"]:
|
|
86
|
+
try:
|
|
87
|
+
mod = __import__(lib)
|
|
88
|
+
libs[lib] = getattr(mod, "__version__", "unknown")
|
|
89
|
+
except ImportError:
|
|
90
|
+
libs[lib] = "not_installed"
|
|
91
|
+
return libs
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def capture_fingerprint(output_dir=None):
|
|
95
|
+
return {
|
|
96
|
+
"schema_version": 1,
|
|
97
|
+
"torch": _vector_torch(),
|
|
98
|
+
"storage": _vector_storage(output_dir),
|
|
99
|
+
"gpu": _vector_gpu(),
|
|
100
|
+
"libraries": _vector_libraries(),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def save_env(output_dir=None, path_override=None):
|
|
105
|
+
lock_path = _get_lock_path(path_override)
|
|
106
|
+
if lock_path is None:
|
|
107
|
+
return False, (
|
|
108
|
+
"No persistent volume found. Cannot save environment fingerprint. "
|
|
109
|
+
"Use --path /your/persistent/dir to specify one."
|
|
110
|
+
)
|
|
111
|
+
fingerprint = capture_fingerprint(output_dir)
|
|
112
|
+
try:
|
|
113
|
+
with open(lock_path, "w") as f:
|
|
114
|
+
json.dump(fingerprint, f, indent=2)
|
|
115
|
+
return True, str(lock_path)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
return False, f"Failed to write {lock_path}: {e}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _diff_vectors(saved, current):
|
|
121
|
+
results = []
|
|
122
|
+
|
|
123
|
+
s_torch = saved.get("torch", {})
|
|
124
|
+
c_torch = current.get("torch", {})
|
|
125
|
+
if s_torch.get("torch_version") != c_torch.get("torch_version"):
|
|
126
|
+
results.append({
|
|
127
|
+
"status": "warn",
|
|
128
|
+
"message": f"[Drift] PyTorch version changed: {s_torch.get('torch_version')} → {c_torch.get('torch_version')}",
|
|
129
|
+
"fix": "Pin torch version in your requirements or Dockerfile.",
|
|
130
|
+
})
|
|
131
|
+
if s_torch.get("cuda_version") != c_torch.get("cuda_version"):
|
|
132
|
+
results.append({
|
|
133
|
+
"status": "warn",
|
|
134
|
+
"message": f"[Drift] CUDA version changed: {s_torch.get('cuda_version')} → {c_torch.get('cuda_version')}",
|
|
135
|
+
"fix": "Ensure your pod template uses the same CUDA base image.",
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
s_stor = saved.get("storage", {})
|
|
139
|
+
c_stor = current.get("storage", {})
|
|
140
|
+
if s_stor.get("hf_home") != c_stor.get("hf_home"):
|
|
141
|
+
results.append({
|
|
142
|
+
"status": "warn",
|
|
143
|
+
"message": f"[Drift] HF_HOME changed: {s_stor.get('hf_home')} → {c_stor.get('hf_home')}",
|
|
144
|
+
"fix": "Set HF_HOME consistently in your startup script.",
|
|
145
|
+
})
|
|
146
|
+
if s_stor.get("output_dir_persistent") and not c_stor.get("output_dir_persistent"):
|
|
147
|
+
results.append({
|
|
148
|
+
"status": "warn",
|
|
149
|
+
"message": "[Drift] Output directory was persistent before — now appears ephemeral.",
|
|
150
|
+
"fix": "Pass --output-dir pointing to /workspace or your persistent mount.",
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
s_gpu = saved.get("gpu", {})
|
|
154
|
+
c_gpu = current.get("gpu", {})
|
|
155
|
+
if s_gpu.get("gpu_count") != c_gpu.get("gpu_count"):
|
|
156
|
+
results.append({
|
|
157
|
+
"status": "warn",
|
|
158
|
+
"message": f"[Drift] GPU count changed: {s_gpu.get('gpu_count')} → {c_gpu.get('gpu_count')}",
|
|
159
|
+
"fix": "Check your pod configuration matches the saved environment.",
|
|
160
|
+
})
|
|
161
|
+
if s_gpu.get("compute_capabilities") != c_gpu.get("compute_capabilities"):
|
|
162
|
+
results.append({
|
|
163
|
+
"status": "warn",
|
|
164
|
+
"message": f"[Drift] GPU compute capability changed: {s_gpu.get('compute_capabilities')} → {c_gpu.get('compute_capabilities')}",
|
|
165
|
+
"fix": "Different GPU hardware detected. Verify model compatibility.",
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
s_libs = saved.get("libraries", {})
|
|
169
|
+
c_libs = current.get("libraries", {})
|
|
170
|
+
for lib in set(list(s_libs.keys()) + list(c_libs.keys())):
|
|
171
|
+
sv = s_libs.get(lib, "not_installed")
|
|
172
|
+
cv = c_libs.get(lib, "not_installed")
|
|
173
|
+
if sv != cv:
|
|
174
|
+
results.append({
|
|
175
|
+
"status": "warn",
|
|
176
|
+
"message": f"[Drift] {lib} version changed: {sv} → {cv}",
|
|
177
|
+
"fix": f"Pin {lib} version in your requirements.",
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
if not results:
|
|
181
|
+
results.append({
|
|
182
|
+
"status": "pass",
|
|
183
|
+
"message": "Environment matches saved fingerprint — no drift detected.",
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
return results
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def check_drift(output_dir=None, path_override=None):
|
|
190
|
+
lock_path = _get_lock_path(path_override)
|
|
191
|
+
if lock_path is None:
|
|
192
|
+
return [{"status": "warn", "message": "No persistent volume found.", "fix": "Use --path to specify your persistent directory."}]
|
|
193
|
+
|
|
194
|
+
if not lock_path.exists():
|
|
195
|
+
return [{"status": "warn", "message": f"No saved fingerprint at {lock_path}.", "fix": "Run: computefence doctor --save-env"}]
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
with open(lock_path) as f:
|
|
199
|
+
saved = json.load(f)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
return [{"status": "warn", "message": f"Could not read fingerprint: {e}", "fix": "Delete the file and re-run: computefence doctor --save-env"}]
|
|
202
|
+
|
|
203
|
+
current = capture_fingerprint(output_dir)
|
|
204
|
+
return _diff_vectors(saved, current)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import hashlib
|
|
2
2
|
import os
|
|
3
3
|
import platform
|
|
4
|
+
import subprocess
|
|
4
5
|
import sys
|
|
5
6
|
import threading
|
|
6
7
|
import uuid
|
|
@@ -16,6 +17,57 @@ def _is_opted_out():
|
|
|
16
17
|
return OPT_OUT_FILE.exists()
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
def get_project_id() -> str:
|
|
21
|
+
# Primary: git remote URL hash — survives pod recreation
|
|
22
|
+
try:
|
|
23
|
+
git_url = subprocess.check_output(
|
|
24
|
+
["git", "config", "--get", "remote.origin.url"],
|
|
25
|
+
stderr=subprocess.DEVNULL
|
|
26
|
+
).decode().strip()
|
|
27
|
+
if git_url:
|
|
28
|
+
return hashlib.sha256(git_url.encode()).hexdigest()[:16]
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
# Fallback: persistent volume token
|
|
33
|
+
token_path = "/workspace/.computefence_id"
|
|
34
|
+
try:
|
|
35
|
+
if os.path.exists(token_path):
|
|
36
|
+
with open(token_path) as f:
|
|
37
|
+
token = f.read().strip()
|
|
38
|
+
if token:
|
|
39
|
+
return token
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
# Last resort: ephemeral cwd hash
|
|
44
|
+
return "ephemeral-" + hashlib.sha256(os.getcwd().encode()).hexdigest()[:8]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_execution_mode() -> str:
|
|
48
|
+
if os.environ.get("GITHUB_ACTIONS"):
|
|
49
|
+
return "github-action"
|
|
50
|
+
if os.environ.get("CI"):
|
|
51
|
+
return "ci"
|
|
52
|
+
try:
|
|
53
|
+
ppid = os.getppid()
|
|
54
|
+
result = subprocess.run(
|
|
55
|
+
["ps", "-p", str(ppid), "-o", "comm="],
|
|
56
|
+
capture_output=True, text=True, timeout=2
|
|
57
|
+
)
|
|
58
|
+
parent = result.stdout.strip().lower()
|
|
59
|
+
if "bash" in parent or "sh" in parent:
|
|
60
|
+
cmdline_path = f"/proc/{ppid}/cmdline"
|
|
61
|
+
if os.path.exists(cmdline_path):
|
|
62
|
+
with open(cmdline_path, "rb") as f:
|
|
63
|
+
cmdline = f.read().decode(errors="replace").replace("\x00", " ")
|
|
64
|
+
if "on-start" in cmdline or "startup" in cmdline or "entrypoint" in cmdline:
|
|
65
|
+
return "on-start"
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
return "manual"
|
|
69
|
+
|
|
70
|
+
|
|
19
71
|
def _send(payload: dict):
|
|
20
72
|
try:
|
|
21
73
|
import requests
|
|
@@ -49,7 +101,6 @@ def record_run(results: list):
|
|
|
49
101
|
gpu_count = torch.cuda.device_count()
|
|
50
102
|
except Exception:
|
|
51
103
|
try:
|
|
52
|
-
import subprocess
|
|
53
104
|
result = subprocess.run(
|
|
54
105
|
["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
|
|
55
106
|
capture_output=True, text=True, timeout=5
|
|
@@ -69,8 +120,11 @@ def record_run(results: list):
|
|
|
69
120
|
"blocker_count": blocker_count,
|
|
70
121
|
"pass_count": pass_count,
|
|
71
122
|
"opted_in": True,
|
|
123
|
+
"project_id": get_project_id(),
|
|
124
|
+
"run_id": str(uuid.uuid4()),
|
|
125
|
+
"execution_mode": get_execution_mode(),
|
|
72
126
|
}
|
|
73
127
|
|
|
74
128
|
thread = threading.Thread(target=_send, args=(payload,), daemon=True)
|
|
75
129
|
thread.start()
|
|
76
|
-
thread.join(timeout=4)
|
|
130
|
+
thread.join(timeout=4)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: computefence
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.13
|
|
4
4
|
Summary: Don't lose another RunPod checkpoint to ephemeral disk before your job bills.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Keywords: runpod,vast.ai,gpu training,hf_home,cuda,preflight,machine learning,mlops,checkpoint
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.11"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|