Skip to content

Run Fluid Speech Mini on the GPU on Linux through parakeet.cpp and Vulkan - #26

Merged
watzon merged 5 commits into
watzon:mainfrom
kittyandrew:linux-parakeet-vulkan
Oct 20, 2026
Merged

watzon merged 5 commits into
watzon:mainfrom
kittyandrew:linux-parakeet-vulkan

Conversation

@kittyandrew

@kittyandrew kittyandrew commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Written by an AI agent (claude-code/claude-opus-5-5), ownership of @kittyandrew.

Closes #25. This follows the answers in #25 (comment).

Note

Fluid Speech Mini is offered only with a discrete GPU: both integrated GPUs I measured were about 3 times slower than the CPU model. That is safe to merge as is. Whether integrated GPUs should get the model is your call, in the comments (#26 (comment)). The untested points are under "Not proven yet".

What changes

  • Catalog (sayso-core). A new EngineKind::ParakeetCpp { repo, revision, file, preview_archive } names one GGUF file in a Hugging Face repository at a pinned commit, plus the Kroko archive for the live preview. Fluid Speech Mini joins the Linux catalog with id fluid-speech-mini, the same id as on macOS. Its Linux entry has its own WER (6.56, measured on Linux, see below), a link to the GGUF repository, its own description ("runs on the graphics card"), and a credit line for parakeet.cpp and ggml (MIT). EngineKind::needs_gpu marks it.
  • Download (download.rs, engine.rs). The engine fetches <repo>/resolve/<revision>/<file> into a folder named by the revision, with the same staging folder and .complete marker as the archives, so a new revision downloads again. SAYSO_HF_ENDPOINT overrides the host, as in the portable engine.
  • Load (parakeet.rs). A parakeet_gguf recipe loads the file through parakeet.cpp's C API. Unless the device parakeet.cpp picks is a discrete GPU, the load fails with the new reason no_gpu, and the app shows "This model needs a dedicated graphics card with a Vulkan driver, and Sayso found none. Choose a model that runs on the CPU in Models." There is no CPU fallback. After the load, a warm-up decodes 1, 30 and 60 s of silence, so the driver compiles its shaders before the first dictation.
  • Models page and onboarding (sayso-app). On Linux, model_runs_here probes Vulkan with ash (already in the lock through wgpu): it counts only a discrete GPU, not an integrated one and not a CPU device such as llvmpipe or lavapipe. Without one, the model is hidden. The app passes the chosen GPU to the engine as GGML_VK_VISIBLE_DEVICES when it starts it, unless the user set that variable. With a discrete GPU and English as the dictation language, onboarding offers Fluid Speech Mini as its second model; Parakeet TDT v2 stays first.
  • Build (build.rs). No submodule. The parakeet feature, on by default and Linux only, downloads two pinned GitHub archives and checks their SHA-256: parakeet.cpp 5c9b0fb and its ggml submodule e705c5f, which the archive leaves out. It applies parakeet.cpp's own ggml patches with patch (its CMake applies them only to a git checkout), and writes a marker file last, so a tree that the CI cache emptied is downloaded again. SAYSO_PARAKEET_CPP_DIR builds a local tree instead. The engine links parakeet.cpp statically and needs only libvulkan.so.1 at run time. --no-default-features builds without it. The build dependencies are Linux-only, so macOS and Windows build what they did before.
  • CI and packaging. scripts/linux-deps.sh adds cmake and patch, and glslc where apt has it. Ubuntu 22.04 has no glslc package and Vulkan headers too old for ggml (1.3.204), so the new scripts/linux-vulkan-deps.sh builds Vulkan-Headers v1.4.363 and shaderc v2026.4's glslc from source. linux.yml caches the result per architecture, keyed by the script. LunarG's SDK has no Linux aarch64 tarball (its latest.json lists only linux, mac, windows and warm, and the aarch64 URL returns 404), so the source build covers both runners.
  • Nix package. vulkan-loader joins buildInputs in packaging/nix/package.nix, so autoPatchelf can resolve the engine's libvulkan.so.1. It stays in runtimeDependencies for the app, which opens it at run time.
  • Docs. docs/linux.md, docs/local-models.md, docs/benchmark.md (a Linux section), CONTRIBUTING.md, README.md, and the reason table in sayso-engine-client/NOTES.md.

How you checked it

On NixOS, x86_64, Rust 1.98.1, Ryzen 7 7700X with an RTX 4070 (driver 595.104.02) and the CPU's integrated Radeon:

  • cargo test --workspace
  • cargo clippy --workspace --all-targets -- -D warnings
  • scripts/check-deps.sh
  • I ran the app and used the changed feature: onboarding in the packaged build on a desktop with an RTX 4070 and on a laptop with only an Intel GPU (see the comments).

New tests: parsing the engine kind and rejecting bad repository, revision and file names; a GGUF download from a local server, and a new revision downloading again; the engine binary refusing a GPU model with no_gpu when VK_DRIVER_FILES names no driver; the GPU pick (only a discrete GPU counts); and the onboarding choices with and without a GPU.

By hand:

  • Probe. With VK_DRIVER_FILES set to each driver in turn, the app's probe chose the RTX 4070 from all drivers and nothing from the Radeon alone, lavapipe alone, or no driver. With the Radeon's driver listed first, it still chose the RTX 4070, and the engine refused when only the Radeon was visible to it.
  • Refusal. With lavapipe as the only driver, ggml found no device, and the load failed with no_gpu.
  • Benchmark. docs/benchmark.md's 1,400 clips, with the same harness and SAYSO_ENGINE_PATH set to this engine:
Linux build WER Accuracy Speed
Fluid Speech Mini, GGUF on Vulkan (RTX 4070) 6.56 93.4% 213×
Parakeet TDT v2, sherpa-onnx on the CPU 6.72 93.3% 40×
For scale, the Core ML build on the M5 Pro 6.61 93.4% 159×
  • Final pass and memory. The engine on 12 recordings, 5 s of GPU idle before each, three fresh processes, medians:
5 s 17 s 42 s 58 s Load RAM after
RTX 4070 77 ms 116 ms 266 ms 347 ms 1.8 s 187-190 MB
Integrated Radeon (RADV) 487 ms 1,082 ms 2,873 ms 3,955 ms 7.3 s 161-168 MB
Laptop Intel UHD, i5-10210U (ANV) 1,917 ms 4,050 ms 10,222 ms 13,545 ms 23.2 s 181 MB
For scale, Parakeet TDT v2 on the laptop's CPU 466 ms 1,202 ms 3,277 ms 4,725 ms 4.5 s 1,436 MB
  • First start. With an empty NVIDIA shader cache, the load took 14.2 s and the first dictation after it 50 ms. With the cache filled, the load took 1.8 s.
  • Release build. In ubuntu:22.04 (Docker, x86_64), linux-deps.sh then linux-vulkan-deps.sh built the tools in 106 s (a 6.9 MB cache), and parakeet.cpp's Vulkan build compiled all its shaders. A control run with 22.04's own headers failed in ggml-vulkan.cpp, as expected. The 24.04 apt-only path, as in test-linux, also built.
  • Nix. I built packaging/nix/package.nix from a tarball of this branch (scripts/bundle-linux.sh). Before the fix below, autoPatchelf failed: it adds runtimeDependencies to RPATH but does not use them to resolve a NEEDED entry, and the engine now needs libvulkan.so.1 directly. With vulkan-loader also in buildInputs, the packaged engine found it, loaded the model on the RTX 4070 and transcribed, with an empty environment.

Not proven yet

  • aarch64. This PR's Linux packages workflow built the tarball and the packages on both runners. The Vulkan tools took 3 min 10 s on x86_64 and 2 min 53 s on aarch64 on a cache miss, and the cache was saved for this branch; main saves its own on its first run. Nothing ran the GPU model on an arm GPU.
  • Large integrated GPUs. The two integrated GPUs I measured were about 3 times slower than the CPU model, with the same text as on NVIDIA on 9 of 12 recordings. A Radeon 780M or Intel Xe is several times larger and might beat the CPU, but I have none to measure, so this PR leaves every integrated GPU out.

The warm-up and the download. You asked for the warm-up to run while the user waits for the download. The engine loads a model only when all of its files are on disk, so the 14 s shader compile starts when the download ends. Continue is enabled at once, though, and the first step that needs the model is the practice step (6 of 7), three steps later. In both click-throughs the load took 13-14 s after the download, so a user who spends that long on those three steps never waits. I left the engine as it is.

Postponed

These do not block this pull request. Each can be a follow-up if you want it.

  • Fewer shaders on 24.04. apt's glslc (2023.8) lacks a few extensions that shaderc v2026.4 has, so test-linux and contributor builds on 24.04 build fewer shader variants than the release, and only the release build covers the integer-dot shaders. test-linux has no GPU, so it never runs them; it stays as it is.
  • A full third-party notices file. This PR ships the MIT licenses of parakeet.cpp and ggml, from the pinned commits, in share/licenses/sayso of every package, and the Models page keeps the credit line. The packages still ship only Sayso's own LICENSE for the other libraries, such as onnxruntime (MIT).
  • Linux figures for the other models. The Linux catalog shows the Mac's WER for its other models. On this run Parakeet TDT v2 on Linux measured 6.72 against 6.37 on the Mac. This PR changes only Fluid Speech Mini's figure.

The conv patch from the issue is not in this pull request and nothing depends on it. With SAYSO_PARAKEET_CPP_DIR pointing at a patched tree, the same engine is 10-24% faster per dictation, with the same text on 12 of 12 recordings and the same memory. Sending it to parakeet.cpp, as you suggested, is a separate step.

…lkan

A new engine kind, ParakeetCpp, names one GGUF file in a Hugging Face
repository at a pinned revision, plus the Kroko archive for the live
preview. The engine downloads that file next to the archives, with the
same staging and .complete marker, and runs its final pass on
parakeet.cpp's C API with ggml's Vulkan backend. A warm-up after the load
compiles the GPU shaders before the first dictation.

Without a usable GPU the model is hidden: on Linux, model_runs_here probes
Vulkan for a discrete GPU, then an integrated one, and never counts a CPU
device such as lavapipe. The app passes the chosen GPU to the engine as
GGML_VK_VISIBLE_DEVICES. A load without a GPU still fails, with the new
reason no_gpu. With a GPU and English, onboarding offers the model second.

build.rs fetches parakeet.cpp and its ggml as two pinned archives checked
by SHA-256, applies parakeet.cpp's ggml patches and links it statically,
so the engine needs only libvulkan.so.1. On Ubuntu 22.04,
scripts/linux-vulkan-deps.sh builds the Vulkan headers and glslc that ggml
needs, and the release job caches them. The Nix package gets
vulkan-loader as a build input, so autoPatchelf resolves the engine's
libvulkan.so.1.

Co-Authored-By: claude-code/claude-opus-5-5 <noreply@anthropic.com>
@kittyandrew

Copy link
Copy Markdown
Contributor Author

Written by an AI agent (claude-code/claude-opus-5-5), ownership of @kittyandrew.

Onboarding click-through

I ran the packaged build of 588ebf4 (scripts/bundle-linux.sh, then packaging/nix/package.nix) with a fresh profile on NixOS. I drove the model step through AT-SPI, once per Vulkan driver, picked with VK_DRIVER_FILES.

NVIDIA driver (RTX 4070) lavapipe only (no GPU)
  • NVIDIA: the probe logs Vulkan devices [DISCRETE_GPU], GPU for GPU models: Some(0). Fluid Speech Mini shows as a card. The 404 MB download took about 14 s, and its sha256 matches the pin. Load plus a cold warm-up took about 13 s, and then the status box read "Downloaded and ready".
  • lavapipe: the probe logs Vulkan devices [CPU], GPU for GPU models: None. Fluid Speech Mini is gone from the cards and from the full list, which shows 10 models instead of 11.

One gap: the Fluid Speech Mini card doesn't say it needs a GPU. On a machine without one, the model just doesn't appear.

… changes

Fluid Speech Mini on an integrated GPU was about three times slower than
Parakeet TDT v2 on the same computer's CPU: 4.0 s for 58 s of speech on a
Ryzen 7 7700X's Radeon, and 13.5 s on a Core i5-10210U laptop's Intel UHD,
where its CPU took 4.7 s. The app's probe and the engine's check now count
only a discrete GPU. The engine checks the device parakeet.cpp would pick, so
a laptop that lists its integrated GPU first refuses too, and the no_gpu text
says a dedicated graphics card.

A GGUF's folder in the store is now its commit, not its file name, so a new
revision of the same file downloads again instead of loading the old weights.
The Hugging Face override is SAYSO_HF_ENDPOINT, as in the portable engine.

The build dependencies are Linux-only, so macOS and Windows build what they
did before. The single-file download shares the archive's staging code, a GGUF
no longer goes through the sherpa-onnx file layout, and the Linux catalog has
its own Fluid Speech Mini description. The docs, the engine's module docs and
CONTRIBUTING's list of packaging triggers follow.

Co-Authored-By: claude-code/claude-opus-5-5 <noreply@anthropic.com>
@kittyandrew

Copy link
Copy Markdown
Contributor Author

Written by an AI agent (claude-code/claude-opus-5-5), ownership of @kittyandrew.

Integrated GPUs are too slow, so e2cea27 counts only discrete GPUs

I measured a laptop GPU for question 1: a ThinkPad with a Core i5-10210U and Intel UHD Graphics (Comet Lake GT2, Mesa ANV). I used the same 12 recordings as the table in the description, 3 fresh engine processes per row, and medians.

Laptop (i5-10210U) 5 s 17 s 42 s 58 s Load
Fluid Speech Mini, Vulkan on the Intel UHD 1,917 ms 4,050 ms 10,222 ms 13,545 ms 23.2 s
Parakeet TDT v2, sherpa-onnx on the same CPU 466 ms 1,202 ms 3,277 ms 4,725 ms 4.5 s

That is about 3 times slower than the CPU model, as on the 7700X's Radeon (4.0 s for 58 s against 1.5 s). The text matched the RTX 4070 on 9 of 12 recordings. So e2cea27 counts only a discrete GPU, in the app's probe and in the engine's check. Question 1 in the description now asks whether that is the rule you want.

I ran both machines again on the packaged build of e2cea27, through AT-SPI, with every Vulkan driver visible:

Desktop: RTX 4070 and integrated Radeon Laptop: Intel UHD only
  • Desktop. The probe logs Vulkan devices [DISCRETE_GPU, INTEGRATED_GPU, CPU], GPU for GPU models: Some(0). The download took 12 s and its SHA-256 matches the pin. The load with a cold shader cache took 14 s. With only the Radeon's or lavapipe's driver, the engine refuses with no_gpu.
  • Laptop. The probe logs [INTEGRATED_GPU], GPU for GPU models: None. Fluid Speech Mini is absent from the cards and from the full list, which shows 10 models. A load sent straight to the engine fails with no_gpu.

e2cea27 also makes a GGUF's folder its commit, so a new revision downloads again. It renames the override to SAYSO_HF_ENDPOINT, as in the portable engine, and makes the build dependencies Linux-only.

The engine links both statically, and their MIT License asks for the
license text in every copy. The texts from the pinned commits sit in
crates/sayso-engine/licenses, and the tarball installs them under
share/licenses/sayso, which the deb, rpm, AUR, Nix, AppImage and
Flatpak packages carry. The Models page keeps its credit line.

The engine's download tests now serve the GGUF from the same test
server as the archives, through its redirect, as Hugging Face's
resolve URLs redirect too.

Co-Authored-By: claude-code/claude-opus-5-5 <noreply@anthropic.com>
@kittyandrew

Copy link
Copy Markdown
Contributor Author

Written by an AI agent (claude-code/claude-opus-5-5), ownership of @kittyandrew.

@watzon, one question is left before this can leave draft: should an integrated GPU get Fluid Speech Mini? Your rule was a discrete GPU first, then an integrated one. This PR now counts only a discrete GPU, because both integrated GPUs I could measure were slower than the CPU model on the same computer. I moved the other questions to "Postponed" in the description; none of them block this PR.

The final pass, with 5 s of GPU idle before each clip, medians of 3 fresh engine processes:

Computer Setup 5 s 17 s 42 s 58 s Load
Desktop, Ryzen 7 7700X Fluid Speech Mini on the RTX 4070 77 ms 116 ms 266 ms 347 ms 1.8 s
Fluid Speech Mini on the CPU's Radeon (2 CUs, RADV) 487 ms 1,082 ms 2,873 ms 3,955 ms 7.3 s
Parakeet TDT v2 on the CPU 165 ms 359 ms 1,041 ms 1,539 ms 2.6 s
Laptop, Core i5-10210U Fluid Speech Mini on the Intel UHD (ANV) 1,917 ms 4,050 ms 10,222 ms 13,545 ms 23.2 s
Parakeet TDT v2 on the CPU 466 ms 1,202 ms 3,277 ms 4,725 ms 4.5 s
  • Both integrated GPUs were 2.6-2.9 times slower than Parakeet TDT v2 on the CPU of the same computer. Their text matched the RTX 4070 on 9 of 12 recordings; the others were a word or a comma apart.
  • Not measured: a large integrated GPU, such as a Radeon 780M (12 CUs) or a recent Intel Xe/Arc laptop GPU. Those are several times larger than these two and might beat their CPU. I have none to test.

The options I see:

  1. Discrete GPUs only, as this PR does now. Nobody gets a model that is slower than the default, and owners of a large integrated GPU lose it.
  2. Integrated GPUs on the Models page only, never in onboarding. Owners of a large one can try it, and someone with a small one can still pick a slower model there. The probe and the engine's check would count integrated GPUs again, and onboarding would ask for a discrete one.
  3. A limit on compute units or memory. This needs a measured number from a large integrated GPU, which I cannot provide.

If option 1 is fine, this PR is done from my side. If you prefer option 2, I can add it here.

@kittyandrew

kittyandrew commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Written by an AI agent (claude-code/claude-opus-5-5), ownership of @kittyandrew.

The conv patch from the issue is now sent upstream as mudler/parakeet.cpp#102: 4-15% faster decode on NVIDIA (Vulkan, CUDA) and on AMD and Intel integrated GPUs, with the same WER on the 1,400-clip set (6.58 against 6.56). This PR does not depend on it. Once it merges, the pin in build.rs can move to that commit.

@kittyandrew
kittyandrew marked this pull request as ready for review October 18, 2026 14:52
@watzon

watzon commented Oct 18, 2026

Copy link
Copy Markdown
Owner

@kittyandrew, I'm going with option 1: discrete GPUs only. Thanks for measuring this.

On both integrated GPUs you tested, the model was 2.6-2.9 times slower than Parakeet TDT v2 on the CPU. A 58 s dictation took 13.5 s on the laptop's GPU and 4.7 s on its CPU. Option 2 would show a slower model to people who can't tell it's slower, and option 3 needs a number from a large integrated GPU that nobody has measured yet. If someone with a Radeon 780M or a recent Intel Xe can run your benchmark later, I'm happy to add integrated GPUs with a limit based on that data.

So this part is done. I'll review the code next and run the manual test and clippy jobs before I merge.

@kittyandrew

Copy link
Copy Markdown
Contributor Author

@kittyandrew, I'm going with option 1: discrete GPUs only. Thanks for measuring this.
...
So this part is done. I'll review the code next and run the manual test and clippy jobs before I merge.

sounds good

@watzon
watzon merged commit 8ad8546 into watzon:main Oct 20, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Linux: run Parakeet on the GPU through parakeet.cpp and Vulkan

2 participants