Files
esh-pfi-infrastructure/stacks/fish-cpp/README.md
T
vh 14f052461e stacks/fish-cpp: Phase 1 — s2.cpp + GGML CUDA backend image, FastAPI shim, deploy playbook
New stack scaffolding for the Fish quantized-realtime experiment. Not
deployed yet — this commit lands the canonical files; deploy follows.

Architecture decisions made in Phase 1:
* CUDA backend, NOT Vulkan. s2.cpp's CMakeLists exposes both
  -DS2_VULKAN and -DS2_CUDA; the most recent upstream commit
  (2026-04-12) was specifically about CUDA improvements, and CUDA
  on the A6000 will be substantially faster than Vulkan for ML
  matmul. -DS2_CUDA=ON in the Dockerfile build args.

* Pinned to s2.cpp commit e48ce8e02d8335bd9a0ba94679f605724b31d12
  (2026-04-12 HEAD of main). Repo is alpha software per README;
  pin tightly so future churn doesn't break our build. Bump
  deliberately when wanting upstream improvements.

* Multi-stage Dockerfile: nvidia/cuda:12.6.0-devel for build (needs
  CMake + ninja + git + the CUDA toolchain) → nvidia/cuda:12.6.0-runtime
  for serve (slimmer; just the s2 binary + GGML libs + a small Python
  shim). Cuts image size by ~50% vs single-stage devel.

* FastAPI shim (server.py) wraps s2.cpp CLI in Fish's `/v1/tts`
  contract so the same bench harness + clients work against fish-cpp
  with no changes. Per-request flow: decode optional reference WAV
  from base64 → write to temp → subprocess.run the s2 binary → stream
  resulting WAV back. Adds ~50-100ms per-request fork+exec overhead;
  negligible vs the multi-second generation cost.

* `streaming: true` accepted in request body but IGNORED — s2.cpp
  writes a complete WAV before returning, so chunked output isn't
  available. Unlike fish-s2 (HF wrapper) where streaming drops TTFB
  to 26ms, fish-cpp's TTFB ≈ total wall time. Speed depends entirely
  on raw generation throughput.

* q6_k as default quant — sweet spot per typical GGUF guidance:
  near-bf16 quality at ~5GB. Other variants (q4_k_m, q5_k_m, q8_0,
  f16) selectable via FISH_CPP_MODEL env.

* Pinned to GPU 1 (A6000) by default to share with fish-s2 for
  direct A/B benching. q6_k weights ~5GB + runtime ~3GB ≈ 8GB —
  comfortable on either GPU.

* Port 8199 (next free in the irv-ml1 TTS slate).

Phase 2 (next) is the actual deploy + first build. Reserved 30-45 min
for cold-cache build + weights pull.
2026-04-28 01:06:14 -07:00

109 lines
3.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# fish-cpp
Fish s2-pro served via [s2.cpp](https://github.com/rodrigomatta/s2.cpp)
— a pure C++/GGML inference engine for Fish s2-pro, with weights from
[rodrigomt/s2-pro-gguf](https://huggingface.co/rodrigomt/s2-pro-gguf).
Wrapped by a tiny FastAPI shim exposing Fish's `/v1/tts` HTTP contract
so it slots into the same bench harness + client patterns as `fish-s2`.
## Why this stack alongside `fish-s2`
`fish-s2` (HF transformers wrapper) measured at **0.78× realtime** on
the A6000 — phenomenal quality but sub-realtime, meaning streaming
clients hit buffer underruns on phrases longer than ~3-4 seconds of
audio. `fish-cpp` targets the same s2-pro architecture but runs it
through s2.cpp's C++/GGML/CUDA inference path with q6_k quantization
— typically 2-5× faster than HF transformers for equivalent precision
(GGML is what makes llama.cpp fast).
**Goal**: hit ≥ 1× realtime on the A6000 so streaming actually flows
without stutters, while keeping Fish's quality near-equivalent to BF16.
## Status: alpha
s2.cpp is alpha software per its upstream README. Pin the SHA in
`.env`, don't track main blindly — community alpha projects break
weekly.
## What works (and what doesn't) vs `fish-s2`
| feature | fish-s2 (HF) | fish-cpp (this) |
|---|---|---|
| `/v1/tts` POST endpoint | ✓ | ✓ |
| `text` body field | ✓ | ✓ |
| `references` body field (cloning) | ✓ | ✓ (single ref only) |
| `streaming: true` | ✓ (TTFB → 26 ms) | ✗ accepted but ignored |
| Paralinguistic tags | ✓ | should work (same weights) |
| Quantization | bf16 | q4_k_m / q5_k_m / **q6_k** (default) / q8_0 |
| Realtime factor | 0.78× | targeting 1-1.5× |
The streaming gap matters: fish-s2 with `streaming: true` returns the
first audio chunk in 26 ms (perceived latency feels instant). fish-cpp
returns nothing until generation completes. So fish-cpp's appeal is
RAW THROUGHPUT, not perceived latency. Combined with realtime+
generation, total wall time stays low enough that polling clients
don't notice.
## API
OpenAPI shape mirrors `fish-s2`:
```bash
# Basic — text only, default voice
curl -fsS -X POST http://10.100.79.3:8199/v1/tts \
-H 'Content-Type: application/json' \
-d '{"text":"Hello there."}' \
> out.wav
# With voice cloning — base64-encoded reference audio inline
B64=$(base64 -w 0 /worktank/fish-cpp/references/glados.wav) # on irv-ml1
echo "{\"text\":\"Welcome.\",\"references\":[{\"audio\":\"$B64\",\"text\":\"transcript\"}]}" \
| curl -fsS -X POST http://10.100.79.3:8199/v1/tts \
-H 'Content-Type: application/json' --data-binary @- \
> out.wav
```
Health probe at `GET /v1/health`.
## Deploy
```bash
scripts/elway irv-ml1 --playbook playbooks/deploy-fish-cpp.yaml
```
Cold deploy ~30-45 min: ~10 min image build (CUDA dev toolchain +
CMake + s2.cpp compile), ~3 min weights pull (~5 GB for q6_k +
12 MB tokenizer), ~1 min container boot.
## Hardware footprint
- **VRAM**: ~8 GB practical for q6_k (5 GB weights + 3 GB runtime).
Pinned to GPU 1 (A6000) by default to share with `fish-s2` for
direct A/B comparison. Could also run on GPU 0 (3090) with room
to spare.
- **Disk**: ~5 GB for q6_k checkpoint + tokenizer.
## Bench plan
Same 3-phrase suite as the other TTS:
```
P1 = "Hello, this is a test of the voice synthesis system. The quick brown fox jumps over the lazy dog."
P2 = "Oh my god, I cannot believe what just happened. That was absolutely incredible!"
P3 = "What the hell is going on. This is some bullshit and I am not putting up with it."
```
Compare:
- TTFB / total wall-clock per phrase
- audio-seconds / wall-seconds (realtime factor)
- Quality (ear test) vs `fish-s2` BF16 baseline
If fish-cpp lands ≥ 1× realtime AND the q6_k quality holds up under
ear test, this stack becomes the default Fish path. fish-s2 stays
deployed for paralinguistic tag fidelity reference + streaming
(if that turns out to matter for any specific use case).
## Lessons learned
(Populate after deploy iteration.)