#OpAmayo - an Alpamayo Openpilot Live Integration Proof And Runbook
This document records:
semanticPlan is being produced by Alpamayo on the PCmodeld is consuming and fusing that planmodelV2 path is consumed downstream by controlsdThis is a runtime proof and operator runbook, not a design note.
This repo contains the source code needed to launch the PC Alpamayo server:
selfdrive/alpamayo/server.pyselfdrive/alpamayo/protocol.pyselfdrive/alpamayo/alpamayod.pycereal/, common/, system/, selfdrive/modeld/, and sunnypilot/modeld*alpamayo1.5/This repo does not contain:
The reason the venv is not stored in git is simple: the working Linux venv is about 8.1 GiB and mostly consists of compiled CUDA, Torch, FlashAttention, and NVIDIA shared libraries. That is not a sane git artifact.
The server and benchmarking path were validated in WSL/Linux with:
3.12.3uv 0.9.16nvcc 12.0.140torch 2.8.0flash_attn 2.8.3transformers 4.57.1accelerate 1.12.0physical_ai_av 0.2.0opencv-python-headless 4.13.0.92zstandard 0.25.0The exact pip environment used to run this is captured in:
alpamayo1.5/requirements.txtAssumptions:
nvcc available on PATHConcrete procedure:
# 1. Enter the vendored Alpamayo source tree
cd /path/to/OPAMAYO/alpamayo1.5
# 2. Install uv if needed
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
# 3. Create and activate the environment
uv venv a1_5_venv --python 3.12
source a1_5_venv/bin/activate
# 4. Install the exact package set used during bring-up
python -m pip install -r requirements.txt
# 5. Authenticate for the gated model + dataset
hf auth login
# 6. Optional but recommended cache location
export HF_HOME=/path/to/hf-cache
# 7. Sanity-check the critical runtime pieces
python -c 'import torch, flash_attn, cv2, zstandard; print(torch.__version__)'
python -m py_compile timed_inference.py src/alpamayo1_5/models/alpamayo1_5.py src/alpamayo1_5/models/base_model.py
Important notes:
flash-attn is part of the proven fast path. Do not skip it if the goal is to reproduce the measured sub-1-second warm semantic loop.opencv-python-headless and zstandard are required for the openpilot-side server transport path.From the OPAMAYO repo root:
cd /path/to/OPAMAYO
source alpamayo1.5/a1_5_venv/bin/activate
export PYTHONPATH=$PWD
export HF_HOME=/path/to/hf-cache
python -m selfdrive.alpamayo.server \
--host 0.0.0.0 \
--port 8081 \
--source remoteServer \
--gpu-mem-gib 15 \
--cpu-mem-gib 96 \
--split-index 16 \
--min-pixels 65536 \
--max-pixels 65536 \
--diffusion-steps 6 \
--num-traj-samples 1 \
--attn-implementation flash_attention_2 \
--expert-attn-implementation eager \
--reasoning-mode prefill_future_start
Operational detail:
PYTHONPATH=$PWD matters.python -m selfdrive.alpamayo.server ... is the safe invocation.python selfdrive/alpamayo/server.py ... directly without the repo root on PYTHONPATH can fail on ModuleNotFoundError: selfdrive.The fast path was not a generic quantization trick. It was a specific runtime simplification and execution-layout change.
Mechanically, the fast path consists of:
Skip runtime CoT token generation
<|traj_future_start|>, the prompt is rewritten so the assistant prefill starts directly at <|traj_future_start|>.prefill_future_start path in:
alpamayo1.5/timed_inference.pyselfdrive/alpamayo/server.pyskip_vlm_generation=True when calling sample_trajectories_from_data_with_vlm_rollout(...) in:
alpamayo1.5/src/alpamayo1_5/models/alpamayo1_5.pyUse a manual 2-GPU layer split
split_index=16.build_manual_split_device_map(...) in:
alpamayo1.5/timed_inference.pyselfdrive/alpamayo/server.pyUse mixed attention backends
flash_attention_2eageralpamayo1.5/src/alpamayo1_5/models/base_model.pyalpamayo1.5/src/alpamayo1_5/models/alpamayo1_5.pyReduce visual/context load without dropping the 2-camera sidecar design
front2265536 min/max16Keep the output semantic
From alpamayo1.5/:
cd /path/to/OPAMAYO/alpamayo1.5
source a1_5_venv/bin/activate
export HF_HOME=/path/to/hf-cache
python timed_inference.py \
--camera-mode front2 \
--num-frames 2 \
--gpu-mem-gib 15 \
--cpu-mem-gib 96 \
--min-pixels 65536 \
--max-pixels 65536 \
--device-map-mode manual_split \
--split-index 16 \
--repeat-infer 3 \
--num-traj-samples 1 \
--diffusion-steps 6 \
--attn-implementation flash_attention_2 \
--expert-attn-implementation eager \
--reasoning-mode prefill_future_start
The measured successful warm-loop profile from this repo was approximately:
~1.52 s~0.65 s~0.66 sThose numbers are recorded in:
alpamayo1.5/timed_inference_prefill_future_start_flash_vlm_eager_expert_front2_2f_64k_split16_diff6_repeat3.logThat is the configuration the PC server defaults were aligned with.
The system is a two-rate planner:
modeld blends that prior into the stock model trajectorymodelV2, not two separate plansSo there is no direct actuator fight between two controllers. There is only one downstream trajectory after fusion.
The intended role split is:
That means Alpamayo is not a reflex controller. It is a low-rate semantic planner prior.
The live runtime path is:
camerad publishes road and wide frames.alpamayod subscribes to those VisionIPC streams on the c3x.alpamayod also reads stock context from the openpilot message bus, including modelV2, livePose, and calibration state.alpamayod builds a request containing:
http://127.0.0.1:8081.adb reverse forwards that request from the c3x to the PC.alpamayod publishes that result as semanticPlan on the c3x.modeld reads semanticPlan, fuses it into the stock plan, and recomputes modelV2.action.controlsd consumes that fused modelV2.modelV2 as part of its feedforward input.The key operational fact is that the sidecar returns a trajectory, not direct steering or brake commands.
The control loop still runs at stock openpilot speed. Alpamayo is not trying to make every 50 ms decision.
What happens instead is:
modelV2modeld merges that slower prior into the medium and far horizoncontrolsd follows the fused result every cycleSo Alpamayo can influence:
It is not trying to own:
This is exactly why freshness around 0.75s can still matter. That latency is too slow for reflex control, but it is still usable for medium/far-horizon planning.
The proof was built from four independent observations, not one.
Transport proof
Server execution proof
status=valid source=remoteServerFusion proof
semanticPlansemantic plan fused ... log entries from modeldDownstream consumption proof
modelV2.action.desiredCurvaturecontrolsState.desiredCurvaturemodelV2The conclusion depends on the combination of all four. Any one of them alone would be weaker.
This document proves runtime causality inside the software stack:
modeld fuses that semantic planmodelV2 is what downstream control consumesThis does not prove, in the strict scientific A/B sense, that the physical driven path of the vehicle changed by a measured amount on the road. That would require paired experimental runs or deliberate toggling, which was not appropriate during live driving.
So the proof level here is:
adb reverse tcp:8081 tcp:8081http://127.0.0.1:8081Run the Alpamayo server in WSL with the proven fast config:
source /mnt/g/alpamayo1.5/a1_5_venv/bin/activate
export PYTHONPATH=/mnt/g/openpilot
cd /mnt/g/openpilot
python -u -m selfdrive.alpamayo.server \
--host 0.0.0.0 \
--port 8081 \
--alpamayo-repo /mnt/g/alpamayo1.5 \
--gpu-mem-gib 15 \
--cpu-mem-gib 96 \
--split-index 16 \
--min-pixels 65536 \
--max-pixels 65536 \
--diffusion-steps 6 \
--num-traj-samples 1 \
--attn-implementation flash_attention_2 \
--expert-attn-implementation eager \
--reasoning-mode prefill_future_start \
--source remoteServer
These must be present on the device:
AlpamayoEnabled=1
AlpamayoServerEndpoint=http://127.0.0.1:8081
They are persistent params.
Install the reverse tunnel from the PC:
adb reverse tcp:8081 tcp:8081
adb reverse --list
Expected:
(null) tcp:8081 tcp:8081
The endpoint is reachable from the c3x through the reverse tunnel. Manual probes from the c3x to http://127.0.0.1:8081/_health succeeded repeatedly with:
200230.008s to 0.029sThis proves the chain below is working:
c3x -> adb reverse -> Windows localhost -> WSL localhost -> Alpamayo server
This rules out WSL routing as the blocker.
The original live request path was too large. The sidecar transport was changed to JPEG-encoded resized BGR frames instead of raw NV12 frame bodies.
Relevant files:
Results after the transport change:
9.1 MB to about 34 KB to 58 KB0.67s to 0.78sThe daemon now polls VisionIPC every loop instead of only on semantic publish ticks. That was the key bug fix required to make live remote inference actually run.
The c3x daemon attached to the real road and wide streams:
alpamayod connected road stream: 1928x1208alpamayod connected wideRoad stream: 1928x1208alpamayod remote provider initializedThis proves the c3x sidecar is attached to live camera feeds and configured for remote Alpamayo inference.
The live WSL server emitted repeated request-completion logs during driving with values like:
alpamayo request ok id=... in=36739 out=5574 read=0.010s decode=0.001s infer=0.666s encode=0.000s total=0.678s status=valid source=remoteServer ...
Observed server timings on real in-car requests:
0.67s to 0.78s0.63s to 0.72svalidremoteServerThis is direct proof that the car is sending requests to the PC and the PC is running Alpamayo inference, not just serving health checks.
Live c3x observations of semanticPlan during driving showed:
semanticPlan.status = valid
semanticPlan.source = remoteServer
semanticPlan.age = 0.7655778527259827
semanticPlan.generationExecutionTime = 1.1171375513076782
semanticPlan.desiredCurvature = -0.027970125898718834
semanticPlan.desiredAcceleration = -0.3513976037502289
and
semanticPlan.status = valid
semanticPlan.source = remoteServer
semanticPlan.age = 0.7464171051979065
semanticPlan.generationExecutionTime = 0.9224347472190857
semanticPlan.desiredCurvature = -0.001730378600768745
semanticPlan.desiredAcceleration = -0.40232858061790466
This proves:
The deployed c3x modeld path is:
model_output in modeld.pyaction from the fused model_output in modeld.pymodelV2 from the fused plan in modeld.pyExact deployed code:
if alpamayo_enabled and sm.seen['semanticPlan']:
model_output, fusion_result = apply_semantic_fusion(model_output, sm['semanticPlan'])
...
action = get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL, v_ego)
fill_model_msg(...)
Live c3x swaglog entries during driving showed repeated successful fusion:
semantic plan fused source=2 alpha=1.00 age=0.75s confidence=0.85 consistency=1.00
semantic plan fused source=2 alpha=1.00 age=0.77s confidence=0.85 consistency=1.00
semantic plan fused source=2 alpha=1.00 age=0.76s confidence=0.85 consistency=1.00
semantic plan fused source=2 alpha=1.00 age=0.78s confidence=0.85 consistency=1.00
This proves modeld is not discarding semanticPlan.
semanticPlan is not copied directly into modelV2.action.
The mechanism is:
model_outputapply_semantic_fusion(...) blends semantic trajectory arrays into the stock planget_action_from_model(...) derives a fresh action from the fused planfill_model_msg(...) publishes the fused trajectory and the recomputed actionSo the scalar action values seen by the rest of openpilot are downstream products of the fused trajectory, not direct copies of scalar fields from semanticPlan.
The horizon policy in the deployed implementation is:
0.0s to 0.7s: zero semantic influence0.7s to 2.5s: ramped blend2.5s+: full semantic weightIn practical terms:
That is why the correct runtime proof is:
semanticPlansemantic plan fused ... logsmodelV2 -> controlsState propagationnot a direct equality check between semanticPlan.desiredCurvature and modelV2.action.desiredCurvature
The active sunnypilot controlsd path directly consumes modelV2.action.desiredCurvature:
Exact deployed code:
new_desired_curvature = model_v2.action.desiredCurvature if CC.latActive else self.curvature
self.desired_curvature, curvature_limited = clip_curvature(...)
actuators.curvature = self.desired_curvature
...
cs.desiredCurvature = self.desired_curvature
Live same-window samples from the c3x matched exactly:
modelV2.action.desiredCurvature @ 7652806111031 = 0.00012016872642561793
controlsState.desiredCurvature @ 7652813064399 = 0.00012016872642561793
modelV2.action.desiredCurvature @ 7652849082035 = 0.00011393759632483125
controlsState.desiredCurvature @ 7652854845648 = 0.00011393759632483125
modelV2.action.desiredCurvature @ 7652902253947 = 0.00010993104660883546
controlsState.desiredCurvature @ 7652912808349 = 0.00010993104660883546
modelV2.action.desiredCurvature @ 7652953017288 = 9.995466098189354e-05
controlsState.desiredCurvature @ 7652962391102 = 9.995466098189354e-05
modelV2.action.desiredCurvature @ 7653004448449 = 0.00010208125604549423
controlsState.desiredCurvature @ 7653012361777 = 0.00010208125604549423
This is the strongest direct runtime proof that the modified modelV2 path is being consumed by the control stack and not discarded.
NNLC is downstream of modelV2, not an alternate planner that bypasses it.
controlsd explicitly pushes modelV2 into the lateral extension in controlsd.py:
self.LaC.extension.update_model_v2(self.sm['modelV2'])
That stores the full message in latcontrol_torque_ext_base.py:
def update_model_v2(self, model_v2):
self.model_v2 = model_v2
NNLC then uses the fused modelV2 trajectory/state in nnlc.py and nnlc.py:
future_rolls = [np.interp(t, ModelConstants.T_IDXS, self.model_v2.orientation.x) ...]
future_planned_lateral_accels = [np.interp(t, ModelConstants.T_IDXS, self.model_v2.acceleration.y) ...]
Meaning:
modeldmodeld publishes the fused modelV2controlsd consumes modelV2.action.desiredCurvaturemodelV2 orientation and lateral-acceleration trajectorySo NNLC does not bypass the fused trajectory. It is downstream of it.
The thing that matters is not whether semanticPlan.desiredCurvature is copied verbatim into modelV2.action.desiredCurvature.
That is not how the integration works.
The actual sequence is:
modeld fuses that trajectory into the stock model_output.modeld recomputes action from the fused model_output.controlsd consumes the recomputed modelV2.action.modelV2 trajectory fields.So the correct proof is:
semanticPlan is valid and remotemodeld logs live fusionmodelV2.action.desiredCurvature is publishedcontrolsState.desiredCurvature matches that modelV2.action.desiredCurvaturemodelV2These are safe read-only checks.
adb reverse --list
adb shell "PYTHONPATH=/data/openpilot /usr/local/venv/bin/python - <<'PY'
import requests, time
t=time.time()
r=requests.get('http://127.0.0.1:8081/_health', timeout=2)
print(r.status_code, len(r.content), time.time()-t, r.headers.get('Content-Type'))
PY"
adb shell "timeout 3 sh -c 'cd /data/openpilot && PYTHONPATH=/data/openpilot /usr/local/venv/bin/python selfdrive/debug/dump.py semanticPlan --values semanticPlan.status,semanticPlan.source,semanticPlan.age,semanticPlan.generationExecutionTime,semanticPlan.desiredCurvature,semanticPlan.desiredAcceleration'"
adb shell "timeout 3 sh -c 'cd /data/openpilot && PYTHONPATH=/data/openpilot /usr/local/venv/bin/python selfdrive/debug/dump.py modelV2 --values modelV2.action.desiredCurvature,modelV2.action.desiredAcceleration'"
adb shell "timeout 3 sh -c 'cd /data/openpilot && PYTHONPATH=/data/openpilot /usr/local/venv/bin/python selfdrive/debug/dump.py controlsState --values controlsState.desiredCurvature'"
adb shell "PYTHONPATH=/data/openpilot /usr/local/venv/bin/python - <<'PY'
from pathlib import Path
files = sorted(Path('/data/log').glob('swaglog.*'), key=lambda p: p.stat().st_mtime, reverse=True)
print(files[0] if files else 'NOLOG')
if files:
lines = files[0].read_text(errors='ignore').splitlines()
hits = [ln for ln in lines if 'semantic plan fused' in ln]
for ln in hits[-5:]:
print(ln)
PY"
semanticPlan.status = unavailable or source = noneMeaning:
semantic plan fused logsMeaning:
modeld is not currently applying the remote semantic plansemanticPlan is missing, invalid, stale, or ignoredmodelV2.action.desiredCurvature and controlsState.desiredCurvature divergeMeaning:
modelV2.actionThe current live evidence proves all of the following:
modeld is fusing those plans into model_outputmodelV2.action.desiredCurvature is being consumed by controlsdmodelV2 and therefore does not bypass the modified trajectory pathThis is sufficient runtime proof that the Alpamayo-modified modelV2 path is real, live, and in the control stack.
9 commits
C++
91.5%
Python
4.0%
C
2.7%
#OpAmayo - an Alpamayo Openpilot Live Integration Proof And Runbook
This document records:
semanticPlan is being produced by Alpamayo on the PCmodeld is consuming and fusing that planmodelV2 path is consumed downstream by controlsdThis is a runtime proof and operator runbook, not a design note.
This repo contains the source code needed to launch the PC Alpamayo server:
selfdrive/alpamayo/server.pyselfdrive/alpamayo/protocol.pyselfdrive/alpamayo/alpamayod.pycereal/, common/, system/, selfdrive/modeld/, and sunnypilot/modeld*alpamayo1.5/This repo does not contain:
The reason the venv is not stored in git is simple: the working Linux venv is about 8.1 GiB and mostly consists of compiled CUDA, Torch, FlashAttention, and NVIDIA shared libraries. That is not a sane git artifact.
The server and benchmarking path were validated in WSL/Linux with:
3.12.3uv 0.9.16nvcc 12.0.140torch 2.8.0flash_attn 2.8.3transformers 4.57.1accelerate 1.12.0physical_ai_av 0.2.0opencv-python-headless 4.13.0.92zstandard 0.25.0The exact pip environment used to run this is captured in:
alpamayo1.5/requirements.txtAssumptions:
nvcc available on PATHConcrete procedure:
# 1. Enter the vendored Alpamayo source tree
cd /path/to/OPAMAYO/alpamayo1.5
# 2. Install uv if needed
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
# 3. Create and activate the environment
uv venv a1_5_venv --python 3.12
source a1_5_venv/bin/activate
# 4. Install the exact package set used during bring-up
python -m pip install -r requirements.txt
# 5. Authenticate for the gated model + dataset
hf auth login
# 6. Optional but recommended cache location
export HF_HOME=/path/to/hf-cache
# 7. Sanity-check the critical runtime pieces
python -c 'import torch, flash_attn, cv2, zstandard; print(torch.__version__)'
python -m py_compile timed_inference.py src/alpamayo1_5/models/alpamayo1_5.py src/alpamayo1_5/models/base_model.py
Important notes:
flash-attn is part of the proven fast path. Do not skip it if the goal is to reproduce the measured sub-1-second warm semantic loop.opencv-python-headless and zstandard are required for the openpilot-side server transport path.From the OPAMAYO repo root:
cd /path/to/OPAMAYO
source alpamayo1.5/a1_5_venv/bin/activate
export PYTHONPATH=$PWD
export HF_HOME=/path/to/hf-cache
python -m selfdrive.alpamayo.server \
--host 0.0.0.0 \
--port 8081 \
--source remoteServer \
--gpu-mem-gib 15 \
--cpu-mem-gib 96 \
--split-index 16 \
--min-pixels 65536 \
--max-pixels 65536 \
--diffusion-steps 6 \
--num-traj-samples 1 \
--attn-implementation flash_attention_2 \
--expert-attn-implementation eager \
--reasoning-mode prefill_future_start
Operational detail:
PYTHONPATH=$PWD matters.python -m selfdrive.alpamayo.server ... is the safe invocation.python selfdrive/alpamayo/server.py ... directly without the repo root on PYTHONPATH can fail on ModuleNotFoundError: selfdrive.The fast path was not a generic quantization trick. It was a specific runtime simplification and execution-layout change.
Mechanically, the fast path consists of:
Skip runtime CoT token generation
<|traj_future_start|>, the prompt is rewritten so the assistant prefill starts directly at <|traj_future_start|>.prefill_future_start path in:
alpamayo1.5/timed_inference.pyselfdrive/alpamayo/server.pyskip_vlm_generation=True when calling sample_trajectories_from_data_with_vlm_rollout(...) in:
alpamayo1.5/src/alpamayo1_5/models/alpamayo1_5.pyUse a manual 2-GPU layer split
split_index=16.build_manual_split_device_map(...) in:
alpamayo1.5/timed_inference.pyselfdrive/alpamayo/server.pyUse mixed attention backends
flash_attention_2eageralpamayo1.5/src/alpamayo1_5/models/base_model.pyalpamayo1.5/src/alpamayo1_5/models/alpamayo1_5.pyReduce visual/context load without dropping the 2-camera sidecar design
front2265536 min/max16Keep the output semantic
From alpamayo1.5/:
cd /path/to/OPAMAYO/alpamayo1.5
source a1_5_venv/bin/activate
export HF_HOME=/path/to/hf-cache
python timed_inference.py \
--camera-mode front2 \
--num-frames 2 \
--gpu-mem-gib 15 \
--cpu-mem-gib 96 \
--min-pixels 65536 \
--max-pixels 65536 \
--device-map-mode manual_split \
--split-index 16 \
--repeat-infer 3 \
--num-traj-samples 1 \
--diffusion-steps 6 \
--attn-implementation flash_attention_2 \
--expert-attn-implementation eager \
--reasoning-mode prefill_future_start
The measured successful warm-loop profile from this repo was approximately:
~1.52 s~0.65 s~0.66 sThose numbers are recorded in:
alpamayo1.5/timed_inference_prefill_future_start_flash_vlm_eager_expert_front2_2f_64k_split16_diff6_repeat3.logThat is the configuration the PC server defaults were aligned with.
The system is a two-rate planner:
modeld blends that prior into the stock model trajectorymodelV2, not two separate plansSo there is no direct actuator fight between two controllers. There is only one downstream trajectory after fusion.
The intended role split is:
That means Alpamayo is not a reflex controller. It is a low-rate semantic planner prior.
The live runtime path is:
camerad publishes road and wide frames.alpamayod subscribes to those VisionIPC streams on the c3x.alpamayod also reads stock context from the openpilot message bus, including modelV2, livePose, and calibration state.alpamayod builds a request containing:
http://127.0.0.1:8081.adb reverse forwards that request from the c3x to the PC.alpamayod publishes that result as semanticPlan on the c3x.modeld reads semanticPlan, fuses it into the stock plan, and recomputes modelV2.action.controlsd consumes that fused modelV2.modelV2 as part of its feedforward input.The key operational fact is that the sidecar returns a trajectory, not direct steering or brake commands.
The control loop still runs at stock openpilot speed. Alpamayo is not trying to make every 50 ms decision.
What happens instead is:
modelV2modeld merges that slower prior into the medium and far horizoncontrolsd follows the fused result every cycleSo Alpamayo can influence:
It is not trying to own:
This is exactly why freshness around 0.75s can still matter. That latency is too slow for reflex control, but it is still usable for medium/far-horizon planning.
The proof was built from four independent observations, not one.
Transport proof
Server execution proof
status=valid source=remoteServerFusion proof
semanticPlansemantic plan fused ... log entries from modeldDownstream consumption proof
modelV2.action.desiredCurvaturecontrolsState.desiredCurvaturemodelV2The conclusion depends on the combination of all four. Any one of them alone would be weaker.
This document proves runtime causality inside the software stack:
modeld fuses that semantic planmodelV2 is what downstream control consumesThis does not prove, in the strict scientific A/B sense, that the physical driven path of the vehicle changed by a measured amount on the road. That would require paired experimental runs or deliberate toggling, which was not appropriate during live driving.
So the proof level here is:
adb reverse tcp:8081 tcp:8081http://127.0.0.1:8081Run the Alpamayo server in WSL with the proven fast config:
source /mnt/g/alpamayo1.5/a1_5_venv/bin/activate
export PYTHONPATH=/mnt/g/openpilot
cd /mnt/g/openpilot
python -u -m selfdrive.alpamayo.server \
--host 0.0.0.0 \
--port 8081 \
--alpamayo-repo /mnt/g/alpamayo1.5 \
--gpu-mem-gib 15 \
--cpu-mem-gib 96 \
--split-index 16 \
--min-pixels 65536 \
--max-pixels 65536 \
--diffusion-steps 6 \
--num-traj-samples 1 \
--attn-implementation flash_attention_2 \
--expert-attn-implementation eager \
--reasoning-mode prefill_future_start \
--source remoteServer
These must be present on the device:
AlpamayoEnabled=1
AlpamayoServerEndpoint=http://127.0.0.1:8081
They are persistent params.
Install the reverse tunnel from the PC:
adb reverse tcp:8081 tcp:8081
adb reverse --list
Expected:
(null) tcp:8081 tcp:8081
The endpoint is reachable from the c3x through the reverse tunnel. Manual probes from the c3x to http://127.0.0.1:8081/_health succeeded repeatedly with:
200230.008s to 0.029sThis proves the chain below is working:
c3x -> adb reverse -> Windows localhost -> WSL localhost -> Alpamayo server
This rules out WSL routing as the blocker.
The original live request path was too large. The sidecar transport was changed to JPEG-encoded resized BGR frames instead of raw NV12 frame bodies.
Relevant files:
Results after the transport change:
9.1 MB to about 34 KB to 58 KB0.67s to 0.78sThe daemon now polls VisionIPC every loop instead of only on semantic publish ticks. That was the key bug fix required to make live remote inference actually run.
The c3x daemon attached to the real road and wide streams:
alpamayod connected road stream: 1928x1208alpamayod connected wideRoad stream: 1928x1208alpamayod remote provider initializedThis proves the c3x sidecar is attached to live camera feeds and configured for remote Alpamayo inference.
The live WSL server emitted repeated request-completion logs during driving with values like:
alpamayo request ok id=... in=36739 out=5574 read=0.010s decode=0.001s infer=0.666s encode=0.000s total=0.678s status=valid source=remoteServer ...
Observed server timings on real in-car requests:
0.67s to 0.78s0.63s to 0.72svalidremoteServerThis is direct proof that the car is sending requests to the PC and the PC is running Alpamayo inference, not just serving health checks.
Live c3x observations of semanticPlan during driving showed:
semanticPlan.status = valid
semanticPlan.source = remoteServer
semanticPlan.age = 0.7655778527259827
semanticPlan.generationExecutionTime = 1.1171375513076782
semanticPlan.desiredCurvature = -0.027970125898718834
semanticPlan.desiredAcceleration = -0.3513976037502289
and
semanticPlan.status = valid
semanticPlan.source = remoteServer
semanticPlan.age = 0.7464171051979065
semanticPlan.generationExecutionTime = 0.9224347472190857
semanticPlan.desiredCurvature = -0.001730378600768745
semanticPlan.desiredAcceleration = -0.40232858061790466
This proves:
The deployed c3x modeld path is:
model_output in modeld.pyaction from the fused model_output in modeld.pymodelV2 from the fused plan in modeld.pyExact deployed code:
if alpamayo_enabled and sm.seen['semanticPlan']:
model_output, fusion_result = apply_semantic_fusion(model_output, sm['semanticPlan'])
...
action = get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL, v_ego)
fill_model_msg(...)
Live c3x swaglog entries during driving showed repeated successful fusion:
semantic plan fused source=2 alpha=1.00 age=0.75s confidence=0.85 consistency=1.00
semantic plan fused source=2 alpha=1.00 age=0.77s confidence=0.85 consistency=1.00
semantic plan fused source=2 alpha=1.00 age=0.76s confidence=0.85 consistency=1.00
semantic plan fused source=2 alpha=1.00 age=0.78s confidence=0.85 consistency=1.00
This proves modeld is not discarding semanticPlan.
semanticPlan is not copied directly into modelV2.action.
The mechanism is:
model_outputapply_semantic_fusion(...) blends semantic trajectory arrays into the stock planget_action_from_model(...) derives a fresh action from the fused planfill_model_msg(...) publishes the fused trajectory and the recomputed actionSo the scalar action values seen by the rest of openpilot are downstream products of the fused trajectory, not direct copies of scalar fields from semanticPlan.
The horizon policy in the deployed implementation is:
0.0s to 0.7s: zero semantic influence0.7s to 2.5s: ramped blend2.5s+: full semantic weightIn practical terms:
That is why the correct runtime proof is:
semanticPlansemantic plan fused ... logsmodelV2 -> controlsState propagationnot a direct equality check between semanticPlan.desiredCurvature and modelV2.action.desiredCurvature
The active sunnypilot controlsd path directly consumes modelV2.action.desiredCurvature:
Exact deployed code:
new_desired_curvature = model_v2.action.desiredCurvature if CC.latActive else self.curvature
self.desired_curvature, curvature_limited = clip_curvature(...)
actuators.curvature = self.desired_curvature
...
cs.desiredCurvature = self.desired_curvature
Live same-window samples from the c3x matched exactly:
modelV2.action.desiredCurvature @ 7652806111031 = 0.00012016872642561793
controlsState.desiredCurvature @ 7652813064399 = 0.00012016872642561793
modelV2.action.desiredCurvature @ 7652849082035 = 0.00011393759632483125
controlsState.desiredCurvature @ 7652854845648 = 0.00011393759632483125
modelV2.action.desiredCurvature @ 7652902253947 = 0.00010993104660883546
controlsState.desiredCurvature @ 7652912808349 = 0.00010993104660883546
modelV2.action.desiredCurvature @ 7652953017288 = 9.995466098189354e-05
controlsState.desiredCurvature @ 7652962391102 = 9.995466098189354e-05
modelV2.action.desiredCurvature @ 7653004448449 = 0.00010208125604549423
controlsState.desiredCurvature @ 7653012361777 = 0.00010208125604549423
This is the strongest direct runtime proof that the modified modelV2 path is being consumed by the control stack and not discarded.
NNLC is downstream of modelV2, not an alternate planner that bypasses it.
controlsd explicitly pushes modelV2 into the lateral extension in controlsd.py:
self.LaC.extension.update_model_v2(self.sm['modelV2'])
That stores the full message in latcontrol_torque_ext_base.py:
def update_model_v2(self, model_v2):
self.model_v2 = model_v2
NNLC then uses the fused modelV2 trajectory/state in nnlc.py and nnlc.py:
future_rolls = [np.interp(t, ModelConstants.T_IDXS, self.model_v2.orientation.x) ...]
future_planned_lateral_accels = [np.interp(t, ModelConstants.T_IDXS, self.model_v2.acceleration.y) ...]
Meaning:
modeldmodeld publishes the fused modelV2controlsd consumes modelV2.action.desiredCurvaturemodelV2 orientation and lateral-acceleration trajectorySo NNLC does not bypass the fused trajectory. It is downstream of it.
The thing that matters is not whether semanticPlan.desiredCurvature is copied verbatim into modelV2.action.desiredCurvature.
That is not how the integration works.
The actual sequence is:
modeld fuses that trajectory into the stock model_output.modeld recomputes action from the fused model_output.controlsd consumes the recomputed modelV2.action.modelV2 trajectory fields.So the correct proof is:
semanticPlan is valid and remotemodeld logs live fusionmodelV2.action.desiredCurvature is publishedcontrolsState.desiredCurvature matches that modelV2.action.desiredCurvaturemodelV2These are safe read-only checks.
adb reverse --list
adb shell "PYTHONPATH=/data/openpilot /usr/local/venv/bin/python - <<'PY'
import requests, time
t=time.time()
r=requests.get('http://127.0.0.1:8081/_health', timeout=2)
print(r.status_code, len(r.content), time.time()-t, r.headers.get('Content-Type'))
PY"
adb shell "timeout 3 sh -c 'cd /data/openpilot && PYTHONPATH=/data/openpilot /usr/local/venv/bin/python selfdrive/debug/dump.py semanticPlan --values semanticPlan.status,semanticPlan.source,semanticPlan.age,semanticPlan.generationExecutionTime,semanticPlan.desiredCurvature,semanticPlan.desiredAcceleration'"
adb shell "timeout 3 sh -c 'cd /data/openpilot && PYTHONPATH=/data/openpilot /usr/local/venv/bin/python selfdrive/debug/dump.py modelV2 --values modelV2.action.desiredCurvature,modelV2.action.desiredAcceleration'"
adb shell "timeout 3 sh -c 'cd /data/openpilot && PYTHONPATH=/data/openpilot /usr/local/venv/bin/python selfdrive/debug/dump.py controlsState --values controlsState.desiredCurvature'"
adb shell "PYTHONPATH=/data/openpilot /usr/local/venv/bin/python - <<'PY'
from pathlib import Path
files = sorted(Path('/data/log').glob('swaglog.*'), key=lambda p: p.stat().st_mtime, reverse=True)
print(files[0] if files else 'NOLOG')
if files:
lines = files[0].read_text(errors='ignore').splitlines()
hits = [ln for ln in lines if 'semantic plan fused' in ln]
for ln in hits[-5:]:
print(ln)
PY"
semanticPlan.status = unavailable or source = noneMeaning:
semantic plan fused logsMeaning:
modeld is not currently applying the remote semantic plansemanticPlan is missing, invalid, stale, or ignoredmodelV2.action.desiredCurvature and controlsState.desiredCurvature divergeMeaning:
modelV2.actionThe current live evidence proves all of the following:
modeld is fusing those plans into model_outputmodelV2.action.desiredCurvature is being consumed by controlsdmodelV2 and therefore does not bypass the modified trajectory pathThis is sufficient runtime proof that the Alpamayo-modified modelV2 path is real, live, and in the control stack.
9 commits
C++
91.5%
Python
4.0%
C
2.7%