mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-06 12:33:51 +08:00
694b460f80
Provide laptop and device build entry points, cache compatibility checks and developer tooling. Keep distribution assets and model packaging available before runtime consumers. Co-authored-by: Danny <dannyirwin31@yahoo.com> Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu> Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com> Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Co-authored-by: prabhaavp <prabhaav@msu.edu>
151 lines
5.8 KiB
Python
151 lines
5.8 KiB
Python
"""Dom-compatible numbered model parts, with verified atomic disk materialization.
|
|
|
|
The count-only .chunkmanifest format remains readable. New packages include an
|
|
integrity sidecar; model compilers and tinygrad's disk-backed loader stay unchanged.
|
|
"""
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
CHUNK_SIZE = 45 * 1024 * 1024
|
|
|
|
|
|
def get_chunk_name(name, idx, num_chunks):
|
|
return f'{name}.chunk{idx + 1:02d}of{num_chunks:02d}'
|
|
|
|
|
|
def get_manifest_path(name):
|
|
return f'{name}.chunkmanifest'
|
|
|
|
|
|
def get_existing_chunks(path):
|
|
path = Path(path)
|
|
manifest = Path(get_manifest_path(path))
|
|
if not manifest.exists() and path.is_file():
|
|
return [path]
|
|
text = manifest.read_text().strip()
|
|
if not text.isdecimal() or not 1 <= int(text) <= 1024:
|
|
raise ValueError('invalid model part count')
|
|
count = int(text)
|
|
parts = [path.with_name(get_chunk_name(path.name, i, count)) for i in range(count)]
|
|
for i, part in enumerate(parts):
|
|
size = part.stat().st_size
|
|
if not 0 < size <= CHUNK_SIZE or (i < count - 1 and size != CHUNK_SIZE):
|
|
raise ValueError('invalid model part size')
|
|
return [manifest, *parts]
|
|
|
|
|
|
def file_chunked_exists(path):
|
|
try:
|
|
get_existing_chunks(path)
|
|
return True
|
|
except (OSError, ValueError):
|
|
return False
|
|
|
|
|
|
def _digest_file(path):
|
|
digest = hashlib.sha256()
|
|
with Path(path).open('rb') as inp:
|
|
while block := inp.read(1024 * 1024):
|
|
digest.update(block)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def materialize_file_chunked(path, expected_sha256=None):
|
|
"""Return a regular file for tinygrad OOB reads; refuse invalid packages.
|
|
|
|
Cache publication uses os.replace so concurrent readers never see partial data.
|
|
Legacy count-only packages can supply an externally pinned expected_sha256.
|
|
"""
|
|
path = Path(path)
|
|
entries = get_existing_chunks(path)
|
|
if entries == [path]:
|
|
if expected_sha256 is not None and _digest_file(path) != expected_sha256:
|
|
raise ValueError('model digest mismatch')
|
|
return path
|
|
sidecar = Path(f'{path}.chunksha256')
|
|
metadata = json.loads(sidecar.read_text()) if sidecar.exists() else None
|
|
if metadata is not None:
|
|
if set(metadata) != {'sha256', 'bytes', 'parts'} or len(metadata['parts']) != len(entries) - 1:
|
|
raise ValueError('invalid model integrity manifest')
|
|
if expected_sha256 is not None and metadata['sha256'] != expected_sha256:
|
|
raise ValueError('model digest mismatch')
|
|
expected_sha256 = metadata['sha256']
|
|
cache = Path(f'{path}.unchunked')
|
|
digest, total = hashlib.sha256(), 0
|
|
for i, part in enumerate(entries[1:]):
|
|
part_hash, part_size = hashlib.sha256(), 0
|
|
with part.open('rb') as inp:
|
|
while block := inp.read(1024 * 1024):
|
|
digest.update(block)
|
|
part_hash.update(block)
|
|
part_size += len(block)
|
|
total += part_size
|
|
if metadata is not None and metadata['parts'][i] != {'bytes': part_size, 'sha256': part_hash.hexdigest()}:
|
|
raise ValueError('model part digest mismatch')
|
|
if expected_sha256 is not None and digest.hexdigest() != expected_sha256:
|
|
raise ValueError('model digest mismatch')
|
|
if metadata is not None and total != metadata['bytes']:
|
|
raise ValueError('model size mismatch')
|
|
if cache.is_file() and cache.stat().st_size == total and _digest_file(cache) == digest.hexdigest():
|
|
return cache
|
|
fd, temporary = tempfile.mkstemp(prefix=f'.{path.name}.', dir=path.parent)
|
|
copied_digest, copied_size = hashlib.sha256(), 0
|
|
try:
|
|
with os.fdopen(fd, 'wb') as out:
|
|
for part in entries[1:]:
|
|
with part.open('rb') as inp:
|
|
while block := inp.read(1024 * 1024):
|
|
out.write(block)
|
|
copied_digest.update(block)
|
|
copied_size += len(block)
|
|
if copied_size != total or copied_digest.hexdigest() != digest.hexdigest():
|
|
raise ValueError('model parts changed during materialization')
|
|
os.replace(temporary, cache)
|
|
return cache
|
|
finally:
|
|
if os.path.exists(temporary):
|
|
os.unlink(temporary)
|
|
|
|
|
|
def read_file_chunked(path, expected_sha256=None):
|
|
return materialize_file_chunked(path, expected_sha256).read_bytes()
|
|
|
|
|
|
def open_file_chunked(path, expected_sha256=None):
|
|
return materialize_file_chunked(path, expected_sha256).open('rb')
|
|
|
|
|
|
def package_file(path, destination):
|
|
"""Package a compiled artifact without deleting the compiler's original output."""
|
|
path, destination = Path(path), Path(destination)
|
|
source_stat = path.stat()
|
|
size = source_stat.st_size
|
|
if size <= 0:
|
|
raise ValueError('empty model artifact')
|
|
count = (size + CHUNK_SIZE - 1) // CHUNK_SIZE
|
|
if count > 1024:
|
|
raise ValueError('model exceeds part count limit')
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
digest, parts = hashlib.sha256(), []
|
|
with path.open('rb') as inp:
|
|
for i in range(count):
|
|
block = inp.read(CHUNK_SIZE)
|
|
digest.update(block)
|
|
destination.with_name(get_chunk_name(destination.name, i, count)).write_bytes(block)
|
|
parts.append({'bytes': len(block), 'sha256': hashlib.sha256(block).hexdigest()})
|
|
final_stat = path.stat()
|
|
unchanged = (source_stat.st_size, source_stat.st_mtime_ns, source_stat.st_ino) == (final_stat.st_size, final_stat.st_mtime_ns, final_stat.st_ino)
|
|
if not unchanged or sum(part['bytes'] for part in parts) != size:
|
|
raise ValueError('compiled model changed while packaging')
|
|
Path(f'{destination}.chunksha256').write_text(json.dumps({'bytes': size, 'sha256': digest.hexdigest(), 'parts': parts}, indent=2) + '\n')
|
|
Path(get_manifest_path(destination)).write_text(str(count))
|
|
active = {get_chunk_name(destination.name, i, count) for i in range(count)}
|
|
for old in destination.parent.glob(destination.name + '.chunk*of*'):
|
|
if old.name not in active and re.fullmatch(re.escape(destination.name) + r'\.chunk[0-9]{2,}of[0-9]{2,}', old.name):
|
|
old.unlink()
|
|
return digest.hexdigest()
|