[Bugfix] Record non-ImportError attention backend probe failures instead of crashing engine init (#51703)
Signed-off-by: Eoin <eoin@turintech.ai> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Eoin-Houstoun committed
Aug 20, 2026 at 10:36 UTC
30e2394c83afe066e91fcc709ac47935a57bbe3c
2 files changed
+114
-4
tests/v1/attention/test_cuda_backend_probe_errors.py
new
+104
@@ -0,0 +1,104 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Tests for error handling in the CUDA attention backend probe.
4
+
5
+Environment-shaped probe failures (missing packages, unreadable caches,
6
+broken driver installs) must mark the backend unavailable rather than
7
+terminating engine init; programming errors must still propagate.
8
+See https://github.com/vllm-project/vllm/issues/51658.
9
+"""
10
+
11
+from unittest.mock import MagicMock, patch
12
+
13
+import pytest
14
+import torch
15
+
16
+from vllm.platforms import current_platform
17
+from vllm.platforms.cuda import CudaPlatform
18
+from vllm.platforms.interface import DeviceCapability
19
+from vllm.v1.attention.backends.registry import AttentionBackendEnum
20
+from vllm.v1.attention.selector import AttentionSelectorConfig
21
+
22
+pytestmark = pytest.mark.skipif(
23
+ not current_platform.is_cuda(), reason="CUDA-specific tests"
24
+)
25
+
26
+SELECTOR_CONFIG = AttentionSelectorConfig(
27
+ head_size=64,
28
+ dtype=torch.float16,
29
+ kv_cache_dtype=None,
30
+ block_size=16,
31
+)
32
+
33
+SM90 = DeviceCapability(major=9, minor=0)
34
+
35
+
36
+@pytest.mark.parametrize(
37
+ "exc",
38
+ [
39
+ ImportError("flashinfer is not installed"),
40
+ PermissionError(13, "Permission denied", "/root/.cache/flashinfer"),
41
+ OSError("libcuda.so.1: cannot open shared object file"),
42
+ ],
43
+)
44
+def test_get_valid_backends_records_environment_failure(exc):
45
+ with patch("vllm.platforms.cuda._get_attn_backend_class", side_effect=exc):
46
+ valid, invalid_reasons = CudaPlatform.get_valid_backends(
47
+ device_capability=SM90,
48
+ attn_selector_config=SELECTOR_CONFIG,
49
+ num_heads=32,
50
+ )
51
+ assert valid == []
52
+ assert invalid_reasons
53
+ for _priority, reasons in invalid_reasons.values():
54
+ assert reasons == [f"{type(exc).__name__}: {exc}"]
55
+
56
+
57
+@pytest.mark.parametrize(
58
+ "exc",
59
+ [
60
+ RuntimeError("CUDA error: no kernel image is available"),
61
+ AttributeError("module has no attribute 'get_builder_cls'"),
62
+ KeyboardInterrupt(),
63
+ ],
64
+)
65
+def test_get_valid_backends_propagates_unexpected_errors(exc):
66
+ with (
67
+ patch("vllm.platforms.cuda._get_attn_backend_class", side_effect=exc),
68
+ pytest.raises(type(exc)),
69
+ ):
70
+ CudaPlatform.get_valid_backends(
71
+ device_capability=SM90,
72
+ attn_selector_config=SELECTOR_CONFIG,
73
+ num_heads=32,
74
+ )
75
+
76
+
77
+def test_get_valid_backends_keeps_probing_after_failure():
78
+ healthy = MagicMock()
79
+ healthy.validate_configuration.return_value = []
80
+ side_effects = [OSError("probe failed")] + [healthy] * 32
81
+
82
+ with patch("vllm.platforms.cuda._get_attn_backend_class", side_effect=side_effects):
83
+ valid, invalid_reasons = CudaPlatform.get_valid_backends(
84
+ device_capability=SM90,
85
+ attn_selector_config=SELECTOR_CONFIG,
86
+ num_heads=32,
87
+ )
88
+ assert len(invalid_reasons) == 1
89
+ assert valid
90
+
91
+
92
+def test_selected_backend_probe_failure_raises_value_error_with_cause():
93
+ exc = OSError("libcuda.so.1: cannot open shared object file")
94
+ with (
95
+ patch("vllm.platforms.cuda._get_attn_backend_class", side_effect=exc),
96
+ patch.object(CudaPlatform, "get_device_capability", return_value=SM90),
97
+ pytest.raises(ValueError, match="OSError") as excinfo,
98
+ ):
99
+ CudaPlatform.get_attn_backend_cls(
100
+ selected_backend=AttentionBackendEnum.FLASH_ATTN,
101
+ attn_selector_config=SELECTOR_CONFIG,
102
+ num_heads=32,
103
+ )
104
+ assert excinfo.value.__cause__ is exc
vllm/platforms/cuda.py
+10
-4
@@ -386,8 +386,11 @@ class CudaPlatformBase(Platform):
386
device_capability=device_capability,
387
**attn_selector_config._asdict(),
388
)
389
- except ImportError:
390
- invalid_reasons_i = ["ImportError"]
389
+ except (ImportError, OSError) as e:
390
+ logger.debug(
391
+ "Attention backend %s is unavailable", backend.name, exc_info=True
392
+ )
393
+ invalid_reasons_i = [f"{type(e).__name__}: {e}"]
394
if invalid_reasons_i:
395
invalid_reasons[backend] = (priority, invalid_reasons_i)
396
else:
@@ -415,8 +418,11 @@ class CudaPlatformBase(Platform):
418
device_capability=device_capability,
419
**attn_selector_config._asdict(),
420
)
418
- except ImportError:
419
- invalid_reasons = ["ImportError"]
421
+ except (ImportError, OSError) as e:
422
+ raise ValueError(
423
+ f"Selected backend {selected_backend} is not valid for "
424
+ f"this configuration. Reason: [{type(e).__name__}: {e}]"
425
+ ) from e
426
if invalid_reasons:
427
raise ValueError(
428
f"Selected backend {selected_backend} is not valid for "