← Whispering

fix: allow Qwen3-TTS to work without flash-attn (eager attention fallback)

f8d7fbe8f7895414e866934844481fe89892d641 · Claude · Feb 10, 2026 18:15

flash-attn fails to build on some systems but Qwen3-TTS works fine
without it using eager attention (slower but functional). Changed
flash-attn from required to recommended: backend detection no longer
requires it, provider falls back to eager attention, and install script
messaging reflects the non-critical nature of the failure.

https://claude.ai/code/session_01965m1v3f1cK6gDV1JHioA8

+51 −25 · 4 files changed

  • INSTALL_TTS.md +12 −2
  • scripts/install.sh +7 −6
  • src/tts_provider.py +28 −16
  • src/whispering_ui/main.py +4 −1
diff --git a/INSTALL_TTS.md b/INSTALL_TTS.md
index 1d15697..0ce2a50 100644
--- a/INSTALL_TTS.md
+++ b/INSTALL_TTS.md
@@ -29,15 +29,17 @@ source .venv/bin/activate
 # Step 1: Install Qwen3-TTS
 pip install qwen-tts
 
-# Step 2: Install flash-attn (REQUIRED - must install AFTER torch)
+# Step 2 (optional but recommended): Install flash-attn for faster inference
 # --no-build-isolation is needed because flash-attn needs torch at build time
 MAX_JOBS=4 pip install flash-attn --no-build-isolation
 
 # NOTE: Do NOT use "uv pip install flash-attn" - it will fail.
 # Use regular pip with --no-build-isolation instead.
+# If flash-attn fails to build, Qwen3-TTS will still work using
+# eager attention (slower but fully functional).
 
 # Verify
