Fixing speech-to-speech + Qwen3-TTS on DGX Spark
TL;DR: If you’re running huggingface/speech-to-speech with a Qwen3-TTS handler on a DGX Spark and it crashes right after model load with CUDA error: unspecified launch failure inside ggml_cuda_kernel_can_use_pdl, the fix is: drop the faster-qwen3-tts[ggml] extra and force the torch backend instead. No CUDA rebuild, no toolkit swap needed.
Setup
- Hardware: NVIDIA DGX Spark (Lenovo ThinkStation PGX), GB10, ARM64, 128GB unified memory, compute capability 12.1 (SM121)
- Pipeline:
huggingface/speech-to-speech, customQwen3TTSHandlerwired up on top offaster-qwen3-tts - STT: Parakeet TDT (nvidia/parakeet-tdt-0.6b-v3)
- LLM:
ggml-org/gemma-4-E4B-it-GGUFviallama.cpp:server-cuda,responses-apibackend - TTS: Qwen3-TTS, originally configured with
backend="ggml"(the default in the handler), pullingServeurperso/Qwen3-TTS-GGUFvia theqwentts.cppGGML port
The crash
Model loading looked completely healthy — talker, code predictor, quantizer, DAC all reported loaded successfully, [Pipeline] Ready printed — then it died on the very first inference:
[Pipeline] Ready: hop 1920 samples @ 24000 Hz mono, 16 codebooks @ 12.5 Hz
CUDA error: unspecified launch failure
current device: 0, in function ggml_cuda_kernel_can_use_pdl at .../ggml/src/ggml-cuda/common.cuh:1602
cudaFuncGetAttributes(&attr, kernel)
Container exited with code 133.
Root cause
ggml_cuda_kernel_can_use_pdl — a Programmatic Dependent Launch (PDL) capability check — is almost certainly not the actual failing kernel. CUDA errors are asynchronous and sticky: once something faults, the next CUDA API call surfaces the error, regardless of what that call actually does. In this case that next call happened to be an unrelated cudaFuncGetAttributes query, right as the Talker/CodePredictor ran its first real forward pass.
The likely underlying issue is a CUDA architecture mismatch specific to GB10:
- The container’s CUDA toolkit reports 12.8.1. Multiple other GB10/SM121 projects across the ecosystem (vLLM’s FP8 CUTLASS
sm120-vs-sm121trap bug, llama.cpp’s own GB10 container build notes) confirm GB10 wants CUDA 13.0 with explicitsm_121/sm_121atargeting — builds against 12.8, or targeting only the genericsm_120, can load/copy weights fine (memory ops) but produce invalid kernels the instant they’re actually dispatched. faster-qwen3-tts[ggml]installsqwentts-cpp-pythonfrom PyPI, and per that library’s own docs, the default wheel is built against CUDA 12.8 — not built with GB10’ssm_121aspecifically in mind.- Separately, NVIDIA’s own DGX Spark forum has an open thread noting PDL behaves oddly on GB10 specifically, so even a “correctly” targeted rebuild isn’t guaranteed to be trouble-free.
The fix
faster-qwen3-tts (and the Qwen3TTSHandler built on top of it) supports two backends: ggml (qwentts.cpp/GGUF) and torch (regular HF checkpoint + CUDA graphs, no Flash Attention). The torch backend is the same tech already proven working on GB10 elsewhere (e.g. martinb78/faster-qwen3-tts-dgx-spark) — CUDA-graph-accelerated PyTorch, no GGML involved at all.
1. In pyproject.toml, drop the [ggml] extra:
- "faster-qwen3-tts[ggml]>=0.3.2; platform_system != 'Darwin' and platform_system != 'Windows'",
+ "faster-qwen3-tts>=0.3.2; platform_system != 'Darwin' and platform_system != 'Windows'",
This alone stops pulling qwentts-cpp-python and its CUDA-12.8 build during uv sync/pip install — saves a several-GB download too.
2. In docker-compose.yml, force the backend explicitly (the handler’s argparse flag defaults to "ggml", so it must be set):
command:
- speech-to-speech
# ...your other flags...
- --qwen3_tts_backend
- torch
3. Rebuild and restart:
docker compose build pipeline
docker compose up -d
docker compose logs -f pipeline
Confirmed working log output after the fix:
Loading Qwen3-TTS model: Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice via faster-qwen3-tts (torch backend)
...
Building CUDA graphs...
Capturing CUDA graph for predictor...
CUDA graph captured!
Capturing CUDA graph for talker decode...
CUDA graphs captured and ready
Talker CUDA graph captured!
Qwen3-TTS TTFA: 2.65s (custom_voice)
Qwen3-TTS generated 1.88s audio in 3.47s (RTF: 0.54, custom_voice)
Qwen3TTSHandler warmed up
(That first RTF includes one-time graph capture — steady-state RTF on later requests came in around 1.7, i.e. faster than real-time.)
Bonus: voice cloning for non-English/Chinese languages
If you’re after a native-sounding voice in a language outside the model’s preset accents (e.g. German), two more things worth knowing:
-
The 9 CustomVoice presets are not multilingual-neutral — each has a fixed native accent (Vivian/Serena/Uncle_Fu/Dylan/Eric = Chinese, Ryan/Aiden = English, Ono_Anna = Japanese, Sohee = Korean). They can speak any of the model’s 10 supported languages, but always in their home accent — so German comes out as “German with an American/Chinese/etc. accent” depending on speaker chosen. There’s no native German or French preset in this set.
-
The CustomVoice checkpoint cannot voice-clone at all — attempting
ref_audio/ref_textagainst it fails at warmup with:model with tts_model_type: custom_voice does not support create_voice_clone_promptVoice cloning requires the Base checkpoint instead:
- --qwen3_tts_model_name - Qwen/Qwen3-TTS-12Hz-1.7B-Base - --qwen3_tts_ref_audio - /root/.cache/voices/your_ref_clip.wav - --qwen3_tts_ref_text - "exact transcript of that clip" - --qwen3_tts_language - germanA short (~10s), clean, single-speaker reference clip plus its exact transcript gets you a cloned voice with native pronunciation, sidestepping the accent problem entirely.
Known open items (not yet solved, flagging for others)
- No live multi-voice/multi-language switching. The pipeline loads one fixed model/backend/voice combo at container startup. There’s a partial hook (
_apply_session_voice_override) for swappingref_audioper-request, but it’s built for the OpenAI-Realtime API server surface, not the--mode socket+listen_and_play.pypath, and even there it doesn’t swapref_text/languagealongside the audio — so it wouldn’t safely support switching cloned voices across languages as-is. STT already computes a per-turnlanguage_code, but the TTS handler currently discards it rather than acting on it.
Hope this saves someone else the loop of chasing a ggml/PDL red herring — happy to answer questions if you hit variations of this.