diff --git a/.github/workflows/test_models.yml b/.github/workflows/test_models.yml new file mode 100644 index 0000000000..443bc14023 --- /dev/null +++ b/.github/workflows/test_models.yml @@ -0,0 +1,87 @@ +name: Test Models Compatibility With New Tinygrad +on: + push: + paths: + - 'tinygrad_repo/**' + - '.github/workflows/test_models.yml' + - 'openpilot/sunnypilot/modeld_v2/**' + pull_request: + paths: + - 'tinygrad_repo/**' + - '.github/workflows/test_models.yml' + - 'openpilot/sunnypilot/modeld_v2/**' + workflow_dispatch: + +jobs: + generate-matrix: + runs-on: ubuntu-latest + outputs: + models: ${{ steps.set-matrix.outputs.models }} + steps: + - uses: actions/checkout@v4 + - name: Fetch and Parse json + id: set-matrix + run: | + python3 -c ' + import json, urllib.request, sys, os, re + + with open("openpilot/sunnypilot/models/fetcher.py", "r") as f: + content = f.read() + match = re.search(r"MODEL_URL\s*=\s*[\"'"'"']([^\"'"'"']+)[\"'"'"']", content) + if not match: + raise ValueError("MODEL_URL not found in fetcher.py") + url = match.group(1) + + req = urllib.request.urlopen(url) + data = json.loads(req.read()) + + artifacts = [] + for bundle in data.get("bundles", []): + for model in bundle.get("models", []): + if "artifact" in model: + artifacts.append(model["artifact"]) + + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write(f"models={json.dumps(artifacts)}\n") + ' + + test-model: + name: Test ${{ matrix.artifact.file_name }} + needs: generate-matrix + runs-on: ubuntu-latest + container: ghcr.io/commaai/openpilot-base:latest + strategy: + fail-fast: false + matrix: + artifact: ${{ fromJson(needs.generate-matrix.outputs.models) }} + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Download Model Chunks in Parallel + run: | + mkdir -p /tmp/model_chunks + echo '${{ toJson(matrix.artifact.chunks) }}' > chunks.json + + BASE_URL="${{ matrix.artifact.download_uri.url }}" + BASE_DIR=$(dirname "$BASE_URL") + + python3 -c ' + import json, os + with open("chunks.json") as f: + chunks = json.load(f) + manifest_path = f"/tmp/model_chunks/${{ matrix.artifact.file_name }}.chunkmanifest" + with open(manifest_path, "w") as f: + f.write(str(len(chunks))) + for c in chunks: + print(c["file_name"]) + ' | xargs -I {} -P 10 bash -c "echo 'Downloading {}...' && curl -f -s -S -L -o '/tmp/model_chunks/{}' \"$BASE_DIR/{}\"" + + - name: Run Model Compatibility Test + env: + MODEL_BASE_NAME: ${{ matrix.artifact.file_name }} + MODEL_CHUNK_DIR: "/tmp/model_chunks" + PYTHONPATH: ".:./tinygrad_repo" + run: | + python3 -m pytest openpilot/sunnypilot/modeld_v2/tests/test_models.py diff --git a/openpilot/sunnypilot/modeld_v2/helpers.py b/openpilot/sunnypilot/modeld_v2/helpers.py new file mode 100644 index 0000000000..15f5e56a8d --- /dev/null +++ b/openpilot/sunnypilot/modeld_v2/helpers.py @@ -0,0 +1,93 @@ +import io +import struct +import pickle +import inspect +import importlib +import enum + + +def _pad_args(func, args, kwargs): + try: + sig = inspect.signature(func) + except Exception: + return args, kwargs + params = list(sig.parameters.values()) + if inspect.isfunction(func) and params and params[0].name in ('cls', 'self'): + params = params[1:] + + new_args = list(args) + has_varargs = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in params) + if len(new_args) > len(params) and not has_varargs: + new_args = new_args[:len(params)] + + for i in range(len(new_args), len(params)): + param = params[i] + if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD): + continue + val = param.default if param.default is not inspect.Parameter.empty else None + new_args.append(val) + return new_args, kwargs + + +def _enum_factory(enum_class): + def factory(*args, **kwargs): + try: + return enum_class(*args, **kwargs) + # OptOps and UOp objects in the .pkl are left over from the compilation phase, + # reassignment does nothing because they aren't tied to the execution graph + # It never executes or evaluates the UOp nodes again. + except ValueError: + return list(enum_class)[0] + factory.__name__ = enum_class.__name__ + factory.__module__ = enum_class.__module__ + return factory + + +def _dynamic_factory(real_class): + if isinstance(real_class, type) and issubclass(real_class, enum.Enum): + return _enum_factory(real_class) + + def factory(*args, **kwargs): + try: + return real_class(*args, **kwargs) + except TypeError: + new_args, new_kwargs = _pad_args(real_class, args, kwargs) + return real_class(*new_args, **new_kwargs) + + class DynamicMeta(type(real_class)): + def __call__(cls, *args, **kwargs): + return factory(*args, **kwargs) + + class DynamicProxy(real_class, metaclass=DynamicMeta): + __slots__ = () + + def __new__(cls, *args, **kwargs): + return factory(*args, **kwargs) + + DynamicProxy.__name__ = real_class.__name__ + DynamicProxy.__module__ = real_class.__module__ + return DynamicProxy + + +class DynamicTinygradUnpickler(pickle.Unpickler): + def find_class(self, module, name): + if module == "tinygrad.ops": + try: + importlib.import_module("tinygrad.uops") + module = "tinygrad.uops" + except ImportError: + pass + real_class = getattr(importlib.import_module(module), name) + if module.startswith("tinygrad"): + return _dynamic_factory(real_class) + return real_class + + +def load_oob(f): + opcodes = f.read(struct.unpack('