diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..fe9e1ab
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+patches/**/*.patch -whitespace
diff --git a/.gitignore b/.gitignore
index ca3edc6..c220675 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,9 @@ dist/
checkpoint/
checkpoints/
*.safetensors
+eval/results/
+/goldens/
+/Testing/
# macOS resource forks
._*
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 28174de..7c6c01b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,9 +1,12 @@
cmake_minimum_required(VERSION 3.16)
project(robotcpp VERSION 0.1.0 LANGUAGES C CXX)
+include(CTest)
+
option(ROBOT_CPP_BUILD_ROBOT_SERVER "Build model-server target" ON)
option(ROBOT_CPP_BUILD_MODEL_CLI "Build model-cli target" OFF)
option(ROBOT_CPP_BUILD_ROBOT_CLIENT "Build C++ robot client targets" OFF)
+option(ROBOT_CPP_BUILD_STARVLA "Build the StarVLA runtime (requires llama.cpp overlay)" OFF)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -12,6 +15,27 @@ set(CMAKE_CXX_EXTENSIONS OFF)
if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/third_party/llama.cpp/CMakeLists.txt")
message(FATAL_ERROR "third_party/llama.cpp is required; run `git submodule update --init --recursive`")
endif()
+if(ROBOT_CPP_BUILD_STARVLA)
+ file(READ
+ "${CMAKE_CURRENT_SOURCE_DIR}/third_party/llama.cpp/tools/mtmd/models/qwen3vl.cpp"
+ ROBOT_CPP_QWEN3VL_MTMD_SOURCE)
+ file(READ
+ "${CMAKE_CURRENT_SOURCE_DIR}/third_party/llama.cpp/include/llama.h"
+ ROBOT_CPP_LLAMA_PUBLIC_HEADER)
+ string(FIND "${ROBOT_CPP_QWEN3VL_MTMD_SOURCE}"
+ "FFN_GELU_ERF" ROBOT_CPP_QWEN3VL_PARITY_PATCH_INDEX)
+ string(FIND "${ROBOT_CPP_LLAMA_PUBLIC_HEADER}"
+ "llama_set_backend_native_graphs_enabled" ROBOT_CPP_LLAMA_GRAPH_PATCH_INDEX)
+ if(ROBOT_CPP_QWEN3VL_PARITY_PATCH_INDEX EQUAL -1 OR
+ ROBOT_CPP_LLAMA_GRAPH_PATCH_INDEX EQUAL -1)
+ message(FATAL_ERROR
+ "StarVLA requires the pinned llama.cpp overlay. Run "
+ "`./tools/apply_patches.sh` from the repository root, "
+ "then configure again.")
+ endif()
+ unset(ROBOT_CPP_QWEN3VL_MTMD_SOURCE)
+ unset(ROBOT_CPP_LLAMA_PUBLIC_HEADER)
+endif()
set(LLAMA_BUILD_COMMON ON CACHE BOOL "" FORCE)
set(LLAMA_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
set(LLAMA_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
@@ -20,7 +44,16 @@ add_subdirectory(third_party/llama.cpp EXCLUDE_FROM_ALL)
if(NOT TARGET ggml OR NOT TARGET llama)
message(FATAL_ERROR "llama.cpp must provide ggml and llama targets")
endif()
-
+if(ROBOT_CPP_BUILD_STARVLA)
+ # mtmd normally inherits this variable when llama.cpp builds all tools.
+ if(NOT LLAMA_INSTALL_VERSION)
+ set(LLAMA_INSTALL_VERSION ${PROJECT_VERSION})
+ endif()
+ add_subdirectory(third_party/llama.cpp/tools/mtmd EXCLUDE_FROM_ALL)
+ if(NOT TARGET mtmd)
+ message(FATAL_ERROR "llama.cpp must provide the mtmd target for Qwen-VL")
+ endif()
+endif()
set(ROBOT_CPP_LLAMA_INCLUDE_DIRS
${CMAKE_CURRENT_SOURCE_DIR}/third_party/llama.cpp
${CMAKE_CURRENT_SOURCE_DIR}/third_party/llama.cpp/include
@@ -30,14 +63,29 @@ set(ROBOT_CPP_LLAMA_INCLUDE_DIRS
)
set(SMOLVLA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/models/smolvla)
+set(STARVLA_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/models/starvla)
set(ROBOT_SERVER_DIR ${CMAKE_CURRENT_SOURCE_DIR}/robot_server)
set(ROBOT_CLIENT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/robot_client)
-add_library(smolvla_runtime STATIC
+add_library(robotcpp_model_common STATIC
src/models/ggml_backend.cpp
src/models/ggml_backend.h
src/models/gguf_loader.cpp
src/models/gguf_loader.h
+ src/models/model_type.cpp
+)
+target_include_directories(robotcpp_model_common
+ PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}/src
+ ${ROBOT_CPP_LLAMA_INCLUDE_DIRS}
+)
+target_link_libraries(robotcpp_model_common PUBLIC ggml)
+target_compile_features(robotcpp_model_common PUBLIC cxx_std_17)
+if(NOT MSVC)
+ target_compile_options(robotcpp_model_common PRIVATE -Wno-cast-qual)
+endif()
+
+add_library(smolvla_runtime STATIC
${SMOLVLA_DIR}/smolvla_engine.cpp
${SMOLVLA_DIR}/smolvla_engine.h
${SMOLVLA_DIR}/state_proj.cpp
@@ -53,17 +101,13 @@ target_include_directories(smolvla_runtime
${SMOLVLA_DIR}
${ROBOT_CPP_LLAMA_INCLUDE_DIRS}
)
-target_link_libraries(smolvla_runtime PUBLIC ggml llama)
+target_link_libraries(smolvla_runtime PUBLIC robotcpp_model_common llama)
target_compile_features(smolvla_runtime PUBLIC cxx_std_17)
if(NOT MSVC)
target_compile_options(smolvla_runtime PRIVATE -Wno-cast-qual)
endif()
add_library(pi0_engine STATIC
- src/models/ggml_backend.cpp
- src/models/ggml_backend.h
- src/models/gguf_loader.cpp
- src/models/gguf_loader.h
src/models/pi0/types.h
src/models/pi0/action.cpp
src/models/pi0/action.h
@@ -87,13 +131,65 @@ target_include_directories(pi0_engine
${CMAKE_CURRENT_SOURCE_DIR}/src
${ROBOT_CPP_LLAMA_INCLUDE_DIRS}
)
-target_link_libraries(pi0_engine PUBLIC ggml llama)
+target_link_libraries(pi0_engine PUBLIC robotcpp_model_common llama)
target_compile_features(pi0_engine PUBLIC cxx_std_17)
if(NOT MSVC)
target_compile_options(pi0_engine PRIVATE -Wno-cast-qual)
endif()
+if(ROBOT_CPP_BUILD_STARVLA)
+add_library(starvla_runtime STATIC
+ ${STARVLA_DIR}/fast_codec.cpp
+ ${STARVLA_DIR}/fast_codec.h
+ ${STARVLA_DIR}/fast_policy.cpp
+ ${STARVLA_DIR}/fast_policy.h
+ ${STARVLA_DIR}/groot_policy.cpp
+ ${STARVLA_DIR}/groot_policy.h
+ ${STARVLA_DIR}/groot_prompt.cpp
+ ${STARVLA_DIR}/groot_prompt.h
+ ${STARVLA_DIR}/normalization.cpp
+ ${STARVLA_DIR}/normalization.h
+ ${STARVLA_DIR}/oft_image_preprocess.cpp
+ ${STARVLA_DIR}/oft_image_preprocess.h
+ ${STARVLA_DIR}/oft_prompt.cpp
+ ${STARVLA_DIR}/oft_prompt.h
+ ${STARVLA_DIR}/oft_policy.cpp
+ ${STARVLA_DIR}/oft_policy.h
+ ${STARVLA_DIR}/pi_policy.cpp
+ ${STARVLA_DIR}/pi_policy.h
+ ${STARVLA_DIR}/pi_v3_policy.cpp
+ ${STARVLA_DIR}/pi_v3_policy.h
+ ${STARVLA_DIR}/policy_gguf.h
+ ${STARVLA_DIR}/qwen3vl_bridge.cpp
+ ${STARVLA_DIR}/qwen3vl_bridge.h
+ ${STARVLA_DIR}/starvla_engine.cpp
+ ${STARVLA_DIR}/starvla_engine.h
+ third_party/llama.cpp/examples/gguf-hash/deps/sha256/sha256.c
+)
+target_include_directories(starvla_runtime
+ PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}/src
+ ${ROBOT_CPP_LLAMA_INCLUDE_DIRS}
+ PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/third_party/llama.cpp/vendor
+ ${CMAKE_CURRENT_SOURCE_DIR}/third_party/llama.cpp/examples/gguf-hash/deps
+ ${CMAKE_CURRENT_SOURCE_DIR}/third_party/llama.cpp/examples/gguf-hash/deps/sha256
+)
+target_link_libraries(starvla_runtime PUBLIC robotcpp_model_common llama mtmd)
+target_compile_features(starvla_runtime PUBLIC cxx_std_17)
+if(GGML_CUDA)
+ enable_language(CUDA)
+ target_sources(starvla_runtime PRIVATE ${STARVLA_DIR}/qwen_bf16_round_cuda.cu)
+ target_compile_definitions(starvla_runtime PRIVATE ROBOTCPP_STARVLA_CUDA=1)
+ set_property(TARGET starvla_runtime PROPERTY CUDA_STANDARD 17)
+endif()
+if(NOT MSVC)
+ target_compile_options(starvla_runtime PRIVATE -Wno-cast-qual)
+endif()
+endif()
+
add_library(robotcpp STATIC
+ src/models/argument_parse.h
src/models/model.h
src/models/model_factory.cpp
src/models/pi0/pi0_model.cpp
@@ -108,6 +204,19 @@ target_include_directories(robotcpp
)
target_link_libraries(robotcpp PUBLIC smolvla_runtime pi0_engine)
target_compile_features(robotcpp PUBLIC cxx_std_17)
+if(ROBOT_CPP_BUILD_STARVLA)
+ target_sources(robotcpp PRIVATE
+ ${STARVLA_DIR}/starvla_model.cpp
+ ${STARVLA_DIR}/starvla_model.h)
+ target_link_libraries(robotcpp PUBLIC starvla_runtime)
+ target_compile_definitions(robotcpp PUBLIC ROBOT_CPP_BUILD_STARVLA=1)
+endif()
+
+if(BUILD_TESTING AND ROBOT_CPP_BUILD_STARVLA)
+ add_executable(robotcpp-starvla-model-test tests/starvla/model_test.cpp)
+ target_link_libraries(robotcpp-starvla-model-test PRIVATE robotcpp)
+ add_test(NAME robotcpp-starvla-model-test COMMAND robotcpp-starvla-model-test)
+endif()
if(ROBOT_CPP_BUILD_ROBOT_SERVER OR ROBOT_CPP_BUILD_ROBOT_CLIENT)
add_library(robot_server_common STATIC
@@ -139,6 +248,7 @@ if(ROBOT_CPP_BUILD_ROBOT_CLIENT)
set_target_properties(model-cpp-client-example PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
target_link_libraries(model-cpp-client-example PRIVATE model_client_cpp)
target_compile_features(model-cpp-client-example PRIVATE cxx_std_17)
+
endif()
if(ROBOT_CPP_BUILD_ROBOT_SERVER)
@@ -165,7 +275,6 @@ if(ROBOT_CPP_BUILD_ROBOT_SERVER)
)
target_link_libraries(model-server PRIVATE robot_server_core robotcpp)
target_compile_features(model-server PRIVATE cxx_std_17)
-
add_executable(smolvla-raw-predict ${ROBOT_SERVER_DIR}/test/smolvla_raw_predict.cpp)
set_target_properties(smolvla-raw-predict PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
target_include_directories(smolvla-raw-predict PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ${SMOLVLA_DIR})
diff --git a/README.md b/README.md
index 055e826..29495dd 100644
--- a/README.md
+++ b/README.md
@@ -54,8 +54,13 @@ We also provide two tools to support robot model development:
git clone https://github.com/Robot-cpp/robot.cpp
cd robot.cpp
git submodule update --init --recursive
+./tools/apply_patches.sh
```
+The launch scripts below configure and build `model-server` automatically. For
+a manual StarVLA build, enable `ROBOT_CPP_BUILD_STARVLA`; see the
+[Robot Server build instructions](robot_server/README.md#manual-build).
+
This section introduces three usage paths to help you quickly understand the repository:
* Starting `model-server` and connecting it to a minimal dummy `model-client`.
@@ -95,6 +100,14 @@ After downloading, run `model-server` like this:
For general local setups, we provide ready-to-use build-and-launch shells for three platforms. You can modify the environment variables inside the scripts, or override them directly with `export`. See [robot_server/README.md](robot_server/README.md) for details.
+For example, from the repository root on Linux with CUDA:
+
+```bash
+export ROBOT_CPP_ROOT="$PWD"
+export GGUF_DIR=/path/to/smolvla-so101-fp32
+bash robot_server/shell/launch_robot_server_linux_cuda.sh
+```
+
| Backend | macOS | Linux | Windows |
| ------- | ------------------------------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------- |
| CUDA | - | `robot_server/shell/launch_robot_server_linux_cuda.sh` | `robot_server/shell/launch_robot_server_windows_cuda.bat` |
@@ -127,7 +140,7 @@ We provide a build-to-run example in `robot_client/shell/cpp_client_example.sh`.
| `ROBOT_CPP_ROOT` | unset; required | Repository root. |
| `BUILD_DIR` | `${ROBOT_CPP_ROOT}/build_robot_client` | C++ client CMake build directory. |
| `PORT` | `5555` | Server port used by the client. |
-| `BUILD_CLIENT` | `0` | Whether to force rebuild the client. Set to`1` to rebuild even if the binary already exists. |
+| `BUILD_CLIENT` | `0` | Whether to force rebuild the client. Set to `1` to rebuild even if the binary already exists. |
| `CMAKE_BIN` | `cmake` | CMake command path, useful for selecting a custom CMake binary. |
Then run:
@@ -150,20 +163,33 @@ See the [SO-101 deployment guide](eval/lerobot_so101/README.md).
## ⚡ Performance
-We benchmark Robot.cpp on several platforms. Each measurement uses 5 warmup runs and 100 loop runs. The reported latency is the average time from receiving the image, through preprocessing and forward inference, to producing a usable action chunk, measured in milliseconds. All state projectors remain in f32 precision.
+We benchmark Robot.cpp on several platforms. Each measurement uses 5 warmup runs and 100 loop runs. The reported latency is the average time from receiving the image, through preprocessing and forward inference, to producing a usable action chunk, measured in milliseconds. State projectors, where present, remain in f32 precision.
For the LIBERO setting, the input contains two 256x256 images and an 8-dimensional state. For the SO-101 real-robot setting, the input contains one 224x224 image and a 6-dimensional state.
For SmolVLA preprocessing, we follow the official default setting: images are first resized to 512x512.
-| Model | Mac M4 Pro (CPU) | Mac M4 Pro (Metal) | RTX 4090 | RTX 3060 | A100 | Jetson AGX Orin |
-| ---------------------- | ---------------: | -----------------: | -------: | ----------: | ---: | --------------: |
-| smolvla@libero (bf16*) | 527 | 216 | 28 | 116 | 43 | 282 |
-| smolvla@libero (f32) | 577 | 236 | 32 | 142 | 42 | 299 |
-| smolvla@so-101 (bf16*) | 339 | 145 | 23 | 77 | 36 | 184 |
-| smolvla@so-101 (f32) | 396 | 158 | 24 | 92 | 34 | 200 |
-| pi0@libero (f32) | 1839 | 710 | 83 | OOM/offload | 71 | 956 |
-| pi0@libero (bf16*) | 1954 | 635 | 57 | 267 | 66 | 498 |
+For StarVLA, the input contains one 224x224 image and no robot state. Qwen and
+the multimodal projector use bf16; OFT, GR00T, PI, and PI_v3 policies use f32.
+FAST stores its action codec in the policy GGUF.
+The StarVLA A100 results use an A100-PCIE-40GB with 8 CPU threads,
+`n_ctx=2048`, `n_batch=2048`, and noise seed 0.
+
+| Model | Mac M4 Pro (CPU) | Mac M4 Pro (Metal) | RTX 4090 | RTX 3060 | A100 | Jetson AGX Orin |
+| ----------------------------- | ---------------: | -----------------: | -------: | ----------: | ---: | --------------: |
+| smolvla@libero (bf16*) | 527 | 216 | 28 | 116 | 43 | 282 |
+| smolvla@libero (f32) | 577 | 236 | 32 | 142 | 42 | 299 |
+| smolvla@so-101 (bf16*) | 339 | 145 | 23 | 77 | 36 | 184 |
+| smolvla@so-101 (f32) | 396 | 158 | 24 | 92 | 34 | 200 |
+| pi0@libero (f32) | 1839 | 710 | 83 | OOM/offload | 71 | 956 |
+| pi0@libero (bf16*) | 1954 | 635 | 57 | 267 | 66 | 498 |
+| starvla/oft@bridge | - | - | - | - | 50 | - |
+| starvla/groot@bridge | - | - | - | - | 54 | - |
+| starvla/pi_v3@bridge | - | - | - | - | 112 | - |
+| starvla/qwen25_oft@bridge | - | - | - | - | 42 | - |
+| starvla/qwen25_groot@bridge | - | - | - | - | 51 | - |
+| starvla/qwen25_pi@bridge | - | - | - | - | 101 | - |
+| starvla/qwen25_fast@bridge | - | - | - | - | 386 | - |
> `bf16*`: on Mac, f16 results are used in place of bf16 because current Mac bf16 support is not ideal.
> `OOM/offload`: pi0@libero (f32) runs out of memory on RTX 3060 and triggers offload, so we do not report a latency number for now.
@@ -230,6 +256,55 @@ This section lists converted GGUF models that can be used directly with `model-s
f32 |
pi0-libero-f32 |
+
+ | StarVLA Qwen3-VL OFT |
+ Bridge |
+ StarVLA/Qwen3VL-OFT-Bridge-RT-1 |
+ bf16 + f32 policy |
+ starvla-qwen3-oft-bridge-bf16 |
+
+
+ | StarVLA Qwen3-VL GR00T |
+ Bridge |
+ StarVLA/Qwen3VL-GR00T-Bridge-RT-1 |
+ bf16 + f32 policy |
+ starvla-qwen3-groot-bridge-bf16 |
+
+
+ | StarVLA Qwen3-VL PI_v3 |
+ Bridge |
+ StarVLA/Qwen3VL-PI_v3-Bridge-RT_1 |
+ bf16 + f32 policy |
+ starvla-qwen3-pi-v3-bridge-bf16 |
+
+
+ | StarVLA Qwen2.5-VL OFT |
+ Bridge |
+ StarVLA/Qwen-OFT-Bridge-RT-1 |
+ bf16 + f32 policy |
+ starvla-qwen25-oft-bridge-bf16 |
+
+
+ | StarVLA Qwen2.5-VL GR00T |
+ Bridge |
+ StarVLA/Qwen-GR00T-Bridge-RT-1 |
+ bf16 + f32 policy |
+ starvla-qwen25-groot-bridge-bf16 |
+
+
+ | StarVLA Qwen2.5-VL PI |
+ Bridge |
+ StarVLA/Qwen-PI-Bridge-RT-1 |
+ bf16 + f32 policy |
+ starvla-qwen25-pi-bridge-bf16 |
+
+
+ | StarVLA Qwen2.5-VL FAST |
+ Bridge |
+ StarVLA/Qwen-FAST-Bridge-RT-1 |
+ bf16 + codec |
+ starvla-qwen25-fast-bridge-bf16 |
+
diff --git a/README_ZH.md b/README_ZH.md
index 78c5e4a..edbe945 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -54,8 +54,13 @@ Robot.cpp是一个轻量化的on-device机器人模型推理框架,在llama.cp
git clone https://github.com/Robot-cpp/robot.cpp
cd robot.cpp
git submodule update --init --recursive
+./tools/apply_patches.sh
```
+下文的启动脚本会自动配置并编译 `model-server`。手动构建 StarVLA 时需要开启
+`ROBOT_CPP_BUILD_STARVLA`,详见
+[Robot Server 构建说明](robot_server/README_ZH.md#手动构建)。
+
我们介绍三类使用案例来帮助你快速了解本仓库:
* model-server的启动,其与最小dummy model-client通信的案例。
@@ -95,6 +100,14 @@ git submodule update --init --recursive
对于更加一般的情况,我们也提供了三个平台的开箱即用编译+启动的shell,可以通过修改shell里的环境变量,或者直接export的形式来快速在本机实现启动。详情参见 [robot_server/README_ZH.md](robot_server/README_ZH.md)
+例如,在 Linux CUDA 环境中从仓库根目录运行:
+
+```bash
+export ROBOT_CPP_ROOT="$PWD"
+export GGUF_DIR=/path/to/smolvla-so101-fp32
+bash robot_server/shell/launch_robot_server_linux_cuda.sh
+```
+
| Backend | macOS | Linux | Windows |
| ------- | ------------------------------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------- |
| CUDA | - | `robot_server/shell/launch_robot_server_linux_cuda.sh` | `robot_server/shell/launch_robot_server_windows_cuda.bat` |
@@ -127,7 +140,7 @@ python robot_client/examples/python/minimal_example.py
| `ROBOT_CPP_ROOT` | 无,必须设置 | 仓库根目录。 |
| `BUILD_DIR` | `${ROBOT_CPP_ROOT}/build_robot_client` | C++ client 的 CMake build 目录 |
| `PORT` | `5555` | client 连接的 server port |
-| `BUILD_CLIENT` | `0` | 是否强制重新build client。设为`1` 时即使 binary 已存在也会重新 build |
+| `BUILD_CLIENT` | `0` | 是否强制重新build client。设为 `1` 时即使 binary 已存在也会重新 build |
| `CMAKE_BIN` | `cmake` | 使用的 CMake 命令路径,可用于指定自定义 CMake |
然后运行下面的bash:
@@ -156,6 +169,11 @@ bash robot_client/shell/cpp_client_example.sh
其中对于smolvla的preprocess设定,参考官方的基本设定,即首先会将图片变成512*512。
+StarVLA 使用一张 224x224 图像且不输入 robot state。Qwen 和 multimodal projector
+使用 bf16,OFT、GR00T、PI 和 PI_v3 policy 使用 f32;FAST 的 policy GGUF 保存 action
+codec。A100 数据在 A100-PCIE-40GB、8 个 CPU 线程、`n_ctx=2048`、`n_batch=2048` 和
+noise seed 0 下测得。
+
| Model | Mac M4 Pro (CPU) | Mac M4 Pro (Metal) | RTX 4090 | RTX 3060 | A100 | Jetson AGX Orin |
| ---------------------- | ---------------: | -----------------: | -------: | ----------: | ---: | --------------: |
| smolvla@libero (bf16*) | 527 | 216 | 28 | 116 | 43 | 282 |
@@ -164,15 +182,24 @@ bash robot_client/shell/cpp_client_example.sh
| smolvla@so-101 (f32) | 396 | 158 | 24 | 92 | 34 | 200 |
| pi0@libero (f32) | 1839 | 710 | 83 | OOM/offload | 71 | 956 |
| pi0@libero (bf16*) | 1954 | 635 | 57 | 267 | 66 | 498 |
+| starvla/oft@bridge | - | - | - | - | 50 | - |
+| starvla/groot@bridge | - | - | - | - | 54 | - |
+| starvla/pi_v3@bridge | - | - | - | - | 112 | - |
+| starvla/qwen25_oft@bridge | - | - | - | - | 42 | - |
+| starvla/qwen25_groot@bridge | - | - | - | - | 51 | - |
+| starvla/qwen25_pi@bridge | - | - | - | - | 101 | - |
+| starvla/qwen25_fast@bridge | - | - | - | - | 386 | - |
> `bf16*`:在 Mac上使用 f16 结果替代 bf16,因为当前 Mac对 bf16 的支持不够好。
> `OOM/offload`:pi0@libero (f32) 在 RTX 3060 上会 OOM 并触发 offload,因此暂时不报告 latency 数值。
---
-## 🧩 model-zoo
+## 🧩 Model Zoo
-这里整理一些已经转换好的 GGUF 模型,可以直接配合 `model-server` 做smoke test,以方便quick start!但针对自己的实际场景,我们推荐使用[hf2gguf](tools/hf2gguf/README_ZH.md)来生成自己的GGUF model!并且对于不同的部分,您还可以自定义不同的精度,来实现不同部分的精度组合(事实上,不同部分的最优精度通常是不同的),我们的例子中,state proj始终保持f32精度,其他的gguf随着precision精度变化而变化,您可以自行组合,探索更好更高效的性能tradeoff!
+下表列出可直接配合 `model-server` 使用的 GGUF 模型。实际部署时,建议使用
+[`hf2gguf`](tools/hf2gguf/README_ZH.md) 转换自己的 checkpoint。各组件可以分别选择
+精度;表中示例的 state projector 固定为 f32,其余组件采用标注的精度。
diff --git a/patches/llama.cpp/0001-qwen3vl-vision-parity.patch b/patches/llama.cpp/0001-qwen3vl-vision-parity.patch
new file mode 100644
index 0000000..a868059
--- /dev/null
+++ b/patches/llama.cpp/0001-qwen3vl-vision-parity.patch
@@ -0,0 +1,36 @@
+diff --git a/tools/mtmd/models/qwen3vl.cpp b/tools/mtmd/models/qwen3vl.cpp
+index fa1100d..5119df6 100644
+--- a/tools/mtmd/models/qwen3vl.cpp
++++ b/tools/mtmd/models/qwen3vl.cpp
+@@ -43,8 +43,11 @@ ggml_cgraph * clip_graph_qwen3vl::build() {
+ cb(inp, "patch_bias", -1);
+ }
+
+- // calculate absolute position embedding and apply
+- ggml_tensor * learned_pos_embd = resize_position_embeddings();
++ // Qwen3-VL constructs interpolation coordinates with torch.linspace(0,
++ // num_grid_per_side - 1, size), which is bilinear align_corners=True
++ // without antialiasing.
++ ggml_tensor * learned_pos_embd = resize_position_embeddings(
++ GGML_SCALE_MODE_BILINEAR | GGML_SCALE_FLAG_ALIGN_CORNERS);
+ learned_pos_embd = ggml_cont_4d(
+ ctx0, learned_pos_embd,
+ n_embd * 2, n_patches_x / 2, n_patches_y, batch_size);
+@@ -154,7 +157,7 @@ ggml_cgraph * clip_graph_qwen3vl::build() {
+ layer.deepstack_fc1_w, layer.deepstack_fc1_b,
+ nullptr, nullptr,
+ layer.deepstack_fc2_w, layer.deepstack_fc2_b,
+- ffn_op_type::FFN_GELU, il);
++ ffn_op_type::FFN_GELU_ERF, il);
+
+ if(!deepstack_features) {
+ deepstack_features = feat;
+@@ -180,7 +183,7 @@ ggml_cgraph * clip_graph_qwen3vl::build() {
+ model.mm_0_w, model.mm_0_b,
+ nullptr, nullptr,
+ model.mm_1_w, model.mm_1_b,
+- ffn_op_type::FFN_GELU, -1);
++ ffn_op_type::FFN_GELU_ERF, -1);
+
+ if (deepstack_features) {
+ embeddings = ggml_concat(ctx0, embeddings, deepstack_features, 0);
diff --git a/patches/llama.cpp/0002-per-context-native-graph-control.patch b/patches/llama.cpp/0002-per-context-native-graph-control.patch
new file mode 100644
index 0000000..3b3a992
--- /dev/null
+++ b/patches/llama.cpp/0002-per-context-native-graph-control.patch
@@ -0,0 +1,267 @@
+diff --git a/ggml/include/ggml-backend.h b/ggml/include/ggml-backend.h
+index d0c7e5a..d86d0b3 100644
+--- a/ggml/include/ggml-backend.h
++++ b/ggml/include/ggml-backend.h
+@@ -215,6 +215,8 @@ extern "C" {
+ typedef ggml_backend_buffer_type_t * (*ggml_backend_dev_get_extra_bufts_t)(ggml_backend_dev_t device);
+ // Set the abort callback for the backend
+ typedef void (*ggml_backend_set_abort_callback_t)(ggml_backend_t backend, ggml_abort_callback abort_callback, void * abort_callback_data);
++ // Enable or disable native graph capture/cache for one backend instance.
++ typedef void (*ggml_backend_set_native_graphs_enabled_t)(ggml_backend_t backend, bool enabled);
+ // Get a list of feature flags supported by the backend (returns a NULL-terminated array)
+ struct ggml_backend_feature {
+ const char * name;
+diff --git a/ggml/src/ggml-cuda/common.cuh b/ggml/src/ggml-cuda/common.cuh
+index 1081750..fb7c279 100644
+--- a/ggml/src/ggml-cuda/common.cuh
++++ b/ggml/src/ggml-cuda/common.cuh
+@@ -1373,6 +1373,8 @@ struct ggml_backend_cuda_context {
+ int curr_stream_no = 0;
+
+ #ifdef USE_CUDA_GRAPH
++ bool cuda_graphs_enabled = true;
++
+ // Map from first_node_ptr to cuda_graph - allows multiple graphs per context
+ // when the computation is split across CPU/GPU (e.g., with --n-cpu-moe)
+ std::unordered_map> cuda_graphs;
+@@ -1405,6 +1407,9 @@ struct ggml_backend_cuda_context {
+ // Check if any CUDA graph is enabled for this context (used by kernels that need to know
+ // if graphs are in use without having access to the specific graph key)
+ bool any_cuda_graph_enabled() const {
++ if (!cuda_graphs_enabled) {
++ return false;
++ }
+ for (const auto & [key, graph] : cuda_graphs) {
+ if (graph && graph->is_enabled()) {
+ return true;
+@@ -1415,6 +1420,9 @@ struct ggml_backend_cuda_context {
+
+ // Check if any CUDA graph has an instance for this context
+ bool any_cuda_graph_has_instance() const {
++ if (!cuda_graphs_enabled) {
++ return false;
++ }
+ for (const auto & [key, graph] : cuda_graphs) {
+ if (graph && graph->instance != nullptr) {
+ return true;
+diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu
+index 8d21b22..42f7838 100644
+--- a/ggml/src/ggml-cuda/ggml-cuda.cu
++++ b/ggml/src/ggml-cuda/ggml-cuda.cu
+@@ -3085,6 +3085,23 @@ static void ggml_backend_cuda_synchronize(ggml_backend_t backend) {
+ GGML_UNUSED(backend);
+ }
+
++static void ggml_backend_cuda_set_native_graphs_enabled(ggml_backend_t backend, bool enabled) {
++ ggml_backend_cuda_context * cuda_ctx = (ggml_backend_cuda_context *) backend->context;
++
++#ifdef USE_CUDA_GRAPH
++ if (cuda_ctx->cuda_graphs_enabled == enabled) {
++ return;
++ }
++
++ ggml_backend_cuda_synchronize(backend);
++ cuda_ctx->cuda_graphs.clear();
++ cuda_ctx->cuda_graphs_enabled = enabled;
++#else
++ GGML_UNUSED(cuda_ctx);
++ GGML_UNUSED(enabled);
++#endif
++}
++
+ #ifdef USE_CUDA_GRAPH
+ static bool ggml_cuda_graph_check_compability(ggml_cgraph * cgraph) {
+
+@@ -4202,8 +4219,8 @@ static void ggml_cuda_graph_evaluate_and_capture(ggml_backend_cuda_context * cud
+ }
+
+ #ifdef USE_CUDA_GRAPH
+- ggml_cuda_graph * graph = cuda_ctx->cuda_graph(graph_key);
+ if (use_cuda_graph && cuda_graph_update_required) { // End CUDA graph capture
++ ggml_cuda_graph * graph = cuda_ctx->cuda_graph(graph_key);
+ if (graph->graph != nullptr) {
+ CUDA_CHECK(cudaGraphDestroy(graph->graph));
+ graph->graph = nullptr;
+@@ -4240,6 +4257,10 @@ static void ggml_cuda_graph_evaluate_and_capture(ggml_backend_cuda_context * cud
+
+ #ifdef USE_CUDA_GRAPH
+ static bool ggml_cuda_graph_set_enabled(ggml_backend_cuda_context * cuda_ctx, const void * graph_key) {
++ if (!cuda_ctx->cuda_graphs_enabled) {
++ return false;
++ }
++
+ ggml_cuda_graph * graph = cuda_ctx->cuda_graph(graph_key);
+
+ if (graph->graph == nullptr) {
+@@ -4267,10 +4288,8 @@ static enum ggml_status ggml_backend_cuda_graph_compute(ggml_backend_t backend,
+ #ifdef USE_CUDA_GRAPH
+ graph_key = ggml_cuda_graph_get_key(cgraph);
+
+- ggml_cuda_graph_set_enabled(cuda_ctx, graph_key);
+-
+- ggml_cuda_graph * graph = cuda_ctx->cuda_graph(graph_key);
+- if (graph->is_enabled()) {
++ if (ggml_cuda_graph_set_enabled(cuda_ctx, graph_key)) {
++ ggml_cuda_graph * graph = cuda_ctx->cuda_graph(graph_key);
+ const bool graph_compatible = ggml_cuda_graph_check_compability(cgraph);
+ if (graph_compatible) {
+ const bool properties_changed = ggml_cuda_graph_update_required(cuda_ctx, cgraph);
+@@ -5400,6 +5419,9 @@ static void * ggml_backend_cuda_reg_get_proc_address(ggml_backend_reg_t reg, con
+ if (strcmp(name, "ggml_backend_get_features") == 0) {
+ return (void *)ggml_backend_cuda_get_features;
+ }
++ if (strcmp(name, "ggml_backend_set_native_graphs_enabled") == 0) {
++ return (void *)ggml_backend_cuda_set_native_graphs_enabled;
++ }
+ return nullptr;
+ }
+
+diff --git a/include/llama.h b/include/llama.h
+index 2ea2267..aa6c656 100644
+--- a/include/llama.h
++++ b/include/llama.h
+@@ -969,6 +969,10 @@ extern "C" {
+ // Set abort callback
+ LLAMA_API void llama_set_abort_callback(struct llama_context * ctx, ggml_abort_callback abort_callback, void * abort_callback_data);
+
++ // Enable or disable native graph capture/cache for each context backend
++ // that exposes this optional capability. Direct graph computation remains enabled.
++ LLAMA_API void llama_set_backend_native_graphs_enabled(struct llama_context * ctx, bool enabled);
++
+ // Wait until all computations are finished
+ // This is automatically done when using one of the functions below to obtain the computation results
+ // and is not necessary to call it explicitly in most cases
+diff --git a/src/llama-context.cpp b/src/llama-context.cpp
+index 71a5939..a705255 100644
+--- a/src/llama-context.cpp
++++ b/src/llama-context.cpp
+@@ -1031,6 +1031,21 @@ void llama_context::set_abort_callback(bool (*abort_callback)(void * data), void
+ }
+ }
+
++void llama_context::set_backend_native_graphs_enabled(bool enabled) {
++ for (auto & backend : backends) {
++ auto * reg = ggml_backend_dev_backend_reg(ggml_backend_get_device(backend.get()));
++ if (reg == nullptr) {
++ continue;
++ }
++ auto * set_enabled = reinterpret_cast(
++ ggml_backend_reg_get_proc_address(
++ reg, "ggml_backend_set_native_graphs_enabled"));
++ if (set_enabled != nullptr) {
++ set_enabled(backend.get(), enabled);
++ }
++ }
++}
++
+ void llama_context::set_embeddings(bool value) {
+ LLAMA_LOG_DEBUG("%s: value = %d\n", __func__, value);
+
+@@ -3365,6 +3380,10 @@ void llama_set_abort_callback(llama_context * ctx, bool (*abort_callback)(void *
+ ctx->set_abort_callback(abort_callback, abort_callback_data);
+ }
+
++void llama_set_backend_native_graphs_enabled(llama_context * ctx, bool enabled) {
++ ctx->set_backend_native_graphs_enabled(enabled);
++}
++
+ void llama_set_embeddings(llama_context * ctx, bool embeddings) {
+ ctx->set_embeddings(embeddings);
+ }
+diff --git a/src/llama-context.h b/src/llama-context.h
+index 92d1b0c..d1354a4 100644
+--- a/src/llama-context.h
++++ b/src/llama-context.h
+@@ -105,6 +105,7 @@ struct llama_context {
+ void set_n_threads(int32_t n_threads, int32_t n_threads_batch);
+
+ void set_abort_callback(bool (*abort_callback)(void * data), void * abort_callback_data);
++ void set_backend_native_graphs_enabled(bool enabled);
+
+ void set_embeddings (bool value);
+ void set_causal_attn(bool value);
+diff --git a/tools/mtmd/clip.cpp b/tools/mtmd/clip.cpp
+index 513b94f..4e63c93 100644
+--- a/tools/mtmd/clip.cpp
++++ b/tools/mtmd/clip.cpp
+@@ -2973,6 +2973,26 @@ void clip_free(clip_ctx * ctx) {
+ delete ctx;
+ }
+
++void clip_set_backend_native_graphs_enabled(clip_ctx * ctx, bool enabled) {
++ if (ctx == nullptr) {
++ return;
++ }
++
++ for (ggml_backend_t backend : ctx->backend_ptrs) {
++ ggml_backend_dev_t dev = ggml_backend_get_device(backend);
++ ggml_backend_reg_t reg = dev ? ggml_backend_dev_backend_reg(dev) : nullptr;
++ if (reg == nullptr) {
++ continue;
++ }
++ auto * set_enabled = reinterpret_cast(
++ ggml_backend_reg_get_proc_address(
++ reg, "ggml_backend_set_native_graphs_enabled"));
++ if (set_enabled != nullptr) {
++ set_enabled(backend, enabled);
++ }
++ }
++}
++
+ // deprecated
+ size_t clip_embd_nbytes(const struct clip_ctx * ctx) {
+ const int32_t nx = ctx->model.hparams.image_size;
+diff --git a/tools/mtmd/clip.h b/tools/mtmd/clip.h
+index a859b38..d63d43d 100644
+--- a/tools/mtmd/clip.h
++++ b/tools/mtmd/clip.h
+@@ -51,6 +51,10 @@ struct clip_init_result clip_init(const char * fname, struct clip_context_params
+
+ void clip_free(struct clip_ctx * ctx);
+
++// Enable or disable native graph capture/cache for each CLIP backend that
++// exposes this optional capability. Direct graph computation remains enabled.
++void clip_set_backend_native_graphs_enabled(struct clip_ctx * ctx, bool enabled);
++
+ size_t clip_embd_nbytes(const struct clip_ctx * ctx);
+ size_t clip_embd_nbytes_by_img(const struct clip_ctx * ctx, int img_w, int img_h);
+
+diff --git a/tools/mtmd/mtmd.cpp b/tools/mtmd/mtmd.cpp
+index 87da687..5fa1bc3 100644
+--- a/tools/mtmd/mtmd.cpp
++++ b/tools/mtmd/mtmd.cpp
+@@ -628,6 +628,19 @@ void mtmd_free(mtmd_context * ctx) {
+ delete ctx;
+ }
+
++void mtmd_set_backend_native_graphs_enabled(mtmd_context * ctx, bool enabled) {
++ if (ctx == nullptr) {
++ return;
++ }
++
++ if (ctx->ctx_v != nullptr) {
++ clip_set_backend_native_graphs_enabled(ctx->ctx_v, enabled);
++ }
++ if (ctx->ctx_a != nullptr) {
++ clip_set_backend_native_graphs_enabled(ctx->ctx_a, enabled);
++ }
++}
++
+ struct mtmd_tokenizer {
+ mtmd_context * ctx;
+ std::vector bitmaps;
+diff --git a/tools/mtmd/mtmd.h b/tools/mtmd/mtmd.h
+index e364174..7cf3af7 100644
+--- a/tools/mtmd/mtmd.h
++++ b/tools/mtmd/mtmd.h
+@@ -110,6 +110,10 @@ MTMD_API mtmd_context * mtmd_init_from_file(const char * mmproj_fname,
+
+ MTMD_API void mtmd_free(mtmd_context * ctx);
+
++// Enable or disable native graph capture/cache for each media backend that
++// exposes this optional capability. Direct graph computation remains enabled.
++MTMD_API void mtmd_set_backend_native_graphs_enabled(mtmd_context * ctx, bool enabled);
++
+ // whether we need to set non-causal mask before llama_decode
+ // if chunk is nullptr, we assume the default case where chunk is an image chunk
+ MTMD_API bool mtmd_decode_use_non_causal(const mtmd_context * ctx, const mtmd_input_chunk * chunk);
diff --git a/patches/llama.cpp/README.md b/patches/llama.cpp/README.md
new file mode 100644
index 0000000..1a17011
--- /dev/null
+++ b/patches/llama.cpp/README.md
@@ -0,0 +1,31 @@
+# llama.cpp patches
+
+The project pins `third_party/llama.cpp` at commit
+`3e941b813b1acbbf06c2203a94ceb33d84748c1e`. The repository applies two
+changes that are not available through that revision's public APIs:
+
+1. `0001-qwen3vl-vision-parity.patch` uses the position interpolation and exact
+ GELU operations from the Qwen3-VL implementation used by StarVLA.
+2. `0002-per-context-native-graph-control.patch` adds an optional backend API to
+ disable CUDA graph capture for the text and vision contexts owned by one
+ StarVLA instance. This avoids retained CUDA graphs growing memory use during
+ long runs without changing the setting for other llama.cpp users.
+
+Apply the repository patch set after initializing submodules and before building:
+
+```bash
+./tools/apply_patches.sh
+```
+
+The command verifies the exact llama.cpp revision and refuses a dirty or
+partially patched checkout. It is safe to run again after a complete apply.
+
+Inspect or remove the overlay with:
+
+```bash
+./tools/apply_patches.sh --check
+./tools/apply_patches.sh --revert
+```
+
+The parent repository commits only these patch assets. It does not advance or
+commit a forked llama.cpp gitlink.
diff --git a/robot_client/cpp/model_client.cpp b/robot_client/cpp/model_client.cpp
index 5496144..3ad82d8 100644
--- a/robot_client/cpp/model_client.cpp
+++ b/robot_client/cpp/model_client.cpp
@@ -55,8 +55,9 @@ bool make_predict_request(const ModelObservation & obs, proto::predict_request &
return false;
}
- req.task = obs.prompt;
- req.state = obs.state;
+ req.task = obs.prompt;
+ req.state = obs.state;
+ req.initial_noise = obs.initial_noise;
req.images.clear();
req.images.reserve(obs.images.size());
diff --git a/robot_client/cpp/model_client.h b/robot_client/cpp/model_client.h
index a6b7730..ea84f47 100644
--- a/robot_client/cpp/model_client.h
+++ b/robot_client/cpp/model_client.h
@@ -21,6 +21,7 @@ struct ModelImage {
struct ModelObservation {
std::vector images;
std::vector state;
+ std::vector initial_noise;
std::string prompt = "grab the block.";
};
diff --git a/robot_client/python/model_client.py b/robot_client/python/model_client.py
index 06e3791..5c79423 100644
--- a/robot_client/python/model_client.py
+++ b/robot_client/python/model_client.py
@@ -9,7 +9,7 @@
MAGIC = 0x414C5653
-VERSION = 3
+VERSION = 4
HEADER_SIZE = 32
OP_HEALTH = 1
@@ -21,8 +21,8 @@
IMAGE_RAW_RGB_U8 = 1
HEADER = struct.Struct(" tuple[int, int, int, bytes]:
def encode_predict_observation(observation: dict[str, Any]) -> bytes:
images = observation["images"]
state = state_to_list(observation["state"])
+ initial_noise = state_to_list(observation.get("initial_noise"))
prompt = str(observation["prompt"])
if not images:
raise ValueError("observation.images must contain at least one image")
@@ -88,13 +89,14 @@ def encode_predict_observation(observation: dict[str, Any]) -> bytes:
encoded_images.append((name, rgb, width, height, stride))
payload = bytearray()
- payload += PREDICT_REQ_V2_FIXED.pack(
+ payload += PREDICT_REQ_FIXED.pack(
len(encoded_images),
len(state),
+ len(initial_noise),
len(prompt_bytes),
)
for name, rgb, width, height, stride in encoded_images:
- payload += PREDICT_REQ_V2_IMAGE.pack(
+ payload += PREDICT_REQ_IMAGE.pack(
IMAGE_RAW_RGB_U8,
len(name),
width,
@@ -105,6 +107,8 @@ def encode_predict_observation(observation: dict[str, Any]) -> bytes:
)
for value in state:
payload += struct.pack("
@@ -26,6 +27,7 @@ struct server_args {
std::string action_decoder_path;
std::string state_proj_path;
std::string action_expert_path;
+ std::string policy_path;
std::string task = "grab the block.";
std::string host = "127.0.0.1";
int port = 5555;
@@ -37,18 +39,6 @@ struct server_args {
int verbosity = 0;
};
-static bool parse_model_type(const std::string & value, robotcpp::model_type & out) {
- if (value == "smolvla") {
- out = robotcpp::model_type::smolvla;
- return true;
- }
- if (value == "pi0") {
- out = robotcpp::model_type::pi0;
- return true;
- }
- return false;
-}
-
static bool parse_noise_mode(const std::string & value, int & out_mode) {
if (value == "gaussian") {
out_mode = SMOLVLA_NOISE_MODE_GAUSSIAN;
@@ -74,9 +64,11 @@ static void print_usage(const char * prog) {
" [options]\n"
" %s --model-type pi0 --vit --mmproj --llm --tokenizer --state-gguf "
" --action-decoder [options]\n"
+ " %s --model-type starvla --llm --mmproj --policy [options]\n"
"\n"
"Common options:\n"
- " --model-type Model type (default: smolvla)\n"
+ " --model-type smolvla|pi0|starvla\n"
+ " (default: smolvla)\n"
"\n"
"SmolVLA options:\n"
" --llm LLM GGUF path\n"
@@ -93,6 +85,11 @@ static void print_usage(const char * prog) {
" --state-gguf State projector GGUF path\n"
" --action-decoder Action decoder GGUF path\n"
"\n"
+ "StarVLA options:\n"
+ " --policy StarVLA policy GGUF path (required)\n"
+ " --llm Qwen text GGUF path (required)\n"
+ " --mmproj Qwen vision GGUF path (required)\n"
+ "\n"
"Runtime options:\n"
" --host Listen host (default: 127.0.0.1)\n"
" --port Listen port (default: 5555)\n"
@@ -103,7 +100,7 @@ static void print_usage(const char * prog) {
" --noise-seed RNG seed, <0 means auto (default: -1)\n"
" --verbosity Log verbosity (default: 0)\n"
" -h, --help Show this help\n",
- prog, prog);
+ prog, prog, prog);
}
// TODO: may need to be cleaned up and optimized
@@ -116,10 +113,12 @@ static bool parse_args(int argc, char ** argv, server_args & args) {
} else if (arg == "--llm" && i + 1 < argc) {
args.llm_path = argv[++i];
} else if (arg == "--model-type" && i + 1 < argc) {
- if (!parse_model_type(argv[++i], args.model_type)) {
+ if (!robotcpp::parse_model_type(argv[++i], args.model_type)) {
std::fprintf(stderr, "Error: unsupported model type '%s'\n", argv[i]);
return false;
}
+ } else if (arg == "--policy" && i + 1 < argc) {
+ args.policy_path = argv[++i];
} else if (arg == "--mmproj" && i + 1 < argc) {
args.mmproj_path = argv[++i];
} else if (arg == "--vit" && i + 1 < argc) {
@@ -139,22 +138,46 @@ static bool parse_args(int argc, char ** argv, server_args & args) {
} else if (arg == "--host" && i + 1 < argc) {
args.host = argv[++i];
} else if (arg == "--port" && i + 1 < argc) {
- args.port = std::atoi(argv[++i]);
+ const char * value = argv[++i];
+ if (!robotcpp::parse_integer_argument(value, args.port)) {
+ std::fprintf(stderr, "Error: invalid --port value '%s'\n", value);
+ return false;
+ }
} else if (arg == "--threads" && i + 1 < argc) {
- args.threads = std::atoi(argv[++i]);
+ const char * value = argv[++i];
+ if (!robotcpp::parse_integer_argument(value, args.threads)) {
+ std::fprintf(stderr, "Error: invalid --threads value '%s'\n", value);
+ return false;
+ }
} else if (arg == "--n-batch" && i + 1 < argc) {
- args.n_batch = std::atoi(argv[++i]);
+ const char * value = argv[++i];
+ if (!robotcpp::parse_integer_argument(value, args.n_batch)) {
+ std::fprintf(stderr, "Error: invalid --n-batch value '%s'\n", value);
+ return false;
+ }
} else if (arg == "--n-ctx" && i + 1 < argc) {
- args.n_ctx = std::atoi(argv[++i]);
+ const char * value = argv[++i];
+ if (!robotcpp::parse_integer_argument(value, args.n_ctx)) {
+ std::fprintf(stderr, "Error: invalid --n-ctx value '%s'\n", value);
+ return false;
+ }
} else if (arg == "--noise-mode" && i + 1 < argc) {
if (!parse_noise_mode(argv[++i], args.noise_mode)) {
std::fprintf(stderr, "Error: invalid noise mode '%s'\n", argv[i]);
return false;
}
} else if (arg == "--noise-seed" && i + 1 < argc) {
- args.noise_seed = (int64_t)std::atoll(argv[++i]);
+ const char * value = argv[++i];
+ if (!robotcpp::parse_integer_argument(value, args.noise_seed)) {
+ std::fprintf(stderr, "Error: invalid --noise-seed value '%s'\n", value);
+ return false;
+ }
} else if (arg == "--verbosity" && i + 1 < argc) {
- args.verbosity = std::atoi(argv[++i]);
+ const char * value = argv[++i];
+ if (!robotcpp::parse_integer_argument(value, args.verbosity)) {
+ std::fprintf(stderr, "Error: invalid --verbosity value '%s'\n", value);
+ return false;
+ }
} else {
std::fprintf(stderr, "Error: unknown argument '%s'\n", arg.c_str());
return false;
@@ -168,15 +191,36 @@ static bool parse_args(int argc, char ** argv, server_args & args) {
std::fprintf(stderr, "Error: model-server only listens on 127.0.0.1 in this phase\n");
return false;
}
+ if (args.threads < 0 || args.n_batch <= 0 || args.n_ctx <= 0 || args.verbosity < 0) {
+ std::fprintf(stderr,
+ "Error: --threads/--verbosity must be non-negative and --n-batch/--n-ctx must be positive\n");
+ return false;
+ }
+ if (robotcpp::is_starvla_model_type(args.model_type) && args.noise_mode != SMOLVLA_NOISE_MODE_GAUSSIAN) {
+ std::fprintf(stderr, "Error: StarVLA does not support --noise-mode debug-sin; use Gaussian noise\n");
+ return false;
+ }
if (args.model_type == robotcpp::model_type::smolvla) {
if (args.llm_path.empty() || args.mmproj_path.empty() || args.state_proj_path.empty() ||
args.action_expert_path.empty()) {
std::fprintf(stderr, "Error: smolvla requires --llm --mmproj --state-proj --action-expert\n");
return false;
}
- } else if (args.vit_path.empty() || args.mmproj_path.empty() || args.llm_path.empty() ||
- args.tokenizer_path.empty() || args.state_path.empty() || args.action_decoder_path.empty()) {
- std::fprintf(stderr, "Error: pi0 requires --vit --mmproj --llm --tokenizer --state-gguf --action-decoder\n");
+ } else if (args.model_type == robotcpp::model_type::pi0) {
+ if (args.vit_path.empty() || args.mmproj_path.empty() || args.llm_path.empty() || args.tokenizer_path.empty() ||
+ args.state_path.empty() || args.action_decoder_path.empty()) {
+ std::fprintf(stderr,
+ "Error: pi0 requires --vit --mmproj --llm --tokenizer --state-gguf --action-decoder\n");
+ return false;
+ }
+ } else if (robotcpp::is_starvla_model_type(args.model_type)) {
+ if (args.llm_path.empty() || args.mmproj_path.empty() || args.policy_path.empty()) {
+ std::fprintf(stderr, "Error: %s requires --llm --mmproj --policy\n",
+ robotcpp::model_type_name(args.model_type));
+ return false;
+ }
+ } else {
+ std::fprintf(stderr, "Error: unsupported model type '%s'\n", robotcpp::model_type_name(args.model_type));
return false;
}
return true;
@@ -195,6 +239,7 @@ static robotcpp::model_args make_model_args(const server_args & args) {
model_args.action_decoder_path = args.action_decoder_path;
model_args.state_proj_path = args.state_proj_path;
model_args.action_expert_path = args.action_expert_path;
+ model_args.policy_path = args.policy_path;
model_args.n_batch = args.n_batch;
model_args.n_ctx = args.n_ctx;
model_args.noise_mode = args.noise_mode;
diff --git a/robot_server/model_adapter.cpp b/robot_server/model_adapter.cpp
index 31556ad..4b1a7b8 100644
--- a/robot_server/model_adapter.cpp
+++ b/robot_server/model_adapter.cpp
@@ -30,8 +30,9 @@ bool model_adapter::predict(const proto::predict_request & req, proto::predict_r
image.stride_bytes = static_cast(src.stride_bytes);
obs.images.push_back(image);
}
- obs.state = req.state;
- obs.task = req.task;
+ obs.state = req.state;
+ obs.initial_noise = req.initial_noise;
+ obs.task = req.task;
robotcpp::model_result result;
if (!model_->predict(obs, result, error)) {
diff --git a/robot_server/protocol.cpp b/robot_server/protocol.cpp
index 8ea4c48..930a7db 100644
--- a/robot_server/protocol.cpp
+++ b/robot_server/protocol.cpp
@@ -1,5 +1,6 @@
#include "protocol.h"
+#include
#include
#include
@@ -122,6 +123,16 @@ static bool checked_u32_count(size_t n, const char * label, std::string & error)
return true;
}
+static bool validate_f32_array(const std::vector & values, const char * label, std::string & error) {
+ for (float value : values) {
+ if (!std::isfinite(value)) {
+ error = std::string(label) + " contains a non-finite value";
+ return false;
+ }
+ }
+ return true;
+}
+
static bool validate_image_payload(const image_payload & image, const char * label, std::string & error) {
if (image.image_format != image_raw_rgb_u8) {
error = std::string(label) + " unsupported image format";
@@ -216,7 +227,13 @@ bool encode_predict_request(const predict_request & req, std::vector &
return false;
}
if (!checked_u32_count(req.images.size(), "images", error) ||
- !checked_u32_count(req.state.size(), "state", error) || !checked_u32_count(req.task.size(), "task", error)) {
+ !checked_u32_count(req.state.size(), "state", error) ||
+ !checked_u32_count(req.initial_noise.size(), "initial noise", error) ||
+ !checked_u32_count(req.task.size(), "task", error)) {
+ return false;
+ }
+ if (!validate_f32_array(req.state, "state", error) ||
+ !validate_f32_array(req.initial_noise, "initial noise", error)) {
return false;
}
for (size_t i = 0; i < req.images.size(); ++i) {
@@ -228,6 +245,7 @@ bool encode_predict_request(const predict_request & req, std::vector &
put_u32(out, (uint32_t)req.images.size());
put_u32(out, (uint32_t)req.state.size());
+ put_u32(out, (uint32_t)req.initial_noise.size());
put_u32(out, (uint32_t)req.task.size());
for (const image_payload & image : req.images) {
put_u32(out, image.image_format);
@@ -242,6 +260,9 @@ bool encode_predict_request(const predict_request & req, std::vector &
for (float v : req.state) {
put_f32(out, v);
}
+ for (float v : req.initial_noise) {
+ put_f32(out, v);
+ }
out.insert(out.end(), req.task.begin(), req.task.end());
for (const image_payload & image : req.images) {
out.insert(out.end(), image.name.begin(), image.name.end());
@@ -255,9 +276,10 @@ bool decode_predict_request(const std::vector & payload, predict_reques
reader r(payload.data(), payload.size());
uint32_t image_count = 0;
uint32_t state_dim = 0;
+ uint32_t noise_dim = 0;
uint32_t task_len = 0;
- if (!r.u32(image_count) || !r.u32(state_dim) || !r.u32(task_len)) {
+ if (!r.u32(image_count) || !r.u32(state_dim) || !r.u32(noise_dim) || !r.u32(task_len)) {
error = "short predict request";
return false;
}
@@ -265,6 +287,11 @@ bool decode_predict_request(const std::vector & payload, predict_reques
error = "predict request requires at least one image";
return false;
}
+ constexpr size_t image_metadata_size = 6 * sizeof(uint32_t) + sizeof(uint64_t);
+ if (image_count > r.remaining() / image_metadata_size) {
+ error = "image count exceeds predict request metadata";
+ return false;
+ }
req.images.assign(image_count, image_payload{});
std::vector name_lens(image_count, 0);
@@ -286,6 +313,12 @@ bool decode_predict_request(const std::vector & payload, predict_reques
}
}
+ const uint64_t scalar_bytes = (static_cast(state_dim) + static_cast(noise_dim)) * sizeof(float);
+ if (scalar_bytes > r.remaining() || task_len > r.remaining() - scalar_bytes) {
+ error = "predict request fields exceed payload";
+ return false;
+ }
+
req.state.assign(state_dim, 0.0f);
for (uint32_t i = 0; i < state_dim; ++i) {
if (!r.f32(req.state[i])) {
@@ -293,6 +326,19 @@ bool decode_predict_request(const std::vector & payload, predict_reques
return false;
}
}
+ if (!validate_f32_array(req.state, "state", error)) {
+ return false;
+ }
+ req.initial_noise.assign(noise_dim, 0.0f);
+ for (uint32_t i = 0; i < noise_dim; ++i) {
+ if (!r.f32(req.initial_noise[i])) {
+ error = "short initial noise array";
+ return false;
+ }
+ }
+ if (!validate_f32_array(req.initial_noise, "initial noise", error)) {
+ return false;
+ }
if (!r.string(req.task, task_len)) {
error = "short task string";
return false;
diff --git a/robot_server/protocol.h b/robot_server/protocol.h
index 97ae387..a5f483e 100644
--- a/robot_server/protocol.h
+++ b/robot_server/protocol.h
@@ -9,7 +9,7 @@ namespace robot_server {
namespace protocol {
static constexpr uint32_t k_magic = 0x414c5653u; // "SVLA" in little-endian bytes.
-static constexpr uint16_t k_version = 3;
+static constexpr uint16_t k_version = 4;
static constexpr uint16_t k_header_size = 32;
static constexpr uint64_t k_default_max_payload = 256ull * 1024ull * 1024ull;
@@ -63,6 +63,7 @@ struct metric {
struct predict_request {
std::vector images;
std::vector state;
+ std::vector initial_noise;
std::string task;
};
diff --git a/robot_server/shell/launch_robot_server_linux_cuda.sh b/robot_server/shell/launch_robot_server_linux_cuda.sh
index 990ed44..c6002b9 100755
--- a/robot_server/shell/launch_robot_server_linux_cuda.sh
+++ b/robot_server/shell/launch_robot_server_linux_cuda.sh
@@ -21,6 +21,10 @@ SKIP_BUILD="${SKIP_BUILD:-0}"
CMAKE_BIN="${CMAKE_BIN:-cmake}"
GGML_NATIVE="${GGML_NATIVE:-OFF}"
GGML_OPENMP="${GGML_OPENMP:-OFF}"
+ROBOT_CPP_BUILD_STARVLA="${ROBOT_CPP_BUILD_STARVLA:-OFF}"
+if [ "${MODEL_TYPE}" = "starvla" ]; then
+ ROBOT_CPP_BUILD_STARVLA=ON
+fi
SERVER_BIN="${BUILD_DIR}/bin/model-server"
@@ -32,7 +36,8 @@ if [ "${SKIP_BUILD}" != "1" ]; then
-DGGML_OPENMP="${GGML_OPENMP}" \
-DGGML_CUDA=ON \
-DGGML_METAL=OFF \
- -DROBOT_CPP_BUILD_ROBOT_SERVER=ON
+ -DROBOT_CPP_BUILD_ROBOT_SERVER=ON \
+ -DROBOT_CPP_BUILD_STARVLA="${ROBOT_CPP_BUILD_STARVLA}"
echo "== build =="
"${CMAKE_BIN}" --build "${BUILD_DIR}" --target model-server -j8
@@ -70,6 +75,17 @@ case "${MODEL_TYPE}" in
--action-decoder "${ACTION_DECODER_GGUF}"
)
;;
+ starvla)
+ LLM_GGUF="${LLM_GGUF:?LLM_GGUF must be set for StarVLA}"
+ MMPROJ_GGUF="${MMPROJ_GGUF:?MMPROJ_GGUF must be set for StarVLA}"
+ POLICY_GGUF="${POLICY_GGUF:?POLICY_GGUF must be set for StarVLA}"
+ MODEL_ARGS=(
+ --model-type starvla
+ --llm "${LLM_GGUF}"
+ --mmproj "${MMPROJ_GGUF}"
+ --policy "${POLICY_GGUF}"
+ )
+ ;;
*)
echo "unsupported MODEL_TYPE=${MODEL_TYPE}" >&2
exit 1
diff --git a/robot_server/test/benchmark_latency.py b/robot_server/test/benchmark_latency.py
index e000113..fa2ef72 100644
--- a/robot_server/test/benchmark_latency.py
+++ b/robot_server/test/benchmark_latency.py
@@ -26,8 +26,15 @@ def make_random_state(dim: int, seed: int) -> np.ndarray:
return rng.uniform(-1.0, 1.0, size=(dim,)).astype(np.float32)
-def make_random_observation(width: int, height: int, state_dim: int, prompt: str, image_names: list[str]) -> dict:
- return {
+def make_random_observation(
+ width: int,
+ height: int,
+ state_dim: int,
+ initial_noise_dim: int,
+ prompt: str,
+ image_names: list[str],
+) -> dict:
+ observation = {
"images": [
{
"name": image_name,
@@ -38,6 +45,9 @@ def make_random_observation(width: int, height: int, state_dim: int, prompt: str
"state": make_random_state(state_dim, seed=1),
"prompt": prompt,
}
+ if initial_noise_dim:
+ observation["initial_noise"] = make_random_state(initial_noise_dim, seed=2)
+ return observation
def ordered_columns(rows: list[dict[str, float]]) -> list[str]:
@@ -141,6 +151,7 @@ def main() -> int:
parser.add_argument("--height", type=int, default=224)
parser.add_argument("--image-name", action="append")
parser.add_argument("--state-dim", type=int, default=6)
+ parser.add_argument("--initial-noise-dim", type=int, default=0)
parser.add_argument("--prompt", default=os.environ.get("SMOLVLA_PROMPT", "grab the block."))
parser.add_argument("--warmup", type=int, default=1)
parser.add_argument("--loops", type=int, default=10)
@@ -167,6 +178,7 @@ def main() -> int:
width=args.width,
height=args.height,
state_dim=args.state_dim,
+ initial_noise_dim=args.initial_noise_dim,
prompt=args.prompt,
image_names=image_names,
)
diff --git a/robot_server/test/test_server_latency.sh b/robot_server/test/test_server_latency.sh
index 3073dff..de19516 100755
--- a/robot_server/test/test_server_latency.sh
+++ b/robot_server/test/test_server_latency.sh
@@ -7,9 +7,9 @@ set -e
# bash robot_server/test/test_server_latency.sh
#
# Positional args:
-# $1: model-type, e.g. smolvla / pi0
+# $1: model-type, e.g. smolvla / pi0 / starvla
# $2: backend, e.g. mac-cpu / mac-metal / linux-cpu / linux-cuda
-# $3: test-suite, e.g. smolvla-libero / smolvla-so101 / pi0-libero
+# $3: test-suite, e.g. smolvla-libero / smolvla-so101 / pi0-libero / starvla-bridge
ROBOT_CPP_ROOT="${ROBOT_CPP_ROOT:?ROBOT_CPP_ROOT must be set}"
GGUF_DIR="${GGUF_DIR:?GGUF_DIR must be set}"
MODEL_TYPE="${1:-${MODEL_TYPE:-smolvla}}"
@@ -39,6 +39,17 @@ case "${MODEL_TYPE}" in
ACTION_DECODER_GGUF="${ACTION_DECODER_GGUF:-${GGUF_DIR}/${MODEL_BASENAME}.action_decoder.gguf}"
LLM_GGUF="${LLM_GGUF:-${GGUF_DIR}/${MODEL_BASENAME}.llm.gguf}"
;;
+ starvla)
+ mapfile -t LLM_CANDIDATES < <(find "${GGUF_DIR}" -maxdepth 1 -type f -name 'qwen-*.gguf' -print)
+ mapfile -t MMPROJ_CANDIDATES < <(find "${GGUF_DIR}" -maxdepth 1 -type f -name 'mmproj-*.gguf' -print)
+ mapfile -t POLICY_CANDIDATES < <(find "${GGUF_DIR}" -maxdepth 1 -type f -name '*policy*.gguf' -print)
+ [[ ${#LLM_CANDIDATES[@]} -eq 1 ]] || { echo "expected one Qwen GGUF in ${GGUF_DIR}" >&2; exit 1; }
+ [[ ${#MMPROJ_CANDIDATES[@]} -eq 1 ]] || { echo "expected one mmproj GGUF in ${GGUF_DIR}" >&2; exit 1; }
+ [[ ${#POLICY_CANDIDATES[@]} -eq 1 ]] || { echo "expected one policy GGUF in ${GGUF_DIR}" >&2; exit 1; }
+ LLM_GGUF="${LLM_GGUF:-${LLM_CANDIDATES[0]}}"
+ MMPROJ_GGUF="${MMPROJ_GGUF:-${MMPROJ_CANDIDATES[0]}}"
+ POLICY_GGUF="${POLICY_GGUF:-${POLICY_CANDIDATES[0]}}"
+ ;;
*)
echo "unsupported MODEL_TYPE=${MODEL_TYPE}" >&2
exit 1
@@ -99,6 +110,12 @@ case "${TEST_SUITE}" in
IMAGE_HEIGHT="${IMAGE_HEIGHT:-256}"
STATE_DIM="${STATE_DIM:-8}"
;;
+ starvla-bridge)
+ IMAGE_NAMES="${IMAGE_NAMES:-${IMAGE_NAME:-image_0}}"
+ IMAGE_WIDTH="${IMAGE_WIDTH:-224}"
+ IMAGE_HEIGHT="${IMAGE_HEIGHT:-224}"
+ STATE_DIM="${STATE_DIM:-0}"
+ ;;
*)
echo "unsupported TEST_SUITE=${TEST_SUITE}" >&2
exit 1
@@ -108,6 +125,7 @@ WARMUP="${WARMUP:-5}"
LOOPS="${LOOPS:-100}"
SERVER_WAIT_S="${SERVER_WAIT_S:-120}"
DTYPE="${DTYPE:-f32}"
+NOISE_SEED="${NOISE_SEED:--1}"
PYTHON="${PYTHON:-python3}"
# ====================================
@@ -155,12 +173,13 @@ run_latency_case() {
TOKENIZER_GGUF="${TOKENIZER_GGUF:-}" \
STATE_GGUF="${STATE_GGUF:-}" \
ACTION_DECODER_GGUF="${ACTION_DECODER_GGUF:-}" \
+ POLICY_GGUF="${POLICY_GGUF:-}" \
HOST="${HOST}" \
PORT="${PORT}" \
THREADS="${threads}" \
TASK="${PROMPT}" \
NOISE_MODE="gaussian" \
- NOISE_SEED="-1" \
+ NOISE_SEED="${NOISE_SEED}" \
bash "${LAUNCH_SHELL}" "${MODEL_TYPE}" >"${server_log}" 2>&1 &
SERVER_PID=$!
diff --git a/src/model-cli.cpp b/src/model-cli.cpp
index bc82d8e..8dc8441 100644
--- a/src/model-cli.cpp
+++ b/src/model-cli.cpp
@@ -1,7 +1,9 @@
// model-cli.cpp — common robotcpp::Model CLI frontend
#include "models/model.h"
+#include "models/argument_parse.h"
#include "models/smolvla/smolvla_engine.h"
+#include "llama.h"
#include "stb_image.h"
#include
@@ -26,16 +28,11 @@ struct loaded_image {
int stride_bytes = 0;
};
-bool parse_model_type(const std::string & value, robotcpp::model_type & out) {
- if (value == "smolvla") {
- out = robotcpp::model_type::smolvla;
- return true;
- }
- if (value == "pi0") {
- out = robotcpp::model_type::pi0;
- return true;
+void quiet_llama_log_callback(ggml_log_level level, const char * text, void * user_data) {
+ (void)user_data;
+ if (level == GGML_LOG_LEVEL_ERROR) {
+ std::fputs(text, stderr);
}
- return false;
}
bool parse_noise_mode(const std::string & value, int & out_mode) {
@@ -54,13 +51,14 @@ void print_usage(const char * prog) {
std::fprintf(stderr, "\nModel CLI - robotcpp::Model frontend\n\n");
std::fprintf(stderr, "Usage:\n");
std::fprintf(stderr, " %s --model-type smolvla [options]\n", prog);
- std::fprintf(stderr, " %s --model-type pi0 [options]\n\n", prog);
+ std::fprintf(stderr, " %s --model-type pi0 [options]\n", prog);
+ std::fprintf(stderr, " %s --model-type starvla --llm --mmproj --policy [options]\n\n", prog);
std::fprintf(stderr, "Common options:\n");
- std::fprintf(stderr, " --model-type Model type (default: smolvla)\n");
+ std::fprintf(stderr, " --model-type smolvla|pi0|starvla\n"
+ " (default: smolvla)\n");
std::fprintf(stderr, " --image Input image (repeatable; order matches --image-name)\n");
- std::fprintf(
- stderr,
- " --image-name Observation image name (repeatable; default: image for single-image input)\n");
+ std::fprintf(stderr,
+ " --image-name Observation image name (default: image_0 for StarVLA, image otherwise)\n");
std::fprintf(stderr, " --state Proprio/state values (comma-separated)\n");
std::fprintf(stderr, " --task Task instruction (default: \"grab the block.\")\n");
std::fprintf(stderr, " --threads Number of threads (default: auto)\n");
@@ -82,6 +80,13 @@ void print_usage(const char * prog) {
std::fprintf(stderr, " --tokenizer Tokenizer GGUF path\n");
std::fprintf(stderr, " --state-gguf State projector GGUF path\n");
std::fprintf(stderr, " --action-decoder Action decoder GGUF path\n");
+ std::fprintf(stderr, "\nStarVLA options:\n");
+ std::fprintf(stderr, " --policy StarVLA policy GGUF path (required)\n");
+ std::fprintf(stderr, " --llm Qwen text GGUF path (required)\n");
+ std::fprintf(stderr, " --mmproj Qwen vision GGUF path (required)\n");
+ std::fprintf(stderr, " --n-batch Qwen batch size (default: 512)\n");
+ std::fprintf(stderr, " --n-ctx Qwen context size (default: 2048)\n");
+ std::fprintf(stderr, " --noise-seed GR00T/PI/PI_v3 noise seed, <0 means auto (default: -1)\n");
}
bool parse_state(const char * csv, std::vector & out) {
@@ -128,7 +133,7 @@ int main(int argc, char ** argv) {
std::vector image_paths;
std::vector image_names;
std::string state_csv;
- std::string task;
+ std::string task = "grab the block.";
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
@@ -139,10 +144,12 @@ int main(int argc, char ** argv) {
} else if (arg == "-v" || arg == "--verbose") {
args.verbosity++;
} else if (arg == "--model-type" && i + 1 < argc) {
- if (!parse_model_type(argv[++i], args.type)) {
+ if (!robotcpp::parse_model_type(argv[++i], args.type)) {
std::fprintf(stderr, "Error: unsupported model type '%s'\n", argv[i]);
return 1;
}
+ } else if (arg == "--policy" && i + 1 < argc) {
+ args.policy_path = argv[++i];
} else if (arg == "--llm" && i + 1 < argc) {
args.llm_path = argv[++i];
} else if (arg == "--mmproj" && i + 1 < argc) {
@@ -168,18 +175,34 @@ int main(int argc, char ** argv) {
} else if (arg == "--task" && i + 1 < argc) {
task = argv[++i];
} else if (arg == "--threads" && i + 1 < argc) {
- args.threads = std::atoi(argv[++i]);
+ const char * value = argv[++i];
+ if (!robotcpp::parse_integer_argument(value, args.threads)) {
+ std::fprintf(stderr, "Error: invalid --threads value '%s'\n", value);
+ return 1;
+ }
} else if (arg == "--n-batch" && i + 1 < argc) {
- args.n_batch = std::atoi(argv[++i]);
+ const char * value = argv[++i];
+ if (!robotcpp::parse_integer_argument(value, args.n_batch)) {
+ std::fprintf(stderr, "Error: invalid --n-batch value '%s'\n", value);
+ return 1;
+ }
} else if (arg == "--n-ctx" && i + 1 < argc) {
- args.n_ctx = std::atoi(argv[++i]);
+ const char * value = argv[++i];
+ if (!robotcpp::parse_integer_argument(value, args.n_ctx)) {
+ std::fprintf(stderr, "Error: invalid --n-ctx value '%s'\n", value);
+ return 1;
+ }
} else if (arg == "--noise-mode" && i + 1 < argc) {
if (!parse_noise_mode(argv[++i], args.noise_mode)) {
std::fprintf(stderr, "Error: invalid noise mode '%s'\n", argv[i]);
return 1;
}
} else if (arg == "--noise-seed" && i + 1 < argc) {
- args.noise_seed = std::atoll(argv[++i]);
+ const char * value = argv[++i];
+ if (!robotcpp::parse_integer_argument(value, args.noise_seed)) {
+ std::fprintf(stderr, "Error: invalid --noise-seed value '%s'\n", value);
+ return 1;
+ }
} else {
std::fprintf(stderr, "Error: unknown argument '%s'\n", arg.c_str());
print_usage(argv[0]);
@@ -187,23 +210,46 @@ int main(int argc, char ** argv) {
}
}
+ if (args.threads < 0 || args.n_batch <= 0 || args.n_ctx <= 0) {
+ std::fprintf(stderr, "Error: --threads must be non-negative and --n-batch/--n-ctx must be positive\n");
+ return 1;
+ }
+ if (robotcpp::is_starvla_model_type(args.type)) {
+ if (args.llm_path.empty() || args.mmproj_path.empty() || args.policy_path.empty()) {
+ std::fprintf(stderr, "Error: %s requires --llm --mmproj --policy\n", robotcpp::model_type_name(args.type));
+ return 1;
+ }
+ if (args.noise_mode != SMOLVLA_NOISE_MODE_GAUSSIAN) {
+ std::fprintf(stderr, "Error: StarVLA does not support --noise-mode debug-sin; use Gaussian noise\n");
+ return 1;
+ }
+ }
+
if (image_paths.empty()) {
std::fprintf(stderr, "Error: --image is required\n");
print_usage(argv[0]);
return 1;
}
+ if (robotcpp::is_starvla_model_type(args.type) && image_paths.size() != 1) {
+ std::fprintf(stderr, "Error: %s requires exactly one --image\n", robotcpp::model_type_name(args.type));
+ return 1;
+ }
if (image_names.empty()) {
if (image_paths.size() != 1) {
std::fprintf(stderr, "Error: multiple --image inputs require one --image-name per image\n");
return 1;
}
- image_names.push_back("image");
+ image_names.push_back(robotcpp::is_starvla_model_type(args.type) ? "image_0" : "image");
}
if (image_names.size() != image_paths.size()) {
std::fprintf(stderr, "Error: --image count (%zu) must match --image-name count (%zu)\n", image_paths.size(),
image_names.size());
return 1;
}
+ if (robotcpp::is_starvla_model_type(args.type) && image_names[0] != "image_0") {
+ std::fprintf(stderr, "Error: %s image must be named 'image_0'\n", robotcpp::model_type_name(args.type));
+ return 1;
+ }
std::vector state_vec;
if (!parse_state(state_csv.c_str(), state_vec)) {
@@ -218,6 +264,8 @@ int main(int argc, char ** argv) {
}
}
+ llama_log_set(args.verbosity > 0 ? nullptr : quiet_llama_log_callback, nullptr);
+
const auto init_start = std::chrono::high_resolution_clock::now();
std::string error;
std::unique_ptr model;
diff --git a/src/models/argument_parse.h b/src/models/argument_parse.h
new file mode 100644
index 0000000..0ad5b60
--- /dev/null
+++ b/src/models/argument_parse.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+namespace robotcpp {
+
+template bool parse_integer_argument(const char * value, Integer & output) {
+ static_assert(std::is_integral::value && !std::is_same::value,
+ "Integer must be a non-bool integral type");
+ if (value == nullptr || value[0] == '\0') {
+ return false;
+ }
+
+ Integer parsed = 0;
+ const char * end = value + std::strlen(value);
+ const std::from_chars_result result = std::from_chars(value, end, parsed, 10);
+ if (result.ec != std::errc{} || result.ptr != end) {
+ return false;
+ }
+ output = parsed;
+ return true;
+}
+
+} // namespace robotcpp
diff --git a/src/models/ggml_backend.cpp b/src/models/ggml_backend.cpp
index 062d895..65ef5ed 100644
--- a/src/models/ggml_backend.cpp
+++ b/src/models/ggml_backend.cpp
@@ -6,7 +6,7 @@
#include
#include
-static const char * backend_mode_name(backend_mode mode) {
+const char * backend_mode_name(backend_mode mode) {
switch (mode) {
case backend_mode::cuda:
return "cuda";
diff --git a/src/models/ggml_backend.h b/src/models/ggml_backend.h
index 08b735d..a044f6f 100644
--- a/src/models/ggml_backend.h
+++ b/src/models/ggml_backend.h
@@ -11,6 +11,8 @@ enum class backend_mode {
metal,
};
+const char * backend_mode_name(backend_mode mode);
+
struct backend_buft_policy {
ggml_backend_buffer_type_t model_buft = nullptr;
ggml_backend_buffer_type_t runtime_buft = nullptr;
diff --git a/src/models/model.h b/src/models/model.h
index 587957c..d89b9d3 100644
--- a/src/models/model.h
+++ b/src/models/model.h
@@ -10,8 +10,13 @@ namespace robotcpp {
enum class model_type {
smolvla,
pi0,
+ starvla,
};
+const char * model_type_name(model_type type);
+bool parse_model_type(const std::string & value, model_type & out);
+bool is_starvla_model_type(model_type type);
+
struct model_image {
std::string name;
const uint8_t * data = nullptr;
@@ -24,6 +29,7 @@ struct model_image {
struct observation {
std::vector images;
std::vector state;
+ std::vector initial_noise;
std::string task;
};
@@ -59,6 +65,9 @@ struct model_args {
std::string tokenizer_path;
std::string state_path;
std::string action_decoder_path;
+
+ // starvla
+ std::string policy_path;
};
class Model {
diff --git a/src/models/model_factory.cpp b/src/models/model_factory.cpp
index d6b1aa3..3f887ad 100644
--- a/src/models/model_factory.cpp
+++ b/src/models/model_factory.cpp
@@ -2,6 +2,9 @@
#include "models/pi0/pi0_model.h"
#include "models/smolvla/smolvla_model.h"
+#ifdef ROBOT_CPP_BUILD_STARVLA
+#include "models/starvla/starvla_model.h"
+#endif
namespace robotcpp {
@@ -13,8 +16,16 @@ bool make_model(const model_args & args, std::unique_ptr & out, std::stri
if (args.type == model_type::pi0) {
return make_pi0_model(args, out, error);
}
+ if (args.type == model_type::starvla) {
+#ifdef ROBOT_CPP_BUILD_STARVLA
+ return make_starvla_model(args, out, error);
+#else
+ error = "StarVLA support was not built; configure with -DROBOT_CPP_BUILD_STARVLA=ON";
+ return false;
+#endif
+ }
- error = "unsupported model type";
+ error = std::string("unsupported model type: ") + model_type_name(args.type);
return false;
}
diff --git a/src/models/model_type.cpp b/src/models/model_type.cpp
new file mode 100644
index 0000000..6a4d3d5
--- /dev/null
+++ b/src/models/model_type.cpp
@@ -0,0 +1,49 @@
+#include "models/model.h"
+
+#include
+
+namespace robotcpp {
+namespace {
+
+struct model_type_entry {
+ model_type type;
+ const char * name;
+};
+
+constexpr std::array MODEL_TYPES = {{
+ {model_type::smolvla, "smolvla"},
+ {model_type::pi0, "pi0"},
+ {model_type::starvla, "starvla"},
+}};
+
+const model_type_entry * find_entry(model_type type) {
+ for (const model_type_entry & entry : MODEL_TYPES) {
+ if (entry.type == type) {
+ return &entry;
+ }
+ }
+ return nullptr;
+}
+
+} // namespace
+
+const char * model_type_name(model_type type) {
+ const model_type_entry * entry = find_entry(type);
+ return entry ? entry->name : "unknown";
+}
+
+bool parse_model_type(const std::string & value, model_type & out) {
+ for (const model_type_entry & entry : MODEL_TYPES) {
+ if (value == entry.name) {
+ out = entry.type;
+ return true;
+ }
+ }
+ return false;
+}
+
+bool is_starvla_model_type(model_type type) {
+ return type == model_type::starvla;
+}
+
+} // namespace robotcpp
diff --git a/src/models/pi0/pi0_model.cpp b/src/models/pi0/pi0_model.cpp
index 5d62866..4d46a0d 100644
--- a/src/models/pi0/pi0_model.cpp
+++ b/src/models/pi0/pi0_model.cpp
@@ -80,6 +80,10 @@ bool Pi0Model::predict(const observation & obs, model_result & out, std::string
error = "Pi0 model is not initialized";
return false;
}
+ if (!obs.initial_noise.empty()) {
+ error = "Pi0 does not accept explicit initial noise";
+ return false;
+ }
if (obs.images.empty()) {
error = "Pi0 requires at least one image";
return false;
diff --git a/src/models/smolvla/smolvla_model.cpp b/src/models/smolvla/smolvla_model.cpp
index a718fd2..db6567c 100644
--- a/src/models/smolvla/smolvla_model.cpp
+++ b/src/models/smolvla/smolvla_model.cpp
@@ -72,6 +72,10 @@ bool SmolVLAModel::predict(const observation & obs, model_result & out, std::str
error = "SmolVLA model is not initialized";
return false;
}
+ if (!obs.initial_noise.empty()) {
+ error = "SmolVLA does not accept explicit initial noise";
+ return false;
+ }
if (obs.images.empty()) {
error = "SmolVLA requires at least one image";
return false;
diff --git a/src/models/starvla/fast_codec.cpp b/src/models/starvla/fast_codec.cpp
new file mode 100644
index 0000000..9ce67fd
--- /dev/null
+++ b/src/models/starvla/fast_codec.cpp
@@ -0,0 +1,537 @@
+#include "models/starvla/fast_codec.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace robotcpp::starvla {
+namespace {
+
+constexpr size_t kMaximumVocabSize = 65536U;
+constexpr size_t kMaximumTimeHorizon = 1024U;
+constexpr size_t kMaximumActionDim = 1024U;
+constexpr size_t kMaximumBatchSize = 1024U;
+constexpr size_t kMaximumTokenSequence = 4096U;
+constexpr size_t kMaximumGeneratedSequence = 2048U;
+constexpr size_t kMaximumDecodedBytes = 1024U * 1024U;
+constexpr size_t kMaximumOutputScalars = 16U * 1024U * 1024U;
+constexpr uint64_t kMaximumIdctMultiplyAdds = 64ULL * 1024ULL * 1024ULL;
+constexpr double kPi = 3.141592653589793238462643383279502884;
+
+bool decode_utf8_strict(const std::string & input, std::vector & output, std::string & error) {
+ output.clear();
+ for (size_t i = 0; i < input.size();) {
+ const uint8_t first = static_cast(input[i]);
+ uint32_t value = 0;
+ size_t length = 0;
+ if (first <= 0x7fU) {
+ value = first;
+ length = 1;
+ } else if (first >= 0xc2U && first <= 0xdfU) {
+ value = first & 0x1fU;
+ length = 2;
+ } else if (first >= 0xe0U && first <= 0xefU) {
+ value = first & 0x0fU;
+ length = 3;
+ } else if (first >= 0xf0U && first <= 0xf4U) {
+ value = first & 0x07U;
+ length = 4;
+ } else {
+ error = "StarVLA FAST tokenizer vocabulary contains invalid UTF-8";
+ return false;
+ }
+ if (i + length > input.size()) {
+ error = "StarVLA FAST tokenizer vocabulary contains truncated UTF-8";
+ return false;
+ }
+ for (size_t j = 1; j < length; ++j) {
+ const uint8_t continuation = static_cast(input[i + j]);
+ if ((continuation & 0xc0U) != 0x80U) {
+ error = "StarVLA FAST tokenizer vocabulary contains invalid UTF-8 continuation";
+ return false;
+ }
+ value = (value << 6U) | (continuation & 0x3fU);
+ }
+ const bool overlong =
+ (length == 2 && value < 0x80U) || (length == 3 && value < 0x800U) || (length == 4 && value < 0x10000U);
+ if (overlong || value > 0x10ffffU || (value >= 0xd800U && value <= 0xdfffU)) {
+ error = "StarVLA FAST tokenizer vocabulary contains a non-scalar UTF-8 value";
+ return false;
+ }
+ output.push_back(value);
+ i += length;
+ }
+ return true;
+}
+
+std::unordered_map byte_level_inverse_alphabet() {
+ std::unordered_map result;
+ std::unordered_set direct;
+ for (int value = 0x21; value <= 0x7e; ++value) {
+ direct.insert(value);
+ result.emplace(static_cast(value), static_cast(value));
+ }
+ for (int value = 0xa1; value <= 0xac; ++value) {
+ direct.insert(value);
+ result.emplace(static_cast(value), static_cast(value));
+ }
+ for (int value = 0xae; value <= 0xff; ++value) {
+ direct.insert(value);
+ result.emplace(static_cast(value), static_cast(value));
+ }
+ uint32_t extra = 0;
+ for (int value = 0; value <= 0xff; ++value) {
+ if (direct.count(value) == 0) {
+ result.emplace(256U + extra, static_cast(value));
+ ++extra;
+ }
+ }
+ return result;
+}
+
+bool compile_token_bytes(const std::vector & vocab_by_id, std::vector> & token_bytes,
+ std::string & error) {
+ const auto inverse_alphabet = byte_level_inverse_alphabet();
+ token_bytes.clear();
+ token_bytes.reserve(vocab_by_id.size());
+ for (size_t token_id = 0; token_id < vocab_by_id.size(); ++token_id) {
+ if (vocab_by_id[token_id].empty()) {
+ error = "StarVLA FAST tokenizer has an empty vocabulary piece at ID " + std::to_string(token_id);
+ return false;
+ }
+ std::vector piece_codepoints;
+ if (!decode_utf8_strict(vocab_by_id[token_id], piece_codepoints, error)) {
+ error += " at token ID " + std::to_string(token_id);
+ return false;
+ }
+ std::vector bytes;
+ bytes.reserve(piece_codepoints.size());
+ for (uint32_t codepoint : piece_codepoints) {
+ const auto found = inverse_alphabet.find(codepoint);
+ if (found == inverse_alphabet.end()) {
+ error = "StarVLA FAST tokenizer piece contains a code point outside the ByteLevel "
+ "alphabet at ID " +
+ std::to_string(token_id);
+ return false;
+ }
+ bytes.push_back(found->second);
+ }
+ token_bytes.push_back(std::move(bytes));
+ }
+ return true;
+}
+
+void decode_utf8_lossy(const std::vector & input, std::vector & output) {
+ output.clear();
+ for (size_t i = 0; i < input.size();) {
+ const uint8_t first = input[i];
+ if (first <= 0x7fU) {
+ output.push_back(first);
+ ++i;
+ continue;
+ }
+
+ size_t length = 0;
+ uint32_t value = 0;
+ if (first >= 0xc2U && first <= 0xdfU) {
+ length = 2;
+ value = first & 0x1fU;
+ } else if (first >= 0xe0U && first <= 0xefU) {
+ length = 3;
+ value = first & 0x0fU;
+ } else if (first >= 0xf0U && first <= 0xf4U) {
+ length = 4;
+ value = first & 0x07U;
+ } else {
+ output.push_back(0xfffdU);
+ ++i;
+ continue;
+ }
+
+ if (i + 1 >= input.size()) {
+ output.push_back(0xfffdU);
+ break;
+ }
+ const uint8_t second = input[i + 1];
+ const bool second_is_continuation = (second & 0xc0U) == 0x80U;
+ const bool second_in_scalar_range = !(first == 0xe0U && second < 0xa0U) &&
+ !(first == 0xedU && second > 0x9fU) &&
+ !(first == 0xf0U && second < 0x90U) && !(first == 0xf4U && second > 0x8fU);
+ if (!second_is_continuation || !second_in_scalar_range) {
+ output.push_back(0xfffdU);
+ ++i;
+ continue;
+ }
+ value = (value << 6U) | (second & 0x3fU);
+
+ bool invalid = false;
+ size_t consumed_prefix = 2;
+ for (size_t j = 2; j < length; ++j) {
+ if (i + j >= input.size()) {
+ output.push_back(0xfffdU);
+ i = input.size();
+ invalid = true;
+ break;
+ }
+ const uint8_t continuation = input[i + j];
+ if ((continuation & 0xc0U) != 0x80U) {
+ output.push_back(0xfffdU);
+ i += consumed_prefix;
+ invalid = true;
+ break;
+ }
+ value = (value << 6U) | (continuation & 0x3fU);
+ ++consumed_prefix;
+ }
+ if (invalid) {
+ continue;
+ }
+ output.push_back(value);
+ i += length;
+ }
+}
+
+bool checked_action_count(const FastCodecConfig & config, size_t batch_size, size_t & per_sample, size_t & total,
+ std::string & error) {
+ if (config.vocab_size > kMaximumVocabSize || config.time_horizon > kMaximumTimeHorizon ||
+ config.action_dim > kMaximumActionDim) {
+ error = "StarVLA FAST codec dimensions exceed the runtime safety limits";
+ return false;
+ }
+ if (batch_size == 0 || batch_size > kMaximumBatchSize) {
+ error = "StarVLA FAST batch size exceeds the runtime safety limit";
+ return false;
+ }
+ if (config.time_horizon > std::numeric_limits::max() / config.action_dim) {
+ error = "StarVLA FAST action shape overflows size_t";
+ return false;
+ }
+ per_sample = config.time_horizon * config.action_dim;
+ if (batch_size > std::numeric_limits::max() / per_sample) {
+ error = "StarVLA FAST batch shape overflows size_t";
+ return false;
+ }
+ total = batch_size * per_sample;
+ if (total > kMaximumOutputScalars) {
+ error = "StarVLA FAST output tensor exceeds the runtime scalar limit";
+ return false;
+ }
+ const uint64_t horizon = static_cast(config.time_horizon);
+ const uint64_t action_dim = static_cast(config.action_dim);
+ const uint64_t batch = static_cast(batch_size);
+ if (horizon > kMaximumIdctMultiplyAdds / horizon) {
+ error = "StarVLA FAST inverse DCT exceeds the runtime work limit";
+ return false;
+ }
+ uint64_t multiply_adds = horizon * horizon;
+ if (action_dim > kMaximumIdctMultiplyAdds / multiply_adds) {
+ error = "StarVLA FAST inverse DCT exceeds the runtime work limit";
+ return false;
+ }
+ multiply_adds *= action_dim;
+ if (batch > kMaximumIdctMultiplyAdds / multiply_adds) {
+ error = "StarVLA FAST inverse DCT exceeds the runtime work limit";
+ return false;
+ }
+ return true;
+}
+
+} // namespace
+
+FastCodec::FastCodec(FastCodecConfig config, std::vector> token_bytes,
+ std::vector fast_to_vlm_id)
+ : config_(config), token_bytes_(std::move(token_bytes)), fast_to_vlm_id_(std::move(fast_to_vlm_id)) {
+ vlm_to_fast_id_.reserve(fast_to_vlm_id_.size());
+ for (size_t fast_id = 0; fast_id < fast_to_vlm_id_.size(); ++fast_id) {
+ vlm_to_fast_id_.emplace_back(fast_to_vlm_id_[fast_id], static_cast(fast_id));
+ }
+ std::sort(vlm_to_fast_id_.begin(), vlm_to_fast_id_.end());
+}
+
+std::unique_ptr FastCodec::create(FastCodecConfig config, std::vector vocab_by_id,
+ std::vector fast_to_vlm_id, std::string & error) {
+ error.clear();
+ if (!std::isfinite(config.scale) || config.scale == 0.0 || config.vocab_size == 0 || config.time_horizon == 0 ||
+ config.action_dim == 0) {
+ error = "StarVLA FAST codec dimensions and scale must be non-zero and finite";
+ return nullptr;
+ }
+ if (config.vocab_size > static_cast(std::numeric_limits::max())) {
+ error = "StarVLA FAST vocabulary exceeds the int32 token-ID range";
+ return nullptr;
+ }
+ if (vocab_by_id.size() != config.vocab_size || fast_to_vlm_id.size() != config.vocab_size) {
+ error = "StarVLA FAST codec vocabulary or action-token map has the wrong size";
+ return nullptr;
+ }
+ size_t per_sample = 0;
+ size_t total = 0;
+ if (!checked_action_count(config, 1, per_sample, total, error)) {
+ return nullptr;
+ }
+ std::unordered_set unique_vlm_ids;
+ for (int32_t vlm_id : fast_to_vlm_id) {
+ if (vlm_id < 0 || !unique_vlm_ids.insert(vlm_id).second) {
+ error = "StarVLA FAST action-token VLM IDs must be unique and non-negative";
+ return nullptr;
+ }
+ }
+ std::vector> token_bytes;
+ if (!compile_token_bytes(vocab_by_id, token_bytes, error)) {
+ return nullptr;
+ }
+ return std::unique_ptr(new FastCodec(config, std::move(token_bytes), std::move(fast_to_vlm_id)));
+}
+
+std::unique_ptr FastCodec::create_compiled(FastCodecConfig config, std::vector token_offsets,
+ std::vector token_bytes,
+ std::vector fast_to_vlm_id, std::string & error) {
+ error.clear();
+ if (!std::isfinite(config.scale) || config.scale == 0.0 || config.vocab_size == 0 || config.time_horizon == 0 ||
+ config.action_dim == 0 || config.vocab_size > static_cast(std::numeric_limits::max())) {
+ error = "StarVLA FAST compiled codec dimensions and scale are invalid";
+ return nullptr;
+ }
+ if (config.vocab_size == std::numeric_limits::max() || token_offsets.size() != config.vocab_size + 1U ||
+ fast_to_vlm_id.size() != config.vocab_size || token_offsets.empty() || token_offsets.front() != 0 ||
+ token_offsets.back() < 0 || static_cast(token_offsets.back()) != token_bytes.size()) {
+ error = "StarVLA FAST compiled codec tensor shapes are incompatible";
+ return nullptr;
+ }
+ size_t per_sample = 0;
+ size_t total = 0;
+ if (!checked_action_count(config, 1, per_sample, total, error)) {
+ return nullptr;
+ }
+
+ std::unordered_set unique_vlm_ids;
+ for (int32_t vlm_id : fast_to_vlm_id) {
+ if (vlm_id < 0 || !unique_vlm_ids.insert(vlm_id).second) {
+ error = "StarVLA FAST compiled action-token IDs must be unique and non-negative";
+ return nullptr;
+ }
+ }
+
+ std::vector> pieces;
+ pieces.reserve(config.vocab_size);
+ for (size_t index = 0; index < config.vocab_size; ++index) {
+ const int32_t begin = token_offsets[index];
+ const int32_t end = token_offsets[index + 1U];
+ if (begin < 0 || end <= begin || static_cast(end) > token_bytes.size()) {
+ error = "StarVLA FAST compiled codec offsets are not strictly increasing";
+ return nullptr;
+ }
+ pieces.emplace_back(token_bytes.begin() + begin, token_bytes.begin() + end);
+ }
+ return std::unique_ptr(new FastCodec(config, std::move(pieces), std::move(fast_to_vlm_id)));
+}
+
+const FastCodecConfig & FastCodec::config() const {
+ return config_;
+}
+
+const std::vector & FastCodec::fast_to_vlm_ids() const {
+ return fast_to_vlm_id_;
+}
+
+bool FastCodec::map_fast_to_vlm(const std::vector & fast_ids, std::vector & vlm_ids,
+ std::string & error) const {
+ vlm_ids.clear();
+ error.clear();
+ if (fast_ids.size() > kMaximumTokenSequence) {
+ error = "StarVLA FAST token sequence exceeds the runtime length limit";
+ return false;
+ }
+ vlm_ids.reserve(fast_ids.size());
+ for (int32_t fast_id : fast_ids) {
+ if (fast_id < 0 || static_cast(fast_id) >= fast_to_vlm_id_.size()) {
+ error = "StarVLA FAST token ID is outside the codec vocabulary";
+ vlm_ids.clear();
+ return false;
+ }
+ vlm_ids.push_back(fast_to_vlm_id_[static_cast(fast_id)]);
+ }
+ return true;
+}
+
+bool FastCodec::map_vlm_to_fast(const std::vector & vlm_ids, std::vector & fast_ids,
+ std::string & error) const {
+ fast_ids.clear();
+ error.clear();
+ if (vlm_ids.size() > kMaximumTokenSequence) {
+ error = "StarVLA FAST action-token sequence exceeds the runtime length limit";
+ return false;
+ }
+ fast_ids.reserve(vlm_ids.size());
+ for (int32_t vlm_id : vlm_ids) {
+ const auto found = std::lower_bound(
+ vlm_to_fast_id_.begin(), vlm_to_fast_id_.end(), vlm_id,
+ [](const std::pair & entry, int32_t value) { return entry.first < value; });
+ if (found == vlm_to_fast_id_.end() || found->first != vlm_id) {
+ error = "Qwen token ID is not present in the StarVLA FAST action-token map";
+ fast_ids.clear();
+ return false;
+ }
+ fast_ids.push_back(found->second);
+ }
+ return true;
+}
+
+bool FastCodec::extract_fast_tokens(const std::vector & generated_ids, std::vector & fast_ids,
+ std::string & error) const {
+ fast_ids.clear();
+ error.clear();
+ if (generated_ids.size() > kMaximumGeneratedSequence) {
+ error = "Qwen generated sequence exceeds the StarVLA FAST runtime length limit";
+ return false;
+ }
+ for (int32_t vlm_id : generated_ids) {
+ const auto found = std::lower_bound(
+ vlm_to_fast_id_.begin(), vlm_to_fast_id_.end(), vlm_id,
+ [](const std::pair & entry, int32_t value) { return entry.first < value; });
+ if (found != vlm_to_fast_id_.end() && found->first == vlm_id) {
+ fast_ids.push_back(found->second);
+ }
+ }
+ return true;
+}
+
+bool FastCodec::byte_level_decode(const std::vector & fast_ids, std::vector & codepoints,
+ std::string & error) const {
+ codepoints.clear();
+ error.clear();
+ if (fast_ids.size() > kMaximumTokenSequence) {
+ error = "StarVLA FAST token sequence exceeds the runtime length limit";
+ return false;
+ }
+ size_t byte_count = 0;
+ for (int32_t fast_id : fast_ids) {
+ if (fast_id < 0 || static_cast(fast_id) >= token_bytes_.size()) {
+ error = "StarVLA FAST token ID is outside the ByteLevel BPE vocabulary";
+ return false;
+ }
+ const size_t piece_size = token_bytes_[static_cast(fast_id)].size();
+ if (byte_count > std::numeric_limits::max() - piece_size) {
+ error = "StarVLA FAST ByteLevel output size overflows size_t";
+ return false;
+ }
+ byte_count += piece_size;
+ if (byte_count > kMaximumDecodedBytes) {
+ error = "StarVLA FAST ByteLevel decode exceeds the runtime byte limit";
+ return false;
+ }
+ }
+ std::vector bytes;
+ bytes.reserve(byte_count);
+ for (int32_t fast_id : fast_ids) {
+ const auto & piece = token_bytes_[static_cast(fast_id)];
+ bytes.insert(bytes.end(), piece.begin(), piece.end());
+ }
+ decode_utf8_lossy(bytes, codepoints);
+ return true;
+}
+
+bool FastCodec::decode_fast_tokens(const std::vector> & batch_fast_ids, FastDecodeResult & result,
+ std::string & error) const {
+ result = {};
+ error.clear();
+ if (batch_fast_ids.empty()) {
+ error = "StarVLA FAST decode batch must contain at least one sequence";
+ return false;
+ }
+ if (batch_fast_ids.size() > kMaximumBatchSize) {
+ error = "StarVLA FAST decode batch exceeds the runtime size limit";
+ return false;
+ }
+ for (const auto & fast_ids : batch_fast_ids) {
+ if (fast_ids.size() > kMaximumTokenSequence) {
+ error = "StarVLA FAST token sequence exceeds the runtime length limit";
+ return false;
+ }
+ }
+ size_t per_sample = 0;
+ size_t total = 0;
+ if (!checked_action_count(config_, batch_fast_ids.size(), per_sample, total, error)) {
+ return false;
+ }
+
+ result.batch_size = batch_fast_ids.size();
+ result.time_horizon = config_.time_horizon;
+ result.action_dim = config_.action_dim;
+ result.actions.assign(total, 0.0);
+
+ const double dc_scale = 1.0 / std::sqrt(static_cast(config_.time_horizon));
+ const double ac_scale = std::sqrt(2.0 / static_cast(config_.time_horizon));
+ for (size_t batch = 0; batch < batch_fast_ids.size(); ++batch) {
+ std::vector codepoints;
+ std::string sequence_error;
+ if (!byte_level_decode(batch_fast_ids[batch], codepoints, sequence_error) || codepoints.size() != per_sample) {
+ error = "StarVLA FAST sequence " + std::to_string(batch) + ": " +
+ (sequence_error.empty() ? "decoded DCT coefficient shape mismatch" : sequence_error);
+ result = {};
+ return false;
+ }
+
+ for (size_t action = 0; action < config_.action_dim; ++action) {
+ const double dc = (static_cast(codepoints[action]) + config_.min_token) / config_.scale;
+ for (size_t time = 0; time < config_.time_horizon; ++time) {
+ double value = dc_scale * dc;
+ for (size_t frequency = 1; frequency < config_.time_horizon; ++frequency) {
+ const size_t coefficient_index = frequency * config_.action_dim + action;
+ const double coefficient =
+ (static_cast(codepoints[coefficient_index]) + config_.min_token) / config_.scale;
+ const double angle = kPi * static_cast(frequency) * static_cast(2U * time + 1U) /
+ (2.0 * static_cast(config_.time_horizon));
+ value += ac_scale * coefficient * std::cos(angle);
+ }
+ result.actions[batch * per_sample + time * config_.action_dim + action] = value;
+ }
+ }
+ }
+ return true;
+}
+
+bool FastCodec::decode_vlm_action_tokens(const std::vector> & batch_vlm_ids,
+ FastDecodeResult & result, std::string & error) const {
+ if (batch_vlm_ids.empty() || batch_vlm_ids.size() > kMaximumBatchSize) {
+ result = {};
+ error = "StarVLA FAST action-token batch is empty or exceeds the runtime size limit";
+ return false;
+ }
+ std::vector> batch_fast_ids;
+ batch_fast_ids.reserve(batch_vlm_ids.size());
+ for (const auto & vlm_ids : batch_vlm_ids) {
+ std::vector fast_ids;
+ if (!map_vlm_to_fast(vlm_ids, fast_ids, error)) {
+ result = {};
+ return false;
+ }
+ batch_fast_ids.push_back(std::move(fast_ids));
+ }
+ return decode_fast_tokens(batch_fast_ids, result, error);
+}
+
+bool FastCodec::decode_generated_tokens(const std::vector> & batch_generated_ids,
+ FastDecodeResult & result, std::string & error) const {
+ if (batch_generated_ids.empty() || batch_generated_ids.size() > kMaximumBatchSize) {
+ result = {};
+ error = "StarVLA FAST generated-token batch is empty or exceeds the runtime size limit";
+ return false;
+ }
+ std::vector> batch_fast_ids;
+ batch_fast_ids.reserve(batch_generated_ids.size());
+ for (const auto & generated_ids : batch_generated_ids) {
+ std::vector fast_ids;
+ if (!extract_fast_tokens(generated_ids, fast_ids, error)) {
+ result = {};
+ return false;
+ }
+ batch_fast_ids.push_back(std::move(fast_ids));
+ }
+ return decode_fast_tokens(batch_fast_ids, result, error);
+}
+
+} // namespace robotcpp::starvla
diff --git a/src/models/starvla/fast_codec.h b/src/models/starvla/fast_codec.h
new file mode 100644
index 0000000..4c3742b
--- /dev/null
+++ b/src/models/starvla/fast_codec.h
@@ -0,0 +1,79 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace robotcpp::starvla {
+
+struct FastCodecConfig {
+ double scale = 0.0;
+ int32_t min_token = 0;
+ size_t vocab_size = 0;
+ size_t time_horizon = 0;
+ size_t action_dim = 0;
+};
+
+struct FastDecodeResult {
+ size_t batch_size = 0;
+ size_t time_horizon = 0;
+ size_t action_dim = 0;
+ std::vector actions;
+};
+
+class FastCodec {
+ public:
+ static std::unique_ptr create(FastCodecConfig config, std::vector vocab_by_id,
+ std::vector fast_to_vlm_id, std::string & error);
+
+ // Constructs directly from the converter-compiled ByteLevel pieces stored
+ // in policy GGUF. offsets has vocab_size + 1 entries and indexes the flat
+ // byte buffer; no external tokenizer JSON is consulted.
+ static std::unique_ptr create_compiled(FastCodecConfig config, std::vector token_offsets,
+ std::vector token_bytes,
+ std::vector fast_to_vlm_id, std::string & error);
+
+ const FastCodecConfig & config() const;
+ const std::vector & fast_to_vlm_ids() const;
+
+ bool map_fast_to_vlm(const std::vector & fast_ids, std::vector & vlm_ids,
+ std::string & error) const;
+ bool map_vlm_to_fast(const std::vector & vlm_ids, std::vector & fast_ids,
+ std::string & error) const;
+
+ // Extracts every mapped action token from a generated Qwen sequence in order.
+ // EOS stopping remains the generator's responsibility; ordinary EOS/pad/text
+ // IDs in the returned sequence are ignored and do not terminate this scan.
+ bool extract_fast_tokens(const std::vector & generated_ids, std::vector & fast_ids,
+ std::string & error) const;
+
+ bool decode_fast_tokens(const std::vector> & batch_fast_ids, FastDecodeResult & result,
+ std::string & error) const;
+
+ // Strict low-level API: every input ID must be an action token. Use
+ // decode_generated_tokens for complete Qwen sequences containing text.
+ bool decode_vlm_action_tokens(const std::vector> & batch_vlm_ids, FastDecodeResult & result,
+ std::string & error) const;
+
+ // Production entry point for complete Qwen generated_ids. Ordinary text and
+ // control tokens are filtered through the explicit inverse action-token map.
+ bool decode_generated_tokens(const std::vector> & batch_generated_ids,
+ FastDecodeResult & result, std::string & error) const;
+
+ private:
+ FastCodec(FastCodecConfig config, std::vector> token_bytes,
+ std::vector fast_to_vlm_id);
+
+ bool byte_level_decode(const std::vector & fast_ids, std::vector & codepoints,
+ std::string & error) const;
+
+ FastCodecConfig config_;
+ std::vector> token_bytes_;
+ std::vector fast_to_vlm_id_;
+ std::vector> vlm_to_fast_id_;
+};
+
+} // namespace robotcpp::starvla
diff --git a/src/models/starvla/fast_policy.cpp b/src/models/starvla/fast_policy.cpp
new file mode 100644
index 0000000..e416fbb
--- /dev/null
+++ b/src/models/starvla/fast_policy.cpp
@@ -0,0 +1,272 @@
+#include "models/starvla/fast_policy.h"
+
+#include "ggml.h"
+#include "gguf.h"
+#include "models/starvla/policy_gguf.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace robotcpp::starvla {
+namespace {
+
+constexpr const char * kArchitecture = "starvla-policy";
+constexpr const char * kActionMapTensor = "starvla.policy.fast.action_token_map";
+constexpr const char * kOffsetsTensor = "starvla.policy.fast.codec.token_offsets";
+constexpr const char * kTokenBytesTensor = "starvla.policy.fast.codec.token_bytes";
+
+using detail::require_f32;
+using detail::require_i32;
+using detail::require_i32_array;
+using detail::require_string;
+using detail::require_string_array;
+
+struct FastRuntimeMetadata {
+ FastCodecConfig codec;
+ int token_bytes_count = 0;
+};
+
+FastRuntimeMetadata parse_metadata(gguf_context * gguf, FastPolicyConfig & config) {
+ if (require_string(gguf, "general.architecture") != kArchitecture ||
+ require_i32(gguf, "starvla.schema_version") != 1 || require_string(gguf, "starvla.framework") != "fast") {
+ throw std::runtime_error("GGUF is not a supported StarVLA FAST policy");
+ }
+ config.backbone_arch = require_string(gguf, "starvla.backbone.arch");
+ config.bundle_uuid = require_string(gguf, "starvla.bundle.uuid");
+ config.text_filename = require_string(gguf, "starvla.component.text.filename");
+ config.mmproj_filename = require_string(gguf, "starvla.component.mmproj.filename");
+ if (config.backbone_arch != "qwen2_5_vl" || config.bundle_uuid.empty() || config.text_filename.empty() ||
+ config.mmproj_filename.empty()) {
+ throw std::runtime_error("StarVLA FAST bundle metadata is incomplete");
+ }
+
+ config.qwen_hidden_dim = require_i32(gguf, "starvla.qwen.hidden_size");
+ config.qwen_input_embedding_dim = require_i32(gguf, "starvla.qwen.input_embedding_size");
+ config.qwen_vocab_size = require_i32(gguf, "starvla.qwen.vocab_size");
+ config.qwen_layer_count = require_i32(gguf, "starvla.qwen.layer_count");
+ config.cot_template = require_string(gguf, "starvla.prompt.cot_template");
+
+ config.action_dim = require_i32(gguf, "starvla.action.dimension");
+ config.horizon = require_i32(gguf, "starvla.action.horizon");
+ config.image_count = require_i32(gguf, "starvla.image.count");
+ config.image_names = require_string_array(gguf, "starvla.image.names");
+ config.image_processor_min_pixels = require_i32(gguf, "starvla.image.processor_min_pixels");
+ config.image_processor_max_pixels = require_i32(gguf, "starvla.image.processor_max_pixels");
+ config.image_patch_size = require_i32(gguf, "starvla.image.patch_size");
+ config.image_spatial_merge_size = require_i32(gguf, "starvla.image.spatial_merge_size");
+ config.image_min_token_count = require_i32(gguf, "starvla.image.min_token_count");
+ config.image_max_token_count = require_i32(gguf, "starvla.image.max_token_count");
+
+ const int max_length = require_i32(gguf, "starvla.fast.generation.max_length");
+ config.generation_eos_token_ids = require_i32_array(gguf, "starvla.fast.generation.eos_token_ids");
+ config.generation_top_k = require_i32(gguf, "starvla.fast.generation.top_k");
+ config.generation_repetition_penalty = require_f32(gguf, "starvla.fast.generation.repetition_penalty");
+
+ FastRuntimeMetadata runtime;
+ runtime.codec.scale = require_f32(gguf, "starvla.fast.codec.scale");
+ runtime.codec.min_token = require_i32(gguf, "starvla.fast.codec.min_token");
+ runtime.codec.vocab_size = static_cast(require_i32(gguf, "starvla.fast.codec.vocab_size"));
+ runtime.codec.time_horizon = static_cast(require_i32(gguf, "starvla.fast.codec.time_horizon"));
+ runtime.codec.action_dim = static_cast(require_i32(gguf, "starvla.fast.codec.action_dimension"));
+ const int action_token_count = require_i32(gguf, "starvla.fast.action_token.count");
+ const int offsets_count = require_i32(gguf, "starvla.fast.codec.token_offsets_count");
+ runtime.token_bytes_count = require_i32(gguf, "starvla.fast.codec.token_bytes_count");
+
+ const bool valid =
+ config.qwen_hidden_dim > 0 && config.qwen_input_embedding_dim > 0 && config.qwen_vocab_size > 0 &&
+ config.qwen_layer_count > 0 && !config.cot_template.empty() && config.action_dim > 0 && config.horizon > 0 &&
+ config.image_count > 0 && config.image_names.size() == static_cast(config.image_count) &&
+ config.image_processor_min_pixels > 0 &&
+ config.image_processor_max_pixels >= config.image_processor_min_pixels && config.image_patch_size > 0 &&
+ config.image_spatial_merge_size > 0 && config.image_min_token_count > 0 &&
+ config.image_max_token_count >= config.image_min_token_count && max_length > 0 &&
+ !config.generation_eos_token_ids.empty() && config.generation_top_k > 0 &&
+ std::isfinite(config.generation_repetition_penalty) && config.generation_repetition_penalty > 0.0f &&
+ runtime.codec.vocab_size > 0 && action_token_count == static_cast(runtime.codec.vocab_size) &&
+ offsets_count == action_token_count + 1 && runtime.token_bytes_count > 0 &&
+ runtime.codec.time_horizon == static_cast(config.horizon) &&
+ runtime.codec.action_dim == static_cast(config.action_dim);
+ if (!valid) {
+ throw std::runtime_error("StarVLA FAST metadata has incompatible dimensions");
+ }
+ config.generation_max_length = static_cast(max_length);
+
+ config.normalization = detail::require_normalization(gguf, config.action_dim);
+ return runtime;
+}
+
+struct RawTensor {
+ ggml_tensor * metadata = nullptr;
+ int index = -1;
+ std::vector bytes;
+};
+
+RawTensor read_tensor(const std::string & path, gguf_context * gguf, ggml_context * metadata_context, const char * name,
+ ggml_type expected_type, int64_t expected_elements) {
+ RawTensor result;
+ result.metadata = ggml_get_tensor(metadata_context, name);
+ result.index = gguf_find_tensor(gguf, name);
+ if (result.metadata == nullptr || result.index < 0 || result.metadata->type != expected_type ||
+ ggml_n_dims(result.metadata) != 1 || result.metadata->ne[0] != expected_elements ||
+ ggml_nelements(result.metadata) != expected_elements) {
+ throw std::runtime_error(std::string("FAST runtime tensor shape/type mismatch: ") + name);
+ }
+ result.bytes.resize(ggml_nbytes(result.metadata));
+ std::ifstream stream(path, std::ios::binary);
+ if (!stream) {
+ throw std::runtime_error("failed to open FAST policy GGUF tensor data");
+ }
+ const size_t offset = gguf_get_data_offset(gguf) + gguf_get_tensor_offset(gguf, result.index);
+ stream.seekg(static_cast(offset), std::ios::beg);
+ if (!stream || offset > static_cast(std::numeric_limits::max())) {
+ throw std::runtime_error(std::string("failed to seek FAST runtime tensor: ") + name);
+ }
+ stream.read(reinterpret_cast(result.bytes.data()), static_cast(result.bytes.size()));
+ if (!stream) {
+ throw std::runtime_error(std::string("failed to read FAST runtime tensor: ") + name);
+ }
+ return result;
+}
+
+} // namespace
+
+struct FastPolicy::Impl {
+ FastPolicyConfig config;
+ std::unique_ptr codec;
+};
+
+FastPolicy::FastPolicy(std::unique_ptr impl) : impl_(std::move(impl)) {}
+
+FastPolicy::~FastPolicy() = default;
+
+std::unique_ptr FastPolicy::load(const std::string & path, int verbosity, std::string & error) {
+ error.clear();
+ if (path.empty()) {
+ error = "StarVLA FAST policy path is required";
+ return nullptr;
+ }
+
+ ggml_context * metadata_context = nullptr;
+ gguf_init_params params{};
+ params.no_alloc = true;
+ params.ctx = &metadata_context;
+ gguf_context * gguf = gguf_init_from_file(path.c_str(), params);
+ if (gguf == nullptr || metadata_context == nullptr) {
+ if (metadata_context != nullptr) {
+ ggml_free(metadata_context);
+ }
+ if (gguf != nullptr) {
+ gguf_free(gguf);
+ }
+ error = "failed to read StarVLA FAST policy GGUF";
+ return nullptr;
+ }
+ auto cleanup = [&]() {
+ ggml_free(metadata_context);
+ metadata_context = nullptr;
+ gguf_free(gguf);
+ gguf = nullptr;
+ };
+
+ std::unique_ptr impl(new Impl());
+ try {
+ const FastRuntimeMetadata runtime = parse_metadata(gguf, impl->config);
+
+ RawTensor action_map = read_tensor(path, gguf, metadata_context, kActionMapTensor, GGML_TYPE_I32,
+ static_cast(runtime.codec.vocab_size));
+ RawTensor offsets = read_tensor(path, gguf, metadata_context, kOffsetsTensor, GGML_TYPE_I32,
+ static_cast(runtime.codec.vocab_size + 1));
+ RawTensor token_bytes =
+ read_tensor(path, gguf, metadata_context, kTokenBytesTensor, GGML_TYPE_I8, runtime.token_bytes_count);
+
+ const uint32_t endian_probe = 1;
+ if (*reinterpret_cast(&endian_probe) != 1) {
+ throw std::runtime_error("FAST runtime currently requires a little-endian host");
+ }
+ std::vector action_ids(runtime.codec.vocab_size);
+ std::vector token_offsets(runtime.codec.vocab_size + 1);
+ std::memcpy(action_ids.data(), action_map.bytes.data(), action_map.bytes.size());
+ std::memcpy(token_offsets.data(), offsets.bytes.data(), offsets.bytes.size());
+ impl->codec = FastCodec::create_compiled(runtime.codec, std::move(token_offsets), std::move(token_bytes.bytes),
+ std::move(action_ids), error);
+ if (impl->codec == nullptr) {
+ throw std::runtime_error("failed to construct embedded FAST codec: " + error);
+ }
+ if (verbosity >= 1) {
+ std::fprintf(stderr,
+ "%s: bundle=%s runtime_tensors=3 codec_vocab=%zu "
+ "generation_max_length=%zu profiles=%zu\n",
+ __func__, impl->config.bundle_uuid.c_str(), runtime.codec.vocab_size,
+ impl->config.generation_max_length, impl->config.normalization.profiles.size());
+ }
+ cleanup();
+ } catch (const std::exception & exception) {
+ cleanup();
+ error = exception.what();
+ return nullptr;
+ }
+ return std::unique_ptr(new FastPolicy(std::move(impl)));
+}
+
+bool FastPolicy::decode_generated(const std::vector & full_sequence, std::vector & normalized_actions,
+ std::string & error) const {
+ normalized_actions.clear();
+ error.clear();
+ if (impl_ == nullptr || impl_->codec == nullptr) {
+ error = "StarVLA FAST policy is not initialized";
+ return false;
+ }
+ FastDecodeResult decoded;
+ if (!impl_->codec->decode_generated_tokens({full_sequence}, decoded, error)) {
+ return false;
+ }
+ if (decoded.batch_size != 1 || decoded.time_horizon != static_cast(impl_->config.horizon) ||
+ decoded.action_dim != static_cast(impl_->config.action_dim) ||
+ decoded.actions.size() != static_cast(impl_->config.horizon * impl_->config.action_dim)) {
+ error = "embedded FAST codec returned an incompatible action tensor";
+ return false;
+ }
+ normalized_actions.reserve(decoded.actions.size());
+ for (double value : decoded.actions) {
+ const float converted = static_cast(value);
+ if (!std::isfinite(converted)) {
+ normalized_actions.clear();
+ error = "embedded FAST codec returned a non-finite action";
+ return false;
+ }
+ normalized_actions.push_back(converted);
+ }
+ return true;
+}
+
+bool FastPolicy::unnormalize(const std::vector & normalized_actions, const std::string & profile_key,
+ std::vector & actions, std::string & error) const {
+ if (impl_ == nullptr) {
+ actions.clear();
+ error = "StarVLA FAST policy is not initialized";
+ return false;
+ }
+ return denormalize_actions(impl_->config.normalization, profile_key, normalized_actions, impl_->config.horizon,
+ impl_->config.action_dim, actions, error);
+}
+
+const FastPolicyConfig & FastPolicy::config() const {
+ if (impl_ == nullptr) {
+ throw std::runtime_error("StarVLA FAST policy is not initialized");
+ }
+ return impl_->config;
+}
+
+const char * FastPolicy::backend_name() const {
+ return "cpu";
+}
+
+} // namespace robotcpp::starvla
diff --git a/src/models/starvla/fast_policy.h b/src/models/starvla/fast_policy.h
new file mode 100644
index 0000000..a92bbfb
--- /dev/null
+++ b/src/models/starvla/fast_policy.h
@@ -0,0 +1,72 @@
+#pragma once
+
+#include "models/starvla/fast_codec.h"
+#include "models/starvla/normalization.h"
+
+#include
+#include