computefence 0.2.12__tar.gz → 0.2.14__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.
Files changed (20) hide show
  1. {computefence-0.2.12 → computefence-0.2.14}/PKG-INFO +1 -1
  2. computefence-0.2.14/computefence/__init__.py +1 -0
  3. {computefence-0.2.12 → computefence-0.2.14}/computefence/cli.py +36 -1
  4. computefence-0.2.14/computefence/drift.py +204 -0
  5. {computefence-0.2.12 → computefence-0.2.14}/computefence/telemetry.py +6 -1
  6. {computefence-0.2.12 → computefence-0.2.14}/computefence.egg-info/PKG-INFO +1 -1
  7. {computefence-0.2.12 → computefence-0.2.14}/computefence.egg-info/SOURCES.txt +1 -0
  8. computefence-0.2.12/computefence/__init__.py +0 -1
  9. {computefence-0.2.12 → computefence-0.2.14}/LICENSE +0 -0
  10. {computefence-0.2.12 → computefence-0.2.14}/README.md +0 -0
  11. {computefence-0.2.12 → computefence-0.2.14}/computefence/checks/__init__.py +0 -0
  12. {computefence-0.2.12 → computefence-0.2.14}/computefence/checks/dataset.py +0 -0
  13. {computefence-0.2.12 → computefence-0.2.14}/computefence/checks/environment.py +0 -0
  14. {computefence-0.2.12 → computefence-0.2.14}/computefence/checks/storage.py +0 -0
  15. {computefence-0.2.12 → computefence-0.2.14}/computefence.egg-info/dependency_links.txt +0 -0
  16. {computefence-0.2.12 → computefence-0.2.14}/computefence.egg-info/entry_points.txt +0 -0
  17. {computefence-0.2.12 → computefence-0.2.14}/computefence.egg-info/requires.txt +0 -0
  18. {computefence-0.2.12 → computefence-0.2.14}/computefence.egg-info/top_level.txt +0 -0
  19. {computefence-0.2.12 → computefence-0.2.14}/pyproject.toml +0 -0
  20. {computefence-0.2.12 → computefence-0.2.14}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: computefence
3
- Version: 0.2.12
3
+ Version: 0.2.14
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.14"
@@ -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)
@@ -29,7 +29,7 @@ def get_project_id() -> str:
29
29
  except Exception:
30
30
  pass
31
31
 
32
- # Fallback: persistent volume token
32
+ # Fallback: persistent volume token — read or create
33
33
  token_path = "/workspace/.computefence_id"
34
34
  try:
35
35
  if os.path.exists(token_path):
@@ -37,6 +37,11 @@ def get_project_id() -> str:
37
37
  token = f.read().strip()
38
38
  if token:
39
39
  return token
40
+ # No token exists — generate and write one
41
+ new_token = str(uuid.uuid4())
42
+ with open(token_path, "w") as f:
43
+ f.write(new_token)
44
+ return new_token
40
45
  except Exception:
41
46
  pass
42
47
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: computefence
3
- Version: 0.2.12
3
+ Version: 0.2.14
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
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  computefence/__init__.py
5
5
  computefence/cli.py
6
+ computefence/drift.py
6
7
  computefence/telemetry.py
7
8
  computefence.egg-info/PKG-INFO
8
9
  computefence.egg-info/SOURCES.txt
@@ -1 +0,0 @@
1
- __version__ = "0.2.12"
File without changes
File without changes
File without changes