-python -c "from qwen_tts import Qwen3TTSModel; import flash_attn; print('Qwen3-TTS OK')"
+python -c "from qwen_tts import Qwen3TTSModel; print('Qwen3-TTS OK')"
 ```
 
 ### Option B: Chatterbox TTS (ResembleAI)
@@ -90,6 +92,14 @@ If you see `ModuleNotFoundError: No module named 'distutils'`:
 - Use `pip install chatterbox-tts --no-deps` (already handled by install.sh)
 - Or switch to Qwen3-TTS which doesn't have this issue
 
+### flash-attn build failure (Qwen3-TTS)
+
+If `pip install flash-attn --no-build-isolation` fails to compile:
+- **This is not critical** - Qwen3-TTS will fall back to eager attention (slower but works)
+- flash-attn requires: CUDA toolkit headers, a compatible GPU, and torch already installed
+- The app will print a note at startup if flash-attn is missing
+- To retry later: `MAX_JOBS=4 pip install flash-attn --no-build-isolation`
+
 ### No audio output
 
 - Check that `sounddevice` is installed: `pip install sounddevice`
diff --git a/scripts/install.sh b/scripts/install.sh
index 7dfbe77..10c40ce 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -246,17 +246,18 @@ install_qwen3() {
         rm -rf "$TEMP_DIR"
     }
 
-    # Install flash-attn (required for Qwen3-TTS performance)
+    # Install flash-attn (recommended for Qwen3-TTS performance, not required)
     # Must use --no-build-isolation because flash-attn needs torch at build time
     # but doesn't declare it as a build dependency
-    echo -e "${YELLOW}Installing flash-attn (required for Qwen3-TTS)...${NC}"
+    echo -e "${YELLOW}Installing flash-attn (recommended for Qwen3-TTS performance)...${NC}"
     echo -e "${BLUE}This may take a few minutes to compile...${NC}"
     MAX_JOBS=4 $PYTHON_CMD -m pip install -U flash-attn --no-build-isolation 2>/dev/null && {
         echo -e "${GREEN}✓ flash-attn installed${NC}"
     } || {
-        echo -e "${RED}✗ flash-attn installation failed${NC}"
-        echo -e "${BLUE}  Try manually: pip install flash-attn --no-build-isolation${NC}"
-        echo -e "${BLUE}  Make sure torch is installed first and you have a compatible GPU + CUDA toolkit.${NC}"
+        echo -e "${YELLOW}⚠ flash-attn installation failed (not critical)${NC}"
+        echo -e "${BLUE}  Qwen3-TTS will still work using eager attention (slower).${NC}"
+        echo -e "${BLUE}  To try again later: pip install flash-attn --no-build-isolation${NC}"
+        echo -e "${BLUE}  Requires: compatible GPU + CUDA toolkit + torch already installed.${NC}"
     }
 
     # Verify installation
@@ -264,7 +265,7 @@ install_qwen3() {
         echo -e "${YELLOW}  Note: Qwen3-TTS import test failed. It may still work at runtime.${NC}"
     }
     $PYTHON_CMD -c "import flash_attn; print('  ✓ flash-attn import successful')" 2>/dev/null || {
-        echo -e "${YELLOW}  Warning: flash-attn not available. Qwen3-TTS will be slower.${NC}"
+        echo -e "${YELLOW}  Note: flash-attn not available. Qwen3-TTS will use eager attention (slower but functional).${NC}"
     }
     echo
 }
diff --git a/src/tts_provider.py b/src/tts_provider.py
index 4564ba8..f385832 100644
--- a/src/tts_provider.py
+++ b/src/tts_provider.py
@@ -36,10 +36,11 @@ def get_available_backends() -> dict[str, bool]:
     except (ImportError, ModuleNotFoundError, Exception):
         backends["chatterbox"] = False
 
-    # Check Qwen3-TTS - verify both the model class and flash-attn are importable
+    # Check Qwen3-TTS - only require qwen_tts itself.
+    # flash-attn is recommended for performance but NOT required;
+    # Qwen3-TTS falls back to manual PyTorch attention without it.
     try:
         from qwen_tts import Qwen3TTSModel  # noqa: F401
-        import flash_attn  # noqa: F401
         backends["qwen3"] = True
     except (ImportError, ModuleNotFoundError, Exception):
         backends["qwen3"] = False
@@ -47,6 +48,15 @@ def get_available_backends() -> dict[str, bool]:
     return backends
 
 
+def is_flash_attn_available() -> bool:
+    """Check if flash-attn is installed (recommended for Qwen3-TTS performance)."""
+    try:
+        import flash_attn  # noqa: F401
+        return True
+    except (ImportError, ModuleNotFoundError, Exception):
+        return False
+
+
 # ---------------------------------------------------------------------------
 # Base provider
 # ---------------------------------------------------------------------------
@@ -240,17 +250,13 @@ class Qwen3TTSProvider(BaseTTSProvider):
             kwargs = {"device_map": f"{self.device}:0" if self.device == "cuda" else self.device}
             kwargs["dtype"] = dtype
 
-            # flash-attn is required for Qwen3-TTS
-            try:
-                import flash_attn  # noqa: F401
+            # Use flash-attn if available (faster), otherwise fall back to eager
+            if is_flash_attn_available():
                 kwargs["attn_implementation"] = "flash_attention_2"
-            except ImportError:
-                raise ImportError(
-                    "flash-attn is required for Qwen3-TTS but is not installed.\n"
-                    "Install with: pip install flash-attn --no-build-isolation\n"
-                    "(torch must be installed first)\n"
-                    "Or run: ./scripts/install.sh --tts=qwen3"
-                )
+            else:
+                print("Note: flash-attn not installed. Using eager attention (slower).")
+                print("  Install for better performance: pip install flash-attn --no-build-isolation")
+                kwargs["attn_implementation"] = "eager"
 
             self.model = Qwen3TTSModel.from_pretrained(model_name, **kwargs)
             self._initialized = True
@@ -268,7 +274,6 @@ class Qwen3TTSProvider(BaseTTSProvider):
 
         try:
             from qwen_tts import Qwen3TTSModel
-            import flash_attn  # noqa: F401
 
             model_name = self._get_model_name(clone=True)
             print(f"Loading Qwen3-TTS clone model ({model_name})...")
@@ -276,15 +281,20 @@ class Qwen3TTSProvider(BaseTTSProvider):
             dtype = torch.bfloat16 if self.device == "cuda" else torch.float32
             kwargs = {"device_map": f"{self.device}:0" if self.device == "cuda" else self.device}
             kwargs["dtype"] = dtype
-            kwargs["attn_implementation"] = "flash_attention_2"
+
+            if is_flash_attn_available():
+                kwargs["attn_implementation"] = "flash_attention_2"
+            else:
+                kwargs["attn_implementation"] = "eager"
 
             self._clone_model = Qwen3TTSModel.from_pretrained(model_name, **kwargs)
             print("Qwen3-TTS clone model loaded")
 
         except ImportError as e:
             raise ImportError(
-                "flash-attn is required for Qwen3-TTS.\n"
-                "Install with: pip install flash-attn --no-build-isolation"
+                "Qwen3-TTS is not installed.\n"
+                "Install with: pip install qwen-tts\n"
+                "Or run: ./scripts/install.sh --tts=qwen3"
             ) from e
         except Exception as e:
             raise RuntimeError(f"Failed to load Qwen3-TTS clone model: {e}") from e
@@ -420,6 +430,8 @@ if __name__ == "__main__":
     for name, available in backends.items():
         status = "INSTALLED" if available else "not installed"
         print(f"  {name}: {status}")
+    flash_ok = is_flash_attn_available()
+    print(f"  flash-attn: {'INSTALLED' if flash_ok else 'not installed (Qwen3-TTS will use eager attention)'}")
 
     # Test with first available backend
     for name, available in backends.items():
diff --git a/src/whispering_ui/main.py b/src/whispering_ui/main.py
index e157fbf..7e76c8c 100644
--- a/src/whispering_ui/main.py
+++ b/src/whispering_ui/main.py
@@ -90,7 +90,7 @@ def main():
     # tts_available=True means the TTS UI section is visible.
     # tts_backends_available tracks which engines are actually installed.
     try:
-        from tts_provider import get_available_backends
+        from tts_provider import get_available_backends, is_flash_attn_available
         from tts_controller import TTSController
 
         backends = get_available_backends()
@@ -122,6 +122,9 @@ def main():
             print(f"  TTS backend {name}: {status}")
             if avail:
                 has_any = True
+        if backends.get("qwen3"):
+            flash_ok = is_flash_attn_available()
+            print(f"  flash-attn: {'available' if flash_ok else 'not installed (Qwen3 will use eager attention)'}")
         if not has_any:
             state.tts_enabled = False  # Don't auto-enable if nothing is installed
             print("  No TTS backends installed.")