spatial-computing 0.5.0__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.
Files changed (58) hide show
  1. spatial/__init__.py +6 -0
  2. spatial/ai/__init__.py +18 -0
  3. spatial/ai/kvcache.py +81 -0
  4. spatial/cli.py +135 -0
  5. spatial/compute/__init__.py +3 -0
  6. spatial/compute/cost.py +69 -0
  7. spatial/compute/detect.py +139 -0
  8. spatial/compute/monitor.py +111 -0
  9. spatial/compute/scheduler.py +168 -0
  10. spatial/compute/taskqueue.py +107 -0
  11. spatial/compute/worker.py +89 -0
  12. spatial/compute/workloads.py +148 -0
  13. spatial/core/__init__.py +1 -0
  14. spatial/core/energy.py +101 -0
  15. spatial/core/ledger.py +76 -0
  16. spatial/core/model.py +82 -0
  17. spatial/core/protocol.py +167 -0
  18. spatial/core/registry.py +59 -0
  19. spatial/holographic/__init__.py +8 -0
  20. spatial/holographic/cgh.py +40 -0
  21. spatial/holographic/model.py +49 -0
  22. spatial/intelligence/__init__.py +6 -0
  23. spatial/intelligence/predictor.py +51 -0
  24. spatial/memory/__init__.py +5 -0
  25. spatial/memory/fararray.py +44 -0
  26. spatial/memory/pageserver.py +141 -0
  27. spatial/memory/secure.py +100 -0
  28. spatial/memory/shared.py +43 -0
  29. spatial/memory/tiered.py +355 -0
  30. spatial/q/__init__.py +4 -0
  31. spatial/q/__main__.py +9 -0
  32. spatial/q/backends/__init__.py +122 -0
  33. spatial/q/codegen.py +153 -0
  34. spatial/q/compiler.py +276 -0
  35. spatial/q/ir.py +121 -0
  36. spatial/q/lexer.py +61 -0
  37. spatial/q/parser.py +330 -0
  38. spatial/q/qir.py +128 -0
  39. spatial/q/runtime.py +274 -0
  40. spatial/q/sim.py +218 -0
  41. spatial/runtime/__init__.py +3 -0
  42. spatial/runtime/envs.py +74 -0
  43. spatial/runtime/inspect.py +48 -0
  44. spatial/runtime/microvm.py +125 -0
  45. spatial/runtime/planner.py +155 -0
  46. spatial/runtime/runner.py +108 -0
  47. spatial/runtime/toolchain.py +35 -0
  48. spatial/scientific/__init__.py +8 -0
  49. spatial/scientific/jobs.py +80 -0
  50. spatial/simulation/__init__.py +7 -0
  51. spatial/simulation/heat.py +48 -0
  52. spatial/simulation/nbody.py +31 -0
  53. spatial_computing-0.5.0.dist-info/METADATA +112 -0
  54. spatial_computing-0.5.0.dist-info/RECORD +58 -0
  55. spatial_computing-0.5.0.dist-info/WHEEL +5 -0
  56. spatial_computing-0.5.0.dist-info/entry_points.txt +3 -0
  57. spatial_computing-0.5.0.dist-info/licenses/LICENSE +202 -0
  58. spatial_computing-0.5.0.dist-info/top_level.txt +1 -0
