Files
StarPilot/openpilot/common/file_chunker.py
T
firestar5683 694b460f80 Developer Tools
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>
2026-10-14 20:02:39 -05:00

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()