From 3f8de74fd2b14ae9bf981ad156a77d6520a8f438 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Fri, 7 Aug 2026 23:19:40 +0000 Subject: [PATCH] feat(abi): explicit device selection through the ONE surface - vllm_model_params.device, ABI v14, --device on server + cli ONE-SURFACE fold ROW 8 (row/DEVICE-KNOB, task #284), the smallest: an embedder can now ASK for a device instead of inheriting the accelerator-first probe. Mirrors vLLM's DeviceConfig.device names (vllm/config/device.py:13 @ 555967922; an explicit device is assigned VERBATIM and never substituted, device.py:61-66 - upstream's own --device flag was removed in v0.10, bc8a8ce5e, so the field mirrors the surviving config surface). include/vllm.h gains vllm_model_params.device (int32, 0=auto/1=cpu/ 2=cuda; 0 MUST be auto so zero-initialized structs keep the pre-v14 probe byte-identical; cpu-before-cuda follows the shipped v12 vllm_video_model_params.device precedent shifted by the auto slot). VLLM_ABI_VERSION 13 -> 14 on top of #122's vllm_complete_tokens v13 (rebased onto 9f772cad; the changelog now reads v10..v14 chronological - the v12-after-v11 move is the #123-review minor kept through the rebase). Floor pin advanced to >= 14 (the #121 == lesson). New vllm/config/device.h (+.cpp): vllm::Device, DeviceFromString, DeviceName. Plumb: EngineParams::device -> SelectQueue explicit arms via the pure LoadedEngine::ResolveExplicitDeviceType (explicit cpu NEVER probes; explicit cuda requires the registered kCUDA platform and fails LOUD otherwise; explicit-arm queue-creation failures propagate, unlike auto's byte-identical catch-and-fall-back). FromModelDir resolves an explicit device BEFORE any path/config I/O (DeviceConfig resolves at config time, arg_utils.py:1878), so a device error is never masked by a path error. capi validates 0/1/2 and refuses explicit cuda on the CPU-hosted transcription stack. Thin clients: --device auto|cpu|cuda on vllm-cli (ABI field only) and the server (DeviceFromString; --video-device untouched, separate engine). Also folds the #123 review's three MINOR findings (tests/docs only): c_header_compile.c actually references the v11+v12 surface + the v14 field (composed with #122's vllm_complete_tokens reference; its "every entry point" claim went stale at v10); the v12 changelog block moved to chronological position; the H3 fold fixture flag list gained --keep-quant. Gates: full CPU build -Werror clean; test_capi (4 new v14 cases + floor >= 14, #122's v13 cases green alongside), test_loaded_engine_dense (+3 device cases incl. the pure policy matrix that pins "explicit cpu beats a REGISTERED accelerator" CPU-side), test_dlopen, test_openai_api_server (+explicit-cpu serving smoke), test_minimax_h3_video_fold, test_model_loader_gguf, test_platform all green; surface-coverage green, allowlist ratchet UNTOUCHED at 8 (post #122); STATUS ratchet paid by removing a stale back-to-back merge duplicate (279200 -> 279150, measured 279111 post-rebase). Ten mutations killed and reverted (defaults, zero-map, range, both plumb sites, ctor arm, unconditional fallback, policy flip, parse flip, forgotten bump); kills recorded in .agents/state.md. Residual: CUDA-build A/B (no dgx in scope), named in the state entry. FOLLOWING_AGENTS_PROTOCOL Assisted-by: Claude Code:claude-fable-5 [ClaudeCode] --- .agents/NOW.md | 2 +- .agents/specs/one-surface-abi.md | 2 +- .agents/specs/surface-coverage-2026-08-07.md | 2 +- .agents/state.md | 81 ++++++++++ CMakeLists.txt | 1 + docs/BENCHMARKS.md | 2 +- docs/FEATURES.md | 1 + docs/STATUS.md | 7 +- examples/cli/main.cpp | 32 +++- examples/server/main.cpp | 21 +++ include/vllm.h | 49 ++++-- include/vllm/config/device.h | 47 ++++++ include/vllm/entrypoints/model_loader.h | 29 ++++ scripts/check-public-doc-tables.py | 9 +- src/capi/vllm_c.cpp | 32 ++++ src/vllm/config/device.cpp | 40 +++++ src/vllm/entrypoints/model_loader.cpp | 71 +++++++- tests/capi/c_header_compile.c | 46 +++++- tests/capi/test_capi.cpp | 153 +++++++++++++++++- .../entrypoints/openai/test_api_server.cpp | 44 +++++ .../entrypoints/test_loaded_engine_dense.cpp | 87 ++++++++++ .../models/minimax_h3_video_fold_fixture.h | 4 +- 22 files changed, 731 insertions(+), 31 deletions(-) create mode 100644 include/vllm/config/device.h create mode 100644 src/vllm/config/device.cpp diff --git a/.agents/NOW.md b/.agents/NOW.md index 2bce1e1e8..1e1d4a525 100644 --- a/.agents/NOW.md +++ b/.agents/NOW.md @@ -28,7 +28,7 @@ Working head: `row/backend-rocm-w0` (#41). Prior: benchmark checkpoint | Supported-models list | **LANDED**: FEATURES arch table CI-bound (33 archs) | — | | `/v1/videos` OpenAI shape | **MERGED** (#71): Sora `model`/`size`/`seconds` + `GET /{id}/content` | `row/SERVE-VIDEOS-REFS` PR open: reference conditioning | | `BACKEND-ROCM` W0 | Skeleton in; **HIP never compiled** (no AMD HW) | #41 contributors build it; a compile error IS the deliverable | -| Surface coverage (`ARCH-ONE-SURFACE`) | **ROW 2 LANDED (#123)**: H3 video on the surface (`vllm_video_*` v12, `/v1/videos` via the seam, both examples thin clients, ratchet 11→9); ROW 1 (#121) before it | GB10 re-verify residual; next fold row | +| Surface coverage (`ARCH-ONE-SURFACE`) | **ROW 8 IN REVIEW (`row/DEVICE-KNOB`)**: ABI v14 `device` knob (0=auto/1=cpu/2=cuda), absent-cuda fails loud, `--device` on server+cli, #123's 3 minors folded | Merge after review; rebased (#122); CUDA A/B residual | In-flight (default-OFF, not pushed): `laguna-fp4proj-prod`, laguna bf16/legacy/pipeline-gemv, `ds4-hc-expand-fuse`. diff --git a/.agents/specs/one-surface-abi.md b/.agents/specs/one-surface-abi.md index ad4a273b4..7eaa31d0c 100644 --- a/.agents/specs/one-surface-abi.md +++ b/.agents/specs/one-surface-abi.md @@ -1,6 +1,6 @@ # ONE SURFACE — every capability ships through the C ABI -Row: `ARCH-ONE-SURFACE`. Status: **AUDIT DONE; remediation IN PROGRESS — ROW 1 (Parakeet ASR / audio transcription) LANDED 2026-08-07: ABI v11 `vllm_transcribe`, live `/v1/audio/transcriptions`, registry refuse-by-task, example folded, ratchet 12 -> 11. ROW 2 (MiniMax-H3 video+audio generation) LANDED 2026-08-08 (`row/H3-VIDEO-ABI`, task #283): ABI v12 `vllm_video_engine_load`/`vllm_video_generate`/`vllm_video_result_free` + `vllm_video_mux_argv` over the `MiniMaxH3VideoEngine` library seam, `/v1/videos` routed through the SAME seam, both H3 examples rewritten as `vllm.h` clients byte-identical to the pre-fold binary, ratchet 11 -> 9. GB10 real-video re-verification is a NAMED RESIDUAL (box on the Kimi campaign).** +Row: `ARCH-ONE-SURFACE`. Status: **AUDIT DONE; remediation IN PROGRESS — ROW 1 (Parakeet ASR / audio transcription) LANDED 2026-08-07: ABI v11 `vllm_transcribe`, live `/v1/audio/transcriptions`, registry refuse-by-task, example folded, ratchet 12 -> 11. ROW 2 (MiniMax-H3 video+audio generation) LANDED 2026-08-08 (`row/H3-VIDEO-ABI`, task #283): ABI v12 `vllm_video_engine_load`/`vllm_video_generate`/`vllm_video_result_free` + `vllm_video_mux_argv` over the `MiniMaxH3VideoEngine` library seam, `/v1/videos` routed through the SAME seam, both H3 examples rewritten as `vllm.h` clients byte-identical to the pre-fold binary, ratchet 11 -> 9. GB10 real-video re-verification is a NAMED RESIDUAL (box on the Kimi campaign). ROW 8 (explicit device selection) LANDED 2026-08-08 (`row/DEVICE-KNOB`, task #284): ABI v14 `vllm_model_params.device` (0=auto/1=cpu/2=cuda, the vLLM `DeviceConfig.device` names) -> `EngineParams::device` -> `SelectQueue`; explicit cpu forces the CPU queue without probing, an explicitly named ABSENT device fails LOUD before any model I/O (the vllm/config/device.py:61-66 never-substitute mirror), `--device` on server + cli as pure field consumers; the #123 review's three MINOR findings closed in the same change (c_header_compile.c now references the v11+v12 surface + the v14 field; the v12 changelog block moved to chronological position; the fold fixture's flag list gained `--keep-quant`). CUDA-build A/B (auto->CUDA vs explicit-cpu->CPU on a GPU box) is a NAMED RESIDUAL — the CPU tier pins that half through the pure `ResolveExplicitDeviceType` matrix instead.** ## The defect diff --git a/.agents/specs/surface-coverage-2026-08-07.md b/.agents/specs/surface-coverage-2026-08-07.md index eccf47aee..5b0a37dbd 100644 --- a/.agents/specs/surface-coverage-2026-08-07.md +++ b/.agents/specs/surface-coverage-2026-08-07.md @@ -141,7 +141,7 @@ lanes are leaves of `ARCH-ONE-SURFACE` (do not open parallel rows). | 5 | Kimi-Linear incremental | expose the incremental decode path through the runner/engine (the recompute forward already routes) | rewrite `kimi_linear_gen` | S–M | `KimiDecodeCache` on the runner | | 6 | Embeddings/pooling | `vllm_embed`/pooling entry point + live `/v1/embeddings`; register a pooling arch (`is_pooling_model=true`); invoke `PoolingRunner` in the step | — | M | pooler live-wiring | | 7 | Multimodal input | multimodal-content entry point on `vllm_chat`; run the vision/audio tower in the engine step (`mm_features`→`ModelForwardInput.mm`) | wire `chat_mm` seam into the ABI | L | `MM-SERVE-E2E` engine mm-forward residual | -| 8 | Device-selection knob | `device` field on `vllm_model_params` (absent today) → `EngineParams` → `SelectQueue`; `--device` on server + cli | additive | S | mirror vLLM `--device`/`DeviceConfig` | +| 8 | Device-selection knob | **DONE (ROW 8, 2026-08-08, `row/DEVICE-KNOB`)**: `vllm_model_params.device` (ABI v14: 0=auto/1=cpu/2=cuda, the vLLM `DeviceConfig.device` names, device.py:13) → `EngineParams::device` → `SelectQueue` explicit arms via `LoadedEngine::ResolveExplicitDeviceType`; explicit cpu never probes, explicit ABSENT cuda fails LOUD before any model I/O (device.py:61-66 never-substitute mirror); `--device` on server + cli | **DONE**: both thin clients consume the field; zero value byte-identical (auto probe) | S | mirror vLLM `--device`/`DeviceConfig` | | 9 | Voxtral + audio chat seam | register `VoxtralForConditionalGeneration` + fold `VoxtralGenerateGreedy` into the registry forward; audio-capable chat fn + an engine consumer for `AudioKwargs` mm_features | rewrite tests→clients | M | mirror upstream `voxtral.py:309`, `SupportsTranscription` | | 10 | Gemma-4 audio e2e | bf16 device audio forward + audio→text merge (residual `gemma4_audio.h:41`); USM log-mel front end | fold into the registered mm forward | M | `MM-SERVE-E2E` | | 11 | Tokenizer/bench ABI + real-load gate | `vllm_tokenize`/`vllm_detokenize`; token-id/count fields on the stream callback for bench; **gate `vllm_engine_load` on a REAL tiny checkpoint at least once** (today bad-path only, `test_capi.cpp:474`) | rewrite `tokenize`/`bench` as clients | S-M | `/tokenize` route exists (`api_server.cpp:432`) | diff --git a/.agents/state.md b/.agents/state.md index 200b027af..02468dc19 100644 --- a/.agents/state.md +++ b/.agents/state.md @@ -42368,3 +42368,84 @@ The full digits and file:line map live in spec §21; the binding facts: capture pushed the unified pool below the floor at 00:25 — reproduces the §19 measured box-safety violation; NOT retried per the safety mandate). Denominator stays the #111 recorded ~21 floor. Box recovered clean; worker auto-restored (--restart=always). + +## 2026-08-08 — ARCH-ONE-SURFACE ROW 8: explicit device selection through the ONE surface (`row/DEVICE-KNOB`, task #284) + + +**What.** The smallest fold row: an embedder can now ASK for a device instead +of inheriting the accelerator-first probe. Mirrors vLLM's `DeviceConfig.device` +names (`${VLLM_SOURCE}/vllm/config/device.py:13` `Device = Literal["auto", +"cuda", "cpu", ...]`; explicit assignment is VERBATIM, never substituted, +device.py:61-66; upstream's `--device` CLI flag itself was removed in v0.10, +`bc8a8ce5e`, so the ABI field mirrors the surviving config surface). + +- W1 ABI: `vllm_model_params.device` appended (int32, 0=auto/1=cpu/2=cuda — + 0 MUST be auto for the zero-value contract; cpu-before-cuda follows the v12 + `vllm_video_model_params.device` precedent shifted by the auto slot). + `VLLM_ABI_VERSION` -> 14 per the operator's version-collision correction + (#122 Kimi `vllm_complete_tokens` reconciles to v13 at its landing; this + branch pre-rebase shows v12 -> v14 with the gap named in the changelog). + Floor pin advanced to `>= 14` (the #121 == lesson). New + `include/vllm/config/device.h` + `src/vllm/config/device.cpp` + (`vllm::Device`, `DeviceFromString`, `DeviceName`), ported from + vllm/config/device.py @ 555967922. +- W2 plumb: `EngineParams::device` -> `SelectQueue(architecture, device)`. + Explicit arms route through `LoadedEngine::ResolveExplicitDeviceType(device, + cuda_platform_registered)` — pure, so the CPU tier gates the full matrix + including "explicit cpu beats a REGISTERED accelerator". Explicit cpu never + consults the probe; explicit cuda requires the registered kCUDA platform + (cuda.cpp Registrar == usable GPU) and THROWS the pinned message otherwise; + queue-creation failure on the explicit arm PROPAGATES (no try/catch), unlike + the auto arm's byte-identical catch-and-fall-back. `FromModelDir` resolves + an explicit device BEFORE any path/config I/O (mirror of DeviceConfig + resolving at config time, arg_utils.py:1878), so the device error is never + masked by a path error — which is also what makes the capi plumb pinnable + with no loadable checkpoint. capi validates 0/1/2 (else + VLLM_ERR_INVALID_ARGUMENT) and refuses explicit cuda on the CPU-hosted + transcription stack instead of silently downgrading. +- W3 thin clients: `--device auto|cpu|cuda` on `vllm-cli` (ABI field only, + local name->int map) and `examples/server` (`vllm::DeviceFromString` -> + `EngineParams.device`; `--video-device` untouched — separate engine, + separate checkpoint, may legitimately differ). +- W5 (the #123 review's 3 MINORS, closed): c_header_compile.c now actually + references the v11 transcription + v12 video entry points + the v14 field + (its "references every ABI entry point" claim went stale at v10); the v12 + changelog block moved to chronological position (after v11); the H3 fold + fixture's flag list gained `--keep-quant` (matches test:85 + the capture). + +**Gates.** Full CPU build -Werror clean (fresh tree). test_capi 44 cases +(4 new v14 + floor >= 14) green; test_loaded_engine_dense +3 device cases +green; test_dlopen green; test_openai_api_server + the new explicit-cpu +serving smoke green; test_minimax_h3_video_fold green (vllm.h touched); +test_model_loader_gguf + test_platform green. check-surface-coverage green +(capability row added, ratchet UNTOUCHED — 8 after the #122 rebase below; +9 at first write). STATUS ratchet: paid for the +new line by removing a stale back-to-back merge DUPLICATE of the metrics +narrative (nothing lost), ratchet 279200 -> 279150 (measured 279108). + +**Mutation kills (each RED then reverted).** M1 default device=2 -> +zero-contract 3 fails; M2 capi 0->kCUDA -> zero-contract bogus-path arm RED +(the auto arm must report the PATH, not a device); M3 range validation +dropped -> range test 6 fails; M4 capi drops the device=2 mapping -> plumb +pin RED; M5 FromModelDir early resolve removed -> capi plumb pin + the +device-before-path test both RED; M6 ctor passes kAuto -> EngineParams-seam +ctor-throw arm RED; M7 SelectQueue explicit arm catch-and-fallback -> same +test RED; M8 policy hands explicit-cpu to a registered accelerator -> matrix +RED; M9 DeviceFromString "cuda"->kCPU -> parse test RED; M10 ABI macro left +at 12 -> floor pin RED. Not uniquely killable (disclosed): the api_server +smoke (same seams as M6-M8), c_header_compile (a compile-time reference), +the two doc-only minors. + +**Residuals (honest).** (1) CUDA-build A/B — auto->CUDA vs explicit-cpu->CPU +vs explicit-cuda->CUDA on a GPU box — not run (no dgx in scope); the CPU tier +pins that half via the pure matrix. (2) The 0-as-explicit-cpu vs 0-as-auto +distinction is behaviorally invisible on a CPU-only tier at the queue level; +pinned structurally (matrix + the bogus-path capi arm) and named for the +CUDA-build pass. (3) EXECUTED post-#122 (9f772cad): rebased onto main — v14 sits +on v13 (changelog now v10..v14 chronological, the minor-#2 v12 move kept), +c_header_compile composes main's `vllm_complete_tokens` reference with the +v14 field, floor pin stays >= 14, allowlist ratchet inherited at 8, keyed +records main-wholesale + these rows re-applied. (4) Explicit +names for xpu/vulkan/metal/rocm are additive later; auto reaches them today. +(5) The transcription stack refuses explicit cuda (CPU pipeline) — revisit +when an accelerated transcription path exists. diff --git a/CMakeLists.txt b/CMakeLists.txt index a7906a079..7d20ebea7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -513,6 +513,7 @@ add_library(vllm STATIC src/vllm/sampling_params.cpp src/vllm/lora/punica_cpu.cpp src/vllm/config/scheduler.cpp + src/vllm/config/device.cpp src/vllm/config/kv_transfer.cpp src/vllm/config/speculative.cpp src/vllm/outputs.cpp diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index acd083f44..a0178ffe5 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -307,7 +307,7 @@ built on it rather than keeping the flattering one. | Track | Status | Next gate | |---|---|---| -| Surface coverage (`ARCH-ONE-SURFACE`) | **No number owed**: a CPU records/tooling change (the ONE SURFACE audit + `scripts/check-surface-coverage.py` guard, two axes, preflight + CI); no kernel or generation path touched | Fold-plan lanes carry their own gates; guard keeps CLI-only capabilities from landing untracked | +| Surface coverage (`ARCH-ONE-SURFACE`) | **No number owed**: CPU tooling/plumbing (guard; ROW 8 device knob, ABI v14 `device`, zero value = the probe, byte-identical); no kernel path touched | Fold lanes carry their own gates; ROW 8 CUDA-build A/B = named residual | | 35B prefill TTFT | 0.93x to 0.98x at every concurrency (2026-08-05) | Attribute the residual, then close | | 35B low-batch MoE decode | CLOSED at low batch (c1 0.975x, c4 wins); c16 0.93x. `VT_ASYNC_DEVICE_MIRROR` **default ON for correctness**. `VT_ASYNC_EXECUTOR` Option A (H2D out of capture) A/B'd speed-NEUTRAL | c16 lever is prefill glue (task #61), not the decode drain. `test_qwen36_async_serving` GREEN | | CPU keep-quant MoE decode | **No number owed**: correctness-only P0. The grouped keep-quant GEMM read activations as f32 whatever their dtype, so CPU MoE decode emitted token-0 garbage from `b4f5610a` (2026-07-31) | Speed unmeasured and unclaimed; `test_ops_quant_dot` GREEN (150224 assertions) | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 21694513b..5b22a6ed1 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -247,6 +247,7 @@ Build with `-DVLLM_CPP_VULKAN=ON`; off by default. | Embeddings / pooling | none | embedder-unreachable | | Audio transcription (Parakeet ASR) | `vllm_transcribe`, `vllm_transcription_params_default`, `vllm_transcription_free` | reachable | | Video+audio generation (MiniMax-H3) | `vllm_video_engine_load`, `vllm_video_generate`, `vllm_video_result_free`, `vllm_video_mux_argv` | reachable | +| Explicit device selection (auto/cpu/cuda) | `device` field on `vllm_model_params` (ABI v14; 0=auto keeps the probe, explicit absent device fails loud) | reachable | | Multimodal input (image/audio/video) | none | embedder-unreachable | ## Parallelism and scale-out diff --git a/docs/STATUS.md b/docs/STATUS.md index d0b72bde2..1db2bd261 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -1563,6 +1563,9 @@ runtime-verified yet. lanes. Guard `scripts/check-surface-coverage.py` (two axes, preflight + CI): every `examples/*` unit is a client of `include/vllm.h` or tracked to a fold row; every `FEATURES.md` C-ABI capability names an entry point or is tracked. + Fold ROW 8: `vllm_model_params.device` (ABI v14, 0=auto/1=cpu/2=cuda); + explicit cpu forces CPU, an absent named device fails loud (never + substituted); `--device` on server+cli. CUDA-build A/B = named residual. - **Automatic prefix caching (APC)** is on by default for dense models (hybrid / GDN and attention-free default off, mirroring vLLM), and it now has an end-to-end cache-ON gate on `Qwen/Qwen3-4B` (a shared common prefix reused @@ -1594,10 +1597,6 @@ runtime-verified yet. remaining work is the async production-serving path wiring, the chat/completion response-body timing surface, and the config-gated metric families (speculative decoding, KV connector, multimodal cache, LoRA). - matching vLLM's own mapping. A behavioural CPU gate drives the reference engine - for several steps and checks the values track the run. The remaining work is - the async production-serving path wiring and the config-gated metric families - (speculative decoding, KV connector, multimodal cache, LoRA). - **SGLang RadixAttention behavior parity** — scoped 2026-07-27, W1+W2 flags now IMPLEMENTED (CPU-gated). SGLang's radix-tree prefix cache is functionally equivalent to our block-hash APC (both do automatic longest-prefix KV sharing diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index 337bbc9ac..3e1969e3d 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -6,7 +6,7 @@ // vllm_last_error(). // // vllm-cli --model --prompt "" -// [--tokenizer-config ] +// [--tokenizer-config ] [--device auto|cpu|cuda] // [--max-tokens N] [--temperature T] [--top-p P] [--top-k K] // [--seed S] [--stream] // @@ -36,13 +36,17 @@ struct Args { bool have_seed = false; bool stream = false; std::string speculative_config; // vLLM --speculative-config JSON; "" => off. + // --device (ABI v14): "auto" (default probe), "cpu", or "cuda" — the names + // of vLLM's DeviceConfig.device this build serves. Mapped to the int the ABI + // takes (0/1/2) in ParseArgs; an unknown name is rejected there. + int32_t device = 0; }; void Usage(const char* argv0, std::FILE* out) { std::fprintf( out, "usage: %s --model --prompt \"\"\n" - " [--tokenizer-config ]\n" + " [--tokenizer-config ] [--device auto|cpu|cuda]\n" " [--max-tokens N] [--temperature T] [--top-p P] [--top-k K]\n" " [--seed S] [--stream]\n" " [--speculative-config '']\n" @@ -90,6 +94,26 @@ bool ParseArgs(int argc, char** argv, Args& a, int& exit_code) { a.stream = true; } else if (flag == "--speculative-config") { a.speculative_config = NextArg(argc, argv, i); + } else if (flag == "--device") { + // The vLLM DeviceConfig.device names (auto/cpu/cuda) -> the ABI int + // (vllm_model_params.device: 0=auto, 1=cpu, 2=cuda). An unknown name is + // a usage error, mirroring vLLM rejecting a non-Literal device value. + const std::string device = NextArg(argc, argv, i); + if (device == "auto") { + a.device = 0; + } else if (device == "cpu") { + a.device = 1; + } else if (device == "cuda") { + a.device = 2; + } else { + std::fprintf(stderr, + "vllm-cli: unknown --device '%s' (expected auto, cpu, or " + "cuda)\n", + device.c_str()); + Usage(argv[0], stderr); + exit_code = 2; + return false; + } } else if (flag == "-h" || flag == "--help") { Usage(argv[0], stdout); exit_code = 0; @@ -150,6 +174,10 @@ int main(int argc, char** argv) { if (!args.speculative_config.empty()) { mp.speculative_config = args.speculative_config.c_str(); } + // --device: explicit device selection (ABI v14). 0 (the default) keeps the + // accelerator-first probe; an explicitly named absent device fails the load + // below with the library's message (never a silent fallback). + mp.device = args.device; vllm_engine* engine = nullptr; std::fprintf(stderr, "vllm-cli: loading model from %s\n", diff --git a/examples/server/main.cpp b/examples/server/main.cpp index 66777c48c..a06d944d4 100644 --- a/examples/server/main.cpp +++ b/examples/server/main.cpp @@ -53,6 +53,7 @@ #include #endif +#include "vllm/config/device.h" #include "vllm/config/kv_transfer.h" #include "vllm/config/scheduler.h" #include "vllm/entrypoints/chat_template.h" @@ -133,6 +134,14 @@ struct Args { int max_model_len = 0; // 0 => config.max_position_embeddings int max_num_seqs = 8; int max_num_batched_tokens = 0; // 0 => per-architecture default. + // --device: explicit device selection for the TEXT engine (ARCH-ONE-SURFACE + // ROW 8), the vLLM DeviceConfig.device names this build serves: "auto" + // (default — the accelerator-first probe, byte-identical to before the flag), + // "cpu" (force the CPU queue), "cuda" (require CUDA; an absent device fails + // startup LOUDLY, never a silent fallback). The video engine keeps its own + // --video-device below: the two engines are loaded from different + // checkpoints and may legitimately serve on different devices. + std::string device = "auto"; // --- MiniMax-H3 video generation (opt-in; absent => /v1/videos is unregistered // and the server behaves exactly as before). --- std::string video_dit, video_vae, video_vae_config, audio_vae, audio_vae_config; @@ -198,6 +207,7 @@ struct Args { "[--num-blocks N] [--max-model-len N]\n" " [--max-num-seqs N] " "[--max-num-batched-tokens N]\n" + " [--device auto|cpu|cuda]\n" " [--cuda-profile-graph-replays N]\n" " [--cuda-profile-graph-batch N]\n" " [--benchmark-shutdown-fifo F]\n" @@ -244,6 +254,11 @@ Args ParseArgs(int argc, char** argv) { a.max_num_seqs = std::stoi(NextArg(argc, argv, i, argv[0])); } else if (flag == "--max-num-batched-tokens") { a.max_num_batched_tokens = std::stoi(NextArg(argc, argv, i, argv[0])); + } else if (flag == "--device") { + // Text-engine device selection (mirrors vLLM's DeviceConfig.device + // names). Validated by vllm::DeviceFromString at engine construction; + // --video-device (below) stays the video engine's separate knob. + a.device = NextArg(argc, argv, i, argv[0]); } else if (flag == "--cuda-profile-graph-replays") { a.cuda_profile_graph_replays = std::stoi(NextArg(argc, argv, i, argv[0])); @@ -457,6 +472,12 @@ int main(int argc, char** argv) { engine_params.max_num_seqs = args.max_num_seqs; engine_params.max_num_batched_tokens = args.max_num_batched_tokens; engine_params.enable_prefix_caching = args.enable_prefix_caching; + // --device: explicit device selection (ARCH-ONE-SURFACE ROW 8). "auto" + // (default) keeps the accelerator-first probe byte-identical; an unknown + // name throws HERE (a startup error), and an explicitly named ABSENT + // device fails FromModelDir loudly — never a silent fallback + // (vllm/config/device.py:61-66). + engine_params.device = vllm::DeviceFromString(args.device); // Reject an unknown policy string (mirrors upstream SchedulingPolicy(value)). engine_params.policy = vllm::SchedulerPolicyFromString(args.scheduling_policy); // ENG-SGLANG-BEHAVIOR-FLAG (SW1): `lpm` needs prefix caching to have any diff --git a/include/vllm.h b/include/vllm.h index 1880348e3..a276381cb 100644 --- a/include/vllm.h +++ b/include/vllm.h @@ -82,6 +82,15 @@ extern "C" { * default, so zero-filling the struct growth keeps a v9 engine byte-identical. * Scheduler policy (incl. SGLang's cache-aware LPM) is selected through the v9 * string field .scheduling_policy = "lpm" — there is no separate int knob. + * v11: AUDIO TRANSCRIPTION (ARCH-ONE-SURFACE fold #4) — vllm_transcribe / + * vllm_transcription_params(_default) / vllm_transcription(_free), appended so + * zero values preserve behaviour. vllm_engine_load now RESOLVES a + * transcription-only checkpoint (Parakeet CTC/RNNT/TDT, the vLLM + * SupportsTranscription mirror) into a transcription engine: the text entry + * points on such a handle report VLLM_ERR_INVALID_ARGUMENT with an actionable + * message instead of serving, and vllm_transcribe on a TEXT handle does the + * same. A pre-v11 caller that never loads a Parakeet directory is + * byte-identical. * v12: VIDEO+AUDIO GENERATION (ARCH-ONE-SURFACE ROW 2, MiniMax-H3) — the * ratified video slice: an opaque vllm_video_engine loaded from the H3 * checkpoint set (vllm_video_engine_load/free, vllm_video_model_params + @@ -94,23 +103,23 @@ extern "C" { * handles refuse each other's checkpoints LOUDLY: vllm_video_engine_load on a * text-model directory names vllm_engine_load, and vllm_engine_load on an H3 * checkpoint directory keeps failing exactly as at v11 (no config.json). - * v11: AUDIO TRANSCRIPTION (ARCH-ONE-SURFACE fold #4) — vllm_transcribe / - * vllm_transcription_params(_default) / vllm_transcription(_free), appended so - * zero values preserve behaviour. vllm_engine_load now RESOLVES a - * transcription-only checkpoint (Parakeet CTC/RNNT/TDT, the vLLM - * SupportsTranscription mirror) into a transcription engine: the text entry - * points on such a handle report VLLM_ERR_INVALID_ARGUMENT with an actionable - * message instead of serving, and vllm_transcribe on a TEXT handle does the - * same. A pre-v11 caller that never loads a Parakeet directory is - * byte-identical. * v13: vllm_complete_tokens — blocking completion from a PRE-TOKENIZED prompt * (vLLM's TokensPrompt), returning the generated token ids (and optionally the * detokenized vllm_completion). The entry point for embedders that manage * their own tokenization and for token-exact gates/benchmarks that compare * whole token streams against a reference (the Kimi-Linear paged-runner fold * battery, ARCH-ONE-SURFACE ROW 7, is the first consumer). Purely additive — - * no struct changed. */ -#define VLLM_ABI_VERSION 13 + * no struct changed. + * v14: vllm_model_params.device — EXPLICIT DEVICE SELECTION (ARCH-ONE-SURFACE + * fold ROW 8), the mirror of vLLM's DeviceConfig.device names + * (vllm/config/device.py:13): 0=auto (the byte-identical default — the + * accelerator-first platform probe that has always selected the queue), 1=cpu + * (force the CPU queue even on an accelerator build), 2=cuda (require the + * CUDA platform; when it is absent the load FAILS with VLLM_ERR_MODEL_LOAD — + * an explicitly named device is never silently substituted, device.py:61-66). + * Appended at the END of vllm_model_params so a zero-initialized struct keeps + * the pre-v14 engine byte-identical. */ +#define VLLM_ABI_VERSION 14 /* ── Export macro ───────────────────────────────────────────────────────────── * Marks the symbols that make up the stable ABI. Default visibility now; Task 3 @@ -257,6 +266,24 @@ typedef struct vllm_model_params { * SGLang's cache-aware LPM) is a SEPARATE knob — the v9 string field * .scheduling_policy = "lpm", not an int here. */ int32_t enable_jump_forward; + /* ── Device selection (ABI v14) ──────────────────────────────────────────── + * Which device the text-generation engine serves on, mirroring vLLM's + * DeviceConfig.device names (vllm/config/device.py:13 — Device = + * Literal["auto", "cuda", "cpu", ...]): + * 0 => AUTO (the byte-identical default): the accelerator-first platform + * probe that has always selected the queue (CUDA first, CPU + * fallback; on other builds ROCm/XPU/Vulkan/Metal probe in between); + * 1 => CPU: force the CPU queue even when an accelerator is available; + * 2 => CUDA: require the CUDA platform. When it is absent in this + * build/process the load FAILS with VLLM_ERR_MODEL_LOAD and a + * message naming the device — an explicitly named device is NEVER + * silently replaced by another (mirror of vLLM assigning an explicit + * device verbatim, device.py:61-66). + * 0 must stay auto so a zero-initialized struct preserves pre-v14 behaviour; + * the cpu-before-cuda value order follows the v12 precedent + * (vllm_video_model_params.device: 0 cpu, 1 cuda) shifted by the auto slot. + * Any other value fails vllm_engine_load with VLLM_ERR_INVALID_ARGUMENT. */ + int32_t device; } vllm_model_params; /* ── Custom logits processor (ABI v8) ───────────────────────────────────────── diff --git a/include/vllm/config/device.h b/include/vllm/config/device.h new file mode 100644 index 000000000..db90a7732 --- /dev/null +++ b/include/vllm/config/device.h @@ -0,0 +1,47 @@ +// Ported from: vllm/config/device.py @ 555967922 (Device Literal:13, +// DeviceConfig:16-78). +// +// Scope (ARCH-ONE-SURFACE fold ROW 8): the explicit device-selection surface — +// the mirror of vLLM's `DeviceConfig.device` names. Upstream the value set is +// `Device = Literal["auto", "cuda", "cpu", "tpu", "xpu"]` (device.py:13); +// "auto" resolves through the platform probe (device.py:49-60) and an explicit +// name is assigned VERBATIM, never silently substituted (device.py:61-66). +// +// PORT NOTES (recorded deviations): +// - Only the members this build can serve are selectable: auto/cpu/cuda. +// "tpu" has no backend here; xpu/vulkan/metal/rocm are reachable through +// the AUTO accelerator-first probe (platform.cpp kCurrentPriority) and +// gain explicit names ADDITIVELY when a lane needs them. +// - The integer values are the C-ABI wire contract +// (vllm_model_params.device, ABI v14): 0 MUST be auto so a zero-initialized +// struct preserves the pre-v14 accelerator-first behaviour byte for byte; +// cpu-before-cuda then follows the shipped v12 precedent +// (vllm_video_model_params.device: 0 cpu, 1 cuda) shifted by the auto slot. +#ifndef VLLM_CONFIG_DEVICE_H_ +#define VLLM_CONFIG_DEVICE_H_ + +#include +#include + +namespace vllm { + +// The device selection an engine is asked to serve on. kAuto is the default +// and the zero value: the accelerator-first platform probe that has always +// selected the queue (CurrentPlatform(), src/vllm/platforms/platform.cpp). +enum class Device : int32_t { + kAuto = 0, // platform-probed, upstream's "auto" default (device.py:20). + kCPU = 1, // force the CPU queue even when an accelerator is present. + kCUDA = 2, // require the CUDA platform; absent => the load fails LOUD. +}; + +// Parse the wire/CLI name ("auto" | "cpu" | "cuda" — the supported subset of +// upstream's Device Literal, device.py:13). Throws std::invalid_argument on +// any other name, mirroring pydantic rejecting a non-Literal value. +Device DeviceFromString(const std::string& value); + +// The canonical name for a selection (static storage; never nullptr). +const char* DeviceName(Device device); + +} // namespace vllm + +#endif // VLLM_CONFIG_DEVICE_H_ diff --git a/include/vllm/entrypoints/model_loader.h b/include/vllm/entrypoints/model_loader.h index 4cf230df1..bdb97e01c 100644 --- a/include/vllm/entrypoints/model_loader.h +++ b/include/vllm/entrypoints/model_loader.h @@ -13,6 +13,7 @@ #include #include +#include "vllm/config/device.h" #include "vllm/config/kv_transfer.h" #include "vllm/config/scheduler.h" #include "vllm/config/speculative.h" @@ -114,6 +115,17 @@ struct EngineParams { // only enables the seam. Non-safetensors (GGUF) checkpoints lack `mtp.*`, so an // MTP config over a GGUF source is rejected. std::optional speculative_config = std::nullopt; + + // ARCH-ONE-SURFACE fold ROW 8: explicit device selection, the mirror of + // vLLM's DeviceConfig.device (vllm/config/device.py). kAuto (default) keeps + // the accelerator-first probe that has always selected the queue — the + // byte-identical default. kCPU forces the CPU queue without consulting the + // probe; kCUDA requires the CUDA platform and the load fails LOUD when it is + // absent (never a silent fallback — an explicit device is assigned verbatim + // upstream, device.py:61-66). Exposed on the C ABI as + // vllm_model_params.device (ABI v14: 0=auto, 1=cpu, 2=cuda) and on the + // server as --device. + vllm::Device device = vllm::Device::kAuto; }; // Owns the full V1 engine stack (config + weights + tokenizer + Scheduler + @@ -170,6 +182,23 @@ class LoadedEngine { int max_model_len, bool is_dense_arch); static bool ResolveEnablePrefixCaching(const EngineParams& params, const ModelInfo& model_info); + // ARCH-ONE-SURFACE ROW 8: the EXPLICIT arms of the device-selection policy + // behind SelectQueue, factored pure over the "is the CUDA platform + // registered" probe answer so the CPU tier can gate the whole matrix without + // registering fake global platforms: + // * kCPU -> vt::DeviceType::kCPU unconditionally — an explicit CPU ask + // never consults the accelerator probe, even when CUDA is registered; + // * kCUDA -> vt::DeviceType::kCUDA when cuda_platform_registered, else + // THROWS std::runtime_error naming the device (fail LOUD; the mirror of + // vLLM assigning an explicit device verbatim and never substituting + // another — vllm/config/device.py:61-66); + // * kAuto is NOT resolved here (it resolves through the accelerator-first + // probe inside SelectQueue, byte-identical to pre-ROW-8) and throws + // std::invalid_argument if passed. + // SelectQueue routes its explicit arms through THIS function, so the gate on + // it pins the production policy, not a parallel copy. + static vt::DeviceType ResolveExplicitDeviceType(vllm::Device requested, + bool cuda_platform_registered); vllm::v1::LLMEngine& engine() { return engine_; } // Lazily start W2's EngineCoreProc + output-handler threads. Once created, diff --git a/scripts/check-public-doc-tables.py b/scripts/check-public-doc-tables.py index 721bdee4f..cf23d414b 100755 --- a/scripts/check-public-doc-tables.py +++ b/scripts/check-public-doc-tables.py @@ -375,7 +375,14 @@ def features_errors(text: str) -> list[str]: # subgroup-reduction outcome, paid for by dropping the native-kernel COUNT, # which is derivable from the linked campaign doc while a void result is not # derivable from anywhere. Net -0. - "chars": 279200, + # + # 279150 since 2026-08-08 (measured 279111 after the #122 rebase): the ONE-SURFACE ROW 8 device + # knob owes this page a status line, paid for by removing a STALE MERGE + # DUPLICATE in the metrics paragraph (the same "matching vLLM's own + # mapping ... behavioural CPU gate ... remaining work" narrative appeared + # twice back to back; the longer, newer version stays). Nothing was lost, + # only de-duplicated. Net -92. + "chars": 279150, "h2_sections": 11, "long_paragraphs": 89, "oversized_cells": 47, diff --git a/src/capi/vllm_c.cpp b/src/capi/vllm_c.cpp index ae5e90613..372e52e54 100644 --- a/src/capi/vllm_c.cpp +++ b/src/capi/vllm_c.cpp @@ -464,6 +464,7 @@ VLLM_API vllm_model_params vllm_model_params_default(void) { p.scheduling_policy = nullptr; // NULL => "fcfs" (ABI v9). p.kv_transfer_config = nullptr; // NULL => no connector (ABI v9). p.enable_jump_forward = 0; // 0 => env-resolved, default OFF (ABI v10). + p.device = 0; // 0 => auto: the accelerator-first probe (ABI v14). return p; } @@ -601,6 +602,26 @@ VLLM_API vllm_status vllm_engine_load(const vllm_model_params* params, "or 2 (off)"); return VLLM_ERR_INVALID_ARGUMENT; } + // ABI v14: explicit device selection (0=auto, 1=cpu, 2=cuda), mirroring + // vLLM's DeviceConfig.device names (vllm/config/device.py:13). 0 leaves + // ep.device at kAuto — the byte-identical accelerator-first probe. An + // explicitly named device that is ABSENT fails inside FromModelDir, before + // any model I/O, and reports VLLM_ERR_MODEL_LOAD with a message naming the + // device (never a silent fallback — device.py:61-66). + switch (params->device) { + case 0: + break; // auto (the default probe) — ep.device stays kAuto. + case 1: + ep.device = vllm::Device::kCPU; + break; + case 2: + ep.device = vllm::Device::kCUDA; + break; + default: + SetError( + "vllm_engine_load: device must be 0 (auto), 1 (cpu), or 2 (cuda)"); + return VLLM_ERR_INVALID_ARGUMENT; + } // ABI v11 task dispatch: a directory whose config.json architectures // resolve to a SupportsTranscription-ONLY registration (Parakeet @@ -621,6 +642,17 @@ VLLM_API vllm_status vllm_engine_load(const vllm_model_params* params, peek = nullptr; } if (peek != nullptr && peek->info.supports_transcription_only) { + // ABI v14: the transcription stack is a CPU-hosted pipeline; an + // explicit CUDA ask cannot be served and is REFUSED rather than + // silently downgraded (the same never-substitute rule as the text + // engine, vllm/config/device.py:61-66). + if (ep.device == vllm::Device::kCUDA) { + SetError( + "vllm_engine_load: device 'cuda' was requested but this " + "transcription-only checkpoint serves on the CPU pipeline; use " + "device=auto or device=cpu"); + return VLLM_ERR_INVALID_ARGUMENT; + } auto* handle = new vllm_engine; handle->transcriber = std::make_unique( diff --git a/src/vllm/config/device.cpp b/src/vllm/config/device.cpp new file mode 100644 index 000000000..a73969553 --- /dev/null +++ b/src/vllm/config/device.cpp @@ -0,0 +1,40 @@ +// See include/vllm/config/device.h. Ported from: vllm/config/device.py @ +// 555967922 (Device Literal:13 — the name set; DeviceConfig:61-66 — an explicit +// device is assigned verbatim, never substituted). +#include "vllm/config/device.h" + +#include + +namespace vllm { + +Device DeviceFromString(const std::string& value) { + if (value == "auto") { + return Device::kAuto; + } + if (value == "cpu") { + return Device::kCPU; + } + if (value == "cuda") { + return Device::kCUDA; + } + // Mirrors pydantic rejecting a value outside the Device Literal + // (vllm/config/device.py:13). "tpu"/"xpu" are upstream names this build + // cannot serve explicitly yet; they are rejected with the same message shape + // rather than mapped to something else. + throw std::invalid_argument("Unknown device: " + value + + " (expected one of: auto, cpu, cuda)"); +} + +const char* DeviceName(Device device) { + switch (device) { + case Device::kAuto: + return "auto"; + case Device::kCPU: + return "cpu"; + case Device::kCUDA: + return "cuda"; + } + return "invalid"; +} + +} // namespace vllm diff --git a/src/vllm/entrypoints/model_loader.cpp b/src/vllm/entrypoints/model_loader.cpp index 00c778eee..b9685d8a7 100644 --- a/src/vllm/entrypoints/model_loader.cpp +++ b/src/vllm/entrypoints/model_loader.cpp @@ -46,7 +46,25 @@ namespace { // a PARTIAL backend decline a model whose kernels it has not registered, instead // of being selected and then failing deep inside a kernel bind. Empty means "no // model resolved yet", which is treated as no constraint. -vt::Queue SelectQueue(std::string_view architecture) { +// +// ARCH-ONE-SURFACE ROW 8: `device` is the caller's explicit selection +// (EngineParams::device / vllm_model_params.device). kAuto keeps the +// accelerator-first probe below byte-identical; an EXPLICIT selection routes +// through LoadedEngine::ResolveExplicitDeviceType and — unlike the auto arm — +// a failure to serve the named device PROPAGATES instead of falling back to +// CPU (mirror of vLLM never substituting an explicitly named device, +// vllm/config/device.py:61-66). +vt::Queue SelectQueue(std::string_view architecture, vllm::Device device) { + if (device != vllm::Device::kAuto) { + const vt::DeviceType resolved = LoadedEngine::ResolveExplicitDeviceType( + device, vllm::platforms::HasPlatform(vt::DeviceType::kCUDA)); + if (resolved == vt::DeviceType::kCPU) { + return vt::Queue{vt::Device{vt::DeviceType::kCPU, 0}, nullptr}; + } + // No try/catch here on purpose: an explicit accelerator whose queue cannot + // be created must FAIL the load loudly, never silently serve on CPU. + return vt::GetBackend(resolved).CreateQueue(); + } // M2.2b: run the engine forward on the ACCELERATOR when one is available, so // (on CUDA/GB10) the fp4-resident MoE/lm_head weights hit vt::MatmulNvfp4 // on-device instead of the CPU dequant reference. @@ -458,6 +476,39 @@ bool LoadedEngine::ResolveEnablePrefixCaching(const EngineParams& params, return !model_info.is_hybrid && !model_info.has_inner_state; } +// ARCH-ONE-SURFACE ROW 8: the explicit arms of the device-selection policy — +// see the contract in model_loader.h. Ported semantics: +// vllm/config/device.py:61-66 @ 555967922 (an explicit device string is +// assigned VERBATIM — never substituted), with the loud failure upstream +// raises when the named device cannot serve (torch/worker init on an absent +// CUDA device; our analogue is the unregistered kCUDA platform, +// src/vllm/platforms/cuda.cpp Registrar — kCUDA registers only when a usable +// GPU probed). +vt::DeviceType LoadedEngine::ResolveExplicitDeviceType( + vllm::Device requested, bool cuda_platform_registered) { + switch (requested) { + case vllm::Device::kCPU: + // Explicit CPU never consults the accelerator probe: even on a + // CUDA-capable build/process this selects the CPU queue. + return vt::DeviceType::kCPU; + case vllm::Device::kCUDA: + if (!cuda_platform_registered) { + throw std::runtime_error( + "device 'cuda' was requested but no CUDA platform is available in " + "this build/process (an explicitly named device is never silently " + "replaced — mirror of vllm/config/device.py:61-66; use device=auto " + "or device=cpu, or run a CUDA build on a machine with a usable " + "GPU)"); + } + return vt::DeviceType::kCUDA; + case vllm::Device::kAuto: + break; // auto resolves through the probe in SelectQueue, not here. + } + throw std::invalid_argument( + "ResolveExplicitDeviceType resolves only explicit device selections " + "(cpu/cuda); auto resolves through the accelerator-first probe"); +} + bool LoadedEngine::EnsureNoneHash() { // Idempotent: init_none_hash just (re)assigns the NONE_HASH global. // @@ -648,7 +699,8 @@ LoadedEngine::LoadedEngine(HfConfig config, runner_(config_, *model_, kv_cfg_, preselected_queue != nullptr ? *preselected_queue - : SelectQueue(model_->registration().architecture), + : SelectQueue(model_->registration().architecture, + params.device), /*max_num_reqs=*/params.max_num_seqs > 0 ? params.max_num_seqs : 8, max_model_len_, /*max_num_batched_tokens=*/max_num_batched_tokens_, @@ -813,6 +865,19 @@ vllm::v1::AsyncLLM& LoadedEngine::async_engine() { std::unique_ptr LoadedEngine::FromModelDir( const std::string& model_dir, const EngineParams& params) { + // ARCH-ONE-SURFACE ROW 8: resolve an EXPLICIT device selection up front, + // BEFORE any path/config/weight I/O — the mirror of vLLM resolving + // DeviceConfig at config-creation time, ahead of the model load + // (vllm/engine/arg_utils.py:1878 builds DeviceConfig first; + // device.py __post_init__ resolves immediately). An explicitly named absent + // device therefore fails HERE, loudly, and is never masked by a later + // path/tokenizer error. The result is discarded: SelectQueue re-runs the + // SAME ResolveExplicitDeviceType when it actually creates the queue, so the + // policy has exactly one owner. + if (params.device != vllm::Device::kAuto) { + (void)ResolveExplicitDeviceType( + params.device, vllm::platforms::HasPlatform(vt::DeviceType::kCUDA)); + } const fs::path dir(model_dir); // A single `.gguf` file: config + weights + tokenizer all come from the @@ -997,7 +1062,7 @@ std::unique_ptr LoadedEngine::FromModelDir( // Select before loading so an eligible discrete-CUDA dense loader stages each // completed layer to the exact queue the runner will use. If construction // fails before the runner takes over, destroy the selected native stream. - vt::Queue load_queue = SelectQueue(registration.architecture); + vt::Queue load_queue = SelectQueue(registration.architecture, params.device); try { std::unique_ptr model = ModelRegistry::Load( config, ModelSource::FromSafetensorsOwned(shards, &load_queue)); diff --git a/tests/capi/c_header_compile.c b/tests/capi/c_header_compile.c index e5a332941..669e4f013 100644 --- a/tests/capi/c_header_compile.c +++ b/tests/capi/c_header_compile.c @@ -17,7 +17,10 @@ static bool c_header_token_cb(const char* delta_text, bool finished, } /* Instantiate the POD structs + a status value so the C compiler actually lays - * them out, and reference every ABI entry point so the declarations are used. */ + * them out, and reference every ABI entry point so the declarations are used — + * including the v11 transcription slice, the v12 video slice, and the v14 + * device field (this file went stale between v10 and v12; the claim above is + * only honest if new surface lands HERE in the same change). */ int vllm_capi_c_header_check(vllm_engine* eng, const char* prompt) { vllm_model_params mp = vllm_model_params_default(); vllm_sampling_params sp = vllm_sampling_params_default(); @@ -26,6 +29,8 @@ int vllm_capi_c_header_check(vllm_engine* eng, const char* prompt) { vllm_request* request = NULL; vllm_status st = VLLM_OK; + mp.device = 0; /* ABI v14: 0=auto (the accelerator-first probe default). */ + st = vllm_engine_load(&mp, &eng); if (st == VLLM_OK) { st = vllm_complete(eng, prompt, &sp, &out); @@ -48,9 +53,48 @@ int vllm_capi_c_header_check(vllm_engine* eng, const char* prompt) { st = vllm_request_wait(request); vllm_request_free(request); } + + /* Chat entry points (ABI v3). */ + { + char* response_json = NULL; + st = vllm_chat(eng, "{}", &response_json); + vllm_string_free(response_json); + st = vllm_chat_stream(eng, "{}", cb, /*user_data=*/NULL); + } + + /* Audio transcription (ABI v11). */ + { + vllm_transcription_params tp = vllm_transcription_params_default(); + vllm_transcription transcript; + st = vllm_transcribe(eng, &tp, &transcript); + vllm_transcription_free(&transcript); + } + vllm_engine_free(eng); } + /* Video+audio generation (ABI v12): the separate vllm_video_engine handle + * plus the engine-free mux-argv composer. */ + { + vllm_video_model_params vmp = vllm_video_model_params_default(); + vllm_video_params vp = vllm_video_params_default(); + vllm_video_engine* veng = NULL; + st = vllm_video_engine_load(&vmp, &veng); + if (st == VLLM_OK) { + vllm_video_result vres; + st = vllm_video_generate(veng, &vp, &vres); + vllm_video_result_free(&vres); + vllm_video_engine_free(veng); + } + { + vllm_video_mux_params mux = vllm_video_mux_params_default(); + char** mux_argv = NULL; + int32_t mux_argc = 0; + st = vllm_video_mux_argv(&mux, &mux_argv, &mux_argc); + vllm_video_mux_argv_free(mux_argv, mux_argc); + } + } + (void)vllm_last_error(); (void)vllm_version(); (void)vllm_abi_version(); diff --git a/tests/capi/test_capi.cpp b/tests/capi/test_capi.cpp index 96df56eca..c25d842b7 100644 --- a/tests/capi/test_capi.cpp +++ b/tests/capi/test_capi.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -25,7 +26,9 @@ #include #include "capi/engine_handle.h" +#include "vllm/config/device.h" #include "vllm/entrypoints/model_loader.h" +#include "vllm/platforms/interface.h" #include "vllm/entrypoints/openai/serving_utils.h" #include "vllm/model_executor/models/qwen3_5_weights.h" #include "vllm/tokenizer/bpe.h" @@ -1291,10 +1294,13 @@ TEST_CASE("capi: version and abi-version are exposed") { // The engine-config growth (max_num_batched_tokens / scheduling_policy / // kv_transfer_config) is ABI v9; the jump-forward toggle is ABI v10; the // transcription slice (vllm_transcribe) is ABI v11; the video-generation - // slice (vllm_video_*) is ABI v12. The >= pin is the one check that can - // catch a WRONG bump: the == VLLM_ABI_VERSION assertions here and in - // test_dlopen compare against the same macro and move with it. - CHECK(vllm_abi_version() >= 13); + // slice (vllm_video_*) is ABI v12; the pre-tokenized completion entry + // point (vllm_complete_tokens) is ABI v13; the device-selection field + // (vllm_model_params.device) is ABI v14. The >= pin is the one check that + // can catch a WRONG bump: the == VLLM_ABI_VERSION assertions here and in + // test_dlopen compare against the same macro and move with it (the #121 + // lesson: an == floor moves with the macro and proves nothing). + CHECK(vllm_abi_version() >= 14); } // ─── ABI v11: audio transcription (ARCH-ONE-SURFACE ROW 1) ─────────────────── @@ -1694,3 +1700,142 @@ TEST_CASE("capi v12: video argument contract") { vllm_video_mux_argv_free(nullptr, 3); // no-op } + +// ─── ABI v14: explicit device selection (ARCH-ONE-SURFACE ROW 8) ───────────── +// The device knob: 0=auto (the byte-identical accelerator-first probe), 1=cpu, +// 2=cuda — the vLLM DeviceConfig.device names (vllm/config/device.py:13). The +// pure policy matrix (explicit cpu beats a registered accelerator; explicit +// cuda never falls back) is gated in test_loaded_engine_dense.cpp; here the +// C-ABI wire contract and the params->EngineParams->SelectQueue plumb. + +namespace { +// A synthetic engine over an explicit EngineParams device selection, sharing +// MakeSyntheticEngine's stack. +vllm_engine* MakeSyntheticEngineWithDevice(vllm::Device device) { + const HfConfig c = MakeConfig(); + EngineParams params = SyntheticParams(); + params.device = device; + auto loaded = std::make_unique(c, MakeWeights(c), BuildFixture(), + params); + return vllm::capi::MakeEngineHandle(std::move(loaded)); +} +} // namespace + +TEST_CASE("capi v14: the device zero-value/default contract") { + // The default is 0 == auto — a zero-initialized struct and + // vllm_model_params_default() agree, so a pre-v14 caller's zero-filled + // growth keeps the accelerator-first probe engine byte-identical. + const vllm_model_params def = vllm_model_params_default(); + CHECK(def.device == 0); + vllm_model_params zeroed; + std::memset(&zeroed, 0, sizeof(zeroed)); + CHECK(zeroed.device == def.device); + + // The zero value maps to AUTO, not to an explicit device: with device left + // at the default and a bogus path, the load must report the PATH (the auto + // arm defers to the probe and never resolves an explicit device up front). + // A mutation that maps 0 to an explicit cuda would surface the device error + // here instead — on the CPU tier that is the distinguishable half; 0-as- + // explicit-cpu is behaviorally identical on this tier and is pinned by the + // pure policy matrix in test_loaded_engine_dense.cpp plus the CUDA-build + // residual named in the spec. + vllm_model_params bogus = vllm_model_params_default(); + bogus.model_path = "/nonexistent/vllm-cpp/model/dir"; + vllm_engine* probe_eng = reinterpret_cast(0x1); + CHECK(vllm_engine_load(&bogus, &probe_eng) == VLLM_ERR_MODEL_LOAD); + CHECK(probe_eng == nullptr); + CHECK(std::string(vllm_last_error()).find("not a directory") != + std::string::npos); + + // Behavior: on a CPU-only process the auto probe resolves CPU, so an + // explicit-cpu engine must generate EXACTLY what the default (auto) engine + // generates. (Guarded: on an accelerator build auto legitimately selects the + // accelerator, and byte-equality with a CPU run is not the contract.) + if (!vllm::platforms::HasPlatform(vt::DeviceType::kCUDA)) { + vllm_engine* auto_eng = MakeSyntheticEngine(); // device unset == kAuto + vllm_engine* cpu_eng = MakeSyntheticEngineWithDevice(vllm::Device::kCPU); + REQUIRE(auto_eng != nullptr); + REQUIRE(cpu_eng != nullptr); + vllm_sampling_params sp = GreedyParams(6); + vllm_completion a{}; + vllm_completion b{}; + REQUIRE(vllm_complete(auto_eng, "hello world", &sp, &a) == VLLM_OK); + REQUIRE(vllm_complete(cpu_eng, "hello world", &sp, &b) == VLLM_OK); + CHECK(std::string(a.text) == std::string(b.text)); + CHECK(a.completion_tokens == b.completion_tokens); + vllm_completion_free(&a); + vllm_completion_free(&b); + vllm_engine_free(auto_eng); + vllm_engine_free(cpu_eng); + } +} + +TEST_CASE("capi v14: device range validates before any load work") { + // An out-of-range device is a CALLER error caught before FromModelDir — the + // bogus path must NOT be what fails here. + for (const int32_t bad : {static_cast(3), static_cast(-1), + static_cast(7)}) { + vllm_model_params p = vllm_model_params_default(); + p.model_path = "/nonexistent/vllm-cpp/model/dir"; + p.device = bad; + vllm_engine* eng = reinterpret_cast(0x1); + CHECK(vllm_engine_load(&p, &eng) == VLLM_ERR_INVALID_ARGUMENT); + CHECK(eng == nullptr); + CHECK(std::string(vllm_last_error()) + .find("device must be 0 (auto), 1 (cpu), or 2 (cuda)") != + std::string::npos); + } +} + +TEST_CASE("capi v14: explicit cuda on a CUDA-less process fails LOUD (plumb pin)") { + if (vllm::platforms::HasPlatform(vt::DeviceType::kCUDA)) { + return; // CUDA build/box: the explicit-cuda arm resolves; nothing to pin. + } + // device=2 with a bogus path: the DEVICE error must surface, not the path + // error — FromModelDir resolves an explicit device BEFORE any I/O, so this + // pins the whole capi -> EngineParams -> FromModelDir plumb (an implementation + // that drops params->device would report the path instead and go RED here). + vllm_model_params p = vllm_model_params_default(); + p.model_path = "/nonexistent/vllm-cpp/model/dir"; + p.device = 2; // cuda + vllm_engine* eng = reinterpret_cast(0x1); + CHECK(vllm_engine_load(&p, &eng) == VLLM_ERR_MODEL_LOAD); + CHECK(eng == nullptr); + CHECK(std::string(vllm_last_error()) + .find("device 'cuda' was requested but no CUDA platform") != + std::string::npos); + + // device=1 (cpu) on the same bogus path proceeds to the path and reports IT — + // the plumb forwards the field's VALUE, not a constant. + p.device = 1; // cpu + eng = reinterpret_cast(0x1); + CHECK(vllm_engine_load(&p, &eng) == VLLM_ERR_MODEL_LOAD); + CHECK(eng == nullptr); + CHECK(std::string(vllm_last_error()).find("not a directory") != + std::string::npos); +} + +TEST_CASE("capi v14: explicit cpu forces the CPU queue at the EngineParams seam") { + // The observable queue seam: an engine constructed with device=kCPU runs its + // runner on the CPU device. On the CPU tier this is trivially true of auto as + // well; on a CUDA build it is the force-CPU pin (the runner would otherwise + // sit on the CUDA queue). The CPU-tier statement of "cpu beats a registered + // accelerator" is the pure matrix in test_loaded_engine_dense.cpp. + const HfConfig c = MakeConfig(); + EngineParams params = SyntheticParams(); + params.device = vllm::Device::kCPU; + LoadedEngine loaded(c, MakeWeights(c), BuildFixture(), params); + CHECK(loaded.runner().device().type == vt::DeviceType::kCPU); + + // And the ctor-path plumb (LoadedEngine's own SelectQueue call, the arm the + // GGUF/MoE branches take): explicit cuda on a CUDA-less process must throw + // the pinned message out of construction — never silently build on CPU. + if (!vllm::platforms::HasPlatform(vt::DeviceType::kCUDA)) { + EngineParams cuda_params = SyntheticParams(); + cuda_params.device = vllm::Device::kCUDA; + CHECK_THROWS_WITH_AS( + LoadedEngine(MakeConfig(), MakeWeights(c), BuildFixture(), cuda_params), + doctest::Contains("device 'cuda' was requested but no CUDA platform"), + std::runtime_error); + } +} diff --git a/tests/vllm/entrypoints/openai/test_api_server.cpp b/tests/vllm/entrypoints/openai/test_api_server.cpp index f0123466d..7a02c86ad 100644 --- a/tests/vllm/entrypoints/openai/test_api_server.cpp +++ b/tests/vllm/entrypoints/openai/test_api_server.cpp @@ -46,7 +46,9 @@ #include #endif +#include "vllm/config/device.h" #include "vllm/config/scheduler.h" +#include "vllm/entrypoints/model_loader.h" #include "vllm/entrypoints/openai/serving_chat.h" #include "vllm/entrypoints/openai/serving_completion.h" #include "vllm/entrypoints/openai/serving_models.h" @@ -2309,3 +2311,45 @@ TEST_CASE("api_server: the audio routes do not exist on a TEXT server") { h.server.stop(); server_thread.join(); } + +// ─── ARCH-ONE-SURFACE ROW 8: the server's --device seam ────────────────────── +// The exact chain examples/server/main.cpp drives for `--device cpu`: +// vllm::DeviceFromString -> EngineParams.device -> LoadedEngine (SelectQueue's +// explicit arm) -> async_engine() -> the OpenAI serving stack — here over the +// synthetic in-memory model (no disk), asserting the device-selected engine +// SERVES and sits on the CPU queue. The policy matrix (cpu beats a registered +// accelerator; explicit cuda never falls back) is test_loaded_engine_dense.cpp; +// the C-ABI plumb is test_capi.cpp. +TEST_CASE("api_server: an explicit-cpu device-selected engine serves /v1/completions") { + const HfConfig c = MakeConfig(); + vllm::entrypoints::EngineParams params; + params.block_size = kBlockSize; + params.num_blocks = 32; + params.max_model_len = kMaxModelLen; + params.max_num_seqs = 8; + // The server's own parse of `--device cpu` (an unknown name throws there at + // startup; pinned in test_loaded_engine_dense.cpp). + params.device = vllm::DeviceFromString("cpu"); + vllm::entrypoints::LoadedEngine loaded(c, MakeWeights(c), BuildFixture(), + params); + // The observable seam: the runner of the explicitly-cpu engine is on the CPU + // device (on a CUDA build this is the force-CPU pin; auto would select CUDA). + CHECK(loaded.runner().device().type == vt::DeviceType::kCPU); + + OpenAIServingModels models("test-model"); + OpenAIServingCompletion completion(loaded.async_engine(), "test-model", + /*enable_force_include_usage=*/false); + OpenAIServingChat chat(loaded.async_engine(), "test-model", InVocabChatPrompt, + "hermes", /*reasoning_parser_name=*/std::string(), + /*enable_force_include_usage=*/false); + ApiServer server(completion, chat, models, "9.9.9"); + + const std::string body = + R"({"model":"test-model","prompt":"hello","max_tokens":5,"temperature":0.0})"; + ApiServer::DispatchResult r = server.handle_completions(body); + CHECK(r.status == 200); + json j = json::parse(r.body); + CHECK(j.at("object") == "text_completion"); + CHECK(j.at("choices").at(0).at("finish_reason") == "length"); + CHECK(j.at("usage").at("completion_tokens") == 5); +} diff --git a/tests/vllm/entrypoints/test_loaded_engine_dense.cpp b/tests/vllm/entrypoints/test_loaded_engine_dense.cpp index c8f1d18ee..5d4974b5b 100644 --- a/tests/vllm/entrypoints/test_loaded_engine_dense.cpp +++ b/tests/vllm/entrypoints/test_loaded_engine_dense.cpp @@ -27,7 +27,9 @@ #include #include +#include "vllm/config/device.h" #include "vllm/config/scheduler.h" +#include "vllm/platforms/interface.h" #include "vllm/v1/core/sched/async_scheduler.h" #include @@ -447,3 +449,88 @@ TEST_CASE("loaded_engine: prefix caching mirrors model-capability defaults") { params.enable_prefix_caching = false; CHECK_FALSE(LoadedEngine::ResolveEnablePrefixCaching(params, decoder)); } + +// ─── ARCH-ONE-SURFACE ROW 8: explicit device selection ─────────────────────── +// The policy matrix behind SelectQueue's explicit arms, gated PURE over the +// "is the CUDA platform registered" probe answer so the CPU tier pins the +// whole contract — including the CUDA-build half ("explicit cpu beats a +// registered accelerator") that a CPU-only process could otherwise never +// exercise. SelectQueue routes its explicit arms through THIS function +// (model_loader.cpp), so these pins bind the production policy, not a copy. +TEST_CASE("loaded_engine: ResolveExplicitDeviceType — cpu never probes, cuda never falls back") { + using vllm::Device; + + // Explicit CPU resolves CPU regardless of what is registered. The `true` arm + // is the CUDA-build pin: a registered accelerator must NOT win over an + // explicit cpu ask (the fold-ROW-8 defect was that an embedder could not ASK + // for CPU at all). + CHECK(LoadedEngine::ResolveExplicitDeviceType(Device::kCPU, true) == + vt::DeviceType::kCPU); + CHECK(LoadedEngine::ResolveExplicitDeviceType(Device::kCPU, false) == + vt::DeviceType::kCPU); + + // Explicit CUDA with the platform registered resolves CUDA. + CHECK(LoadedEngine::ResolveExplicitDeviceType(Device::kCUDA, true) == + vt::DeviceType::kCUDA); + + // Explicit CUDA WITHOUT the platform throws the pinned message — never a + // silent CPU fallback (mirror of vLLM assigning an explicit device verbatim, + // vllm/config/device.py:61-66). + CHECK_THROWS_WITH_AS( + LoadedEngine::ResolveExplicitDeviceType(Device::kCUDA, false), + doctest::Contains("device 'cuda' was requested but no CUDA platform"), + std::runtime_error); + + // kAuto is not an explicit selection: it resolves through the probe inside + // SelectQueue, and this seam refuses it rather than guessing. + CHECK_THROWS_AS(LoadedEngine::ResolveExplicitDeviceType(Device::kAuto, true), + std::invalid_argument); +} + +TEST_CASE("loaded_engine: DeviceFromString mirrors the vLLM Device names") { + using vllm::Device; + // The supported subset of upstream's Device Literal (vllm/config/device.py:13) + // — the strings the server's --device flag consumes. + CHECK(vllm::DeviceFromString("auto") == Device::kAuto); + CHECK(vllm::DeviceFromString("cpu") == Device::kCPU); + CHECK(vllm::DeviceFromString("cuda") == Device::kCUDA); + CHECK_THROWS_WITH_AS(vllm::DeviceFromString("tpu"), + doctest::Contains("Unknown device: tpu"), + std::invalid_argument); + CHECK_THROWS_AS(vllm::DeviceFromString(""), std::invalid_argument); + + // The wire contract (vllm_model_params.device, ABI v14): 0 MUST stay auto so + // a zero-initialized struct preserves pre-v14 behaviour; cpu/cuda follow the + // v12 vllm_video_model_params.device precedent (0 cpu, 1 cuda) shifted by + // the auto slot. + CHECK(static_cast(Device::kAuto) == 0); + CHECK(static_cast(Device::kCPU) == 1); + CHECK(static_cast(Device::kCUDA) == 2); + CHECK(std::string(vllm::DeviceName(Device::kAuto)) == "auto"); + CHECK(std::string(vllm::DeviceName(Device::kCPU)) == "cpu"); + CHECK(std::string(vllm::DeviceName(Device::kCUDA)) == "cuda"); +} + +TEST_CASE("loaded_engine: FromModelDir resolves an explicit absent device BEFORE any path I/O") { + // The device error must win over the path error (the mirror of vLLM building + // DeviceConfig at config-creation time, before the model load — + // arg_utils.py:1878, device.py __post_init__). This is also what makes the + // EngineParams->FromModelDir plumb pinnable on the CPU tier with no loadable + // checkpoint: a bogus path + device=cuda must report the DEVICE, not the path. + if (vllm::platforms::HasPlatform(vt::DeviceType::kCUDA)) { + return; // CUDA build/box: the explicit-cuda arm resolves; nothing to pin. + } + EngineParams params; + params.device = vllm::Device::kCUDA; + CHECK_THROWS_WITH_AS( + LoadedEngine::FromModelDir("/nonexistent/vllm-cpp/model/dir", params), + doctest::Contains("device 'cuda' was requested but no CUDA platform"), + std::runtime_error); + + // An explicit CPU ask is legal and proceeds to the path (the path error, not + // a device error, surfaces) — the plumb forwards the field, not a constant. + params.device = vllm::Device::kCPU; + CHECK_THROWS_WITH_AS( + LoadedEngine::FromModelDir("/nonexistent/vllm-cpp/model/dir", params), + doctest::Contains("not a directory"), std::runtime_error); +} diff --git a/tests/vllm/models/minimax_h3_video_fold_fixture.h b/tests/vllm/models/minimax_h3_video_fold_fixture.h index 2a850eb93..4562f8955 100644 --- a/tests/vllm/models/minimax_h3_video_fold_fixture.h +++ b/tests/vllm/models/minimax_h3_video_fold_fixture.h @@ -19,7 +19,9 @@ // hidden 64, latents 8/6) + a video-VAE whose PARSED config reproduces the // reduced ViT3D decoder of the "WHOLE t2va path composes end to end" case, and // the same reduced BigVGAN audio VAE. Render request: --partition fl2va -// --frames 5 --height 32 --width 32 --steps 3 (latent 2x2x2, audio_t 8). +// --keep-quant --frames 5 --height 32 --width 32 --steps 3 (latent 2x2x2, +// audio_t 8) — keep-quant is the arm the goldens were captured on +// (FixtureModelParams sets dequant_bf16 = 0, test_minimax_h3_video_fold.cpp). #pragma once #include