[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 "