Files
esh-pfi-infrastructure/playbooks/deploy-fish-cpp.yaml
T
vh ee35fcd0a9 fish-cpp: add CUDA stubs to build linker path; fix verify step's masked failure
Two issues from the first deploy attempt:

1) Build failure (real): linker errors on s2.cpp's CUDA build —
   undefined references to cuMemSetAccess, cuDeviceGet, etc. These
   are CUDA Driver API symbols (in libcuda.so), not Runtime API
   (libcudart.so). The driver lib is provided by NVIDIA's container
   runtime at RUN time, not BUILD time.

   Fix: nvidia/cuda:devel images ship a stubs library at
   /usr/local/cuda/lib64/stubs/libcuda.so that provides the symbols
   for linking but is non-runnable. Adding that path via
   LIBRARY_PATH + CMAKE_LIBRARY_PATH lets the linker resolve while
   leaving runtime unchanged (real libcuda.so comes from the
   driver mount).

2) Verify false positive: the /v1/tts verify step's last command was
   `rm -f "$out"` — which always exits 0. This made the shell's
   final exit code 0 regardless of whether curl/file/grep succeeded,
   so verify reported OK even when nothing was running on host_port.

   Fix: `set -e` at top + trap-based cleanup. Failures now propagate;
   the rm still runs on either path via EXIT trap.
2026-04-28 01:17:05 -07:00

137 lines
5.4 KiB
YAML

# Deploy fish-cpp (Fish s2-pro via s2.cpp + GGML CUDA inference) to irv-ml1.
#
# Builds the image locally — multi-stage CUDA devel base (CMake + s2.cpp
# compile, ~10 min cold) → CUDA runtime base + binary + python shim.
# Pre-pulls rodrigomt/s2-pro-gguf weights (q6_k default, ~5 GB) into
# the bind-mounted weights dir.
#
# Usage:
# scripts/elway irv-ml1 --playbook playbooks/deploy-fish-cpp.yaml
#
# Idempotent — every step is creates-/when-gated; rerun is safe.
vars:
compose_dir: /opt/docker/compose/fish-cpp
references_dir: /worktank/fish-cpp/references
weights_dir: /worktank/fish-cpp/weights
host_port: "8199"
weights_repo: rodrigomt/s2-pro-gguf
default_quant: s2-pro-q6_k.gguf
steps:
# ── host-side dirs ──────────────────────────────────────────────────
- name: Ensure /worktank/fish-cpp root exists (one-time, sudo)
shell: mkdir -p /worktank/fish-cpp
sudo: true
creates: /worktank/fish-cpp
- name: Chown /worktank/fish-cpp to lkraven
shell: chown -R lkraven:lkraven /worktank/fish-cpp
sudo: true
when: "[ \"$(stat -c %U /worktank/fish-cpp)\" != \"lkraven\" ]"
- name: Ensure references dir exists
shell: mkdir -p {{ references_dir }}
creates: "{{ references_dir }}"
- name: Ensure weights dir exists
shell: mkdir -p {{ weights_dir }}
creates: "{{ weights_dir }}"
- name: Ensure compose dir exists
shell: mkdir -p {{ compose_dir }}
creates: "{{ compose_dir }}"
# ── deploy build context ────────────────────────────────────────────
# s2.cpp is built INSIDE the docker image, but the Dockerfile + shim
# need to be present in the compose dir so `docker compose build`
# can find them.
- name: Upload compose.yaml
upload:
src: stacks/fish-cpp/compose.yaml
dest: "{{ compose_dir }}/compose.yaml"
mode: "0644"
- name: Upload Dockerfile
upload:
src: stacks/fish-cpp/Dockerfile
dest: "{{ compose_dir }}/Dockerfile"
mode: "0644"
- name: Upload server.py (FastAPI shim)
upload:
src: stacks/fish-cpp/server.py
dest: "{{ compose_dir }}/server.py"
mode: "0644"
- name: Seed .env from template (only if absent)
upload:
src: stacks/fish-cpp/.env.example
dest: "{{ compose_dir }}/.env"
mode: "0644"
when: "[ ! -f {{ compose_dir }}/.env ]"
# ── pre-pull weights ────────────────────────────────────────────────
# q6_k + tokenizer.json (~5 GB total). Same one-shot
# python:3.12-slim + huggingface_hub.snapshot_download + hf_transfer
# pattern we've used for fish-s2, voxtral, etc. Idempotent on rerun
# via `creates:` on the model file.
- name: Pre-pull rodrigomt/s2-pro-gguf weights (q6_k + tokenizer, ~5 GB)
shell: |
docker run --rm --user 1000:1000 \
-e HOME=/tmp/h -e HF_HUB_ENABLE_HF_TRANSFER=1 \
-v {{ weights_dir }}:/dest \
python:3.12-slim sh -c 'set -e; mkdir -p /tmp/h /tmp/pip /tmp/site; PIP_CACHE_DIR=/tmp/pip pip install --quiet --target /tmp/site huggingface_hub hf_transfer; PYTHONPATH=/tmp/site python -c "from huggingface_hub import snapshot_download; snapshot_download(repo_id=\"{{ weights_repo }}\", local_dir=\"/dest\", allow_patterns=[\"{{ default_quant }}\",\"tokenizer.json\"])"'
creates: "{{ weights_dir }}/{{ default_quant }}"
# ── build + bring up ────────────────────────────────────────────────
- name: docker compose build (~10 min first time; CUDA toolchain + s2.cpp compile)
shell: |
set -o pipefail
cd {{ compose_dir }} && docker compose build 2>&1 \
| grep -vE '^#[0-9]+ |^ => |^=> |Collecting|Downloading|Requirement|Using cached|Installing collected|Successfully (installed|built)|━'
- name: docker compose up -d
shell: cd {{ compose_dir }} && docker compose up -d
- name: Wait for /v1/health to respond
shell: |
for i in $(seq 1 60); do
curl -sf -o /dev/null --max-time 3 http://localhost:{{ host_port }}/v1/health && exit 0
sleep 5
done
exit 1
changed_when: "false"
verify:
- name: /v1/health returns 200 + reports model loaded
shell: |
curl -sf http://localhost:{{ host_port }}/v1/health \
| python3 -c "import json,sys; d=json.load(sys.stdin); assert d.get('status')=='ok' and d.get('model')"
changed_when: "false"
- name: /v1/tts returns a real WAV (POST with text body)
# `set -e` so curl/file/grep failures actually propagate. The
# previous version put `rm -f` as the last command, which always
# exits 0 — masking real failures (verify reported OK even when
# nothing was running on host_port). Trap-based cleanup runs the
# rm even on failure.
shell: |
set -e
out=$(mktemp --suffix=.wav)
trap 'rm -f "$out"' EXIT
curl -sf -X POST http://localhost:{{ host_port }}/v1/tts \
-H 'Content-Type: application/json' \
-d '{"text":"Verify."}' \
-o "$out" --max-time 60
file -b "$out" | grep -q '^RIFF.*WAVE'
changed_when: "false"
- name: Container is running
shell: docker inspect fish-cpp --format '{{.State.Status}}' | grep -q running
changed_when: "false"