vibehash 0.4.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.
vibehash/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Vibe Hash: a proof-of-useful-work cryptocurrency prototype that pays for verified AI training."""
vibehash/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
vibehash/blobs.py ADDED
@@ -0,0 +1,157 @@
1
+ """Content-addressed files for model weights, so checkpoints never travel inside JSON.
2
+
3
+ A blob's id is the SHA-256 of its bytes: raw little-endian floats at the job's
4
+ precision. Whoever receives a blob re-hashes it, so a node or a worker can't
5
+ be handed different bytes than the id promises. Uploads arrive in sequential
6
+ chunks and resume from whatever already arrived. Loaded arrays are
7
+ memory-mapped, so a 1 GB checkpoint costs disk space, not RAM.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import gc
12
+ import hashlib
13
+ import os
14
+ import re
15
+ import threading
16
+ from pathlib import Path
17
+
18
+ import numpy as np
19
+
20
+ CHUNK = 8 * 2**20 # transfer unit, not a size limit
21
+ _ID = re.compile(r"^[0-9a-f]{64}$")
22
+ _OWNER = re.compile(r"^[0-9a-z]{0,64}$")
23
+
24
+
25
+ class BlobError(ValueError):
26
+ pass
27
+
28
+
29
+ def check_id(blob_id: str) -> str:
30
+ if not isinstance(blob_id, str) or not _ID.match(blob_id):
31
+ raise BlobError("bad blob id")
32
+ return blob_id
33
+
34
+
35
+ def array_bytes(arr: np.ndarray, dtype: str) -> np.ndarray:
36
+ """The canonical byte layout of an array at `dtype` (a contiguous array, no copy if already right)."""
37
+ return np.ascontiguousarray(arr, dtype=dtype)
38
+
39
+
40
+ def array_id(arr: np.ndarray, dtype: str) -> str:
41
+ return hashlib.sha256(memoryview(array_bytes(arr, dtype)).cast("B")).hexdigest()
42
+
43
+
44
+ class BlobStore:
45
+ def __init__(self, root: str | Path):
46
+ self.root = Path(root)
47
+ self.root.mkdir(parents=True, exist_ok=True)
48
+ self._finishing = threading.Lock() # uploads of one blob can complete at the same moment
49
+
50
+ def path(self, blob_id: str) -> Path:
51
+ return self.root / check_id(blob_id)
52
+
53
+ def _part(self, blob_id: str, owner: str = "") -> Path:
54
+ """A partial upload. Each uploader (`owner`) gets its own, so two uploads of one blob never mix: two
55
+ workers holding a replicated unit produce identical checkpoints, and either may finish first."""
56
+ if not _OWNER.match(owner):
57
+ raise BlobError("bad uploader")
58
+ return self.root / (f"{check_id(blob_id)}.{owner}.part" if owner else f"{check_id(blob_id)}.part")
59
+
60
+ def has(self, blob_id: str) -> bool:
61
+ return self.path(blob_id).exists()
62
+
63
+ def size(self, blob_id: str) -> int | None:
64
+ p = self.path(blob_id)
65
+ return p.stat().st_size if p.exists() else None
66
+
67
+ def received(self, blob_id: str, owner: str = "") -> int:
68
+ """Bytes already stored: the full size if complete, else how far `owner`'s partial upload got."""
69
+ done = self.size(blob_id)
70
+ if done is not None:
71
+ return done
72
+ part = self._part(blob_id, owner)
73
+ return part.stat().st_size if part.exists() else 0
74
+
75
+ # --- writing ------------------------------------------------------------------
76
+
77
+ def put_array(self, arr: np.ndarray, dtype: str) -> str:
78
+ data = array_bytes(arr, dtype)
79
+ blob_id = hashlib.sha256(memoryview(data).cast("B")).hexdigest()
80
+ if not self.has(blob_id):
81
+ tmp = self.root / f"{blob_id}.{os.getpid()}.tmp"
82
+ data.tofile(tmp)
83
+ os.replace(tmp, self.path(blob_id))
84
+ return blob_id
85
+
86
+ def write_chunk(self, blob_id: str, offset: int, chunk: bytes, total: int, owner: str = "") -> int:
87
+ """Append one chunk of `owner`'s upload. Returns bytes received so far.
88
+
89
+ Chunks must arrive in order (offset == bytes already received), which makes
90
+ retries safe: a repeated chunk is ignored. The finished file must hash to its id.
91
+ """
92
+ if self.has(blob_id):
93
+ return total
94
+ part = self._part(blob_id, owner)
95
+ have = part.stat().st_size if part.exists() else 0
96
+ if offset < have and offset + len(chunk) <= have:
97
+ return have # duplicate of something we already have
98
+ if offset != have:
99
+ raise BlobError(f"expected offset {have}, got {offset}")
100
+ if have + len(chunk) > total:
101
+ raise BlobError("upload longer than declared")
102
+ with open(part, "ab") as f:
103
+ f.write(chunk)
104
+ have += len(chunk)
105
+ if have == total:
106
+ if file_sha256(part) != blob_id:
107
+ part.unlink(missing_ok=True)
108
+ raise BlobError("upload doesn't match its id; discarded")
109
+ with self._finishing:
110
+ if self.has(blob_id): # another uploader finished first; same bytes, and the file may be in use
111
+ part.unlink(missing_ok=True)
112
+ else:
113
+ os.replace(part, self.path(blob_id))
114
+ return have
115
+
116
+ # --- reading ------------------------------------------------------------------
117
+
118
+ def read_chunk(self, blob_id: str, offset: int, length: int) -> bytes:
119
+ p = self.path(blob_id)
120
+ if not p.exists():
121
+ raise KeyError(blob_id)
122
+ with open(p, "rb") as f:
123
+ f.seek(offset)
124
+ return f.read(min(length, CHUNK))
125
+
126
+ def load(self, blob_id: str, dtype: str, n: int) -> np.ndarray:
127
+ """Memory-mapped, read-only view. Checks the length matches the model."""
128
+ p = self.path(blob_id)
129
+ if not p.exists():
130
+ raise KeyError(blob_id)
131
+ if p.stat().st_size != np.dtype(dtype).itemsize * n:
132
+ raise BlobError("blob is the wrong size for this model")
133
+ return np.memmap(p, dtype=dtype, mode="r", shape=(n,))
134
+
135
+ # --- housekeeping -------------------------------------------------------------
136
+
137
+ def prune(self, keep: set[str] = frozenset()) -> int:
138
+ """Delete every blob (and partial upload) except `keep`. Returns how many were removed."""
139
+ gc.collect() # release memory maps so Windows lets the files go
140
+ removed = 0
141
+ for p in self.root.iterdir():
142
+ if p.name.split(".")[0] in keep:
143
+ continue
144
+ try:
145
+ p.unlink()
146
+ removed += 1
147
+ except OSError:
148
+ pass # still mapped somewhere; the next prune gets it
149
+ return removed
150
+
151
+
152
+ def file_sha256(path: Path) -> str:
153
+ h = hashlib.sha256()
154
+ with open(path, "rb") as f:
155
+ for block in iter(lambda: f.read(1 << 20), b""):
156
+ h.update(block)
157
+ return h.hexdigest()