Compare commits

...

5 Commits

Author SHA1 Message Date
Jason Wen b742557d62 sunnylink: add model resolver (#1931)
* models: add get_default_model resolver for sunnylink

* models: move get_default_model to default_model.py
2026-08-20 21:56:57 -04:00
Nayan 5ecd05aedf models: add big model to default model resolution (#1929)
* device

* sunnylink

* lint

* lfs?

* Revert "lfs?"

This reverts commit bcdaec6b4c.

* update path

* Scope the default big model down to the sunnylink schema

* Drop the mock-only default model test

* Move the default model resolver out to separate PR

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-20 21:31:32 -04:00
Jason Wen 5ae100aa1d models fetcher: bump big model to v21 2026-08-20 19:19:59 -04:00
Jason Wen be76a88b80 ci override LFS fetch exclude for real ONNX file retrieval (#1928)
ci: override lfs.fetchexclude so the model fetch pulls real ONNX files instead of pointers
2026-08-20 19:12:59 -04:00
James Vecellio-Grant 049d225d5a ci: Dedicated Model Runner (#1922)
* ci: Dedicated Model Runner

* recurse

* not needed

* fix wrapper

* whoops

* bypass

* modeld_v2: restore chestnut link check before big model build

* modeld_v2: stage onnx to disk instead of shared memory

* ci: clear unchunked onnx temps before model build

* ci: stream the pkl hash instead of loading it into memory

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-20 16:18:17 -04:00
9 changed files with 123 additions and 73 deletions
+34 -16
View File
@@ -103,20 +103,25 @@ jobs:
- run: | - run: |
cd ${{ github.workspace }}/openpilot/openpilot cd ${{ github.workspace }}/openpilot/openpilot
if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; then if [ "${{ inputs.target_hardware }}" != "usbgpu" ]; then
git lfs pull -X "selfdrive/modeld/models/big_*.onnx" -X "selfdrive/modeld/models/dmonitoring_*.onnx" git lfs pull -X "**/selfdrive/modeld/models/big_*.onnx,**/selfdrive/modeld/models/dmonitoring_*.onnx"
rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx rm -f selfdrive/modeld/models/big_*.onnx selfdrive/modeld/models/dmonitoring_*.onnx
else else
git lfs pull -I "selfdrive/modeld/models/big_*.onnx" git lfs pull -I "**/selfdrive/modeld/models/big_*.onnx" -X ""
find selfdrive/modeld/models -name "*.onnx" ! -name "big_*.onnx" -delete find selfdrive/modeld/models -name "*.onnx" ! -name "big_*.onnx" -delete
fi fi
if grep -lIF "version https://git-lfs.github.com/spec/v1" selfdrive/modeld/models/*.onnx; then
echo "::error::the ONNX files above are still LFS pointers, not real models"
exit 1
fi
- name: 'Upload Artifact' - name: 'Upload Artifact'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: models-${{ env.REF }}${{ inputs.artifact_suffix }} name: models-${{ env.REF }}${{ inputs.artifact_suffix }}
path: ${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx path: ${{ github.workspace }}/openpilot/openpilot/selfdrive/modeld/models/*.onnx
if-no-files-found: error
build_model: build_model:
runs-on: [self-hosted, tici] runs-on: [self-hosted, usbgpu]
needs: get_model needs: get_model
env: env:
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }}) MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
@@ -127,7 +132,6 @@ jobs:
fetch-depth: 1 fetch-depth: 1
submodules: recursive submodules: recursive
- run: git lfs pull
- name: Set environment variables - name: Set environment variables
id: set-env id: set-env
@@ -160,7 +164,7 @@ jobs:
fi fi
source ${UV_PROJECT_ENVIRONMENT}/bin/activate source ${UV_PROJECT_ENVIRONMENT}/bin/activate
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --disable
rm -rf ${{ env.MODELS_DIR }}/*.onnx rm -rf ${{ env.MODELS_DIR }}/*.onnx*
- name: Download model artifacts - name: Download model artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
@@ -180,6 +184,7 @@ jobs:
MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')") MODEL_SIZE=$(python3 -c "from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')")
CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')") CAMERA_RES=$(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')")
TG_FLAGS_QCOM="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then
echo "USBGPU build" echo "USBGPU build"
export USBGPU=1 export USBGPU=1
@@ -187,27 +192,40 @@ jobs:
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl" OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
else else
echo "QCOM build" echo "QCOM build"
TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1" TG_FLAGS="$TG_FLAGS_QCOM"
OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl" OUTPUT_PKL="${{ env.MODELS_DIR }}/driving_tinygrad.pkl"
fi fi
# Generate metadata for all ONNX files # Generate metadata for all ONNX files
find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do find "${{ env.MODELS_DIR }}" -maxdepth 1 -name '*.onnx' | while IFS= read -r onnx_file; do
echo "Generating metadata: $onnx_file" echo "Generating metadata: $onnx_file"
env ${TG_FLAGS} python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true env ${TG_FLAGS_QCOM} python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
done done
# Detect model type and build compile args # Detect model type and build compile args
VISION_ONNX="${{ env.MODELS_DIR }}/driving_vision.onnx" VISION_ONNX=""
POLICY_ONNX="${{ env.MODELS_DIR }}/driving_policy.onnx" for f in "${{ env.MODELS_DIR }}/driving_vision.onnx" "${{ env.MODELS_DIR }}/big_driving_vision.onnx"; do
OFF_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_off_policy.onnx" [ -f "$f" ] && VISION_ONNX="$f" && break
ON_POLICY_ONNX="${{ env.MODELS_DIR }}/driving_on_policy.onnx" done
POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_policy.onnx"; do
[ -f "$f" ] && POLICY_ONNX="$f" && break
done
OFF_POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_off_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_off_policy.onnx"; do
[ -f "$f" ] && OFF_POLICY_ONNX="$f" && break
done
ON_POLICY_ONNX=""
for f in "${{ env.MODELS_DIR }}/driving_on_policy.onnx" "${{ env.MODELS_DIR }}/big_driving_on_policy.onnx"; do
[ -f "$f" ] && ON_POLICY_ONNX="$f" && break
done
SUPERCOMBO_ONNX="" SUPERCOMBO_ONNX=""
for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx"; do for f in "${{ env.MODELS_DIR }}/supercombo.onnx" "${{ env.MODELS_DIR }}/driving_supercombo.onnx" "${{ env.MODELS_DIR }}/big_supercombo.onnx" "${{ env.MODELS_DIR }}/big_driving_supercombo.onnx"; do
if [ -f "$f" ]; then [ -f "$f" ] && SUPERCOMBO_ONNX="$f" && break
SUPERCOMBO_ONNX="$f"
break
fi
done done
MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME="" MODEL_TYPE="" ONNX_ARGS="" OUTPUT_NAME=""
@@ -272,18 +272,17 @@ def _parse_size(size_str: str) -> tuple[int, int]:
return int(width), int(height) return int(width), int(height)
def read_file_chunked_to_shm(path): def read_file_chunked_to_disk(path):
if not path: if not path:
return None return None
import atexit import atexit
import shutil import shutil
from openpilot.common.file_chunker import open_file_chunked from openpilot.common.file_chunker import open_file_chunked
from openpilot.common.hardware.hw import Paths tmp_path = f'{path}.unchunked'
shm_path = os.path.join(Paths.shm_path(), os.path.basename(path)) with open(tmp_path, 'wb') as f, open_file_chunked(path) as src:
atexit.register(lambda: os.path.exists(shm_path) and os.remove(shm_path)) shutil.copyfileobj(src, f)
with open(shm_path, 'wb') as dst, open_file_chunked(path) as src: atexit.register(lambda: os.path.exists(tmp_path) and os.remove(tmp_path))
shutil.copyfileobj(src, dst) return tmp_path
return shm_path
def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]: def _load_policy_runners(args: argparse.Namespace) -> tuple[list, list]:
@@ -327,11 +326,11 @@ if __name__ == "__main__":
model_w, model_h = args.model_size model_w, model_h = args.model_size
output_data = {} output_data = {}
args.vision_onnx = read_file_chunked_to_shm(args.vision_onnx) args.vision_onnx = read_file_chunked_to_disk(args.vision_onnx)
args.policy_onnx = read_file_chunked_to_shm(args.policy_onnx) args.policy_onnx = read_file_chunked_to_disk(args.policy_onnx)
args.off_policy_onnx = read_file_chunked_to_shm(args.off_policy_onnx) args.off_policy_onnx = read_file_chunked_to_disk(args.off_policy_onnx)
args.on_policy_onnx = read_file_chunked_to_shm(args.on_policy_onnx) args.on_policy_onnx = read_file_chunked_to_disk(args.on_policy_onnx)
args.supercombo_onnx = read_file_chunked_to_shm(args.supercombo_onnx) args.supercombo_onnx = read_file_chunked_to_disk(args.supercombo_onnx)
vision_runner = OnnxRunner(args.vision_onnx) if args.vision_onnx else None vision_runner = OnnxRunner(args.vision_onnx) if args.vision_onnx else None
@@ -5,10 +5,15 @@ This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details. See the LICENSE.md file in the root directory for more details.
""" """
import os
import tempfile
from pathlib import Path
import numpy as np import numpy as np
from openpilot.common.parameterized import parameterized from openpilot.common.parameterized import parameterized
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, _detect_desire_key from openpilot.common.file_chunker import chunk_file, get_chunk_targets
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, _detect_desire_key, read_file_chunked_to_disk
from openpilot.common.test import OpenpilotTestCase from openpilot.common.test import OpenpilotTestCase
@@ -160,3 +165,33 @@ class TestOutputSlicePreservation(OpenpilotTestCase):
policy_slices = {'plan': slice(0, 495), 'meta': slice(495, 550)} policy_slices = {'plan': slice(0, 495), 'meta': slice(495, 550)}
assert set(vision_slices.keys()) & set(policy_slices.keys()) == set(), \ assert set(vision_slices.keys()) & set(policy_slices.keys()) == set(), \
"vision and policy slices should not overlap in keys" "vision and policy slices should not overlap in keys"
class TestReadFileChunkedToDisk(OpenpilotTestCase):
def test_none_passthrough(self):
assert read_file_chunked_to_disk(None) is None
def test_unchunked_source_staged_on_disk(self):
with tempfile.TemporaryDirectory() as d:
src = Path(d) / "driving_supercombo.onnx"
payload = os.urandom(1024)
src.write_bytes(payload)
out = Path(read_file_chunked_to_disk(str(src)))
assert out.parent == Path(d)
assert out.name == "driving_supercombo.onnx.unchunked"
assert out.read_bytes() == payload
def test_chunked_source_reassembled_on_disk(self):
with tempfile.TemporaryDirectory() as d:
src = Path(d) / "driving_supercombo.onnx"
payload = os.urandom(4096)
src.write_bytes(payload)
chunk_file(str(src), get_chunk_targets(str(src), len(payload)))
assert not src.exists()
out = Path(read_file_chunked_to_disk(str(src)))
assert out.parent == Path(d)
assert out.read_bytes() == payload
+23 -30
View File
@@ -4,7 +4,13 @@ import hashlib
from openpilot.common.basedir import BASEDIR from openpilot.common.basedir import BASEDIR
from openpilot.sunnypilot import get_file_hash from openpilot.sunnypilot import get_file_hash
from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL from openpilot.selfdrive.modeld.helpers import usbgpu_present
from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL
def get_default_model() -> str:
return DEFAULT_BIG_MODEL if usbgpu_present() else DEFAULT_MODEL
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py") DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py")
MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash") MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash")
@@ -13,7 +19,6 @@ SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld",
def update_model_hash(): def update_model_hash():
supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH) supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH)
combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest() combined_hash = hashlib.sha256(supercombo_hash.encode()).hexdigest()
with open(MODEL_HASH_PATH, "w") as f: with open(MODEL_HASH_PATH, "w") as f:
@@ -22,40 +27,28 @@ def update_model_hash():
print(f"Generated and updated new combined model hash to {MODEL_HASH_PATH}") print(f"Generated and updated new combined model hash to {MODEL_HASH_PATH}")
def get_current_default_model_name(): def update_default_model_names(default_model_name: str, default_big_model_name: str):
print("[GET DEFAULT MODEL NAME]") print("[CHANGE DEFAULT MODEL NAMES]")
name = DEFAULT_MODEL
print(f'Current default model name: "{name}"')
return name
def update_default_model_name(name: str):
print("[CHANGE DEFAULT MODEL NAME]")
with open(DEFAULT_MODEL_NAME_PATH, "w") as f: with open(DEFAULT_MODEL_NAME_PATH, "w") as f:
f.write(f'DEFAULT_MODEL = "{name}"\n') f.write(f'DEFAULT_MODEL = "{default_model_name}"\n')
print(f'New default model name: "{name}"') f.write(f'DEFAULT_BIG_MODEL = "{default_big_model_name}"\n')
print(f'New default small model name: "{default_model_name}"')
print(f'New default big model name: "{default_big_model_name}"')
print("[DONE]") print("[DONE]")
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Update default model name and hash") parser = argparse.ArgumentParser(description="Update default model names and hash")
parser.add_argument("--new_name", type=str, help="New default model name") parser.add_argument("--new_small_model_name", type=str, help="New default small model name")
parser.add_argument("--new_big_model_name", type=str, help="New default big model name")
args = parser.parse_args() args = parser.parse_args()
if not args.new_name: if args.new_small_model_name is None and args.new_big_model_name is None:
print("Warning: No new default model name provided. Use --new_name to specify") new_name = input(f'Enter new default small model name (current: "{DEFAULT_MODEL}", leave empty to keep): ').strip()
print("Default model name and hash will not be updated! (aborted)") new_big_model_name = input(f'Enter new default big model name (current: "{DEFAULT_BIG_MODEL}", leave empty to keep): ').strip()
exit(0) else:
new_name, new_big_model_name = args.new_small_model_name, args.new_big_model_name
current_name = get_current_default_model_name() update_default_model_names(new_name or DEFAULT_MODEL, new_big_model_name or DEFAULT_BIG_MODEL)
new_name = args.new_name
if current_name == new_name:
print(f'Proposed default model name: "{new_name}"')
confirm = input("Proposed default model name is the same as the current default model name. Confirm? (y/n): ").upper().strip()
if confirm != "Y":
print("Default model name and hash will not be updated! (aborted)")
exit(0)
update_default_model_name(new_name)
update_model_hash() update_model_hash()
+1 -1
View File
@@ -141,7 +141,7 @@ class ModelCache:
class ModelFetcher: class ModelFetcher:
"""Handles fetching and caching of model data from remote source""" """Handles fetching and caching of model data from remote source"""
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v20.json" MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v20.json"
MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v20.json" MODEL_URL_USBGPU = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v21.json"
def __init__(self, params: Params): def __init__(self, params: Params):
self.params = params self.params = params
@@ -1 +1,2 @@
DEFAULT_MODEL = "CD210" DEFAULT_MODEL = "CD210"
DEFAULT_BIG_MODEL = "Lebowski"
@@ -20,4 +20,4 @@ class TestDefaultModel(OpenpilotTestCase):
with open(MODEL_HASH_PATH) as f: with open(MODEL_HASH_PATH) as f:
current_hash = f.read().strip() current_hash = f.read().strip()
assert combined_hash == current_hash, "Run sunnypilot/models/default_model.py to update the default model name and hash" assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash"
@@ -28,7 +28,7 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce
create_connection, WebSocketConnectionClosedException) create_connection, WebSocketConnectionClosedException)
import openpilot.cereal.messaging as messaging import openpilot.cereal.messaging as messaging
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL from openpilot.sunnypilot.models.default_model import get_default_model
from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi from openpilot.sunnypilot.sunnylink.api import SunnylinkApi
from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string
@@ -181,7 +181,7 @@ def getParamsMetadata() -> str:
schema = generate_schema() schema = generate_schema()
schema["capabilities"] = generate_capabilities() schema["capabilities"] = generate_capabilities()
schema["capability_labels"] = CAPABILITY_LABELS schema["capability_labels"] = CAPABILITY_LABELS
schema["default_model"] = DEFAULT_MODEL schema["default_model"] = get_default_model()
raw = json.dumps(schema, separators=(",", ":")).encode("utf-8") raw = json.dumps(schema, separators=(",", ":")).encode("utf-8")
return base64.b64encode(gzip.compress(raw)).decode("utf-8") return base64.b64encode(gzip.compress(raw)).decode("utf-8")
except Exception: except Exception:
+14 -10
View File
@@ -53,24 +53,28 @@ def create_pkl_name(full_name: str) -> str:
return pkl return pkl
def _read_pkl_bytes(pkl_path: Path) -> bytes: def _hash_pkl(pkl_path: Path) -> str:
manifest = Path(f"{pkl_path}.chunkmanifest") manifest = Path(f"{pkl_path}.chunkmanifest")
if manifest.exists(): if manifest.exists():
num_chunks = int(manifest.read_text().strip()) num_chunks = int(manifest.read_text().strip())
parts = [] paths = [Path(f"{pkl_path}.chunk{i + 1:02d}of{num_chunks:02d}") for i in range(num_chunks)]
for i in range(num_chunks): else:
chunk = Path(f"{pkl_path}.chunk{i + 1:02d}of{num_chunks:02d}") paths = [pkl_path]
parts.append(chunk.read_bytes())
return b''.join(parts) digest = hashlib.sha256()
return pkl_path.read_bytes() for path in paths:
with path.open('rb') as f:
while block := f.read(1024 * 1024):
digest.update(block)
return digest.hexdigest()
def _find_driving_pkl(output_path: Path) -> Path | None: def _find_driving_pkl(output_path: Path) -> Path | None:
for pattern in ('driving_tinygrad.pkl', 'driving_*_tinygrad.pkl'): for pattern in ('*driving_tinygrad.pkl', '*driving_*_tinygrad.pkl'):
matches = sorted(output_path.glob(pattern)) matches = sorted(output_path.glob(pattern))
if matches: if matches:
return matches[0] return matches[0]
for pattern in ('driving_tinygrad.pkl.chunkmanifest', 'driving_*_tinygrad.pkl.chunkmanifest'): for pattern in ('*driving_tinygrad.pkl.chunkmanifest', '*driving_*_tinygrad.pkl.chunkmanifest'):
matches = sorted(output_path.glob(pattern)) matches = sorted(output_path.glob(pattern))
if matches: if matches:
return Path(str(matches[0]).removesuffix('.chunkmanifest')) return Path(str(matches[0]).removesuffix('.chunkmanifest'))
@@ -87,7 +91,7 @@ def _rename_pkl_with_chunks(old_pkl: Path, new_pkl: Path) -> Path:
def generate_chunked_model(driving_pkl: Path) -> dict: def generate_chunked_model(driving_pkl: Path) -> dict:
tinygrad_hash = hashlib.sha256(_read_pkl_bytes(driving_pkl)).hexdigest() tinygrad_hash = _hash_pkl(driving_pkl)
chunks_config = [] chunks_config = []
manifest_file = Path(f"{driving_pkl}.chunkmanifest") manifest_file = Path(f"{driving_pkl}.chunkmanifest")