diff --git a/.github/workflows/build_and_push_docker.yaml b/.github/workflows/build_and_push_docker.yaml index 442dc07..1b35e23 100644 --- a/.github/workflows/build_and_push_docker.yaml +++ b/.github/workflows/build_and_push_docker.yaml @@ -7,12 +7,18 @@ on: - development workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build-and-push: - runs-on: self-hosted + runs-on: [self-hosted, cpu, heavy, gpu] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + lfs: true - name: Log in to Docker Hub uses: docker/login-action@v3 @@ -20,36 +26,26 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Build and push all images + env: + AMM_IMG_SUFFIX: ${{ env.SUFFIX }} + run: | + docker compose -f docker/compose.yaml build + docker compose -f docker/compose.yaml push - - name: Build and push ROS 2 image - uses: docker/build-push-action@v6 - with: - context: . - file: docker/Dockerfile.ros2 - push: true - tags: | - robotecai/mobile-manipulator-demo-ros2${{ github.ref_name == 'development' && '-development' || '' }}:latest - - - name: Build and push Agents image - uses: docker/build-push-action@v6 + smoke-test: + needs: build-and-push + runs-on: [self-hosted, cpu, heavy, gpu] + steps: + - name: Log in to Docker Hub + uses: docker/login-action@v3 with: - context: . - file: docker/Dockerfile.agents - push: true - build-args: | - ROS2_IMAGE=robotecai/mobile-manipulator-demo-ros2${{ github.ref_name == 'development' && '-development' || '' }}:latest - tags: | - robotecai/mobile-manipulator-demo-agents${{ github.ref_name == 'development' && '-development' || '' }}:latest + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push O3DE image - uses: docker/build-push-action@v6 - with: - context: . - file: docker/Dockerfile.o3de - push: true - build-args: | - ROS2_IMAGE=robotecai/mobile-manipulator-demo-ros2${{ github.ref_name == 'development' && '-development' || '' }}:latest - tags: | - robotecai/mobile-manipulator-demo-o3de${{ github.ref_name == 'development' && '-development' || '' }}:latest + - name: Headless sim smoke test (/clock) + run: | + IMAGE="robotecai/agentic-mobile-manipulator-o3de:latest" + docker pull "$IMAGE" + docker run --rm --entrypoint pixi "$IMAGE" run sim-smoke + diff --git a/docker/compose.yaml b/docker/compose.yaml index 8e9c3fc..d39b6e2 100644 --- a/docker/compose.yaml +++ b/docker/compose.yaml @@ -10,7 +10,7 @@ # # Build everything: docker compose -f docker/compose.yaml build # Run the demo: xhost +local:root && docker compose -f docker/compose.yaml up demo -# Attach to a session: docker exec -it $(docker ps -qf ancestor=robotecai/mobile-manipulator-demo:latest) tmux attach -t agentic-mobile-manipulator-sim +# Attach to a session: docker exec -it $(docker ps -qf ancestor=robotecai/agentic-mobile-manipulator:latest) tmux attach -t agentic-mobile-manipulator-sim # (sessions: -sim -stack -llm-servers -agents -hmi) # # Target is single-pc-gpu-and-npu (the default). For a GPU-only image — llama.cpp @@ -28,14 +28,14 @@ services: # ── Base: RoboStack ROS 2 + built workspace ──────────────────────────────── ros2: - image: robotecai/mobile-manipulator-demo-ros2:latest + image: robotecai/agentic-mobile-manipulator-ros2:latest build: context: .. dockerfile: docker/Dockerfile.ros2 # ── Base: + O3DE SDK and built simulation ────────────────────────────────── o3de: - image: robotecai/mobile-manipulator-demo-o3de:latest + image: robotecai/agentic-mobile-manipulator-o3de:latest build: context: .. dockerfile: docker/Dockerfile.o3de @@ -44,7 +44,7 @@ services: # ── The demo: + agents, local inference engines; runs the whole stack ─────── demo: - image: robotecai/mobile-manipulator-demo:latest + image: robotecai/agentic-mobile-manipulator:latest build: context: .. dockerfile: docker/Dockerfile.demo @@ -68,7 +68,6 @@ services: - /dev/kfd:/dev/kfd # AMD GPU compute (ROCm) - /dev/dri:/dev/dri # AMD GPU render (Vulkan) - /dev/accel/accel0:/dev/accel/accel0 # Ryzen AI NPU (amdxdna) - # The NPU pins memory; without unlimited memlock FastFlowLM fails to allocate. ulimits: memlock: soft: -1 @@ -77,10 +76,5 @@ services: - /tmp/.X11-unix:/tmp/.X11-unix - ${XAUTHORITY:-$HOME/.Xauthority}:/root/.Xauthority - ../config.toml:/root/MobileManipulatorDemo/config.toml - # All weights live under the repo's models/: llama ggufs in models/, NPU - # models in models/flm (FLM_MODEL_PATH below). `pixi run download-models` - # fetches both. No host-home (~/.config/flm) coupling. - ../models:/root/MobileManipulatorDemo/models - # Single host: ROS 2 DDS + every inference endpoint talk over localhost, and - # host networking lets RViz/ros2 on the host introspect the running demo. network_mode: host diff --git a/docs/quickstart.md b/docs/quickstart.md index 9a91f77..4f92772 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -17,7 +17,9 @@ git clone https://github.com/RobotecAI/agentic-mobile-manipulator.git cd agentic-mobile-manipulator ``` -## 2. Build the image +## 2. Get the image: build _or_ pull + +**Build locally:** ```bash docker compose -f docker/compose.yaml build @@ -25,6 +27,14 @@ docker compose -f docker/compose.yaml build This builds three cacheable layers — `…-ros2` (RoboStack ROS 2 + workspace) → `…-o3de` (O3DE SDK + simulation) → `…demo` (Python agents + llama.cpp + FastFlowLM). The O3DE layer is large, so the first build takes a while; later rebuilds reuse the cached bases. +**Or pull the prebuilt images:** + +```bash +docker compose -f docker/compose.yaml pull +``` + +If you pull, remember to pass `--no-build` when starting the demo (see step 4). + ## 3. Download the model weights Weights are **not** baked into the image. They are downloaded into the repo's `models/` directory, bind-mounted at run time. `config.toml` is the single source of truth for which models are fetched. Pull them once into `models/` by running: @@ -44,9 +54,12 @@ Allow local connections to your X server, then start the `demo` service: ```bash xhost +local:root -docker compose -f docker/compose.yaml up demo +docker compose -f docker/compose.yaml up demo # if you built (step 2) +docker compose -f docker/compose.yaml up --no-build demo # if you pulled (step 2) ``` +> [!IMPORTANT] > `docker compose up` rebuilds any service that has a `build:` section — even when the image is already present locally. So after `pull`, a bare `up demo` ignores the pulled image and rebuilds `ros2 → o3de → demo`. Pass `--no-build` to run the pulled image as-is. + ## What to expect ![Demo Windows](demo_windows.jpg) @@ -60,7 +73,7 @@ docker compose -f docker/compose.yaml up demo Each component runs in its own tmux session inside the container. Attach to one (e.g. the simulation) from the host: ```bash -docker exec -it $(docker ps -qf ancestor=robotecai/mobile-manipulator-demo:latest) \ +docker exec -it $(docker ps -qf ancestor=robotecai/agentic-mobile-manipulator:latest) \ tmux attach -t agentic-mobile-manipulator-sim ``` diff --git a/pixi.toml b/pixi.toml index c21de34..e667135 100644 --- a/pixi.toml +++ b/pixi.toml @@ -162,6 +162,10 @@ description = "Launch O3DE simulation in its own tmux session" cmd = "$DEMO_ROOT/sim/build/linux/bin/profile/MobileManipulatorDemo.GameLauncher -bg_ConnectToAssetProcessor=0" description = "Launch O3DE simulation in the foreground (no tmux)" +[feature.sim.tasks.sim-smoke] +cmd = "bash scripts/sim_smoke.sh" +description = "Headless sim smoke test: boot with --rhi=null, pass if /clock ticks (no GPU/display)" + [feature.sim.tasks.editor] cmd = "$O3DE_ENGINE_PATH/bin/Linux/profile/Default/Editor --project-path $DEMO_ROOT/sim" description = "Launch O3DE Editor from SDK (project must be built first)" diff --git a/scripts/sim_smoke.sh b/scripts/sim_smoke.sh new file mode 100644 index 0000000..5c626a8 --- /dev/null +++ b/scripts/sim_smoke.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Headless smoke test for the O3DE sim. +# +# Boots the GameLauncher with the null renderer (--rhi=null) — no GPU, no X +# display needed — and passes if the simulation is actually *ticking*, proven by +# catching a message on /clock within a timeout. That single check exercises the +# whole cold path: binary runs, assets + level load, PhysX ticks, and the ROS 2 +# bridge publishes. +# +# NOT checked: camera / rendered sensors. Those need the Atom (Vulkan) renderer, +# which headless needs a DRI3-capable display for — Xvfb doesn't provide DRI3, so +# the GPU+Vulkan path wedges on swapchain creation. rhi=null has no renderer at +# all, so camera topics are advertised but never publish. Verifying rendered +# output headless is a separate, unsolved problem; keep it out of the fast gate. +# +# Run: pixi run sim-smoke +# In Docker: docker run --rm --entrypoint pixi run sim-smoke +# Tune: SIM_SMOKE_TIMEOUT=120 pixi run sim-smoke +# SIM_SMOKE_TOPIC=/joint_states SIM_SMOKE_TYPE=sensor_msgs/msg/JointState pixi run sim-smoke +set -uo pipefail + +# Hermetic: only ever see our own sim on loopback, so a neighbouring sim on a +# shared CI runner can't cause a false pass. +export ROS_LOCALHOST_ONLY=1 + +LAUNCHER="${DEMO_ROOT:-$PWD}/sim/build/linux/bin/profile/MobileManipulatorDemo.GameLauncher" +TIMEOUT="${SIM_SMOKE_TIMEOUT:-90}" # boot + first message; rhi=null is faster than Vulkan +TOPIC="${SIM_SMOKE_TOPIC:-/clock}" +# Type must be explicit: with no publisher up yet, `ros2 topic echo` can't infer +# the type from the graph and bails instantly instead of waiting. +TYPE="${SIM_SMOKE_TYPE:-rosgraph_msgs/msg/Clock}" +LOG="$(mktemp)" + +[ -x "$LAUNCHER" ] || { echo "[FAIL] launcher not built: $LAUNCHER"; exit 1; } + +# GameLauncher ignores SIGINT (won't stop on Ctrl+C), so hard-kill on exit. +# Kill the captured PID, plus a pkill by full command line as a fallback (the +# comm name isn't reliably set during early boot). pkill -f is safe: this shell's +# own argv is just "bash scripts/sim_smoke.sh", so it can't match itself. +LAUNCHER_PID="" +cleanup() { + [ -n "$LAUNCHER_PID" ] && kill -9 "$LAUNCHER_PID" 2>/dev/null + pkill -9 -f 'MobileManipulatorDemo.GameLauncher' 2>/dev/null +} +trap cleanup EXIT + +echo "[smoke] launching sim headless (--rhi=null)" +env -u DISPLAY "$LAUNCHER" -bg_ConnectToAssetProcessor=0 --rhi=null >"$LOG" 2>&1 & +LAUNCHER_PID=$! + +# /clock is published BEST_EFFORT; a default (reliable) subscriber gets nothing. +echo "[smoke] waiting up to ${TIMEOUT}s for a message on ${TOPIC} (${TYPE})" +if timeout "$TIMEOUT" ros2 topic echo --once --qos-reliability best_effort "$TOPIC" "$TYPE" >/dev/null 2>&1; then + echo "[PASS] ${TOPIC} is publishing — sim ticks headless" + exit 0 +fi + +echo "[FAIL] no message on ${TOPIC} within ${TIMEOUT}s" +echo "----- last 30 log lines -----" +tail -30 "$LOG" +exit 1