Compare commits

..

20 Commits

Author SHA1 Message Date
discountchubbs 59776a7f95 wmma instruct 2026-11-07 20:58:45 -08:00
discountchubbs 7668c8402c resolver head 2026-11-07 19:21:58 -08:00
discountchubbs e4153e6329 limit das speed 2026-11-07 17:54:43 -08:00
discountchubbs 014e9edbbf sp chd model v2 2026-11-07 17:27:12 -08:00
discountchubbs ac638611bd sp chd model 2026-11-07 09:28:15 -08:00
discountchubbs 103ee81db1 bump 2026-11-07 09:21:53 -08:00
discountchubbs 1bf5473451 rm 2026-11-06 23:15:01 -08:00
discountchubbs 2c94b99ee8 emuuuu 2026-11-06 23:13:00 -08:00
discountchubbs 87e56f8294 replay check 2026-11-06 22:30:29 -08:00
discountchubbs cb97631449 parity cache 2026-11-06 22:20:54 -08:00
discountchubbs 848c8c1af9 cache by cpu instruct 2026-11-06 21:53:59 -08:00
discountchubbs 3ed5849abb emu cache 2026-11-06 21:38:45 -08:00
discountchubbs 24112edb95 cache 2026-11-06 21:15:50 -08:00
discountchubbs e77f239ace oop 2026-11-06 21:05:10 -08:00
discountchubbs e972ee35c0 serve over libusb 2026-11-06 21:01:34 -08:00
discountchubbs 5f7a17ed7b mock amd: x86 hw fma 2026-11-06 20:35:52 -08:00
discountchubbs 8bea2d48c1 modeld_v2: emu 2026-11-06 15:18:45 -08:00
Jason Wen 3d8f211d4b bump opendbc 2026-11-06 16:03:02 -05:00
James Vecellio-Grant 02ef6e4ce0 modeld_v2: artifact parity with upstream tg (#2093)
* modeld_v2: artifact parity with upstream tg

* m

* m

* Update helpers.py

* ew

* Update convert_upstream_pkl.py

* Update test_convert_upstream_pkl.py

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-11-05 16:11:27 -05:00
Nayan a1a28e467e [models] Support major.minor versioning (#2092)
support major.minor versioning, becase why not
2026-11-05 15:35:07 -05:00
28 changed files with 600 additions and 207 deletions
@@ -34,6 +34,11 @@ on:
required: false
type: string
default: 'qcom'
on_device:
description: 'Compile on the tici / chestnut runner instead of on a hosted runner with tinygrad''s qcom / amd mock'
required: false
type: boolean
default: false
hf_repo:
description: 'Hugging Face dataset repository (e.g. sunnypilot/sunnypilot_models_v1)'
required: false
@@ -107,6 +112,11 @@ on:
options:
- qcom
- chestnut
on_device:
description: 'Compile on the tici / chestnut runner instead of on a hosted runner with tinygrad''s qcom / amd mock'
required: false
type: boolean
default: false
hf_repo:
description: 'Hugging Face dataset repository'
required: false
@@ -130,6 +140,7 @@ jobs:
is_20hz: ${{ inputs.is_20hz }}
artifact_suffix: ${{ inputs.artifact_suffix }}
target_hardware: ${{ inputs.target_hardware }}
on_device: ${{ inputs.on_device || false }}
secrets: inherit
publish_model:
@@ -0,0 +1,61 @@
name: Chestnut mock build parity
description: >-
Convert one commaai precompiled chestnut model (LS2) on this runner under tinygrad's amd mock, as a hosted chestnut build does, and require the
exact bytes the chestnut runner built (the published artifact). Needs setup-amd-mock, numpy and zstandard; tinygrad_repo checked out.
runs:
using: composite
steps:
- name: Key this check by everything its result depends on
id: key
shell: bash
env:
SOURCES: ${{ hashFiles('tinygrad_repo/tinygrad/**/*.py', 'tinygrad_repo/test/mockgpu/**', 'openpilot/sunnypilot/modeld_v2/*.py', '.github/workflows/chestnut-build-parity/action.yml') }}
run: echo "key=chestnut-build-parity-$SOURCES-$(sha256sum "$LLVM_PATH" | cut -c1-16)-$(python3 -c 'import platform; print(platform.python_version())')" >> "$GITHUB_OUTPUT"
- name: Passed before with all of it identical?
id: passed
uses: actions/cache/restore@v4
with:
path: ~/.chestnut_build_parity.passed
key: ${{ steps.key.outputs.key }}
- name: Rebuild LS2 on the mock and require the chestnut runner's artifact byte for byte
if: steps.passed.outputs.cache-hit != 'true'
shell: bash
env:
REF: b92fc4828664f8767bce5e92f1630ed319cbe509 # commaai/openpilot: precompiled big_driving_tinygrad.pkl, pins tinygrad ec91b74b5b
PKL_OID: b4dead3ee091d66ab002a6b8e716c26f4f79e3c931e4be5f48e017ba92f6e960
PKL_SIZE: "767228907"
TG_SHA: ec91b74b5b6b0fc924b72d3f27689679ca2438b9
WANT: 680a8d629785f2b1d1a2b0e40d0dfbcd0f7291c21e20c51259f1d768a22ec217 # driving_light_speed_2_november_04_2026_tinygrad.pkl, recompiled46
run: |
set -euo pipefail
work="$RUNNER_TEMP/chestnut_parity" && rm -rf "$work" && mkdir -p "$work"
href=$(curl -fsSL --retry 3 -X POST -H "Accept: application/vnd.git-lfs+json" -H "Content-Type: application/vnd.git-lfs+json" \
https://huggingface.co/commaai/openpilot-lfs.git/info/lfs/objects/batch \
-d "{\"operation\":\"download\",\"transfer\":[\"basic\"],\"objects\":[{\"oid\":\"$PKL_OID\",\"size\":$PKL_SIZE}]}" | jq -r '.objects[0].actions.download.href')
curl -fsSL --retry 3 -o "$work/big_driving_tinygrad.pkl" "$href"
echo "$PKL_OID $work/big_driving_tinygrad.pkl" | sha256sum -c -
tg="$work/tinygrad" && git init -q "$tg" && git -C "$tg" fetch -q --depth 1 https://github.com/tinygrad/tinygrad "$TG_SHA" && git -C "$tg" checkout -q FETCH_HEAD
CHECK_OOB=0 GMMU=0 PYTHONPATH="$GITHUB_WORKSPACE/tinygrad_repo" python3 tinygrad_repo/test/mockgpu/usb_server.py \
env DEV=USB+AMD PYTHONPATH="$tg:$GITHUB_WORKSPACE" python3 openpilot/sunnypilot/modeld_v2/convert_upstream_pkl.py "$work/big_driving_tinygrad.pkl" "$work/LS2.pkl"
got=$(sha256sum "$work/LS2.pkl" | cut -d' ' -f1)
if [ "$got" != "$WANT" ]; then
echo "::error::the mock's LS2 build ($got) is not the chestnut runner's ($WANT): this runner's LLVM or the mock no longer builds what the chestnut does"
exit 1
fi
echo "LS2 rebuilt on the mock: byte-identical to the chestnut runner's build"
touch ~/.chestnut_build_parity.passed
- name: Remember the pass
if: steps.passed.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ~/.chestnut_build_parity.passed
key: ${{ steps.key.outputs.key }}
- name: Already passed
if: steps.passed.outputs.cache-hit == 'true'
shell: bash
run: |
echo "LS2 rebuild skipped: it already matched the chestnut runner's build with this exact mock, converter, LLVM and python"
@@ -0,0 +1,60 @@
name: Set up tinygrad's amd mock
description: LLVM 20 and ROCm comgr, as tinygrad_repo/.github/actions/setup-tinygrad installs them for the mock gpu
inputs:
emu_cache:
description: >-
Keep tinygrad's cache db (the emulator's compiled kernels and gate results) between runs under this name, e.g. the model's file name.
Empty: no cache.
default: ''
runs:
using: composite
steps:
- name: Install LLVM 20 and ROCm comgr
shell: bash
run: |
. /etc/os-release
curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc > /dev/null
echo "deb http://apt.llvm.org/$VERSION_CODENAME/ llvm-toolchain-$VERSION_CODENAME-20 main" | sudo tee /etc/apt/sources.list.d/llvm.list
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.radeon.com/rocm/rocm.gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/rocm.gpg > /dev/null
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/rocm/apt/7.1 $VERSION_CODENAME main" | sudo tee /etc/apt/sources.list.d/rocm.list
printf 'Package: *\nPin: release o=repo.radeon.com\nPin-Priority: 600\n' | sudo tee /etc/apt/preferences.d/rocm-pin-600
sudo apt-get -qq update && sudo apt-get -qq install -y --no-install-recommends libllvm20 clang-20 lld-20 comgr
printf '/opt/rocm/lib\n/opt/rocm/lib64\n' | sudo tee /etc/ld.so.conf.d/rocm.conf && sudo ldconfig
echo "/usr/lib/llvm-20/bin" >> "$GITHUB_PATH"
llvm=$(ls /usr/lib/llvm-20/lib/libLLVM*.so* /usr/lib/*-linux-gnu/libLLVM-20.so* 2>/dev/null | head -1 || true)
[ -n "$llvm" ] || { echo "::error::libLLVM 20 not found"; exit 1; }
echo "LLVM_PATH=$llvm" >> "$GITHUB_ENV"
- name: Cache the chestnut's firmware
uses: actions/cache@v4
with:
path: ~/.cache/tinygrad/downloads/fw
key: chestnut-fw-${{ hashFiles('tinygrad_repo/tinygrad/runtime/autogen/am/fw.py') }}
- name: Fetch the chestnut's firmware (gitlab times out now and then; the mock boots from it)
shell: bash
run: |
for i in 1 2 3 4 5; do
PYTHONPATH="$GITHUB_WORKSPACE/tinygrad_repo" python3 -c "
from tinygrad.helpers import fetch_fw
from tinygrad.runtime.autogen.am import fw
for n, h in fw.hashes.items():
if n.startswith(('psp_14_0_2_', 'smu_14_0_2.', 'sdma_7_0_0.', 'gc_12_0_0_')): fetch_fw('amdgpu', n, h)" && exit 0
sleep $((i * 10))
done
echo "::error::could not fetch the chestnut's firmware from gitlab"; exit 1
- name: runner cpu
if: inputs.emu_cache != ''
id: cpu
shell: bash
run: echo "cpu=$( (grep -m1 '^model name' /proc/cpuinfo; grep -m1 '^flags' /proc/cpuinfo) | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT"
- name: Keep the emulator's compiled kernels between runs
if: inputs.emu_cache != ''
uses: actions/cache@v4
with:
path: ~/.cache/tinygrad/cache.db
key: emu-${{ steps.cpu.outputs.cpu }}-${{ hashFiles('tinygrad_repo/tinygrad/**/*.py', 'tinygrad_repo/test/mockgpu/**') }}-${{ inputs.emu_cache }}
restore-keys: emu-${{ steps.cpu.outputs.cpu }}-${{ hashFiles('tinygrad_repo/tinygrad/**/*.py', 'tinygrad_repo/test/mockgpu/**') }}-
+88 -12
View File
@@ -35,6 +35,11 @@ on:
required: false
type: string
default: 'qcom'
on_device:
description: 'Compile on the tici / chestnut runner instead of on a hosted runner with tinygrad''s qcom / amd mock'
required: false
type: boolean
default: false
workflow_dispatch:
inputs:
upstream_branch:
@@ -59,6 +64,11 @@ on:
- qcom
- chestnut
default: 'qcom'
on_device:
description: 'Compile on the tici / chestnut runner instead of on a hosted runner with tinygrad''s qcom / amd mock'
required: false
type: boolean
default: false
run-name: Build model [${{ inputs.custom_name || inputs.upstream_branch }}] from ref [${{ inputs.upstream_branch }}]
@@ -153,18 +163,67 @@ jobs:
if-no-files-found: error
build_model:
runs-on: [self-hosted, "${{ inputs.target_hardware == 'chestnut' && 'chestnut' || 'tici' }}"]
runs-on: ${{ inputs.on_device && fromJSON(inputs.target_hardware == 'chestnut' && '["self-hosted","chestnut"]' || '["self-hosted","tici"]') || 'ubuntu-24.04' }}
needs: get_model
env:
MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }})
REF: ${{ inputs.upstream_branch }}
MOCK_BUILD: ${{ !inputs.on_device }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
submodules: recursive
- name: Set up the hosted mock build
if: env.MOCK_BUILD == 'true'
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install LLVM 20 and ROCm comgr for the amd mock
if: env.MOCK_BUILD == 'true' && inputs.target_hardware == 'chestnut'
uses: ./.github/workflows/setup-amd-mock
with:
emu_cache: build
- name: Install the amd mock build dependencies
if: env.MOCK_BUILD == 'true' && inputs.target_hardware == 'chestnut'
run: |
python3 -m pip install -q numpy zstandard
- name: Install the qcom mock build dependencies
if: env.MOCK_BUILD == 'true' && inputs.target_hardware != 'chestnut'
id: qcom_deps
run: |
sudo apt-get -qq update && sudo apt-get -qq install -y --no-install-recommends qemu-user-static aria2
python3 -m pip install -q numpy zstandard tinymesa==25.2.7.2
echo "qemu=$(dpkg-query -W -f='${Version}' qemu-user-static)" >> "$GITHUB_OUTPUT"
- name: Compiler check passed before with all of it identical?
if: env.MOCK_BUILD == 'true' && inputs.target_hardware != 'chestnut'
id: qcom_compiler
uses: actions/cache/restore@v4
with:
path: ~/.qcom_compiler_parity.passed
key: qcom-compiler-parity-${{ steps.qcom_deps.outputs.qemu }}-${{ hashFiles('tinygrad_repo/test/device/qcom/compiler_parity.py', 'tinygrad_repo/test/mockgpu/qcom/a630_compiled.pkl.xz', 'tinygrad_repo/tinygrad/runtime/support/compiler_qcom.py') }}
- name: Check the off-device qcom compiler builds the a630's bytes
if: env.MOCK_BUILD == 'true' && inputs.target_hardware != 'chestnut' && steps.qcom_compiler.outputs.cache-hit != 'true'
run: |
# every kernel an a630 has compiled must compile to the same bytes here before a model is built here
PYTHONPATH="$TINYGRAD_PATH" python3 "$TINYGRAD_PATH/test/device/qcom/compiler_parity.py" check "$TINYGRAD_PATH/test/mockgpu/qcom/a630_compiled.pkl.xz"
touch ~/.qcom_compiler_parity.passed
- name: Remember the compiler check passed
if: env.MOCK_BUILD == 'true' && inputs.target_hardware != 'chestnut' && steps.qcom_compiler.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ~/.qcom_compiler_parity.passed
key: ${{ steps.qcom_compiler.outputs.cache-primary-key }}
- name: Set environment variables
if: env.MOCK_BUILD != 'true'
id: set-env
run: |
# Set up common environment
@@ -177,6 +236,7 @@ jobs:
printenv >> $GITHUB_ENV
- name: Setup build environment
if: env.MOCK_BUILD != 'true'
run: |
mkdir -p "${BUILD_DIR}/"
sudo find $BUILD_DIR/ -mindepth 1 -delete
@@ -198,21 +258,28 @@ jobs:
- name: Build Model
run: |
source /etc/profile
export UV_PROJECT_ENVIRONMENT=${HOME}/venv
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
if [ "$MOCK_BUILD" = "true" ]; then
TASKSET=""
export CHECK_OOB=0 GMMU=0 # as test/mockgpu runs the amd mock
else
source /etc/profile
export UV_PROJECT_ENVIRONMENT=${HOME}/venv
export VIRTUAL_ENV=$UV_PROJECT_ENVIRONMENT
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
TASKSET="taskset -c 7"
fi
export PYTHONPATH="${PYTHONPATH}:${{ env.TINYGRAD_PATH }}:${{ github.workspace }}"
COMPILE_MODELD="${{ github.workspace }}/openpilot/sunnypilot/modeld_v2/compile_modeld.py"
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}')")
TG_FLAGS_QCOM="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 OPENPILOT_HACKS=1"
MOCK=$([ "$MOCK_BUILD" = "true" ] && echo MOCK || true)
TG_FLAGS_QCOM="DEV=${MOCK:+MOCK+}QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 OPENPILOT_HACKS=1"
if [ "${{ inputs.target_hardware }}" == "chestnut" ]; then
echo "CHESTNUT build"
export CHESTNUT=1
TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 GMMU=0 TC_OPT=2 TC_MIN_GLOBALS=32"
TG_FLAGS="DEBUG=1 DEV=${MOCK}USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 GMMU=0 TC_OPT=2 TC_MIN_GLOBALS=32"
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
else
echo "QCOM build"
@@ -227,14 +294,16 @@ jobs:
echo "Converting commaai's precompiled model with tinygrad $TG_SHA"
UPSTREAM_TG="${{ runner.temp }}/upstream_tinygrad"
rm -rf "$UPSTREAM_TG" && git init -q "$UPSTREAM_TG" && git -C "$UPSTREAM_TG" fetch -q --depth 1 "$TG_URL" "$TG_SHA" && git -C "$UPSTREAM_TG" checkout -q FETCH_HEAD
ON_MOCK=""
if [ "$MOCK_BUILD" = "true" ]; then ON_MOCK="env PYTHONPATH=$TINYGRAD_PATH python3 $TINYGRAD_PATH/test/mockgpu/usb_server.py"; fi
CONVERTED="${{ runner.temp }}/big_driving_tinygrad_plain.pkl"
env DEV=USB+AMD PYTHONPATH="$UPSTREAM_TG:${{ github.workspace }}" taskset -c 7 python3 openpilot/sunnypilot/modeld_v2/convert_upstream_pkl.py \
$ON_MOCK env DEV=USB+AMD PYTHONPATH="$UPSTREAM_TG:${{ github.workspace }}" $TASKSET python3 openpilot/sunnypilot/modeld_v2/convert_upstream_pkl.py \
"${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl" "$CONVERTED"
mv "$CONVERTED" "$OUTPUT_PKL"
elif NEW_ARCH_ONNX=$(find "${{ env.MODELS_DIR }}" -maxdepth 1 -name "${{ inputs.target_hardware == 'chestnut' && 'big_' || '' }}*driving*.onnx" -print -quit) &&
[ -n "$NEW_ARCH_ONNX" ] && grep -qa new_img "$NEW_ARCH_ONNX"; then # the new architecture: one ONNX taking the warped frames as new_img
echo "Compiling new architecture ONNX: $NEW_ARCH_ONNX"
env ${TG_FLAGS} taskset -c 7 python3 openpilot/sunnypilot/modeld_v2/compile_modeld_v2.py \
env ${TG_FLAGS} $TASKSET python3 openpilot/sunnypilot/modeld_v2/compile_modeld_v2.py \
"$NEW_ARCH_ONNX" "$OUTPUT_PKL" --device-input "*" --benchmark-runs 1
else
echo "Detected onnx architecture. Routing to native compile_modeld.py."
@@ -284,7 +353,7 @@ jobs:
if [ -n "$MODEL_TYPE" ]; then
echo "Detected: $MODEL_TYPE -> $OUTPUT_PKL"
env ${TG_FLAGS} taskset -c 7 python3 "$COMPILE_MODELD" \
env ${TG_FLAGS} $TASKSET python3 "$COMPILE_MODELD" \
--model-type $MODEL_TYPE \
--model-size $MODEL_SIZE \
--camera-resolutions $CAMERA_RES \
@@ -293,6 +362,13 @@ jobs:
fi
fi
- name: Replay its check frame on the mock and require the result bit for bit
if: env.MOCK_BUILD == 'true'
env:
DEV: ${{ inputs.target_hardware == 'chestnut' && 'MOCKUSB+AMD' || 'MOCK+QCOM' }}
PYTHONPATH: ${{ env.TINYGRAD_PATH }}:${{ github.workspace }}
run: python3 -m openpilot.sunnypilot.modeld_v2.mock_parity check "${{ env.MODELS_DIR }}/${{ inputs.target_hardware == 'chestnut' && 'big_' || '' }}driving_tinygrad.pkl"
- name: Prepare Output
run: |
sudo rm -rf ${{ env.OUTPUT_DIR }}
@@ -304,7 +380,7 @@ jobs:
--include='/${{ inputs.target_hardware == 'chestnut' && 'big_' || '' }}driving_tinygrad.pkl' \
--exclude='*' \
--delete-excluded \
--chown=comma:comma \
$([ "$MOCK_BUILD" = "true" ] || echo --chown=comma:comma) \
${{ env.MODELS_DIR }}/ ${{ env.OUTPUT_DIR }}/
python3 "${{ github.workspace }}/release/ci/model_generator.py" \
@@ -333,7 +409,7 @@ jobs:
path: ${{ env.OUTPUT_DIR }}/artifact_name.txt
- name: Re-enable powersave
if: always()
if: always() && env.MOCK_BUILD != 'true'
run: |
source ${UV_PROJECT_ENVIRONMENT}/bin/activate
PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable
+105 -13
View File
@@ -28,24 +28,30 @@ jobs:
env:
DEV: CPU
run: tools/op.sh test -v openpilot/sunnypilot/modeld_v2/tests/test_plain_artifact.py
- name: Install LLVM 20 and ROCm comgr for the amd mock, as tinygrad_repo/.github/actions/setup-tinygrad does
run: |
. /etc/os-release
curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc > /dev/null
echo "deb http://apt.llvm.org/$VERSION_CODENAME/ llvm-toolchain-$VERSION_CODENAME-20 main" | sudo tee /etc/apt/sources.list.d/llvm.list
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.radeon.com/rocm/rocm.gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/rocm.gpg > /dev/null
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/rocm/apt/7.1 $VERSION_CODENAME main" | sudo tee /etc/apt/sources.list.d/rocm.list
printf 'Package: *\nPin: release o=repo.radeon.com\nPin-Priority: 600\n' | sudo tee /etc/apt/preferences.d/rocm-pin-600
sudo apt-get -qq update && sudo apt-get -qq install -y --no-install-recommends libllvm20 clang-20 lld-20 comgr
printf '/opt/rocm/lib\n/opt/rocm/lib64\n' | sudo tee /etc/ld.so.conf.d/rocm.conf && sudo ldconfig
echo "/usr/lib/llvm-20/bin" >> "$GITHUB_PATH"
- uses: ./.github/workflows/setup-amd-mock
- name: Convert a pickled USB+AMD jit and replay it under tinygrad's amd mock
env:
DEV: MOCKUSB+AMD
CHECK_OOB: 0
GMMU: 0
run: tools/op.sh test -v openpilot/sunnypilot/modeld_v2/tests/test_convert_upstream_pkl.py
- name: Resolve commaai's tinygrad pin, which their precompiled chestnut model is built and converted with
id: upstream-tg
env:
GH_TOKEN: ${{ github.token }}
run: |
read -r sha url < <(gh api repos/commaai/openpilot/contents/tinygrad_repo --jq '.sha + " " + .submodule_git_url')
echo "commaai/openpilot master pins $url@$sha, ours is $(git rev-parse HEAD:tinygrad_repo)"
dir="$RUNNER_TEMP/upstream_tinygrad"
git init -q "$dir" && git -C "$dir" fetch -q --depth 1 "$url" "$sha" && git -C "$dir" checkout -q FETCH_HEAD
echo "path=$dir" >> "$GITHUB_OUTPUT"
- name: Convert a pickled USB+AMD jit under commaai's tinygrad, as the chestnut model build does
env:
DEV: MOCKUSB+AMD
CHECK_OOB: 0
GMMU: 0
PYTHONPATH: ${{ steps.upstream-tg.outputs.path }}:${{ github.workspace }}
run: tools/op.sh test -v openpilot/sunnypilot/modeld_v2/tests/test_convert_upstream_pkl.py
- name: Where tinygrad moved, for the helper or failure point named above
if: failure() && github.event_name == 'pull_request'
run: |
@@ -63,6 +69,8 @@ jobs:
runs-on: ubuntu-latest
outputs:
models: ${{ steps.set-matrix.outputs.models }}
chestnut: ${{ steps.set-matrix.outputs.chestnut }}
qcom: ${{ steps.set-matrix.outputs.qcom }}
steps:
- uses: actions/checkout@v4
- name: Fetch and Parse json
@@ -74,16 +82,22 @@ jobs:
with open("openpilot/sunnypilot/models/fetcher.py", "r") as f:
urls = re.findall(r"MODEL_URL(?:_CHESTNUT)?\s*=\s*[\"'"'"']([^\"'"'"']+)[\"'"'"']", f.read())
artifacts = []
artifacts, chestnut, qcom = [], [], []
for url in urls:
data = json.loads(urllib.request.urlopen(url).read())
for bundle in data.get("bundles", []):
for model in bundle.get("models", []):
if "artifact" in model:
artifacts.append(model["artifact"])
if "chestnut" in url:
chestnut.append(model["artifact"])
elif "tinygrad" in model["artifact"]["file_name"]:
qcom.append(model["artifact"])
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write(f"models={json.dumps(artifacts)}\n")
f.write(f"chestnut={json.dumps(chestnut)}\n")
f.write(f"qcom={json.dumps(qcom)}\n")
'
test-model:
@@ -120,3 +134,81 @@ jobs:
export PYTHONPATH="$GITHUB_WORKSPACE/tinygrad_repo:$GITHUB_WORKSPACE:$PYTHONPATH"
python3 -m pip uninstall -y tinygrad || uv pip uninstall -y tinygrad --system || true
python3 -m pytest openpilot/sunnypilot/modeld_v2/tests/test_models.py
chestnut-mock-parity:
name: Mock parity ${{ matrix.artifact.file_name }}
needs: generate-matrix
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
artifact: ${{ fromJson(needs.generate-matrix.outputs.chestnut) }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-amd-mock
with:
emu_cache: ${{ matrix.artifact.file_name }}
- run: python3 -m pip install -q numpy
- name: Download model
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
MODEL_URL: ${{ matrix.artifact.download_uri.url }}
MODEL_SHA: ${{ matrix.artifact.download_uri.sha256 }}
shell: bash
run: |
AUTH=()
if [ -n "$HF_TOKEN" ]; then AUTH=(-H "Authorization: Bearer $HF_TOKEN"); fi
curl --retry 3 --retry-all-errors -s -S -f -L "${AUTH[@]}" -o "$RUNNER_TEMP/model.pkl" "$MODEL_URL"
if [ -n "$MODEL_SHA" ]; then echo "$MODEL_SHA $RUNNER_TEMP/model.pkl" | sha256sum -c -; fi
- name: Replay its check frame on the amd mock and require the chestnut's result bit for bit
env:
PYTHONPATH: ${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}
run: python3 -m openpilot.sunnypilot.modeld_v2.mock_parity check "$RUNNER_TEMP/model.pkl"
qcom-mock-parity:
name: QCOM mock parity ${{ matrix.artifact.file_name }}
needs: generate-matrix
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
artifact: ${{ fromJson(needs.generate-matrix.outputs.qcom) }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: python3 -m pip install -q numpy tinymesa==25.2.7.2
- name: Download model
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
MODEL_URL: ${{ matrix.artifact.download_uri.url }}
MODEL_SHA: ${{ matrix.artifact.download_uri.sha256 }}
shell: bash
run: |
AUTH=()
if [ -n "$HF_TOKEN" ]; then AUTH=(-H "Authorization: Bearer $HF_TOKEN"); fi
curl --retry 3 --retry-all-errors -s -S -f -L "${AUTH[@]}" -o "$RUNNER_TEMP/model.pkl" "$MODEL_URL"
if [ -n "$MODEL_SHA" ]; then echo "$MODEL_SHA $RUNNER_TEMP/model.pkl" | sha256sum -c -; fi
- name: Replay its check frame on the qcom mock and require the comma device's result bit for bit
env:
DEV: MOCK+QCOM
PYTHONPATH: ${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}
run: python3 -m openpilot.sunnypilot.modeld_v2.mock_parity check "$RUNNER_TEMP/model.pkl"
chestnut-mock-build-parity:
name: Chestnut build on the amd mock = chestnut runner build
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: ./.github/workflows/setup-amd-mock
with:
emu_cache: chestnut-build-parity
- run: python3 -m pip install -q numpy zstandard
- uses: ./.github/workflows/chestnut-build-parity
-1
View File
@@ -4,7 +4,6 @@
[submodule "opendbc"]
path = opendbc_repo
url = https://github.com/sunnypilot/opendbc.git
branch = toyotabsmhack
[submodule "msgq"]
path = msgq_repo
url = https://github.com/sunnypilot/msgq.git
-1
View File
@@ -245,7 +245,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}},
{"ToyotaEnhancedBsm", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaTSS2Smooth", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:65a08adc31d5c456219687d99b7bf5e44d61dae2d49ea67850e76105c7248cce
size 60918562
oid sha256:24c31e45870474b48e0394be95b70db00829f58ceaf4cb8bce2a776c37fb1acb
size 82587108
@@ -12,16 +12,10 @@ from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
from opendbc.car.toyota.values import ToyotaFlags
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
SNG_HACK_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control must be available and enabled for your vehicle to use this feature.")
BSM_UNAVAILABLE = tr_noop("Enhanced Blind Spot Monitoring requires a TSS2 Toyota/Lexus without SecOC, " +
"or a smartDSU with sunnypilot longitudinal control.")
BSM_STOCK_LONG_UNAVAILABLE = tr_noop("Enhanced Blind Spot Monitoring is not available while Enforce Factory Longitudinal Control is enabled.")
BSM_REQUIRED = tr_noop("This vehicle does not report blind spot status over the standard CAN signals. " +
"Enhanced Blind Spot Monitoring is required for blind spot detection.")
PLATFORM_UNAVAILABLE = tr_noop("This feature is currently not available on this platform.")
DESCRIPTIONS = {
@@ -32,10 +26,6 @@ DESCRIPTIONS = {
'sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. ' +
'This feature is only applicable to certain models that are able to use longitudinal control. This is an alpha feature. Use at your own risk.'
),
'enhanced_bsm': tr_noop(
'sunnypilot will poll the blind spot monitor diagnostics directly from the radar. Use this on vehicles whose blind spot monitors ' +
'do not report blind spot status over the standard CAN signals. This is an alpha feature. Use at your own risk.'
),
'tss2_smooth': tr_noop(
'Enable smoother longitudinal control for TSS2 vehicles. ' +
'Uses a custom speed-dependent integral gain curve for smoother traffic driving.'
@@ -63,14 +53,6 @@ class ToyotaSettings(BrandSettings):
enabled=lambda: not ui_state.engaged,
)
self.enhanced_bsm = toggle_item_sp(
lambda: tr("Enhanced Blind Spot Monitoring (Alpha)"),
description=lambda: tr(DESCRIPTIONS["enhanced_bsm"]),
initial_state=ui_state.params.get_bool("ToyotaEnhancedBsm"),
callback=self._on_enable_enhanced_bsm,
enabled=lambda: not ui_state.engaged,
)
self.tss2_smooth = toggle_item_sp(
lambda: tr("TSS2 Smooth Longitudinal (Alpha)"),
description=lambda: tr(DESCRIPTIONS["tss2_smooth"]),
@@ -82,7 +64,6 @@ class ToyotaSettings(BrandSettings):
self.items = [
self.enforce_stock_longitudinal,
self.stop_and_go_hack,
self.enhanced_bsm,
self.tss2_smooth,
]
@@ -149,25 +130,6 @@ class ToyotaSettings(BrandSettings):
ui_state.params.put_bool("ToyotaStopAndGoHack", False)
ui_state.params.put_bool("OnroadCycleRequested", True)
def _on_enable_enhanced_bsm(self, state: bool):
if state:
def confirm_callback(result: int):
if result == DialogResult.CONFIRM:
ui_state.params.put_bool("ToyotaEnhancedBsm", True)
ui_state.params.put_bool("OnroadCycleRequested", True)
else:
self.enhanced_bsm.action_item.set_state(False)
content = (f"<h1>{self.enhanced_bsm.title}</h1><br>" +
f"<p>{self.enhanced_bsm.description}</p>")
dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback)
gui_app.push_widget(dlg)
else:
ui_state.params.put_bool("ToyotaEnhancedBsm", False)
ui_state.params.put_bool("OnroadCycleRequested", True)
def update_settings(self):
if ui_state.CP is not None:
longitudinal = ui_state.CP.openpilotLongitudinalControl
@@ -188,31 +150,6 @@ class ToyotaSettings(BrandSettings):
if show_desc:
self.stop_and_go_hack.show_description(True)
# Enhanced BSM needs the radar diagnostic address, which is only in the TX allowlist for
# TSS2 cars without SecOC, or for TSS-P cars whose smartDSU lets sunnypilot control
# longitudinal. Enforcing factory longitudinal control rules both of those out.
sdsu_bsm_supported = (longitudinal and ui_state.CP_SP is not None and
bool(ui_state.CP_SP.flags & ToyotaFlagsSP.SMART_DSU))
bsm_supported = (not bool(ui_state.CP.flags & ToyotaFlags.SECOC) and
(bool(ui_state.CP.flags & ToyotaFlags.TSS2) or sdsu_bsm_supported))
bsm_available = bsm_supported and not enforce_stock
self.enhanced_bsm.action_item.set_enabled(bsm_available and not ui_state.engaged)
bsm_desc = tr(DESCRIPTIONS["enhanced_bsm"])
if not bsm_supported:
self.enhanced_bsm.action_item.set_state(False)
bsm_desc = "<b>" + tr(BSM_UNAVAILABLE) + "</b>\n\n" + bsm_desc
elif enforce_stock:
self.enhanced_bsm.action_item.set_state(False)
bsm_desc = "<b>" + tr(BSM_STOCK_LONG_UNAVAILABLE) + "</b>\n\n" + bsm_desc
elif ui_state.CP_SP is not None and ui_state.CP_SP.flags & ToyotaFlagsSP.SP_NEED_DEBUG_BSM:
bsm_desc = "<b>" + tr(BSM_REQUIRED) + "</b>\n\n" + bsm_desc
if self.enhanced_bsm.description != bsm_desc:
self.enhanced_bsm.set_description(bsm_desc)
if not bsm_available:
self.enhanced_bsm.show_description(True)
tss2 = bool(ui_state.CP.flags & ToyotaFlags.TSS2)
if longitudinal and not enforce_stock and tss2:
self.tss2_smooth.action_item.set_enabled(not ui_state.engaged)
@@ -236,12 +173,6 @@ class ToyotaSettings(BrandSettings):
self.stop_and_go_hack.set_description(new_desc)
self.stop_and_go_hack.show_description(True)
self.enhanced_bsm.action_item.set_enabled(False)
bsm_desc = "<b>" + tr(ONROAD_ONLY_DESCRIPTION) + "</b>\n\n" + tr(DESCRIPTIONS["enhanced_bsm"])
if self.enhanced_bsm.description != bsm_desc:
self.enhanced_bsm.set_description(bsm_desc)
self.enhanced_bsm.show_description(True)
self.tss2_smooth.action_item.set_enabled(False)
new_desc = "<b>" + tr(ONROAD_ONLY_DESCRIPTION) + "</b>\n\n" + tr(DESCRIPTIONS["tss2_smooth"])
if self.tss2_smooth.description != new_desc:
@@ -371,7 +371,7 @@ def _compile_warp_resolution_worker(cam_w, cam_h, model_w, model_h, benchmark_ru
if __name__ == "__main__":
if 'USB' in os.getenv('DEV', '') or os.getenv('CHESTNUT'):
if ('USB' in os.getenv('DEV', '') or os.getenv('CHESTNUT')) and not os.getenv('DEV', '').startswith('MOCK'):
from openpilot.system.hardware.chestnut.flash import link_up
for _ in range(10):
if link_up():
@@ -10,6 +10,7 @@ import dataclasses
import inspect
import itertools
import mmap
import os
import re
import struct
import tempfile
@@ -66,47 +67,75 @@ def _patched(buf, topo) -> tuple[bytearray, dict]:
return blob, rows
def _evaluate(u, env):
def _packed_args(program) -> dict[int, int]:
from tinygrad.device import TinyELF
params = getattr(program.arg, "params", None) or ()
return {off: p.arg.slot for (off, _), p in zip(TinyELF.iter_sig(tuple((None, p.addrspace, p.dtype, ()) for p in params)), params, strict=True)}
def _outer_slot(u, packed: dict[int, int]) -> int | None:
from tinygrad.uop.ops import Ops
if u.op is Ops.PARAM:
return None if packed else u.arg.slot
if u.op.name == "REFPARAM":
return u.arg.slot
if packed and u.op is Ops.LOAD and u.src[0].op is Ops.INDEX and _root(u.src[0].src[0]).op is Ops.PARAM:
index = _evaluate(u.src[0].src[1], {}, packed)
return None if index is None else packed.get(index * u.dtype.itemsize)
return None
def _evaluate(u, env, packed: dict[int, int]):
from tinygrad.uop.ops import GroupOp, Ops, exec_alu
if u.op in (Ops.AFTER, Ops.CAST, Ops.BITCAST):
return _evaluate(u.src[0], env)
return _evaluate(u.src[0], env, packed)
if u.op is Ops.CONST:
return u.arg
if u.op in (Ops.PARAM, Ops.RANGE):
return env.get(u.arg.slot if u.op is Ops.PARAM else u)
if u.op is Ops.RANGE:
return env.get(u)
if (slot := _outer_slot(u, packed)) is not None:
return env.get(slot)
if u.op is Ops.LOAD:
table, index = _root(u.src[0].src[0]), _evaluate(u.src[0].src[1], env)
table, index = _root(u.src[0].src[0]), _evaluate(u.src[0].src[1], env, packed)
return struct.unpack_from(u.dtype.fmt, table.arg, index * u.dtype.itemsize)[0] if table.op is Ops.BINARY and index is not None else None
if u.op in GroupOp.ALU and None not in (values := [_evaluate(x, env) for x in u.src]):
if u.op in GroupOp.ALU and None not in (values := [_evaluate(x, env, packed) for x in u.src]):
return exec_alu(u.op, u.dtype, values, *([False] if "truncate_output" in inspect.signature(exec_alu).parameters else []))
return None
def _input_pokes(submit, kernargs) -> dict[int, tuple[int, int]]:
from tinygrad.uop.ops import Ops
outer = dict(enumerate(submit.src[1:]))
outer, packed = dict(enumerate(submit.src[1:])), _packed_args(submit.src[0])
kernargs_params = {i for i, a in outer.items() if a.op is Ops.GETADDR and _root(a.src[0]) is kernargs}
inputs = {i: _root(a.src[0]).arg.slot for i, a in outer.items() if a.op is Ops.GETADDR and _root(a.src[0]).op is Ops.PARAM}
pokes = {}
for call in (u for u in submit.src[0].src[0].toposort() if u.op is Ops.CALL and len(u.src) > 3):
_, address, value, *_ = call.src[1:]
used = list({x.arg.slot for x in value.toposort() if x.op is Ops.PARAM} & inputs.keys())
if len(used) != 1 or not kernargs_params & {x.arg.slot for x in address.toposort() if x.op is Ops.PARAM}:
used = list({slot for x in value.toposort() if (slot := _outer_slot(x, packed)) is not None} & inputs.keys())
if len(used) != 1 or not kernargs_params & {_outer_slot(x, packed) for x in address.toposort()}:
continue
ranges = list(dict.fromkeys(x for x in (*address.toposort(), *value.toposort()) if x.op is Ops.RANGE))
for trip in itertools.product(*[range(int(r.vmax) + 1) for r in ranges]):
env = dict.fromkeys(outer, 0) | dict(zip(ranges, trip, strict=True))
if (offset := _evaluate(address, env)) % 8 == 0:
pokes[offset] = (inputs[used[0]], _evaluate(value, env))
if (offset := _evaluate(address, env, packed)) % 8 == 0:
pokes[offset] = (inputs[used[0]], _evaluate(value, env, packed))
return pokes
def _elf(image: bytes, rodata: int) -> bytes:
names, offset, data, headers = b"\0.text\0.rodata\0.shstrtab\0", 64, b"", []
for name, kind, address, content in [(0, 0, 0, b""), (1, 1, ALIGNMENT, image[:rodata]), (7, 1, ALIGNMENT + rodata, image[rodata:]), (15, 3, 0, names)]:
headers.append(struct.pack("<IIQQQQIIQQ", name, kind, 2 if kind == 1 else 0, address, offset, len(content), 0, 0, 1, 0))
def _elf(image: bytes, rodata: int, entry: int) -> bytes:
spans = [(".text", 0, rodata), (".rodata", rodata, len(image))] if entry < rodata else \
[(".data", 0, rodata), (".rodata", rodata, entry), (".text", entry, len(image))]
sections, names = [(0, 0, 0, 0, b"")], b"\0"
for name, start, end in (span for span in spans if span[1] < span[2]):
sections.append((len(names), 1, 6 if name == ".text" else 2, ALIGNMENT + start, image[start:end]))
names += name.encode() + b"\0"
sections.append((len(names), 3, 0, 0, names + b".shstrtab\0"))
offset, data, headers = 64, b"", []
for name, kind, flags, address, content in sections:
headers.append(struct.pack("<IIQQQQIIQQ", name, kind, flags, address, offset, len(content), 0, 0, 1, 0))
data, offset = data + content, offset + len(content)
return b"\x7fELF\x02\x01\x01" + bytes(9) + struct.pack("<HHIQQQIHHHHHH", 1, 0xe0, 1, 0, 0, offset, 0, 64, 0, 0, 64, 4, 3) + data + b"".join(headers)
header = struct.pack("<HHIQQQIHHHHHH", 1, 0xe0, 1, 0, 0, offset, 0, 64, 0, 0, 64, len(sections), len(sections) - 1)
return b"\x7fELF\x02\x01\x01" + bytes(9) + header + data + b"".join(headers)
def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
@@ -155,7 +184,7 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
if image_alloc not in programs:
image = bytes(_patched(image_alloc, topo)[0])
rodata = next(r for r in range(0, len(image) - 24, 64) if r + struct.unpack_from("<q", image, r + 16)[0] == entry)
programs[image_alloc] = blob.add(_elf(image, rodata))
programs[image_alloc] = blob.add(_elf(image, rodata, entry))
start, size = _view(arguments.src[0])
args = []
for slot in range(start, start + unwrap(size), 8):
@@ -166,7 +195,8 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
base, at, nbytes = _address(argument_words[slot])
args.append({"buffer": buffers.setdefault(base, len(buffers)), "offset": at, "nbytes": nbytes or base.max_numel() * base.dtype.itemsize, "dtype": u8})
outs, ins = kernel[6]
calls.append({"kind": "program", "name": re.sub(r"\x1b\[[0-9;]*m", "", kernel[1]), "lib": programs[image_alloc], "target": dataclasses.asdict(target),
device_target = dataclasses.asdict(dataclasses.replace(target, interface=target.interface.removeprefix("MOCK")))
calls.append({"kind": "program", "name": re.sub(r"\x1b\[[0-9;]*m", "", kernel[1]), "lib": programs[image_alloc], "target": device_target,
"signature": [[f"data{i}", i, u8, [a.get("nbytes", 1)]] for i, a in enumerate(args)], "global_size": grid, "local_size": local,
"globals": list(range(len(args))), "outs": list(outs), "ins": list(ins), "args": args})
needs_init |= {args[i]["buffer"] for i in ins if "buffer" in args[i] and args[i]["buffer"] not in written}
@@ -207,14 +237,16 @@ def main():
frame, _ = capture_frame(artifact["run"], kwargs, "output_buffers")
part = convert(artifact["run"], kwargs, frame, Device[Device.DEFAULT].renderer.target)
plain = PlainJit(*part) # replays the frame, within tolerance
replay = {path[0]: Tensor(array, device=device).realize() for path, array, device in frame["inputs"] if len(path) == 1}
replay["output_buffers"] = {path[1]: Tensor(array, device=device).realize() for path, array, device in frame["inputs"] if len(path) == 2}
flatten(plain(**replay))
for index, (got, expected) in enumerate(zip([t.numpy() for t in replay["output_buffers"].values()], frame["outputs"], strict=True)):
assert np.array_equal(got, expected), f"output {index} of the plain artifact differs from the pickle"
if not os.getenv("MOCKUSB_SOCKET"):
plain = PlainJit(*part)
replay = {path[0]: Tensor(array, device=device).realize() for path, array, device in frame["inputs"] if len(path) == 1}
replay["output_buffers"] = {path[1]: Tensor(array, device=device).realize() for path, array, device in frame["inputs"] if len(path) == 2}
flatten(plain(**replay))
for index, (got, expected) in enumerate(zip([t.numpy() for t in replay["output_buffers"].values()], frame["outputs"], strict=True)):
assert np.array_equal(got, expected), f"output {index} of the plain artifact differs from the pickle"
write_bundle(args.output, {"run": part}, {key: artifact[key] for key in ("metadata", "input_specs", "output_specs")})
print(f"converted {len(part[0]['calls'])} kernels, bit-exact with the pickle: {args.output}")
checked = "left to mock_parity" if os.getenv("MOCKUSB_SOCKET") else "bit-exact with the pickle"
print(f"converted {len(part[0]['calls'])} kernels, {checked}: {args.output}")
if __name__ == "__main__":
+2 -2
View File
@@ -264,8 +264,8 @@ def _program_uop(call: dict, lib: bytes):
parameter = UOp.param(call["globals"][compact_slot], _dtype(dtype), math.prod(shape))
image = (shape[0], shape[1]) if len(shape) == 3 and shape[2] == 4 else None
parameters.append(parameter.replace(arg=_fields(parameter.arg, name=name, image=image)))
info = _fields(ProgramInfo, global_size=tuple(call["global_size"]), local_size=tuple(call["local_size"]), globals=tuple(call["globals"]),
outs=tuple(call["outs"]), ins=tuple(call["ins"]), target=_fields(Target, **call["target"]))
info = _fields(ProgramInfo, global_size=tuple(call["global_size"]), local_size=tuple(call["local_size"]), params=tuple(parameters),
globals=tuple(call["globals"]), outs=tuple(call["outs"]), ins=tuple(call["ins"]), target=_fields(Target, **call["target"]))
return UOp(Ops.PROGRAM, src=(UOp(Ops.SINK, arg=KernelInfo(name=call["name"])), UOp(Ops.LINEAR, src=tuple(parameters)),
UOp(Ops.SOURCE, arg=""), UOp(Ops.BINARY, arg=lib)), arg=info)
@@ -0,0 +1,153 @@
import json
import os
import shutil
import struct
import sys
import tempfile
import time
os.environ.setdefault("DEV", "MOCKUSB+AMD")
QCOM = "QCOM" in os.environ["DEV"]
ON_MOCK = os.environ["DEV"].startswith("MOCK")
ARCH, HARDWARE = ("a630", "comma 3x/4 (a630)") if QCOM else ("gfx1200", "chestnut")
os.environ.setdefault("CHECK_OOB", "0")
os.environ.setdefault("GMMU", "0")
if sys.platform == "darwin" and os.environ["DEV"].startswith("MOCKUSB"): # the mock's VRAM is a memfd, which macOS lacks: an unlinked file is the same
from tinygrad.runtime.autogen import libc
def _memfd_create(name, flags):
fd, path = tempfile.mkstemp()
os.unlink(path)
return fd
libc.memfd_create = _memfd_create
import numpy as np
from openpilot.sunnypilot.modeld_v2.helpers import ALIGNMENT, MAGIC, PlainJit, _device, read_parts
class ParityError(RuntimeError):
pass
def _emulator():
try:
if QCOM:
from test.mockgpu.qcom import emu
else:
from test.mockgpu.amd import emu
except ImportError as e:
raise ParityError(f"tinygrad's {'qcom' if QCOM else 'amd'} mock is not importable: put tinygrad_repo on PYTHONPATH") from e
if not hasattr(emu, "hw_unverified"):
raise ParityError(f"this tinygrad's emulator has no {ARCH} rounding model, its check frames would not match a {HARDWARE}")
return emu
def unverified(header: dict, blob: memoryview) -> dict[str, int]:
emu, out = _emulator(), {}
for call in header["calls"]:
if call["kind"] != "program":
continue
if (arch := call["target"].get("arch", "")).split(",")[0] != ARCH: # IMAGE builds append ",IMAGE_PITCH_ALIGNMENT=64"
out[f"arch={arch}"] = out.get(f"arch={arch}", 0) + 1
continue
for sig, n in emu.hw_unverified(bytes(blob[slice(*call["lib"])])).items():
out[sig] = out.get(sig, 0) + n
return out
def _array(blob: memoryview, stored: dict) -> np.ndarray:
return np.frombuffer(blob[slice(*stored["data"])], dtype=np.dtype(stored["dtype"])).reshape(stored["shape"]).copy()
def replay(header: dict, blob: memoryview, shared: dict) -> list[np.ndarray]:
from tinygrad import Tensor
check = header["check"]
jit = PlainJit(header, blob, None, verify=False, shared=shared)
kwargs: dict = {}
for path, stored, device in check["inputs"]:
tensor = Tensor(_array(blob, stored), device=_device(device, None)).realize()
kwargs[path[0]] = tensor if len(path) == 1 else {**kwargs.get(path[0], {}), path[1]: tensor}
result = jit(**kwargs)
outputs = kwargs[check["output_container"]].values() if check["output_container"] else result
return [t.numpy() for t in (outputs if isinstance(outputs, (list, tuple)) or check["output_container"] else [outputs])]
def _parts_with_offsets(path: str) -> tuple[dict, dict]:
parts, _ = read_parts(path)
with open(path, "rb") as f:
head = f.read(len(MAGIC) + 8)
start = len(MAGIC) + 8 + struct.unpack_from("<Q", head, len(MAGIC))[0]
f.seek(len(MAGIC) + 8)
entries = json.loads(f.read(start - len(MAGIC) - 8))["parts"]
base = start + -start % ALIGNMENT
offsets = {tuple(e["key"]) if isinstance(e["key"], list) else e["key"]: base + e["blob"][0] for e in entries}
return parts, offsets
def _gate(parts: dict) -> None:
missing: dict[str, int] = {}
for header, blob in parts.values():
if "check" not in header:
raise ParityError("artifact has no check frame")
for sig, n in unverified(header, blob).items():
missing[sig] = missing.get(sig, 0) + n
if missing:
raise ParityError(f"kernels use instructions the emulator is not verified against a {HARDWARE} for: " +
", ".join(f"{s} ({n}x)" for s, n in sorted(missing.items(), key=lambda kv: -kv[1])))
def check(path: str) -> None:
parts, _ = read_parts(path)
if ON_MOCK:
_gate(parts)
shared: dict = {}
for key, (header, blob) in parts.items():
got = replay(header, blob, shared)
for i, (g, stored) in enumerate(zip(got, header["check"]["outputs"], strict=True)):
want = _array(blob, stored)
if g.shape != want.shape or not np.array_equal(g.view(np.uint8), want.view(np.uint8)):
n = int(np.count_nonzero(g != want)) if g.shape == want.shape else -1
where = "mock" if ON_MOCK else "device"
raise ParityError(f"part {key} output {i}: the {where}'s check frame differs from the stored one in {n} of {want.size} values")
def record(path: str, out: str) -> None:
parts, offsets = _parts_with_offsets(path)
_gate(parts)
shared, writes = {}, []
for key, (header, blob) in parts.items():
for g, stored in zip(replay(header, blob, shared), header["check"]["outputs"], strict=True):
want = np.dtype(stored["dtype"])
if tuple(g.shape) != tuple(stored["shape"]) or g.dtype != want:
raise ParityError(f"part {key}: replayed output {g.dtype}{g.shape} does not fit the stored {want}{tuple(stored['shape'])}")
writes.append((offsets[key] + stored["data"][0], np.ascontiguousarray(g).tobytes()))
shutil.copyfile(path, out)
with open(out, "r+b") as f:
for offset, data in writes:
f.seek(offset)
f.write(data)
def main(argv: list[str]) -> int:
if len(argv) < 2 or argv[0] not in ("check", "record") or (argv[0] == "record" and len(argv) != 3):
print("usage: mock_parity check ARTIFACT... | record ARTIFACT OUT", file=sys.stderr)
return 2
try:
if argv[0] == "record":
st = time.perf_counter()
record(argv[1], argv[2])
print(f"{argv[2]}: check frame recorded on the mock in {time.perf_counter() - st:.0f}s")
return 0
for path in argv[1:]:
st = time.perf_counter()
check(path)
print(f"{os.path.basename(path)}: bit-identical to its {HARDWARE} check frame on the {'mock' if ON_MOCK else 'device'} ({time.perf_counter() - st:.0f}s)")
return 0
except ParityError as e:
print(f"PARITY FAIL: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
+6 -2
View File
@@ -171,7 +171,7 @@ class ModelState(ModelStateBase):
np.where(inputs[desire_key] - self.prev_desire > .99, inputs[desire_key], 0)
self.prev_desire[:] = inputs[desire_key]
for key in ('traffic_convention', 'lateral_control_params', 'action_t'):
for key in ('traffic_convention', 'lateral_control_params', 'action_t', 'speed_limit'):
if key in self.numpy_inputs and key in inputs:
self.numpy_inputs[key][:] = inputs[key]
@@ -338,7 +338,7 @@ def main(demo=False):
pub_socks = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"] + (["chestnutGpuState"] if CHESTNUT else [])
pm = PubMaster(pub_socks)
sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState",
"carControl", "controlsState", "lateralDelay"])
"carControl", "controlsState", "lateralDelay", "longitudinalPlanSP"])
publish_state = PublishState()
chestnut_state = ChestnutGpuState(pm, model.chestnut) if CHESTNUT else None
@@ -463,6 +463,10 @@ def main(demo=False):
if 'action_t' in model.numpy_inputs:
inputs['action_t'] = np.array([lat_action_t, long_action_t], dtype=np.float32)
if 'speed_limit' in model.numpy_inputs:
resolver = sm['longitudinalPlanSP'].speedLimit.resolver
inputs['speed_limit'] = np.array([resolver.speedLimitFinal if resolver.speedLimitValid else 0.0], dtype=np.float32)
mt1 = time.perf_counter()
try:
send_chestnut = (chestnut_state is not None and
@@ -80,7 +80,8 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
if ast.op is Ops.PROGRAM:
elf, info = ast.to_elf(), ast.arg
global_size, local_size = info.launch_dims({})
calls.append({"kind": "program", "name": elf.name, "lib": blob.add(bytes(elf.lib)), "target": dataclasses.asdict(elf.target),
device_target = dataclasses.asdict(dataclasses.replace(elf.target, interface=elf.target.interface.removeprefix("MOCK")))
calls.append({"kind": "program", "name": elf.name, "lib": blob.add(bytes(elf.lib)), "target": device_target,
"signature": kernel_signature(ast, elf),
"global_size": [int(x) for x in global_size], "local_size": [int(x) for x in local_size],
"globals": list(info.globals), "outs": list(info.outs), "ins": list(info.ins), "args": arguments})
@@ -6,6 +6,7 @@ See the LICENSE.md file in the root directory for more details.
"""
import os
import re
import struct
import sys
import tempfile
import unittest
@@ -25,8 +26,8 @@ if sys.platform == "darwin" and os.environ.get("DEV", "").startswith("MOCKUSB"):
from openpilot.common.test import OpenpilotTestCase
from openpilot.sunnypilot.modeld_v2 import convert_upstream_pkl
from openpilot.sunnypilot.modeld_v2.convert_upstream_pkl import convert
from openpilot.sunnypilot.modeld_v2.helpers import PlainJit, load_model
from openpilot.sunnypilot.modeld_v2.convert_upstream_pkl import _elf, convert
from openpilot.sunnypilot.modeld_v2.helpers import ALIGNMENT, PlainJit, load_model
from openpilot.sunnypilot.modeld_v2.plain_artifact_compiler import capture_frame
IMG, DESIRE, STATE, OUT = (6, 8, 8), (8,), (4, 16), (1, 32)
@@ -119,3 +120,18 @@ class TestConvertUpstreamPkl(OpenpilotTestCase):
bundle = load_model(out, cache=False, verify=True)
assert bundle["metadata"] == {"name": "tiny"} and bundle["input_specs"] == self.input_specs and bundle["output_specs"] == self.output_specs
self.assert_matches_eager(bundle["run"], 6)
class TestKernelElf(OpenpilotTestCase):
def test_sections_follow_the_code(self):
from tinygrad.runtime.support.elf import elf_loader
code = bytes(range(1, 129))
for rodata, entry in ((len(code), 0), (0, 256)):
image = bytearray(code + bytes(64) if entry < rodata else bytes(256) + code)
struct.pack_into("<q", image, rodata + 16, entry - rodata)
loaded, sections, _ = elf_loader(_elf(bytes(image), rodata, entry))
headers = {section.name: section.header for section in sections}
assert bytes(loaded[ALIGNMENT:]) == bytes(image), "the uploaded program differs from the pickle's"
assert headers[".rodata"].sh_addr == ALIGNMENT + rodata, "the loader reads the kernel descriptor at .rodata"
text = headers[".text"]
assert text.sh_addr <= ALIGNMENT + entry < text.sh_addr + text.sh_size, "the amd mock sizes the code from the entry to the end of .text"
@@ -17,4 +17,4 @@ class TestRegistryModels(unittest.TestCase):
self.skipTest("MODEL_BASE_NAME env var not set, skipping integration test.")
bundle = load_model(os.path.join(os.environ.get("MODEL_DIR", "/tmp/models"), base_name), {"QCOM": "CPU", "AMD": "CPU"}, verify=False, cache=False)
jits = [value for value in bundle.values() if isinstance(value, PlainJit)]
assert "metadata" in bundle and jits and all(len(jit.captured.linear.src) > 0 for jit in jits)
assert "metadata" in bundle and jits and all(len(jit.captured._linear.src) > 0 for jit in jits)
@@ -126,7 +126,6 @@ def initialize_params(params) -> list[dict[str, Any]]:
keys.extend([
"ToyotaEnforceStockLongitudinal",
"ToyotaStopAndGoHack",
"ToyotaEnhancedBsm",
"ToyotaTSS2Smooth",
])
+3 -14
View File
@@ -13,7 +13,6 @@ from opendbc.car.subaru.values import CAR as SUBARU_CAR, SubaruFlags
from opendbc.car.toyota.values import CAR as TOYOTA_CAR, ToyotaFlags
from opendbc.sunnypilot.car.interfaces import has_bsm
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
from opendbc.sunnypilot.car.toyota.values import ToyotaFlagsSP
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.common.hardware import HARDWARE
@@ -45,7 +44,6 @@ CAPABILITY_FIELDS = (
"device_type",
"subaru_has_sng",
"hyundai_alpha_long_available",
"toyota_enhanced_bsm_available",
"toyota_tss2",
"custom_model_active",
)
@@ -70,7 +68,6 @@ CAPABILITY_LABELS: dict[str, str] = {
"device_type": "Device type",
"subaru_has_sng": "Subaru Stop-and-Go available",
"hyundai_alpha_long_available": "Hyundai Alpha Longitudinal available",
"toyota_enhanced_bsm_available": "Toyota Enhanced BSM available",
"toyota_tss2": "Toyota TSS2 platform",
"custom_model_active": "Non-Default model selected",
}
@@ -92,7 +89,6 @@ CAR_CAPABILITY_FIELDS = (
"subaru_has_sng",
"hyundai_alpha_long_available",
"toyota_tss2",
"toyota_enhanced_bsm_available",
)
# Explicit defaults for non-boolean capability fields
@@ -108,7 +104,7 @@ def _bundle_field(bundle: dict | None, key: str) -> str:
return bundle.get(key, "") if isinstance(bundle, dict) else ""
def _resolve_brand_capabilities(caps: dict, bundle_platform: str, CP, CP_SP=None) -> None:
def _resolve_brand_capabilities(caps: dict, bundle_platform: str, CP) -> None:
"""Set brand-specific capabilities from bundle platform or CarParams fallback.
Bundle (manual car selection) is a pre-fingerprint approximation.
@@ -142,18 +138,11 @@ def _resolve_brand_capabilities(caps: dict, bundle_platform: str, CP, CP_SP=None
elif brand == "toyota":
if bundle_platform:
try:
flags = TOYOTA_CAR[bundle_platform].config.flags
caps["toyota_tss2"] = bool(flags & ToyotaFlags.TSS2)
caps["toyota_enhanced_bsm_available"] = bool(flags & ToyotaFlags.TSS2) and not bool(flags & ToyotaFlags.SECOC)
caps["toyota_tss2"] = bool(TOYOTA_CAR[bundle_platform].config.flags & ToyotaFlags.TSS2)
except KeyError:
cloudlog.exception(f"capabilities: unknown toyota platform {bundle_platform!r}")
elif CP is not None:
caps["toyota_tss2"] = bool(CP.flags & ToyotaFlags.TSS2)
# same gate as _initialize_toyota: TSS2 without SecOC, or TSS-P with a smartDSU and openpilot
# longitudinal control (the bundle path can't see 0x2FF, so it stays TSS2-only)
sdsu_bsm = CP_SP is not None and bool(CP_SP.flags & ToyotaFlagsSP.SMART_DSU) and bool(CP.openpilotLongitudinalControl)
caps["toyota_enhanced_bsm_available"] = (not bool(CP.flags & ToyotaFlags.SECOC) and
(bool(CP.flags & ToyotaFlags.TSS2) or sdsu_bsm))
def generate_capabilities(params: Params | None = None) -> dict:
@@ -228,7 +217,7 @@ def generate_capabilities(params: Params | None = None) -> dict:
if CP is not None:
caps["has_bsm"] = has_bsm(CP, CP_SP)
_resolve_brand_capabilities(caps, bundle_platform, CP, CP_SP)
_resolve_brand_capabilities(caps, bundle_platform, CP)
return caps
@@ -156,7 +156,7 @@ The tables below describe the **compiled** `settings_ui.json` schema — what th
**Enablement rules**: Grayed out (disabled) when rules fail. Frontend shows a contextual badge explaining why.
**Capability fields** (referenced in rules): `has_longitudinal_control`, `has_icbm`, `icbm_available`, `torque_allowed`, `brand`, `pcm_cruise`, `alpha_long_available`, `steer_control_type`, `has_bsm`, `is_release`, `is_sp_release`, `is_development`, `tesla_has_vehicle_bus`, `has_stop_and_go`, `stock_longitudinal`, `toyota_enhanced_bsm_available`
**Capability fields** (referenced in rules): `has_longitudinal_control`, `has_icbm`, `icbm_available`, `torque_allowed`, `brand`, `pcm_cruise`, `alpha_long_available`, `steer_control_type`, `has_bsm`, `is_release`, `is_sp_release`, `is_development`, `tesla_has_vehicle_bus`, `has_stop_and_go`, `stock_longitudinal`
---
@@ -2361,28 +2361,6 @@
}
]
},
{
"key": "ToyotaEnhancedBsm",
"widget": "toggle",
"needs_onroad_cycle": true,
"title": "Enhanced Blind Spot Monitoring (Alpha)",
"description": "sunnypilot will poll the blind spot monitor diagnostics directly from the radar. Use this on vehicles whose blind spot monitors do not report blind spot status over the standard CAN signals. This is an alpha feature. Use at your own risk.",
"enablement": [
{
"type": "not_engaged"
},
{
"type": "capability",
"field": "toyota_enhanced_bsm_available",
"equals": true
},
{
"type": "capability",
"field": "stock_longitudinal",
"equals": false
}
]
},
{
"key": "ToyotaTSS2Smooth",
"widget": "toggle",
@@ -101,21 +101,6 @@ sections:
- type: param
key: ToyotaEnforceStockLongitudinal
equals: false
- key: ToyotaEnhancedBsm
widget: toggle
needs_onroad_cycle: true
title: Enhanced Blind Spot Monitoring (Alpha)
description: sunnypilot will poll the blind spot monitor diagnostics directly from the radar. Use this on vehicles whose
blind spot monitors do not report blind spot status over the standard CAN signals. This is an alpha feature. Use at
your own risk.
enablement:
- $ref: '#/macros/not_engaged'
- type: capability
field: toyota_enhanced_bsm_available
equals: true
- type: capability
field: stock_longitudinal
equals: false
- key: ToyotaTSS2Smooth
widget: toggle
needs_onroad_cycle: true
@@ -70,18 +70,12 @@ class TestOpaquePerBrandFlags(OpenpilotTestCase):
def test_hyundai_alpha_long_available_field_present(self):
assert "hyundai_alpha_long_available" in CAPABILITY_FIELDS
def test_toyota_enhanced_bsm_available_field_present(self):
assert "toyota_enhanced_bsm_available" in CAPABILITY_FIELDS
def test_subaru_has_sng_default_false(self, caps):
assert caps["subaru_has_sng"] is False
def test_hyundai_alpha_long_available_default_false(self, caps):
assert caps["hyundai_alpha_long_available"] is False
def test_toyota_enhanced_bsm_available_default_false(self, caps):
assert caps["toyota_enhanced_bsm_available"] is False
def test_toyota_tss2_field_present(self):
assert "toyota_tss2" in CAPABILITY_FIELDS
@@ -207,7 +207,6 @@ class TestNotEngagedReplacement(OpenpilotTestCase):
"AlphaLongitudinalEnabled",
"ToyotaEnforceStockLongitudinal",
"ToyotaStopAndGoHack",
"ToyotaEnhancedBsm",
"ToyotaTSS2Smooth",
], names=["key"])
def test_offroad_only_replaced_with_not_engaged(self, schema, key):
@@ -286,7 +286,6 @@ class TestKnownVehicleSettings(OpenpilotTestCase):
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("toyota"))}
assert "ToyotaEnforceStockLongitudinal" in keys
assert "ToyotaStopAndGoHack" in keys
assert "ToyotaEnhancedBsm" in keys
assert "ToyotaTSS2Smooth" in keys
def test_tesla_has_coop_steering(self, schema):
+27 -12
View File
@@ -13,26 +13,41 @@ from pathlib import Path
from datetime import datetime, UTC
_VERSION_REGEX = re.compile(r'^(?:\d+[a-zA-Z]+|\d+[vVbB]\d+|[vVbB]\d+|\d{4})$')
_DECIMAL_VERSION_REGEX = re.compile(r'^([vVbB]?)(\d+(?:\.\d+)+)$')
def _version(word: str) -> str | None:
match = _DECIMAL_VERSION_REGEX.match(word.strip('()[]{}.,;:'))
if match:
return match.group(1).upper() + match.group(2)
stripped = re.sub(r'[^a-zA-Z0-9]', '', word)
return stripped.upper() if _VERSION_REGEX.match(stripped) else None
def _truncate(short_name: str) -> str:
return short_name[:8].rstrip('.')
def create_short_name(full_name: str) -> str:
# Remove parentheses and extract alphanumeric words
# Remove parentheses and extract alphanumeric words, keeping decimals for versioning
clean_name = re.sub(r'\([^)]*\)', '', full_name)
words = [re.sub(r'[^a-zA-Z0-9]', '', word) for word in clean_name.split() if re.sub(r'[^a-zA-Z0-9]', '', word)]
words = [re.sub(r'[^a-zA-Z0-9.]', '', word) for word in clean_name.split()]
words = [word for word in words if word]
if len(words) == 1:
return words[0][:8].upper()
return _truncate((_version(words[0]) or words[0]).upper())
# Handle special case: Name + Version (e.g., "Word A1" -> "WordA1")
if len(words) == 2 and re.match(r'^[A-Za-z]\d+$', words[1]):
return (words[0] + words[1])[:8].upper()
# Handle special case: Name + Version (e.g., "Word A1" -> "WordA1", "Model v1.1" -> "ModelV1.1")
if len(words) == 2 and re.match(r'^[A-Za-z]?\d+$', words[1]):
return _truncate((words[0] + (_version(words[1]) or words[1])).upper())
result = ""
for word in words:
# Version or number patterns
if (re.match(r'^\d+[a-zA-Z]+$', word) or
re.match(r'^\d+[vVbB]\d+$', word) or
re.match(r'^[vVbB]\d+$', word) or
re.match(r'^\d{4}$', word)):
result += word.upper()
version = _version(word)
if version:
result += version
# All uppercase abbreviations (2-3 letters)
elif re.match(r'^[A-Z]{2,3}$', word):
result += word
@@ -45,7 +60,7 @@ def create_short_name(full_name: str) -> str:
result += word
else:
result += word[0].upper()
return result[:8]
return _truncate(result)
def create_pkl_name(full_name: str) -> str: