Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
401 changes: 401 additions & 0 deletions .agents/specs/attn-capability-unit.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions .agents/specs/attn-validate-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,13 @@ platform that cannot answer, and Metal answers in a different unit;
fixed in this flow because every candidate repair changes what backend selection
means for kCPU/kMETAL/kVULKAN/kTENSTORRENT, which `AGENTS.md` routes to the
normal row, spec and fresh-review path. Owner: `BACKEND-ATTN-REGISTRY`.
**REPAIRED** by [attn-capability-unit.md](attn-capability-unit.md), which took
that path. Measured, it moved two device types and not four: kCPU and
kTENSTORRENT already report an absent capability and did not move. Vulkan
carried the same defect and was worse — the Vulkan API version has `major == 1`
on every device that will ever exist, so FLASH_ATTN was refused there
unconditionally, and the lane's test asserted the priority list rather than the
selector, so nothing saw it.

## Stop conditions

Expand Down
74 changes: 64 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1071,11 +1071,43 @@ jobs:
- name: Build
# Bounded parallelism for the same reason as build-test-cpu: an unbounded
# parallel link OOM-kills the runner.
run: cmake --build build-vulkan -j 2 --target test_vulkan_backend test_backend_cross_device
run: cmake --build build-vulkan -j 2 --target test_vulkan_backend test_backend_cross_device test_platform
- name: Vulkan backend gate
run: ./build-vulkan/tests/test_vulkan_backend
- name: Cross-device numerics vs the CPU oracle
run: ./build-vulkan/tests/test_backend_cross_device
- name: Platform seam gate, with a non-CUDA accelerator REGISTERED
# #1823. `test_platform` also runs on build-test-cpu, but that tier has
# only kCPU registered, so the capability-UNIT contract it asserts
# (`get_device_capability()` is an NVIDIA SM value or it is absent) walks
# exactly one platform there and cannot catch the defect it exists for.
# This lane registers kVULKAN, which is where that walk has teeth — and
# kVULKAN is one of the two platforms that carried the defect.
#
# THE GREP IS THE POINT, not the exit status. `VulkanPresent()` is a
# silent early return in the suite, so on a runner where the ICD failed
# to install BOTH executables report `Status: SUCCESS!` with rc 0 while
# measuring nothing about Vulkan: test_vulkan_backend drops from 2109
# assertions to 344, and test_platform walks kCPU alone. A lane whose
# only claim is "kVULKAN is registered here" must prove that, or the
# claim is the thing being tested and nothing else is.
run: |
set -euo pipefail
./build-vulkan/tests/test_platform | tee platform-gate.log
grep -q 'platform vulkan get_device_capability()' platform-gate.log
grep -q 'Status: SUCCESS!' platform-gate.log
- name: The Vulkan suite MEASURED a device rather than skipping past one
# The same positive control for the suite itself: assert the platform
# case ran its #1823 assertions, which it does not do when
# `VulkanPresent()` is false.
run: |
set -euo pipefail
./build-vulkan/tests/test_vulkan_backend \
-tc="Vulkan platform is registered and reports unified/no-pool residency" -s \
| tee vulkan-platform-case.log
grep -q 'kVULKAN Platform::get_device_capability() present=false' vulkan-platform-case.log
grep -q 'SelectAttentionBackendName(p) == "FLASH_ATTN" ) is correct' vulkan-platform-case.log
grep -qE 'test cases: *1 \| *1 passed' vulkan-platform-case.log
device-leakage:
# The DSR RATCHET (work row `S1` of .agents/specs/accelerator-seam-audit.md).
# Counts device-specific references in `src/vllm/` + `include/vllm/` — the
Expand Down Expand Up @@ -1566,17 +1598,21 @@ jobs:
# What that run could NOT cover is the #1584 repair, which landed in
# 944d7d947, after it.
#
# THIS JOB IS RED ON `main` RIGHT NOW, on #1823, and that is what a lane
# which had never executed anything is for. Its first run reported
# `test_metal_backend.cpp:170` THREW `"No valid attention backend for device
# type 2 from {FLASH_ATTN: [compute capability not supported]}"`, because
# THIS JOB'S FIRST RUN WAS RED, ON #1823, and that is what a lane which had
# never executed anything is for. It reported `test_metal_backend.cpp:170`
# THREW `"No valid attention backend for device type 2 from {FLASH_ATTN:
# [compute capability not supported]}"`, because
# `FlashAttentionBackend::supports_compute_capability` is upstream's NVIDIA
# `>= (8,0)` while `MetalPlatform::get_device_capability` answers with the
# `>= (8,0)` while `MetalPlatform::get_device_capability` answered with the
# Apple GPU FAMILY. Red since 369ea7fd4 (2026-08-19), which is not an
# ancestor of 7020de936, so nothing could see it for four days. It is
# ancestor of 7020de936, so nothing could see it for four days. It was
# deliberately NOT hidden behind `continue-on-error`, a skip or a tuned
# floor, and the exactness step runs FIRST so this row's own gate is not
# hostage to it.
# floor, and the exactness step runs FIRST so that row's own gate was not
# hostage to it. FIXED by `.agents/specs/attn-capability-unit.md`: the Metal
# platform now reports an ABSENT capability, mirroring upstream's own answer
# for a foreign capability format (xpu.py:228-236), so the SM predicate is
# skipped rather than misapplied. The four days this went unseen are the
# exposure window this job exists to close, measured.
concurrency:
group: ci-macos-metal-mlx-${{ github.event_name }}-${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && github.run_id || github.ref }}-${{ github.repository }}
cancel-in-progress: ${{ github.event_name != 'schedule' && github.event_name != 'workflow_dispatch' }}
Expand Down Expand Up @@ -1617,7 +1653,7 @@ jobs:
-DVLLM_CPP_HIP=OFF \
-DVLLM_CPP_TRITON=OFF \
-DVLLM_CPP_VULKAN=OFF
cmake --build build-metal --target vllm test_metal_backend -j 3
cmake --build build-metal --target vllm test_metal_backend test_platform -j 3
- name: Every Metal translation unit produced an object
# The postcondition, not the exit status. A green build proves nothing
# if VLLM_CPP_METAL resolved OFF, if a TU left `target_sources`, or if
Expand Down Expand Up @@ -1763,6 +1799,24 @@ jobs:
EOF
test "${rc}" -eq 0
grep -qE 'Status: SUCCESS!' metal-suite.log
- name: Platform seam gate, with kMETAL REGISTERED
# #1823. `test_platform` asserts the capability-UNIT contract
# (`Platform::get_device_capability()` is an NVIDIA SM value, or it is
# ABSENT) for every platform REGISTERED in the build it runs in. On
# build-test-cpu that is kCPU alone, and on build-test-vulkan it is kCPU
# plus kVULKAN. Without this step the contract had NO gate on the Metal
# tier at all, and the case's own comment claimed otherwise -- the
# forward-protection claim ("a platform added later is covered with no
# edit") was false for the one accelerator tier that is not Vulkan.
#
# The grep is the point, for the same reason as everywhere else in this
# job: a device-less runner registers no Metal platform, the walk covers
# kCPU only, and the executable still prints `Status: SUCCESS!` with rc 0.
run: |
set -euo pipefail
./build-metal/tests/test_platform --no-colors=1 2>&1 | tee platform-gate.log
grep -q 'platform metal get_device_capability()' platform-gate.log
grep -q 'Status: SUCCESS!' platform-gate.log
baseline-summary:
# THE PUBLISHED VERDICT (issue #274, spec .agents/specs/main-verifiability.md).
#
Expand Down
32 changes: 29 additions & 3 deletions include/vllm/platforms/interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,40 @@ class Platform {
// Graph/command capture capability (backend.h:80 SupportsGraphCapture).
bool supports_graph_capture() const { return backend().SupportsGraphCapture(); }

// interface.py:409-415 get_device_capability. `present() == false` on CPU.
// interface.py:420-431 get_device_capability. **THE UNIT IS AN NVIDIA SM
// VERSION**, and upstream's docstring says so outright: "Stateless version of
// torch.cuda.get_device_capability". Every predicate written against this value
// — FlashAttentionBackend::supports_compute_capability (flash_attn.py:200-202,
// `>= (8, 0)`), CudaPlatform::supports_fp8 (`has_device_capability(8, 9)`) — is
// a statement about SM versions and about nothing else.
//
// A PLATFORM WITH NO SM VERSION REPORTS ABSENT (`present() == false`). It must
// never answer in a unit of its own, however natural that unit is on the
// device. That is upstream's rule, applied by upstream to itself in
// xpu.py:228-234: "capacity format differs from cuda's and will cause
// unexpected failure, so use None directly".
//
// Absent is also what makes ONE shared selector correct. Upstream reaches
// `validate_configuration` only from CudaPlatform (cuda.py:381,410, guarded by
// `assert device_capability is not None` at :404) and RocmPlatform
// (rocm.py:531,558); every other platform has its own get_attn_backend_cls and
// never evaluates the SM predicate (cpu.py:75-87). Our
// SelectAttentionBackendName is shared across every DeviceType, so what
// upstream gets from its callers we get from this contract: absent skips the
// predicate exactly as upstream's CPU/XPU platforms skip it, present feeds it
// an SM value exactly as upstream's CUDA/ROCm platforms do.
//
// Answering in a foreign unit was #1823: Metal reported the Apple GPU family
// and Vulkan the Vulkan API version, and an SM-8.0 bar was applied to both.
// tests/vllm/platforms/test_platform.cpp gates the contract for every
// registered platform.
virtual DeviceCapability get_device_capability() const = 0;

// interface.py:417-439 has_device_capability — is this platform >= a required
// interface.py:433-454 has_device_capability — is this platform >= a required
// (major, minor)? False when the platform has no queryable capability (CPU).
bool has_device_capability(int major, int minor) const;

// interface.py:441-476 is_device_capability_family — is the device capability
// interface.py:481-493 is_device_capability_family — is the device capability
// any <major>.x (CUDA-13 "family" architecture semantics, e.g. 10.x, 11.x,
// 12.x)? Argument is a full capability int (e.g. 120), mirroring upstream's
// `(current_capability.to_int() // 10) == (capability // 10)`. False when the
Expand Down
3 changes: 2 additions & 1 deletion src/vllm/platforms/cpu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class CpuPlatform final : public Platform {
DeviceType device_type() const override { return DeviceType::kCPU; }
Backend& backend() const override { return vt::GetBackend(DeviceType::kCPU); }

// cpu.py get_device_capability -> None: a CPU has no queryable compute
// cpu.py inherits interface.py:420-431 get_device_capability -> None: a CPU
// has no queryable compute
// capability, so has_device_capability(...) is always false.
DeviceCapability get_device_capability() const override { return {}; }

Expand Down
31 changes: 22 additions & 9 deletions src/vllm/platforms/metal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,28 @@ class MetalPlatform final : public Platform {
DeviceType device_type() const override { return DeviceType::kMETAL; }
Backend& backend() const override { return vt::GetBackend(DeviceType::kMETAL); }

// interface.py:409-415 get_device_capability. CUDA answers with (sm_major,
// sm_minor); the Apple-silicon analogue is the MTLGPUFamilyApple GENERATION,
// which is what src/vt/metal/metal_context.mm probes and the backend exposes.
// {9, 0} on the M4 gate box. This makes has_device_capability(N, 0) mean
// "Apple family >= N", the same shape of question CUDA code already asks.
DeviceCapability get_device_capability() const override {
Backend& b = backend();
return DeviceCapability{b.DeviceCapabilityMajor(), b.DeviceCapabilityMinor()};
}
// interface.py:420-431 get_device_capability, whose docstring defines the UNIT:
// "Stateless version of torch.cuda.get_device_capability". It is an NVIDIA SM
// version, and every predicate written against it — most of all
// FlashAttentionBackend::supports_compute_capability (flash_attn.py:200-202,
// `capability >= (8, 0)`) — is a statement about SM versions.
//
// Apple silicon has no SM version, so the honest answer is ABSENT. This mirrors
// upstream's own answer for the same situation, xpu.py:228-234: "capacity
// format differs from cuda's and will cause unexpected failure, so use None
// directly".
//
// #1823 is what this used to be: it reported the MTLGPUFamilyApple GENERATION
// here, so an SM-8.0 bar was compared against an Apple family number. Family 9
// on the M4 gate box cleared it by COINCIDENCE, a GitHub macos-15 runner
// reported lower, and FLASH_ATTN — the only entry in
// get_attn_backend_priority() — was refused on Metal's only attention path.
//
// The family number is not lost and was never this seam's to report: it stays
// on vt::Backend::DeviceCapabilityMajor/Minor
// (src/vt/metal/metal_backend.mm:127-130), which is where a Metal-unit question
// belongs and where tests/vt/test_metal_backend.cpp asks it.
DeviceCapability get_device_capability() const override { return DeviceCapability{}; }

// interface.py:181-187 supported_dtypes order (bf16 default fallback). Metal 3
// on Apple family 9 handles all three; the kernels in src/vt/metal/metal_msl.h
Expand Down
4 changes: 2 additions & 2 deletions src/vllm/platforms/platform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace vllm::platforms {

// interface.py:417-439 has_device_capability — is this platform's capability >=
// interface.py:433-454 has_device_capability — is this platform's capability >=
// the required (major, minor)? Lexicographic on (major, minor), mirroring the
// DeviceCapability tuple comparison; false when there is no queryable
// capability (get_device_capability() -> None).
Expand All @@ -21,7 +21,7 @@ bool Platform::has_device_capability(int major, int minor) const {
return cap.minor >= minor;
}

// interface.py:441-476 is_device_capability_family — is the device capability any
// interface.py:481-493 is_device_capability_family — is the device capability any
// <major>.x? Mirrors upstream exactly: `(to_int() // 10) == (capability // 10)`,
// so sm_120 and sm_121 both map to the 12.x family. False when there is no
// queryable capability (CPU / get_device_capability() -> None).
Expand Down
8 changes: 4 additions & 4 deletions src/vllm/platforms/tenstorrent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ class TenstorrentPlatform final : public Platform {
DeviceType device_type() const override { return DeviceType::kTENSTORRENT; }
Backend& backend() const override { return vt::GetBackend(DeviceType::kTENSTORRENT); }

// interface.py:409-415 get_device_capability. Tenstorrent's Tensix cores
// have no CUDA-SM-shaped "compute capability" to report; the base {0, 0}
// ("no meaningful compute capability", backend.h) is the honest answer,
// same as CPU.
// interface.py:420-431 get_device_capability, whose unit is an NVIDIA SM
// version. Tenstorrent's Tensix cores have no SM version to report, so ABSENT
// is the honest answer — same as CPU, and the same answer Metal and Vulkan
// give since #1823.
DeviceCapability get_device_capability() const override { return DeviceCapability{}; }

// OPT-125m runs BF16 weights/activations with F32 logits. The adapter
Expand Down
33 changes: 22 additions & 11 deletions src/vllm/platforms/vulkan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,28 @@ class VulkanPlatform final : public Platform {
DeviceType device_type() const override { return DeviceType::kVULKAN; }
Backend& backend() const override { return vt::GetBackend(DeviceType::kVULKAN); }

// interface.py:409-415 get_device_capability. CUDA answers with (sm_major,
// sm_minor) and the Metal skeleton with the Apple GPU family; the Vulkan
// analogue is the API VERSION the physical device reports — {1, 4} on GB10
// (Vulkan 1.4.312). That makes has_device_capability(1, 1) mean "Vulkan >= 1.1",
// the same shape of question the CUDA code already asks, and it is the version
// the feature gates that matter here (16-bit storage, cooperative matrix,
// subgroup ops) are actually keyed to.
DeviceCapability get_device_capability() const override {
Backend& b = backend();
return DeviceCapability{b.DeviceCapabilityMajor(), b.DeviceCapabilityMinor()};
}
// interface.py:420-431 get_device_capability, whose docstring defines the UNIT:
// "Stateless version of torch.cuda.get_device_capability". It is an NVIDIA SM
// version. A Vulkan device has no SM version, so the honest answer is ABSENT —
// upstream's own answer for the same situation, xpu.py:228-234: "capacity
// format differs from cuda's and will cause unexpected failure, so use None
// directly".
//
// #1823, the Vulkan half. This used to report the Vulkan API VERSION ({1, 4} on
// GB10, and 1.x on every Vulkan device that will ever exist), which
// FlashAttentionBackend::supports_compute_capability (flash_attn.py:200-202)
// then compared against `>= (8, 0)`. FLASH_ATTN is the ONLY entry in
// get_attn_backend_priority(), so SelectAttentionBackendName threw on kVULKAN
// unconditionally. Metal at least had a coincidence; this had none, and it was
// invisible because the lane's test asserted that FLASH_ATTN is NAMED in the
// priority list rather than that the selector REACHES it.
//
// The API version is not lost and was never this seam's to report: it stays on
// vt::Backend::DeviceCapabilityMajor/Minor
// (src/vt/vulkan/vulkan_backend.cpp:144-145) and VulkanContext::api_major/minor,
// which is where the 16-bit-storage / cooperative-matrix / subgroup feature
// gates actually read it.
DeviceCapability get_device_capability() const override { return DeviceCapability{}; }

// interface.py:181-187 supported_dtypes order (bf16 default fallback). All
// three are implemented as STORAGE dtypes by the shaders in
Expand Down
21 changes: 16 additions & 5 deletions src/vllm/v1/attention/backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,22 @@ std::vector<std::string> AttentionBackend::validate_configuration(
// CudaPlatform.get_attn_backend_cls asserts `device_capability is not None`
// before it calls this (cuda.py:403-404), and CpuPlatform has a separate
// selector that never reaches it (cpu.py:75-87). Our selector is shared across
// every DeviceType, and DeviceCapability::present() is already false for every
// platform that cannot answer the question, so the predicate applies exactly
// where upstream applies it. Without this, FLASH_ATTN — which this tree also
// registers for kCPU/kMETAL/kVULKAN/kTENSTORRENT — would be refused on every
// one of them by a rule about NVIDIA compute capability.
// every DeviceType, so the guard is where upstream's caller-side assert lands.
// Without it, FLASH_ATTN — which this tree also registers for
// kCPU/kMETAL/kVULKAN/kTENSTORRENT — would be refused on every one of them by
// a rule about NVIDIA compute capability.
//
// #1823: this comment used to argue that `present()` is "already false for
// every platform that cannot answer the question". THAT WAS NOT TRUE, and it
// was not true of two platforms at once — Metal answered with the Apple GPU
// family and Vulkan with the Vulkan API version, so an SM-8.0 bar was compared
// against numbers that have nothing to do with SM versions. `present()` is a
// guard on WHETHER a platform answers, and it can say nothing about the UNIT.
// The unit is a contract on the value, stated on
// Platform::get_device_capability (include/vllm/platforms/interface.h) and
// gated for every registered platform by
// tests/vllm/platforms/test_platform.cpp. This line is correct only because
// that contract holds.
if (capability.present() && !supports_compute_capability(capability)) {
invalid_reasons.emplace_back("compute capability not supported");
}
Expand Down
Loading
Loading