spatial/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """SPATIAL COMPUTING — research prototype.
2
+
3
+ Status of every engine is declared in spatial.core.registry and must be backed
4
+ by a test or benchmark before being set to OPERATIONAL.
5
+ """
6
+ __version__ = "0.5.0"
spatial/ai/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """Spatial AI Engine: first components in the public repository.
2
+
3
+ kvcache.py: a toy NumPy transformer with a real key/value cache, used to measure recompute
4
+ versus restore through the tiered memory store (CPU, toy sizes: says nothing about GPUs).
5
+
6
+ The model-placement planner (layers over heterogeneous nodes, memory, network, availability)
7
+ lives in the private `spatial_pro` package. When that package is installed, `plan_pipeline`
8
+ and friends are re-exported here; otherwise they are None and nothing pretends to work.
9
+ No real language model is executed by this repository.
10
+ """
11
+ try: # pragma: no cover - depends on a private package
12
+ from spatial_pro.placement import ModelSpec, NodeSpec, plan_pipeline # noqa: F401
13
+ HAVE_PLANNER = True
14
+ except ImportError:
15
+ ModelSpec = NodeSpec = plan_pipeline = None
16
+ HAVE_PLANNER = False
17
+
18
+ from .kvcache import ToyTransformer, measure_restore_vs_recompute # noqa: E402,F401
spatial/ai/kvcache.py ADDED
@@ -0,0 +1,81 @@
1
+ """Toy causal transformer with a key/value cache, to MEASURE recompute versus restore.
2
+
3
+ A small random-weight decoder (NumPy, CPU). It is not a language model and its speed says
4
+ nothing about GPUs. It exists so the KV-cache mechanics are real and testable:
5
+ * prefill(tokens) builds the per-layer K/V tensors;
6
+ * continuing from a restored cache gives exactly the same logits as recomputing;
7
+ * the cache can be parked in a TieredStore, so restore time and bytes are measured.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import time
12
+
13
+ import numpy as np
14
+
15
+
16
+ class ToyTransformer:
17
+ def __init__(self, layers: int = 4, d: int = 64, heads: int = 4, vocab: int = 256, seed: int = 0):
18
+ rng = np.random.default_rng(seed)
19
+ self.L, self.d, self.h, self.vocab = layers, d, heads, vocab
20
+ g = lambda *s: (rng.standard_normal(s) / np.sqrt(s[0])).astype(np.float32)
21
+ self.emb = g(vocab, d)
22
+ self.W = [{k: g(d, d) for k in ("q", "k", "v", "o", "m1", "m2")} for _ in range(layers)]
23
+ self.out = g(d, vocab)
24
+
25
+ def _pos(self, i0: int, n: int) -> np.ndarray:
26
+ pos = np.arange(i0, i0 + n)[:, None] / (10000 ** (np.arange(self.d)[None, :] / self.d))
27
+ return np.sin(pos).astype(np.float32)
28
+
29
+ def forward(self, tokens, cache: list | None = None):
30
+ """Process `tokens` after the positions already in `cache`. Returns (logits of the last
31
+ token, new cache). cache = [(K, V)] per layer, K and V of shape [positions, d]."""
32
+ n0 = 0 if cache is None else cache[0][0].shape[0]
33
+ x = self.emb[np.asarray(tokens)] + self._pos(n0, len(tokens))
34
+ new, dh = [], self.d // self.h
35
+ for li, w in enumerate(self.W):
36
+ k_new, v_new = x @ w["k"], x @ w["v"]
37
+ K = k_new if cache is None else np.concatenate([cache[li][0], k_new])
38
+ V = v_new if cache is None else np.concatenate([cache[li][1], v_new])
39
+ q = (x @ w["q"]).reshape(len(tokens), self.h, dh).transpose(1, 0, 2)
40
+ Kh = K.reshape(-1, self.h, dh).transpose(1, 0, 2)
41
+ Vh = V.reshape(-1, self.h, dh).transpose(1, 0, 2)
42
+ att = q @ Kh.transpose(0, 2, 1) / np.sqrt(dh)
43
+ mask = np.arange(K.shape[0])[None, :] > (n0 + np.arange(len(tokens)))[:, None] # causal
44
+ att = np.where(mask[None], np.float32(-1e30), att)
45
+ att = np.exp(att - att.max(-1, keepdims=True))
46
+ att /= att.sum(-1, keepdims=True)
47
+ x = x + (att @ Vh).transpose(1, 0, 2).reshape(len(tokens), self.d) @ w["o"]
48
+ x = x + np.maximum(x @ w["m1"], 0) @ w["m2"]
49
+ x = x.astype(np.float32) # keep the cache in one dtype (float32)
50
+ new.append((K, V))
51
+ return x[-1] @ self.out, new
52
+
53
+
54
+ def cache_to_pages(cache: list) -> list[bytes]:
55
+ return [a.tobytes() for kv in cache for a in kv]
56
+
57
+
58
+ def cache_from_pages(pages: list[bytes], d: int) -> list:
59
+ arrs = [np.frombuffer(p, dtype=np.float32).reshape(-1, d) for p in pages]
60
+ return [(arrs[i], arrs[i + 1]) for i in range(0, len(arrs), 2)]
61
+
62
+
63
+ def measure_restore_vs_recompute(model: ToyTransformer, prefix, store, repeat: int = 3) -> dict:
64
+ """Times, on this machine: recomputing the prefix cache versus restoring it from `store`."""
65
+ t0 = time.perf_counter()
66
+ _, cache = model.forward(prefix)
67
+ recompute_s = time.perf_counter() - t0
68
+ for _ in range(repeat - 1):
69
+ t0 = time.perf_counter()
70
+ model.forward(prefix)
71
+ recompute_s = min(recompute_s, time.perf_counter() - t0)
72
+ pages = cache_to_pages(cache)
73
+ for i, p in enumerate(pages):
74
+ store.put(i, p)
75
+ restore_s = float("inf")
76
+ for _ in range(repeat):
77
+ t0 = time.perf_counter()
78
+ restored = cache_from_pages([store.get(i) for i in range(len(pages))], model.d)
79
+ restore_s = min(restore_s, time.perf_counter() - t0)
80
+ return {"prefix_tokens": len(prefix), "cache_bytes": sum(map(len, pages)), "recompute_s": recompute_s,
81
+ "restore_s": restore_s, "restore_faster": restore_s < recompute_s, "cache": cache, "restored": restored}
spatial/cli.py ADDED
@@ -0,0 +1,135 @@
1
+ """`spatial` command line: one entry point for the three operational engines."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ ROOT = Path(__file__).resolve().parents[1]
11
+ BENCHES = {"cpu": "bench_cpu", "memory": "bench_memory", "gpu": "bench_gpu",
12
+ "runtime": "bench_runtime", "q": "bench_q"}
13
+
14
+
15
+ def cmd_audit(a):
16
+ from .compute import detect_node, limitations, snapshot
17
+ from .compute.monitor import underused
18
+ node, snap = detect_node(), snapshot()
19
+ if a.json:
20
+ print(json.dumps({"node": node.to_dict(), "snapshot": snap, "limitations": limitations(node)},
21
+ indent=1, default=str))
22
+ return 0
23
+ print(f"{node.attrs.get('model')} | {node.attrs.get('cpu_brand')} | {node.os} | {node.arch}")
24
+ for r in node.resources:
25
+ cap = f"{r.capacity / 2**30:.1f} GiB" if r.unit == "bytes" else f"{r.capacity:g} {r.unit}"
26
+ if "utilization_percent" in r.attrs:
27
+ cap += f" ({r.attrs['utilization_percent']:.0f}% busy)"
28
+ print(f" {r.kind.value:<11} {r.name:<42} {cap}")
29
+ print(f"memory pressure: {snap.get('memory_pressure')} | compressor ratio: {snap.get('compressor_ratio')} "
30
+ f"| swap used: {snap['swap_used'] / 2**30:.2f} GiB")
31
+ for u in underused(snap):
32
+ print(" idle:", u)
33
+ print("limitations:")
34
+ for x in limitations(node):
35
+ print(" -", x)
36
+ return 0
37
+
38
+
39
+ def cmd_status(a):
40
+ from .core.registry import ENGINES
41
+ for e in ENGINES:
42
+ print(f"{e.name:<42} {e.status.value:<24} not done: {e.not_done}")
43
+ return 0
44
+
45
+
46
+ def cmd_plan(a):
47
+ from .runtime import inspect, plan
48
+ art = inspect(a.file)
49
+ print(f"{a.file}: {art.format} / {art.os} / {','.join(art.arch)} {art.detail}")
50
+ for r in plan(art):
51
+ print(f" [{'x' if r.feasible else ' '}] {r.name:<16} {r.reason}")
52
+ return 0
53
+
54
+
55
+ def cmd_run(a):
56
+ from .runtime import execute
57
+ e = execute(a.file, a.args, route=a.route, timeout_s=a.timeout)
58
+ if e.stdout:
59
+ print(e.stdout)
60
+ print(f"[spatial] route={e.route} ok={e.ok} wall={e.wall_s:.2f}s "
61
+ + (f"reason: {e.extra.get('not_runnable')}" if not e.ok and e.extra.get("not_runnable") else ""),
62
+ file=sys.stderr)
63
+ return 0 if e.ok else 1
64
+
65
+
66
+ def cmd_q(a):
67
+ from .q import runtime
68
+ sys.argv = ["spatialq", *a.rest]
69
+ runtime.main()
70
+ return 0
71
+
72
+
73
+ def cmd_bench(a):
74
+ names = list(BENCHES) if a.name == "all" else [a.name]
75
+ bdir = ROOT / "benchmarks"
76
+ if not bdir.exists():
77
+ print("benchmarks/ not found: run from a source checkout", file=sys.stderr)
78
+ return 2
79
+ rc = 0
80
+ for n in names:
81
+ rc |= subprocess.run([sys.executable, f"{BENCHES[n]}.py"], cwd=bdir).returncode
82
+ rc |= subprocess.run([sys.executable, "report.py"], cwd=bdir).returncode
83
+ return rc
84
+
85
+
86
+ def cmd_worker(a):
87
+ mod = {"compute": "spatial.compute.worker", "memory": "spatial.memory.pageserver",
88
+ "runtime": "spatial.runtime.runner"}[a.kind]
89
+ cmd = [sys.executable, "-m", mod, "--token-file", a.token_file, "--host", a.host, "--port", str(a.port)]
90
+ if a.allow_remote:
91
+ cmd.append("--allow-remote")
92
+ return subprocess.run(cmd).returncode
93
+
94
+
95
+ def main(argv=None):
96
+ ap = argparse.ArgumentParser(prog="spatial", description="SPATIAL COMPUTING — measure, decide, prove")
97
+ sub = ap.add_subparsers(dest="cmd", required=True)
98
+ p = sub.add_parser("audit", help="detect hardware, usage and limits of this machine")
99
+ p.add_argument("--json", action="store_true")
100
+ p.set_defaults(fn=cmd_audit)
101
+ sub.add_parser("status", help="honest status of every engine").set_defaults(fn=cmd_status)
102
+ p = sub.add_parser("plan", help="inspect a binary and list execution routes")
103
+ p.add_argument("file")
104
+ p.set_defaults(fn=cmd_plan)
105
+ p = sub.add_parser("run", help="run a binary through the best feasible route")
106
+ p.add_argument("file")
107
+ p.add_argument("args", nargs="*")
108
+ p.add_argument("--route")
109
+ p.add_argument("--timeout", type=float, default=300)
110
+ p.set_defaults(fn=cmd_run)
111
+ p = sub.add_parser("q", help="SPATIAL Q: run|check|qasm|ir <file.sq>")
112
+ p.add_argument("rest", nargs=argparse.REMAINDER)
113
+ p.set_defaults(fn=cmd_q)
114
+ p = sub.add_parser("bench", help="run benchmarks and regenerate docs/RESULTATS.md")
115
+ p.add_argument("name", choices=[*BENCHES, "all"])
116
+ p.set_defaults(fn=cmd_bench)
117
+ p = sub.add_parser("serve", help="start a compute worker, a page server or a sandboxed binary runner (loopback by default)")
118
+ p.add_argument("kind", choices=["compute", "memory", "runtime"])
119
+ p.add_argument("--token-file", required=True)
120
+ p.add_argument("--host", default="127.0.0.1")
121
+ p.add_argument("--port", type=int, default=0)
122
+ p.add_argument("--allow-remote", action="store_true", help="explicit consent to accept LAN connections")
123
+ p.set_defaults(fn=cmd_worker)
124
+ a = ap.parse_args(argv)
125
+ try:
126
+ return a.fn(a)
127
+ except (Exception, SystemExit) as e: # user-facing tool: one clean line, no traceback
128
+ if isinstance(e, SystemExit) and not isinstance(e.code, str):
129
+ raise
130
+ print(f"spatial: {type(e).__name__}: {e}", file=sys.stderr)
131
+ return 1
132
+
133
+
134
+ if __name__ == "__main__":
135
+ sys.exit(main())
@@ -0,0 +1,3 @@
1
+ """Spatial Compute Engine: detect, monitor, compare strategies, offload, cost."""
2
+ from .detect import detect_node, limitations # noqa: F401
3
+ from .monitor import snapshot, Measure # noqa: F401
@@ -0,0 +1,69 @@
1
+ """Cost model. Every price is an ASSUMPTION with a stated basis; override via CostModel(...).
2
+
3
+ Local cost is derived from MEASURED time/energy. Remote/cloud cost is an ESTIMATE:
4
+ no cloud machine was rented for these experiments, so cloud rows are labelled
5
+ `estimated=True` and assume the measured local runtime scaled by `speed_ratio`.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+
11
+
12
+ @dataclass
13
+ class CloudOffer:
14
+ name: str
15
+ usd_per_hour: float
16
+ vcpus: int
17
+ gpu: str | None
18
+ basis: str # where the number comes from; to be re-checked before any business use
19
+
20
+
21
+ # List prices recalled from public price pages (us-east-1 on-demand). NOT re-verified today.
22
+ OFFERS = {
23
+ "aws_c7i_large": CloudOffer("AWS c7i.large", 0.0893, 2, None, "AWS on-demand list price, unverified"),
24
+ "aws_c7i_4xlarge": CloudOffer("AWS c7i.4xlarge", 0.714, 16, None, "AWS on-demand list price, unverified"),
25
+ "aws_g4dn_xlarge": CloudOffer("AWS g4dn.xlarge (T4)", 0.526, 4, "T4", "AWS on-demand list price, unverified"),
26
+ }
27
+
28
+
29
+ @dataclass
30
+ class CostModel:
31
+ usd_per_kwh: float = 0.17 # assumption: ~US residential average
32
+ laptop_price_usd: float = 0.0 # sunk by default; set to amortise hardware
33
+ laptop_life_hours: float = 4 * 365 * 8 # 4 years x 8 h/day
34
+ egress_usd_per_gb: float = 0.09 # assumption: typical hyperscaler internet egress
35
+ min_billing_s: float = 60.0 # assumption: per-second billing, 60 s minimum
36
+ offers: dict = field(default_factory=lambda: dict(OFFERS))
37
+
38
+ def local(self, wall_s: float, energy_j: float | None, idle_watts: float | None = None) -> dict:
39
+ """Cost of a measured local run. Energy cost uses the marginal energy above idle
40
+ when an idle baseline is known, otherwise the whole-machine energy."""
41
+ out = {"estimated": False, "hardware_usd": 0.0, "energy_usd": None, "total_usd": None}
42
+ if self.laptop_price_usd:
43
+ out["hardware_usd"] = self.laptop_price_usd / self.laptop_life_hours * wall_s / 3600
44
+ if energy_j is not None:
45
+ j = energy_j - (idle_watts * wall_s if idle_watts else 0.0)
46
+ out["marginal_energy_j"] = max(j, 0.0)
47
+ out["energy_usd"] = max(j, 0.0) / 3.6e6 * self.usd_per_kwh
48
+ out["total_usd"] = out["hardware_usd"] + out["energy_usd"]
49
+ return out
50
+
51
+ def cloud(self, offer_key: str, local_wall_s: float, speed_ratio: float,
52
+ payload_bytes: int = 0, result_bytes: int = 0, startup_s: float = 0.0) -> dict:
53
+ """ESTIMATE of running the same job on a cloud offer.
54
+ speed_ratio = assumed (cloud throughput / local throughput)."""
55
+ o = self.offers[offer_key]
56
+ run_s = local_wall_s / speed_ratio + startup_s
57
+ billed = max(run_s, self.min_billing_s)
58
+ compute = o.usd_per_hour * billed / 3600
59
+ net = result_bytes / 1e9 * self.egress_usd_per_gb # ingress assumed free
60
+ return {"estimated": True, "offer": o.name, "basis": o.basis, "assumed_speed_ratio": speed_ratio,
61
+ "run_s": run_s, "billed_s": billed, "compute_usd": compute, "network_usd": net,
62
+ "total_usd": compute + net}
63
+
64
+ def break_even_seconds(self, offer_key: str, local_watts_marginal: float) -> float | None:
65
+ """Local run time above which cloud min-billing stops dominating (same speed)."""
66
+ o = self.offers[offer_key]
67
+ local_per_s = local_watts_marginal / 3.6e6 * self.usd_per_kwh
68
+ cloud_per_s = o.usd_per_hour / 3600
69
+ return None if cloud_per_s <= local_per_s else self.min_billing_s
@@ -0,0 +1,139 @@
1
+ """Hardware detection (macOS first; degrades gracefully elsewhere). Nothing is assumed."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import platform
6
+ import shutil
7
+ import subprocess
8
+ import uuid
9
+
10
+ import psutil
11
+
12
+ from ..core.model import Node, Resource, ResourceKind
13
+
14
+
15
+ def _sysctl(key: str) -> str | None:
16
+ try:
17
+ r = subprocess.run(["sysctl", "-n", key], capture_output=True, text=True, timeout=5)
18
+ return r.stdout.strip() or None
19
+ except Exception:
20
+ return None
21
+
22
+
23
+ def _os_name() -> str:
24
+ if platform.system() == "Darwin":
25
+ try: # platform.mac_ver() can report a compatibility version (10.16)
26
+ v = subprocess.run(["sw_vers", "-productVersion"], capture_output=True, text=True, timeout=5).stdout.strip()
27
+ return f"macOS {v}"
28
+ except Exception:
29
+ pass
30
+ return f"{platform.system()} {platform.release()}"
31
+
32
+
33
+ def _gpus() -> list[dict]:
34
+ try:
35
+ out = subprocess.run(["system_profiler", "SPDisplaysDataType", "-json"],
36
+ capture_output=True, text=True, timeout=30).stdout
37
+ return json.loads(out).get("SPDisplaysDataType", [])
38
+ except Exception:
39
+ return []
40
+
41
+
42
+ def parse_nvidia_smi(csv: str) -> list[dict]:
43
+ """Parse `nvidia-smi --query-gpu=name,memory.total,memory.used,utilization.gpu --format=csv,noheader,nounits`."""
44
+ out = []
45
+ for line in csv.strip().splitlines():
46
+ parts = [p.strip() for p in line.split(",")]
47
+ if len(parts) != 4:
48
+ continue
49
+ name, total, used, util = parts
50
+ try:
51
+ out.append({"name": name, "vram_bytes": int(float(total)) * 2**20,
52
+ "vram_used_bytes": int(float(used)) * 2**20, "utilization_percent": float(util)})
53
+ except ValueError:
54
+ continue
55
+ return out
56
+
57
+
58
+ def _nvidia_gpus() -> list[dict]:
59
+ exe = shutil.which("nvidia-smi")
60
+ if not exe:
61
+ return []
62
+ try:
63
+ r = subprocess.run([exe, "--query-gpu=name,memory.total,memory.used,utilization.gpu",
64
+ "--format=csv,noheader,nounits"], capture_output=True, text=True, timeout=20)
65
+ return parse_nvidia_smi(r.stdout) if r.returncode == 0 else []
66
+ except Exception:
67
+ return []
68
+
69
+
70
+ def virtualization() -> dict:
71
+ """What can actually run guests here."""
72
+ info = {"hvf": _sysctl("kern.hv_support") == "1"}
73
+ for tool in ("qemu-system-x86_64", "qemu-system-aarch64", "docker", "podman", "limactl",
74
+ "wine", "wine64", "node", "wasmtime"):
75
+ info[tool] = shutil.which(tool)
76
+ return info
77
+
78
+
79
+ def detect_node() -> Node:
80
+ vm = psutil.virtual_memory()
81
+ arch = platform.machine()
82
+ node = Node(node_id=uuid.uuid5(uuid.NAMESPACE_DNS, platform.node()).hex[:12],
83
+ hostname="local", os=_os_name(),
84
+ arch=arch)
85
+ node.attrs["model"] = _sysctl("hw.model")
86
+ node.attrs["cpu_brand"] = _sysctl("machdep.cpu.brand_string")
87
+ node.attrs["virtualization"] = virtualization()
88
+ feats = (_sysctl("machdep.cpu.features") or "") + " " + (_sysctl("machdep.cpu.leaf7_features") or "")
89
+ simd = [f for f in ("SSE4.2", "AVX1.0", "AVX2", "AVX512F", "FMA") if f in feats.split()]
90
+ node.resources.append(Resource(
91
+ ResourceKind.CPU, node.attrs["cpu_brand"] or "cpu",
92
+ capacity=psutil.cpu_count(logical=False) or 1, unit="physical cores",
93
+ attrs={"logical": psutil.cpu_count(logical=True), "simd": simd,
94
+ "l2_bytes": int(_sysctl("hw.l2cachesize") or 0),
95
+ "l3_bytes": int(_sysctl("hw.l3cachesize") or 0)}))
96
+ node.resources.append(Resource(ResourceKind.RAM, "physical RAM", capacity=vm.total,
97
+ unit="bytes", available=vm.available))
98
+ sw = psutil.swap_memory()
99
+ node.resources.append(Resource(
100
+ ResourceKind.FAR_MEMORY, "OS swap (SSD-backed, NOT RAM)", capacity=sw.total, unit="bytes",
101
+ available=sw.free, attrs={"compressor_mode": _sysctl("vm.compressor_mode")}))
102
+ for g in _gpus():
103
+ node.resources.append(Resource(
104
+ ResourceKind.GPU, g.get("sppci_model", "gpu"), capacity=1, unit="device",
105
+ attrs={"vram": g.get("spdisplays_vram_shared") or g.get("spdisplays_vram") or g.get("_spdisplays_vram"),
106
+ "metal": g.get("spdisplays_mtlgpufamilysupport"),
107
+ "vendor": g.get("spdisplays_vendor"), "integrated": "builtin" in str(g.get("sppci_bus", "")).lower().replace("-", "")}))
108
+ for g in _nvidia_gpus():
109
+ node.resources.append(Resource(
110
+ ResourceKind.GPU, g["name"], capacity=g["vram_bytes"], unit="bytes",
111
+ available=g["vram_bytes"] - g["vram_used_bytes"],
112
+ attrs={"vendor": "NVIDIA", "cuda": True, "integrated": False,
113
+ "utilization_percent": g["utilization_percent"]}))
114
+ du = psutil.disk_usage("/System/Volumes/Data" if platform.system() == "Darwin" else "/")
115
+ node.resources.append(Resource(ResourceKind.STORAGE, "system volume", capacity=du.total,
116
+ unit="bytes", available=du.free))
117
+ return node
118
+
119
+
120
+ def limitations(node: Node) -> list[str]:
121
+ """Hard limits derived from what was detected (used by the scheduler and the audit)."""
122
+ out = []
123
+ cpu = node.by_kind(ResourceKind.CPU)[0]
124
+ if cpu.capacity <= 2:
125
+ out.append(f"Only {int(cpu.capacity)} physical cores: parallel speed-up is bounded near 2x "
126
+ "(hyper-threads add little for compute-bound code).")
127
+ if "AVX512F" not in cpu.attrs.get("simd", []):
128
+ out.append("No AVX-512; SIMD width limited to AVX2 (256-bit).")
129
+ if node.arch == "x86_64":
130
+ out.append("Intel Mac: no Apple Neural Engine/NPU, no MLX, no Apple Virtualization.framework "
131
+ "Rosetta-for-Linux; ARM guests run under slow TCG emulation only.")
132
+ for g in node.by_kind(ResourceKind.GPU):
133
+ if g.attrs.get("integrated"):
134
+ out.append(f"GPU {g.name} is integrated: shares system RAM, no CUDA, low compute throughput.")
135
+ if not node.attrs["virtualization"].get("wine") and not node.attrs["virtualization"].get("wine64"):
136
+ out.append("No Wine installed: Windows PE binaries cannot be executed locally.")
137
+ if not node.attrs["virtualization"].get("docker"):
138
+ out.append("No Docker/Podman: OCI containers unavailable; QEMU micro-VM is the Linux path.")
139
+ return out
@@ -0,0 +1,111 @@
1
+ """Resource monitoring: CPU, RAM, macOS compressor and swap, memory pressure."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import re
6
+ import resource
7
+ import subprocess
8
+ import sys
9
+ import time
10
+
11
+ import psutil
12
+
13
+ from ..core import energy
14
+ from ..core.model import Measurement
15
+
16
+ _PRESSURE = {1: "normal", 2: "warning", 4: "critical"}
17
+
18
+
19
+ def vm_stat() -> dict[str, int]:
20
+ """Parsed `vm_stat` in bytes (macOS). Empty dict elsewhere."""
21
+ try:
22
+ out = subprocess.run(["vm_stat"], capture_output=True, text=True, timeout=5).stdout
23
+ except Exception:
24
+ return {}
25
+ page = int(re.search(r"page size of (\d+)", out).group(1))
26
+ d = {}
27
+ for k, v in re.findall(r'^"?([^":]+)"?:\s+(\d+)\.', out, re.M):
28
+ d[k.strip().lower().replace(" ", "_").replace("-", "_")] = int(v)
29
+ for k in list(d):
30
+ if k.startswith("pages_"):
31
+ d[k + "_bytes"] = d[k] * page
32
+ d["page_size"] = page
33
+ return d
34
+
35
+
36
+ def snapshot() -> dict:
37
+ vm = psutil.virtual_memory()
38
+ sw = psutil.swap_memory()
39
+ vs = vm_stat()
40
+ snap = {
41
+ "ts": time.time(),
42
+ "cpu_percent": psutil.cpu_percent(interval=0.3),
43
+ "load1": os.getloadavg()[0],
44
+ "ram_total": vm.total, "ram_available": vm.available, "ram_used_percent": vm.percent,
45
+ "swap_used": sw.used, "swap_total": sw.total,
46
+ }
47
+ if vs:
48
+ stored = vs.get("pages_stored_in_compressor_bytes", 0)
49
+ occupied = vs.get("pages_occupied_by_compressor_bytes", 0)
50
+ snap.update({
51
+ "compressor_logical_bytes": stored, # uncompressed size held by compressor
52
+ "compressor_physical_bytes": occupied, # RAM actually used to hold it
53
+ "compressor_ratio": round(stored / occupied, 2) if occupied else None,
54
+ "compressor_saved_bytes": stored - occupied,
55
+ "swapins": vs.get("swapins"), "swapouts": vs.get("swapouts"),
56
+ })
57
+ try:
58
+ lvl = int(subprocess.run(["sysctl", "-n", "kern.memorystatus_vm_pressure_level"],
59
+ capture_output=True, text=True, timeout=5).stdout.strip())
60
+ snap["memory_pressure"] = _PRESSURE.get(lvl, str(lvl))
61
+ except Exception:
62
+ snap["memory_pressure"] = None
63
+ return snap
64
+
65
+
66
+ def underused(snap: dict) -> list[str]:
67
+ """Flag resources that are idle right now (input for the optimizer)."""
68
+ out = []
69
+ if snap["cpu_percent"] < 25:
70
+ out.append(f"CPU {snap['cpu_percent']:.0f}% busy: {100 - snap['cpu_percent']:.0f}% idle capacity")
71
+ if snap["ram_available"] / snap["ram_total"] > 0.4:
72
+ out.append(f"RAM {snap['ram_available'] / 2**30:.1f} GiB available of {snap['ram_total'] / 2**30:.0f}")
73
+ return out
74
+
75
+
76
+ def _peak_rss() -> int:
77
+ r = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
78
+ return r if sys.platform == "darwin" else r * 1024 # bytes on macOS, KiB on Linux
79
+
80
+
81
+ class Measure:
82
+ """Measure a block: wall, CPU (self + waited children), peak RSS, battery energy."""
83
+
84
+ def __init__(self, engine: str, experiment: str, strategy: str, power: bool = True):
85
+ self.m = Measurement(engine, experiment, strategy, ok=False, wall_s=0.0)
86
+ self._power = energy.PowerSampler() if power else None
87
+
88
+ def __enter__(self) -> Measurement:
89
+ t = os.times()
90
+ self._cpu0 = t.user + t.system + t.children_user + t.children_system
91
+ if self._power:
92
+ self._power.__enter__()
93
+ self._t0 = time.perf_counter()
94
+ return self.m
95
+
96
+ def __exit__(self, et, ev, tb):
97
+ self.m.wall_s = time.perf_counter() - self._t0
98
+ if self._power:
99
+ self._power.__exit__(et, ev, tb)
100
+ self.m.energy_j = self._power.joules
101
+ if self._power.mean_watts is not None:
102
+ self.m.metrics["mean_watts_machine"] = round(self._power.mean_watts, 2)
103
+ self.m.metrics["power_gauge_static"] = self._power.static
104
+ t = os.times()
105
+ self.m.cpu_s = t.user + t.system + t.children_user + t.children_system - self._cpu0
106
+ self.m.peak_rss_bytes = _peak_rss()
107
+ if et is None:
108
+ self.m.ok = True
109
+ else:
110
+ self.m.notes = f"{et.__name__}: {ev}"
111
+ return True # failures are recorded, not raised