I wanted long-form speech synthesis that runs entirely on my own hardware: hand a machine a twenty-minute script, get back a finished, mastered audio file, with nothing leaving the house. The hardware is a DGX Spark. The hard part was that its GPU is newer than most of the software meant to run on it.
Here’s how the pipeline works, and the GPU fight it took to make it fast.
The model
The engine is VoxCPM2, an Apache-2.0 text-to-speech model that does zero-shot voice cloning — give it a few seconds of reference audio and it renders new text in that voice. It runs fully offline on the DGX Spark in my homelab. Nothing leaves the house and nothing hits a cloud API. Voice cloning has an obvious dark side, so one rule comes with it: only clone a voice whose owner has agreed to it.
The naive path works on day one. There’s a reference voxcpm package, you make a virtual environment, you point it at a reference WAV and a block of text, and out comes audio. The problem is speed. On the host path, the model renders at a real-time factor (RTF) of about 1.3 — meaning thirty minutes of audio takes roughly thirty-nine minutes of compute. Workable, but every script revision means another forty-minute wait. I wanted to iterate faster than that.
The Blackwell problem
The Spark’s GPU is a GB10 — sm_121, Blackwell architecture. It is genuinely fast hardware. It is also new enough that most of the prebuilt ML wheels you’d reach for have never heard of it.
The accelerated inference path I wanted relies on nano-vllm plus flash-attention. And flash-attn is exactly where it falls apart:
- flash-attn won’t build from source on this box. The host ships CUDA 13.0; the torch build wants cu12.9. Beyond the version mismatch, FlashAttention-2 simply predates Blackwell — there’s no kernel for
sm_121to compile in the first place. You can sit through the build and still get nothing that runs.
So you stop trying to build it and go find someone who already did. NVIDIA’s container, nvcr.io/nvidia/vllm:25.12.post1-py3, ships flash-attn 2.7.4 prebuilt for sm_121. nano-vllm pins flash-attn with no specific version, so the prebuilt one satisfies it cleanly, and the image’s torch is recent enough that nothing tries to “helpfully” replace the NVIDIA build with a stock wheel. The whole acceleration story turns on using the right base image instead of fighting the compiler.
That decision creates three smaller fires, each of which cost me an evening:
- torchaudio’s ABI doesn’t match. NVIDIA’s torch is built against a custom ABI, and the PyPI torchaudio won’t load against it. It turns out VoxCPM2 never actually calls torchaudio — it resamples through librosa — so the fix is to not have the real one at all. I dropped in a tiny shim package backed by soundfile and librosa that satisfies the import and nothing else.
spawnre-imports your script. nano-vllm spins up worker processes withspawn, which re-imports the entry module in each worker. Any code that isn’t guarded underif __name__ == "__main__":runs again in every worker — which, for model-loading code, is exactly the catastrophe it sounds like. Move everything under the guard.- cuDNN is missing a Blackwell engine. The AudioVAE that turns latents back into a waveform leans on
conv_transpose1d, and cuDNN doesn’t yet ship an engine for it on this GPU. Settingtorch.backends.cudnn.enabled = Falsemakes the convolutions fall back to native CUDA — which works fine — while flash-attention keeps doing the heavy lifting in the attention layers, so the speedup survives.
The payoff for all of that:
| Task | Host venv | Container (nano-vllm) | Speedup |
|---|---|---|---|
| Plain TTS | RTF ~1.25 | RTF 0.39 | 3.2× |
| Voice-clone render | RTF ~1.40 | RTF 0.40 | 3.5× |
RTF below 1.0 means faster than real time. Thirty minutes of audio now renders in about twelve minutes instead of thirty-nine — same model, same quality, roughly 3× faster — and that’s the difference between “I’ll re-render after dinner” and “let me just fix that one sentence and run it again.”
Script to mp3, in one command
The pipeline is public as voxcpm-longform-pipeline. One command takes a script, chunks it by sentence, renders it on the GPU, runs QA, masters the audio, and emits a tagged mp3. A couple of things I learned the hard way are baked into the house style now:
- Spell numbers out as words. “Eighty percent,” not “80%.” The model reads digits and symbols poorly, and worse, the QA step catches the mismatch and fails the render. Writing “twenty twenty-six” instead of “2026” is a small tax that pays for itself.
- QA is a gate, not a suggestion. Every render gets transcribed and checked two ways: does the transcript match the script, and is the voice consistent across the whole render (a similarity score that has to clear 0.93). If either fails, the render doesn’t ship. This is what keeps a forty-minute render from going out with a garbled paragraph in the middle that I’d only notice on playback.
- Match the loudness of your reference clip. The output comes out at roughly the level of the voice sample you cloned from. Record at a healthy level close to the mic, or every render lands quiet and needs a normalization pass afterward.
Why bother
The interesting problem was never “make speech.” It was: can I run a real voice-cloning model on my own hardware, fast enough to be a tool rather than a demo, on a GPU so new that half the software stack doesn’t admit it exists yet? The answer turned out to be yes, and the path there was a tour through exactly the kind of thing I like: a brand-new accelerator, a software ecosystem that hasn’t caught up to it, and a stack of small, specific incompatibilities that each have a real fix once you stop expecting the happy path to work.