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.
- spatial/__init__.py +6 -0
- spatial/ai/__init__.py +18 -0
- spatial/ai/kvcache.py +81 -0
- spatial/cli.py +135 -0
- spatial/compute/__init__.py +3 -0
- spatial/compute/cost.py +69 -0
- spatial/compute/detect.py +139 -0
- spatial/compute/monitor.py +111 -0
- spatial/compute/scheduler.py +168 -0
- spatial/compute/taskqueue.py +107 -0
- spatial/compute/worker.py +89 -0
- spatial/compute/workloads.py +148 -0
- spatial/core/__init__.py +1 -0
- spatial/core/energy.py +101 -0
- spatial/core/ledger.py +76 -0
- spatial/core/model.py +82 -0
- spatial/core/protocol.py +167 -0
- spatial/core/registry.py +59 -0
- spatial/holographic/__init__.py +8 -0
- spatial/holographic/cgh.py +40 -0
- spatial/holographic/model.py +49 -0
- spatial/intelligence/__init__.py +6 -0
- spatial/intelligence/predictor.py +51 -0
- spatial/memory/__init__.py +5 -0
- spatial/memory/fararray.py +44 -0
- spatial/memory/pageserver.py +141 -0
- spatial/memory/secure.py +100 -0
- spatial/memory/shared.py +43 -0
- spatial/memory/tiered.py +355 -0
- spatial/q/__init__.py +4 -0
- spatial/q/__main__.py +9 -0
- spatial/q/backends/__init__.py +122 -0
- spatial/q/codegen.py +153 -0
- spatial/q/compiler.py +276 -0
- spatial/q/ir.py +121 -0
- spatial/q/lexer.py +61 -0
- spatial/q/parser.py +330 -0
- spatial/q/qir.py +128 -0
- spatial/q/runtime.py +274 -0
- spatial/q/sim.py +218 -0
- spatial/runtime/__init__.py +3 -0
- spatial/runtime/envs.py +74 -0
- spatial/runtime/inspect.py +48 -0
- spatial/runtime/microvm.py +125 -0
- spatial/runtime/planner.py +155 -0
- spatial/runtime/runner.py +108 -0
- spatial/runtime/toolchain.py +35 -0
- spatial/scientific/__init__.py +8 -0
- spatial/scientific/jobs.py +80 -0
- spatial/simulation/__init__.py +7 -0
- spatial/simulation/heat.py +48 -0
- spatial/simulation/nbody.py +31 -0
- spatial_computing-0.5.0.dist-info/METADATA +112 -0
- spatial_computing-0.5.0.dist-info/RECORD +58 -0
- spatial_computing-0.5.0.dist-info/WHEEL +5 -0
- spatial_computing-0.5.0.dist-info/entry_points.txt +3 -0
- spatial_computing-0.5.0.dist-info/licenses/LICENSE +202 -0
- spatial_computing-0.5.0.dist-info/top_level.txt +1 -0
spatial/memory/secure.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Far-tier wrappers: encryption and replication.
|
|
2
|
+
|
|
3
|
+
EncryptedBackend : pages are sealed with AES-256-GCM before they leave the process; the
|
|
4
|
+
page key is bound as associated data, so a page served under another
|
|
5
|
+
key (swap attack) or modified at rest fails to open.
|
|
6
|
+
ReplicatedBackend : every page is written to all replicas; reads fall back to the next
|
|
7
|
+
replica when one fails or returns a corrupted page.
|
|
8
|
+
Neither adds capacity: they add confidentiality and availability to the far tier.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import struct
|
|
14
|
+
|
|
15
|
+
from cryptography.exceptions import InvalidTag
|
|
16
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PageIntegrityError(Exception):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EncryptedBackend:
|
|
24
|
+
def __init__(self, inner, key: bytes | None = None):
|
|
25
|
+
self.inner = inner
|
|
26
|
+
self.key = key or AESGCM.generate_key(bit_length=256) # lives only in this process
|
|
27
|
+
self._aead = AESGCM(self.key)
|
|
28
|
+
self.name = f"encrypted({inner.name})"
|
|
29
|
+
|
|
30
|
+
def put(self, key: int, blob: bytes) -> None:
|
|
31
|
+
nonce = os.urandom(12)
|
|
32
|
+
self.inner.put(key, nonce + self._aead.encrypt(nonce, blob, struct.pack(">q", key)))
|
|
33
|
+
|
|
34
|
+
def _open(self, key: int, sealed: bytes) -> bytes:
|
|
35
|
+
try:
|
|
36
|
+
return self._aead.decrypt(sealed[:12], sealed[12:], struct.pack(">q", key))
|
|
37
|
+
except InvalidTag:
|
|
38
|
+
raise PageIntegrityError(f"far page {key} failed authentication")
|
|
39
|
+
|
|
40
|
+
def get(self, key: int) -> bytes:
|
|
41
|
+
return self._open(key, self.inner.get(key))
|
|
42
|
+
|
|
43
|
+
def get_many(self, keys):
|
|
44
|
+
return [None if b is None else self._open(k, b) for k, b in zip(keys, self.inner.get_many(keys))]
|
|
45
|
+
|
|
46
|
+
def delete(self, key: int) -> None:
|
|
47
|
+
self.inner.delete(key)
|
|
48
|
+
|
|
49
|
+
def close(self) -> None:
|
|
50
|
+
self.inner.close()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ReplicatedBackend:
|
|
54
|
+
def __init__(self, replicas: list):
|
|
55
|
+
if len(replicas) < 2:
|
|
56
|
+
raise ValueError("replication needs at least two backends")
|
|
57
|
+
self.replicas = replicas
|
|
58
|
+
self.name = "replicated(" + ",".join(r.name for r in replicas) + ")"
|
|
59
|
+
self.failovers = 0
|
|
60
|
+
self.write_errors = 0
|
|
61
|
+
|
|
62
|
+
def put(self, key: int, blob: bytes) -> None:
|
|
63
|
+
ok = 0
|
|
64
|
+
for r in self.replicas:
|
|
65
|
+
try:
|
|
66
|
+
r.put(key, blob)
|
|
67
|
+
ok += 1
|
|
68
|
+
except Exception:
|
|
69
|
+
self.write_errors += 1
|
|
70
|
+
if ok == 0:
|
|
71
|
+
raise IOError(f"far page {key}: no replica accepted the write")
|
|
72
|
+
|
|
73
|
+
def get(self, key: int) -> bytes:
|
|
74
|
+
last = None
|
|
75
|
+
for i, r in enumerate(self.replicas):
|
|
76
|
+
try:
|
|
77
|
+
b = r.get(key)
|
|
78
|
+
if i:
|
|
79
|
+
self.failovers += 1
|
|
80
|
+
return b
|
|
81
|
+
except Exception as e:
|
|
82
|
+
last = e
|
|
83
|
+
raise IOError(f"far page {key}: all replicas failed ({last})")
|
|
84
|
+
|
|
85
|
+
def get_many(self, keys):
|
|
86
|
+
return [self.get(k) for k in keys]
|
|
87
|
+
|
|
88
|
+
def delete(self, key: int) -> None:
|
|
89
|
+
for r in self.replicas:
|
|
90
|
+
try:
|
|
91
|
+
r.delete(key)
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
def close(self) -> None:
|
|
96
|
+
for r in self.replicas:
|
|
97
|
+
try:
|
|
98
|
+
r.close()
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
spatial/memory/shared.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Shared pages with several writers: optimistic concurrency on the page server.
|
|
2
|
+
|
|
3
|
+
Each page has a version. A write succeeds only if the writer saw the current version
|
|
4
|
+
(compare-and-swap); otherwise it re-reads and retries. This gives lost-update-free
|
|
5
|
+
read-modify-write for pages shared by several processes. It is NOT distributed shared
|
|
6
|
+
memory: every access is a network round trip, and there is one server (no consensus).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VersionConflict(Exception):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SharedPages:
|
|
16
|
+
def __init__(self, client):
|
|
17
|
+
self.c = client
|
|
18
|
+
self.conflicts = 0
|
|
19
|
+
|
|
20
|
+
def read(self, key: int) -> tuple[bytes | None, int]:
|
|
21
|
+
rh, blob = self.c.call({"op": "vget", "key": key})
|
|
22
|
+
return (blob if rh["exists"] else None), rh["version"]
|
|
23
|
+
|
|
24
|
+
def write(self, key: int, data: bytes, expected_version: int) -> int:
|
|
25
|
+
try:
|
|
26
|
+
rh, _ = self.c.call({"op": "vput", "key": key, "expected": expected_version}, data)
|
|
27
|
+
except RuntimeError as e:
|
|
28
|
+
if "VersionConflict" in str(e):
|
|
29
|
+
raise VersionConflict(str(e)) from None
|
|
30
|
+
raise
|
|
31
|
+
return rh["version"]
|
|
32
|
+
|
|
33
|
+
def update(self, key: int, fn, max_retries: int = 1000) -> bytes:
|
|
34
|
+
"""Atomically replace the page by fn(current bytes or None)."""
|
|
35
|
+
for _ in range(max_retries):
|
|
36
|
+
cur, ver = self.read(key)
|
|
37
|
+
new = fn(cur)
|
|
38
|
+
try:
|
|
39
|
+
self.write(key, new, ver)
|
|
40
|
+
return new
|
|
41
|
+
except VersionConflict:
|
|
42
|
+
self.conflicts += 1
|
|
43
|
+
raise VersionConflict(f"page {key}: still contended after {max_retries} retries")
|
spatial/memory/tiered.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Spatial Resource Engine — user-space tiered memory (application-integrated far memory).
|
|
2
|
+
|
|
3
|
+
Tiers:
|
|
4
|
+
T0 hot : uncompressed pages in local RAM (LRU, byte budget)
|
|
5
|
+
T1 compressed : compressed + content-deduplicated pages in local RAM (LRU, byte budget)
|
|
6
|
+
T2 far : compressed pages on a FarBackend (SSD file or remote page server)
|
|
7
|
+
|
|
8
|
+
HONESTY CONTRACT: T1 and T2 are NOT RAM. T1 trades CPU for space; T2 trades latency.
|
|
9
|
+
`stats()` always separates logical bytes from physical local RAM bytes, and every
|
|
10
|
+
access is timed per tier so the cost of leaving T0 is measured, never hidden.
|
|
11
|
+
This is an object store used through an API, not transparent kernel paging.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import os
|
|
17
|
+
import time
|
|
18
|
+
import zlib
|
|
19
|
+
from collections import OrderedDict
|
|
20
|
+
from typing import Iterable, Protocol
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import lz4.frame as _lz4
|
|
24
|
+
except Exception: # pragma: no cover
|
|
25
|
+
_lz4 = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------- codecs
|
|
29
|
+
class Codec:
|
|
30
|
+
name = "none"
|
|
31
|
+
|
|
32
|
+
def enc(self, b: bytes) -> bytes:
|
|
33
|
+
return b
|
|
34
|
+
|
|
35
|
+
def dec(self, b: bytes) -> bytes:
|
|
36
|
+
return b
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Lz4Codec(Codec):
|
|
40
|
+
name = "lz4"
|
|
41
|
+
|
|
42
|
+
def enc(self, b):
|
|
43
|
+
return _lz4.compress(b, compression_level=0, store_size=True)
|
|
44
|
+
|
|
45
|
+
def dec(self, b):
|
|
46
|
+
return _lz4.decompress(b)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ZlibCodec(Codec):
|
|
50
|
+
name = "zlib1"
|
|
51
|
+
|
|
52
|
+
def enc(self, b):
|
|
53
|
+
return zlib.compress(b, 1)
|
|
54
|
+
|
|
55
|
+
def dec(self, b):
|
|
56
|
+
return zlib.decompress(b)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def codec(name: str) -> Codec:
|
|
60
|
+
if name == "lz4":
|
|
61
|
+
if _lz4 is None:
|
|
62
|
+
raise RuntimeError("lz4 not installed")
|
|
63
|
+
return Lz4Codec()
|
|
64
|
+
return {"none": Codec, "zlib1": ZlibCodec}[name]()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------- far backends
|
|
68
|
+
class FarBackend(Protocol):
|
|
69
|
+
name: str
|
|
70
|
+
|
|
71
|
+
def put(self, key: int, blob: bytes) -> None: ...
|
|
72
|
+
def get(self, key: int) -> bytes: ...
|
|
73
|
+
def get_many(self, keys: list[int]) -> list[bytes | None]: ...
|
|
74
|
+
def delete(self, key: int) -> None: ...
|
|
75
|
+
def close(self) -> None: ...
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class FileBackend:
|
|
79
|
+
"""Append-only log on local storage (SSD). Storage, not memory."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, path: str):
|
|
82
|
+
self.name = "ssd-file"
|
|
83
|
+
self.path = path
|
|
84
|
+
self._f = open(path, "w+b", buffering=0)
|
|
85
|
+
self._idx: dict[int, tuple[int, int]] = {}
|
|
86
|
+
self._end = 0
|
|
87
|
+
|
|
88
|
+
def put(self, key, blob):
|
|
89
|
+
os.pwrite(self._f.fileno(), blob, self._end)
|
|
90
|
+
self._idx[key] = (self._end, len(blob))
|
|
91
|
+
self._end += len(blob)
|
|
92
|
+
|
|
93
|
+
def get(self, key):
|
|
94
|
+
off, ln = self._idx[key]
|
|
95
|
+
return os.pread(self._f.fileno(), ln, off)
|
|
96
|
+
|
|
97
|
+
def get_many(self, keys):
|
|
98
|
+
return [self.get(k) if k in self._idx else None for k in keys]
|
|
99
|
+
|
|
100
|
+
def delete(self, key):
|
|
101
|
+
self._idx.pop(key, None)
|
|
102
|
+
|
|
103
|
+
def close(self):
|
|
104
|
+
self._f.close()
|
|
105
|
+
try:
|
|
106
|
+
os.unlink(self.path)
|
|
107
|
+
except OSError:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class RemoteBackend:
|
|
112
|
+
"""Pages held in the RAM of a page server reached over SWP/0 (see pageserver.py)."""
|
|
113
|
+
|
|
114
|
+
def __init__(self, client, label: str = "remote"):
|
|
115
|
+
self.name = label
|
|
116
|
+
self.c = client
|
|
117
|
+
|
|
118
|
+
def put(self, key, blob):
|
|
119
|
+
self.c.call({"op": "put", "key": key}, blob)
|
|
120
|
+
|
|
121
|
+
def get(self, key):
|
|
122
|
+
return self.c.call({"op": "get", "key": key})[1]
|
|
123
|
+
|
|
124
|
+
def get_many(self, keys):
|
|
125
|
+
rh, blob = self.c.call({"op": "get_many", "keys": keys})
|
|
126
|
+
out, off = [], 0
|
|
127
|
+
for ln in rh["lens"]:
|
|
128
|
+
if ln < 0:
|
|
129
|
+
out.append(None)
|
|
130
|
+
else:
|
|
131
|
+
out.append(blob[off:off + ln])
|
|
132
|
+
off += ln
|
|
133
|
+
return out
|
|
134
|
+
|
|
135
|
+
def delete(self, key):
|
|
136
|
+
self.c.call({"op": "del", "key": key})
|
|
137
|
+
|
|
138
|
+
def close(self):
|
|
139
|
+
self.c.close()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------- store
|
|
143
|
+
class _TierStat:
|
|
144
|
+
__slots__ = ("hits", "ns")
|
|
145
|
+
|
|
146
|
+
def __init__(self):
|
|
147
|
+
self.hits = 0
|
|
148
|
+
self.ns = 0
|
|
149
|
+
|
|
150
|
+
def add(self, ns):
|
|
151
|
+
self.hits += 1
|
|
152
|
+
self.ns += ns
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TieredStore:
|
|
156
|
+
def __init__(self, hot_bytes: int, compressed_bytes: int, far: FarBackend | None = None,
|
|
157
|
+
codec_name: str = "lz4", dedup: bool = True, prefetch: int = 0,
|
|
158
|
+
keep_latencies: bool = False, keep_far_copy: bool = False):
|
|
159
|
+
self.keep_far_copy = keep_far_copy
|
|
160
|
+
self.clean_evictions = self.far_writes = 0
|
|
161
|
+
self.hot_cap, self.comp_cap = hot_bytes, compressed_bytes
|
|
162
|
+
self.far = far
|
|
163
|
+
self.codec = codec(codec_name)
|
|
164
|
+
self.dedup = dedup
|
|
165
|
+
self.prefetch = prefetch
|
|
166
|
+
self._hot: OrderedDict[int, bytes] = OrderedDict()
|
|
167
|
+
self._hot_bytes = 0
|
|
168
|
+
self._comp: OrderedDict[int, bytes] = OrderedDict() # key -> digest (dedup) or blob
|
|
169
|
+
self._blobs: dict[bytes, list] = {} # digest -> [blob, refcount]
|
|
170
|
+
self._comp_bytes = 0
|
|
171
|
+
self._far_keys: dict[int, int] = {} # key -> blob length
|
|
172
|
+
self._far_bytes = 0
|
|
173
|
+
self.logical_bytes = 0
|
|
174
|
+
self._sizes: dict[int, int] = {}
|
|
175
|
+
self.st = {"hot": _TierStat(), "compressed": _TierStat(), "far": _TierStat()}
|
|
176
|
+
self.compress_ns = self.decompress_ns = 0
|
|
177
|
+
self.raw_in = self.comp_out = 0
|
|
178
|
+
self.dedup_hits = 0
|
|
179
|
+
self.prefetched = self.prefetch_used = 0
|
|
180
|
+
self._prefetched_keys: set[int] = set()
|
|
181
|
+
self._last_far_key: int | None = None
|
|
182
|
+
self.lat = {"hot": [], "compressed": [], "far": []} if keep_latencies else None
|
|
183
|
+
|
|
184
|
+
# ---- internals
|
|
185
|
+
def _comp_put(self, key: int, blob: bytes, digest: bytes | None):
|
|
186
|
+
if self.dedup and digest is not None:
|
|
187
|
+
ent = self._blobs.get(digest)
|
|
188
|
+
if ent:
|
|
189
|
+
ent[1] += 1
|
|
190
|
+
self.dedup_hits += 1
|
|
191
|
+
else:
|
|
192
|
+
self._blobs[digest] = [blob, 1]
|
|
193
|
+
self._comp_bytes += len(blob)
|
|
194
|
+
self._comp[key] = digest
|
|
195
|
+
else:
|
|
196
|
+
self._comp[key] = blob
|
|
197
|
+
self._comp_bytes += len(blob)
|
|
198
|
+
self._evict_comp()
|
|
199
|
+
|
|
200
|
+
def _comp_take(self, key: int) -> bytes:
|
|
201
|
+
v = self._comp.pop(key)
|
|
202
|
+
if self.dedup:
|
|
203
|
+
ent = self._blobs[v]
|
|
204
|
+
ent[1] -= 1
|
|
205
|
+
blob = ent[0]
|
|
206
|
+
if ent[1] == 0:
|
|
207
|
+
del self._blobs[v]
|
|
208
|
+
self._comp_bytes -= len(blob)
|
|
209
|
+
return blob
|
|
210
|
+
self._comp_bytes -= len(v)
|
|
211
|
+
return v
|
|
212
|
+
|
|
213
|
+
def _evict_comp(self):
|
|
214
|
+
while self._comp_bytes > self.comp_cap and self._comp:
|
|
215
|
+
k = next(iter(self._comp))
|
|
216
|
+
blob = self._comp_take(k)
|
|
217
|
+
if self.far is None:
|
|
218
|
+
raise MemoryError("tiered store full and no far tier configured")
|
|
219
|
+
if k in self._far_keys: # clean copy already there: eviction is free
|
|
220
|
+
self.clean_evictions += 1
|
|
221
|
+
continue
|
|
222
|
+
self.far.put(k, blob)
|
|
223
|
+
self.far_writes += 1
|
|
224
|
+
self._far_keys[k] = len(blob)
|
|
225
|
+
self._far_bytes += len(blob)
|
|
226
|
+
|
|
227
|
+
def _evict_hot(self):
|
|
228
|
+
while self._hot_bytes > self.hot_cap and len(self._hot) > 1:
|
|
229
|
+
k, raw = self._hot.popitem(last=False)
|
|
230
|
+
self._hot_bytes -= len(raw)
|
|
231
|
+
t = time.perf_counter_ns()
|
|
232
|
+
blob = self.codec.enc(raw)
|
|
233
|
+
self.compress_ns += time.perf_counter_ns() - t
|
|
234
|
+
self.raw_in += len(raw)
|
|
235
|
+
self.comp_out += len(blob)
|
|
236
|
+
digest = hashlib.blake2b(raw, digest_size=16).digest() if self.dedup else None
|
|
237
|
+
self._comp_put(k, blob, digest)
|
|
238
|
+
|
|
239
|
+
def _admit(self, key: int, raw: bytes):
|
|
240
|
+
self._hot[key] = raw
|
|
241
|
+
self._hot_bytes += len(raw)
|
|
242
|
+
self._evict_hot()
|
|
243
|
+
|
|
244
|
+
def _drop(self, key: int):
|
|
245
|
+
if key in self._hot:
|
|
246
|
+
self._hot_bytes -= len(self._hot.pop(key))
|
|
247
|
+
if key in self._comp:
|
|
248
|
+
self._comp_take(key)
|
|
249
|
+
if key in self._far_keys: # also invalidates a retained clean copy
|
|
250
|
+
self._far_bytes -= self._far_keys.pop(key)
|
|
251
|
+
self.far.delete(key)
|
|
252
|
+
|
|
253
|
+
# ---- API
|
|
254
|
+
def put(self, key: int, raw: bytes) -> None:
|
|
255
|
+
if key in self._sizes:
|
|
256
|
+
self.logical_bytes -= self._sizes[key]
|
|
257
|
+
self._drop(key)
|
|
258
|
+
self._sizes[key] = len(raw)
|
|
259
|
+
self.logical_bytes += len(raw)
|
|
260
|
+
self._admit(key, raw)
|
|
261
|
+
|
|
262
|
+
def get(self, key: int) -> bytes:
|
|
263
|
+
t0 = time.perf_counter_ns()
|
|
264
|
+
if key in self._hot:
|
|
265
|
+
self._hot.move_to_end(key)
|
|
266
|
+
raw = self._hot[key]
|
|
267
|
+
tier = "hot"
|
|
268
|
+
elif key in self._comp:
|
|
269
|
+
blob = self._comp_take(key)
|
|
270
|
+
t = time.perf_counter_ns()
|
|
271
|
+
raw = self.codec.dec(blob)
|
|
272
|
+
self.decompress_ns += time.perf_counter_ns() - t
|
|
273
|
+
if key in self._prefetched_keys:
|
|
274
|
+
self._prefetched_keys.discard(key)
|
|
275
|
+
self.prefetch_used += 1
|
|
276
|
+
self._admit(key, raw)
|
|
277
|
+
tier = "compressed"
|
|
278
|
+
elif key in self._far_keys:
|
|
279
|
+
raw = self._far_fetch(key)
|
|
280
|
+
tier = "far"
|
|
281
|
+
else:
|
|
282
|
+
raise KeyError(key)
|
|
283
|
+
ns = time.perf_counter_ns() - t0
|
|
284
|
+
self.st[tier].add(ns)
|
|
285
|
+
if self.lat is not None:
|
|
286
|
+
self.lat[tier].append(ns)
|
|
287
|
+
return raw
|
|
288
|
+
|
|
289
|
+
def _far_fetch(self, key: int) -> bytes:
|
|
290
|
+
# Sequential detector: only read ahead when the previous far miss was key-1.
|
|
291
|
+
seq = self.prefetch and self._last_far_key is not None and key == self._last_far_key + 1
|
|
292
|
+
self._last_far_key = key
|
|
293
|
+
if seq:
|
|
294
|
+
extra = [k for k in range(key + 1, key + 1 + self.prefetch)
|
|
295
|
+
if k in self._far_keys and k not in self._hot and k not in self._comp]
|
|
296
|
+
blobs = self.far.get_many([key] + extra)
|
|
297
|
+
blob = blobs[0]
|
|
298
|
+
for k, b in zip(extra, blobs[1:]):
|
|
299
|
+
if b is None:
|
|
300
|
+
continue
|
|
301
|
+
# Prefetched pages stay compressed in T1: cheap if never used.
|
|
302
|
+
self._far_release(k)
|
|
303
|
+
self._comp_put(k, b, hashlib.blake2b(b, digest_size=16).digest() if self.dedup else None)
|
|
304
|
+
self._prefetched_keys.add(k)
|
|
305
|
+
self.prefetched += 1
|
|
306
|
+
self._last_far_key = k
|
|
307
|
+
else:
|
|
308
|
+
blob = self.far.get(key)
|
|
309
|
+
self._far_release(key)
|
|
310
|
+
t = time.perf_counter_ns()
|
|
311
|
+
raw = self.codec.dec(blob)
|
|
312
|
+
self.decompress_ns += time.perf_counter_ns() - t
|
|
313
|
+
self._admit(key, raw)
|
|
314
|
+
return raw
|
|
315
|
+
|
|
316
|
+
def _far_release(self, key: int):
|
|
317
|
+
"""After a far read: drop the far copy, or keep it as a clean copy (keep_far_copy) so a
|
|
318
|
+
later eviction of this unmodified page costs no network write."""
|
|
319
|
+
if self.keep_far_copy:
|
|
320
|
+
return
|
|
321
|
+
self._far_bytes -= self._far_keys.pop(key)
|
|
322
|
+
self.far.delete(key)
|
|
323
|
+
|
|
324
|
+
def keys(self) -> Iterable[int]:
|
|
325
|
+
return self._sizes.keys()
|
|
326
|
+
|
|
327
|
+
def stats(self) -> dict:
|
|
328
|
+
n = sum(s.hits for s in self.st.values()) or 1
|
|
329
|
+
d = {
|
|
330
|
+
"logical_bytes": self.logical_bytes,
|
|
331
|
+
"local_ram_bytes": self._hot_bytes + self._comp_bytes, # physical RAM really used
|
|
332
|
+
"hot_bytes": self._hot_bytes, "compressed_tier_bytes": self._comp_bytes,
|
|
333
|
+
"far_bytes": self._far_bytes, "far_backend": self.far.name if self.far else None,
|
|
334
|
+
"codec": self.codec.name,
|
|
335
|
+
"compression_ratio": round(self.raw_in / self.comp_out, 3) if self.comp_out else None,
|
|
336
|
+
"dedup_hits": self.dedup_hits,
|
|
337
|
+
"compress_cpu_s": self.compress_ns / 1e9, "decompress_cpu_s": self.decompress_ns / 1e9,
|
|
338
|
+
"prefetched": self.prefetched, "prefetch_used": self.prefetch_used,
|
|
339
|
+
"far_writes": self.far_writes, "clean_evictions": self.clean_evictions,
|
|
340
|
+
}
|
|
341
|
+
for name, s in self.st.items():
|
|
342
|
+
d[f"{name}_hits"] = s.hits
|
|
343
|
+
d[f"{name}_hit_ratio"] = round(s.hits / n, 4)
|
|
344
|
+
d[f"{name}_mean_us"] = round(s.ns / s.hits / 1e3, 2) if s.hits else None
|
|
345
|
+
if self.lat is not None:
|
|
346
|
+
for name, xs in self.lat.items():
|
|
347
|
+
if xs:
|
|
348
|
+
xs = sorted(xs)
|
|
349
|
+
d[f"{name}_p50_us"] = round(xs[len(xs) // 2] / 1e3, 2)
|
|
350
|
+
d[f"{name}_p99_us"] = round(xs[min(len(xs) - 1, int(len(xs) * 0.99))] / 1e3, 2)
|
|
351
|
+
return d
|
|
352
|
+
|
|
353
|
+
def close(self):
|
|
354
|
+
if self.far:
|
|
355
|
+
self.far.close()
|
spatial/q/__init__.py
ADDED
spatial/q/__main__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .codegen import SQCodegenError
|
|
2
|
+
from .compiler import SQCompileError
|
|
3
|
+
from .runtime import SQRuntimeError, main
|
|
4
|
+
from . import backends
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
main()
|
|
8
|
+
except (SyntaxError, SQCodegenError, SQCompileError, SQRuntimeError, backends.BackendUnavailable) as e:
|
|
9
|
+
raise SystemExit(f"spatialq: {e}")
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""SPATIAL Q backends. A backend either really executes a circuit or raises
|
|
2
|
+
BackendUnavailable with the reason; it never returns invented results."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..ir import Circuit
|
|
6
|
+
from .. import sim
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BackendUnavailable(RuntimeError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Backend:
|
|
14
|
+
name = "?"
|
|
15
|
+
kind = "qsim" # qsim | qpu | export
|
|
16
|
+
|
|
17
|
+
def sample(self, c: Circuit, shots: int, seed: int | None = None) -> dict[str, int]:
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
def expectation(self, c: Circuit, pauli: str) -> float:
|
|
21
|
+
raise BackendUnavailable(f"{self.name} does not provide exact expectation values")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StateVectorBackend(Backend):
|
|
25
|
+
"""Built-in NumPy simulator."""
|
|
26
|
+
name = "statevector"
|
|
27
|
+
|
|
28
|
+
def sample(self, c, shots, seed=None, noise: float = 0.0):
|
|
29
|
+
return sim.sample(c, shots, seed, noise=noise)
|
|
30
|
+
|
|
31
|
+
def expectation(self, c, pauli):
|
|
32
|
+
return sim.expectation(c, pauli)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def to_qiskit(c: Circuit, measure_all: bool = True):
|
|
36
|
+
from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
|
|
37
|
+
q = QuantumRegister(c.n_qubits, "q")
|
|
38
|
+
regs = [q]
|
|
39
|
+
m = None
|
|
40
|
+
if c.n_cbits:
|
|
41
|
+
m = ClassicalRegister(c.n_cbits, "m")
|
|
42
|
+
regs.append(m)
|
|
43
|
+
qc = QuantumCircuit(*regs)
|
|
44
|
+
|
|
45
|
+
def emit(o):
|
|
46
|
+
if o.name == "measure":
|
|
47
|
+
qc.measure(q[o.qubits[0]], m[o.cbit])
|
|
48
|
+
elif o.name == "reset":
|
|
49
|
+
qc.reset(q[o.qubits[0]])
|
|
50
|
+
else:
|
|
51
|
+
getattr(qc, o.name)(*o.params, *[q[i] for i in o.qubits])
|
|
52
|
+
|
|
53
|
+
for o in c.ops:
|
|
54
|
+
if o.cond:
|
|
55
|
+
def nest(conds):
|
|
56
|
+
if not conds:
|
|
57
|
+
emit(o)
|
|
58
|
+
return
|
|
59
|
+
b, v = conds[0]
|
|
60
|
+
with qc.if_test((m[b], v)):
|
|
61
|
+
nest(conds[1:])
|
|
62
|
+
nest(list(o.cond))
|
|
63
|
+
else:
|
|
64
|
+
emit(o)
|
|
65
|
+
if measure_all:
|
|
66
|
+
cr = ClassicalRegister(c.n_qubits, "c")
|
|
67
|
+
qc.add_register(cr)
|
|
68
|
+
qc.measure(q, cr)
|
|
69
|
+
return qc
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AerBackend(Backend):
|
|
73
|
+
"""Qiskit Aer simulator (independent implementation used as the reference)."""
|
|
74
|
+
name = "aer"
|
|
75
|
+
|
|
76
|
+
def __init__(self):
|
|
77
|
+
try:
|
|
78
|
+
from qiskit_aer import AerSimulator
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise BackendUnavailable(f"qiskit-aer not importable: {e}")
|
|
81
|
+
self._sim = AerSimulator()
|
|
82
|
+
|
|
83
|
+
def sample(self, c, shots, seed=None):
|
|
84
|
+
qc = to_qiskit(c)
|
|
85
|
+
res = self._sim.run(qc, shots=shots, seed_simulator=seed).result()
|
|
86
|
+
counts: dict[str, int] = {}
|
|
87
|
+
for k, v in res.get_counts().items():
|
|
88
|
+
key = k.split()[0] # registers print as "c m"; keep the final read-out
|
|
89
|
+
counts[key] = counts.get(key, 0) + v
|
|
90
|
+
return counts
|
|
91
|
+
|
|
92
|
+
def expectation(self, c, pauli):
|
|
93
|
+
from qiskit.quantum_info import Pauli, Statevector
|
|
94
|
+
return float(Statevector(to_qiskit(c, measure_all=False)).expectation_value(Pauli(pauli)).real)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Qasm3Export(Backend):
|
|
98
|
+
"""Not an executor: produces the OpenQASM 3 program a provider would receive."""
|
|
99
|
+
name = "qasm3"
|
|
100
|
+
kind = "export"
|
|
101
|
+
|
|
102
|
+
def sample(self, c, shots, seed=None):
|
|
103
|
+
raise BackendUnavailable("qasm3 is an export target; use Circuit.to_qasm3()")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class QpuBackend(Backend):
|
|
107
|
+
"""Real quantum hardware. No provider credentials are configured in this prototype, so
|
|
108
|
+
this backend refuses to run instead of silently falling back to a simulator."""
|
|
109
|
+
name = "qpu"
|
|
110
|
+
kind = "qpu"
|
|
111
|
+
|
|
112
|
+
def sample(self, c, shots, seed=None):
|
|
113
|
+
raise BackendUnavailable(
|
|
114
|
+
"no QPU provider configured (needs an account/API token with IBM Quantum, IonQ, "
|
|
115
|
+
"Quantinuum, AWS Braket...). The circuit can be exported with to_qasm3().")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get(name: str) -> Backend:
|
|
119
|
+
table = {"statevector": StateVectorBackend, "aer": AerBackend, "qasm3": Qasm3Export, "qpu": QpuBackend}
|
|
120
|
+
if name not in table:
|
|
121
|
+
raise BackendUnavailable(f"unknown backend '{name}'")
|
|
122
|
+
return table[name]()
|