From e4c29b34946083d1be6be87da43981123e67a2af Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sat, 8 Aug 2026 00:50:23 +0000 Subject: [PATCH] =?UTF-8?q?feat(abi):=20embeddings/pooling=20through=20the?= =?UTF-8?q?=20ONE=20surface=20=E2=80=94=20LlamaModel=20pooling=20arch,=20P?= =?UTF-8?q?oolingRunner=20in=20the=20engine=20step,=20vllm=5Fembed=20(ABI?= =?UTF-8?q?=20v15),=20live=20/v1/embeddings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ARCH-ONE-SURFACE fold ROW 6 (task #285, branch row/EMBEDDINGS-ONE-SURFACE, PR #137). The engine-side pooler (ENG-POOLER-SEQ ops + ENG-POOLING-RUNNER PoolingRunner) is now INVOKED LIVE through one path: vllm_engine_load on a pooling checkpoint -> LLMEngine::embed -> scheduler -> GPUModelRunner::pool_tokens -> pooled vector, driven identically by vllm_embed (ABI v15) and the live task-conditional /v1/embeddings. W1 registry+runner: NEW arch LlamaModel (llama_embedding_registry.cpp, is_pooling_model=true) — the mirror of _EMBEDDING_MODELS "LlamaModel": ("llama", "LlamaForCausalLM") (registry.py:230) + as_embedding_model (adapters.py:230): the SHARED dense backbone to the post-final-norm hidden with NO lm_head (Qwen3DenseModel::ForwardHidden, additive tail; text callers byte-identical); loader accepts both name layouts (adapters.py:178-181) and never loads lm_head. The runner builds a PoolingRunner iff the registration declares pooling (model_runner.py:368-369) and sample_tokens routes to pool_tokens() (model_runner.py:1586-1607); validity == the discard predicate (pooling_runner.py:40-41); scheduler pooling stop at the marked DEFERRED site (scheduler.py:1718-1721); pooling_output out through EngineCoreOutput/RequestOutput; async scheduling OFF for pooling models (config/vllm.py:1068-1073 — the landed ResolveAsyncScheduling arm now WIRED). Every hook task-gated on is_pooling_model / pooling_params: default nullopt/false = byte-identical text path (engine suites re-run green). W2 ABI: vllm_embed + vllm_embedding_result_free, VLLM_ABI_VERSION 14 -> 15, floor pin >= 15; strict-C references; dlopen symbols; refuse-by-task BOTH directions; FIXED en route: v13's vllm_complete_tokens shipped without the v11 task guard (null-deref on a transcription handle). W3 server: handle_embeddings (OpenAI shape, embed/protocol.py:34, 173-185) registered ONLY when an embedder is attached; server main dispatches pooling archs to a serving-less embedding server; socket-level 404 pins BOTH directions. W4 guard/records: abi-capability-allowlist embeddings row REMOVED (1 left: mm-input); FEATURES row -> reachable; ARCH_TOKEN_RE widened to bare *Model; the routing checker gains the POOLING classification; runnable-baseline re-pinned; SERVE-POOLING-ENDPOINTS and MODEL-EMBED-llama-llama-for-causal-lm rows ACTIVE; STATUS/BENCHMARKS/NOW/state updated. Correctness anchor: the pooling lane's cosine gate re-anchored THROUGH the registry/runner path on the COMMITTED deterministic fixture (scripts/mm/llama_embed_fixture_gen.py): test_llama_embedding_fold 4/4-231 — direct registry path == f64 LAST+normalize reference, FULL-ENGINE path == direct path IDENTICAL vectors, chunked-prefill is_valid arm — plus test_capi 48/48-462 (real fixture-checkpoint load through the public ABI), test_dlopen 30/30, server suite 50/50, registry 24/24-820. 9 mutation kills (floor pin, refusals both directions, route gating both ways, engine-step invocation, scheduler stop, registry info pin, async-off wire). Residual: REAL embedding checkpoint (e5-mistral class) + the LLM(task="embed") oracle cosine — no cosine-vs-oracle number fabricated. FOLLOWING_AGENTS_PROTOCOL Assisted-by: Claude Code:claude-fable-5 [ClaudeCode] --- .agents/NOW.md | 8 +- .agents/coordination.md | 2 + .agents/engine-matrix.md | 8 +- .agents/model-matrix.md | 9 +- .agents/specs/embeddings-one-surface.md | 180 ++++++++++++ .agents/specs/one-surface-abi.md | 2 +- .agents/specs/surface-coverage-2026-08-07.md | 15 +- .agents/state.md | 90 ++++++ CMakeLists.txt | 1 + docs/BENCHMARKS.md | 1 + docs/FEATURES.md | 20 +- docs/STATUS.md | 2 +- examples/server/main.cpp | 71 +++++ include/vllm.h | 53 +++- include/vllm/entrypoints/model_loader.h | 14 +- include/vllm/entrypoints/openai/api_server.h | 29 ++ include/vllm/model_executor/models/llama.h | 8 + .../model_executor/models/model_registry.h | 9 + include/vllm/model_executor/models/qwen3.h | 16 ++ include/vllm/outputs.h | 7 + include/vllm/v1/engine/llm_engine.h | 24 ++ include/vllm/v1/engine/output_processor.h | 7 +- include/vllm/v1/engine/types.h | 35 ++- include/vllm/v1/request.h | 9 + include/vllm/v1/worker/gpu/runner.h | 20 ++ scripts/abi-capability-allowlist.txt | 1 - scripts/check-gate-commands.py | 5 + scripts/check-runner-routing-consistency.py | 24 +- scripts/check-supported-models.py | 3 +- scripts/mm/llama_embed_fixture_gen.py | 189 ++++++++++++ src/capi/vllm_c.cpp | 146 +++++++++- src/vllm/entrypoints/model_loader.cpp | 14 +- src/vllm/entrypoints/openai/api_server.cpp | 122 ++++++++ .../models/llama_embedding_registry.cpp | 135 +++++++++ .../model_executor/models/llama_weights.cpp | 46 +++ src/vllm/model_executor/models/qwen3.cpp | 63 +++- src/vllm/v1/core/sched/scheduler.cpp | 24 +- src/vllm/v1/engine/llm_engine.cpp | 55 ++++ src/vllm/v1/engine/output_processor.cpp | 19 +- src/vllm/v1/request.cpp | 3 + src/vllm/v1/worker/gpu/runner.cpp | 111 +++++++ tests/CMakeLists.txt | 15 +- tests/capi/c_header_compile.c | 8 + tests/capi/test_capi.cpp | 126 +++++++- tests/capi/test_dlopen.cpp | 9 + .../test_check_runner_routing_consistency.py | 6 + tests/scripts/test_check_supported_models.py | 8 +- .../entrypoints/openai/test_api_server.cpp | 183 ++++++++++++ .../fixtures/llama_embed_e2e/config.json | 19 ++ .../llama_embed_e2e/model.safetensors | Bin 0 -> 154259 bytes .../fixtures/llama_embed_e2e/tokenizer.json | 50 ++++ .../vllm/models/test_llama_embedding_fold.cpp | 272 ++++++++++++++++++ tests/vllm/models/test_model_registry.cpp | 29 +- tests/vllm/test_model_loader_gguf.cpp | 2 +- 54 files changed, 2243 insertions(+), 84 deletions(-) create mode 100644 .agents/specs/embeddings-one-surface.md create mode 100644 scripts/mm/llama_embed_fixture_gen.py create mode 100644 src/vllm/model_executor/models/llama_embedding_registry.cpp create mode 100644 tests/vllm/models/fixtures/llama_embed_e2e/config.json create mode 100644 tests/vllm/models/fixtures/llama_embed_e2e/model.safetensors create mode 100644 tests/vllm/models/fixtures/llama_embed_e2e/tokenizer.json create mode 100644 tests/vllm/models/test_llama_embedding_fold.cpp diff --git a/.agents/NOW.md b/.agents/NOW.md index bca56be40..1bb14d227 100644 --- a/.agents/NOW.md +++ b/.agents/NOW.md @@ -30,7 +30,7 @@ Working head: `row/backend-rocm-w0` (#41). Prior: benchmark checkpoint | `BACKEND-ROCM` W0 | Skeleton in; **HIP never compiled** (no AMD HW) | #41 contributors build it; a compile error IS the deliverable | | TP spike #287 (PR #143) | **LANDED** ([spec](specs/tensor-parallelism-spike.md)); DSpark rider grounded | dispatch TP-W1 (CPU-able) | | Release | SPIKE; 30/30 | #129 | -| Surface coverage (`ARCH-ONE-SURFACE`) | **ROW 8 LANDED; #139 repair CPU-GREEN**: ABI v14 stable; registry-resolved named platform; DSR 39→32; execution guard 52/52 | Fresh re-review #139; CUDA A/B residual | +| Surface coverage (`ARCH-ONE-SURFACE`) | ROW 8 + #139 IN; **ROW 6 IN REVIEW (#137): embeddings LIVE — `LlamaModel` arch, PoolingRunner in the step, `vllm_embed` v15, `/v1/embeddings`, fold gate 4/4-231, 9 kills** | Merge #137; real-ckpt oracle cosine residual | In-flight (default-OFF, not pushed): `laguna-fp4proj-prod`, laguna bf16/legacy/pipeline-gemv, `ds4-hc-expand-fuse`. @@ -46,10 +46,8 @@ both gate models, reproduced 2–3x on an idle box. See [gates.md](gates.md) and ## Next actions -1. **Spike the Parakeet encoder row.** Upstream vLLM has `parakeet.py` + - `conformer_encoder.py` as the audio encoder of `nano_nemotron_vl.py`, which we - already carry `MODEL-MM-nano-nemotron-vl-*` rows for, so it is owed mirror work. - The transducer decode half (RNN-T/TDT/CTC) is NOT in vLLM: separate scope call. +1. **Spike the Parakeet encoder row** (vLLM carries it inside + `nano_nemotron_vl.py`; the transducer half is NOT in vLLM: separate call). 2. **Qwen3.5-4B serving follow-up:** bind the default-ON async-serving path against the same oracle before attributing the remaining TPOT gap. 2. **Merge the invocation-parity prevention** (CI guard + AGENTS.md checklist); diff --git a/.agents/coordination.md b/.agents/coordination.md index 61f1d9e46..335539aef 100644 --- a/.agents/coordination.md +++ b/.agents/coordination.md @@ -1619,6 +1619,8 @@ items a-runner/b stay with the async/GDN `runner.cpp` owners. | `CLAIM-POOLING` | `ENG-POOLER-SEQ` (INVENTORIED-implicit→ACTIVE, W1→**W2**), `ENG-POOLING-RUNNER` (**NEW row, ACTIVE, W3**), `SERVE-POOLING-ENDPOINTS` (INVENTORIED→SPIKE) | Claude Code (opus-4-8) | isolated worktree `.claude/worktrees/claim-pooling-w2w3` (CPU build `build-cpu` `-DVLLM_CPP_CUDA=OFF -DVLLM_CPP_SERVER=ON` Release + CPU run; NO dgx/GPU — the pooling reductions + activations + runner are host arithmetic) | branch `claim-pooling-w2w3`, base `main` `edf68c91` (confirmed via `git rev-parse HEAD`) | Pooling task class HIGH-priority feature-gap #2. W0 spike + W1 CPU pooler OP (prior pass); **W2 pooler HEADS composite + `SequencePooler`/`DispatchPooler` + `PoolerConfig`/`PoolingParams` and W3 pooling RUNNER path (this pass).** Owns ONLY: NEW `include/vllm/model_executor/layers/pooler/{pooling_metadata,methods,activations,common,pooling_params,pooler_config,heads,poolers,dispatch_pooler}.h` + `src/vllm/model_executor/layers/pooler/{methods,activations,heads,poolers,dispatch_pooler}.cpp` + NEW `include/vllm/v1/worker/gpu/pool/pooling_runner.h` + `src/vllm/v1/worker/gpu/pool/pooling_runner.cpp`; NEW `tests/vllm/model_executor/layers/pooler/{test_pooler,test_pooler_heads}.cpp` + `tests/vllm/v1/worker/gpu/pool/test_pooling_runner.cpp`; `CMakeLists.txt` (6 source lines) + `tests/CMakeLists.txt` (3 tests); NEW `.agents/specs/pooling-task-class.md`; the `ENG-POOLER-SEQ` + NEW `ENG-POOLING-RUNNER` engine-matrix rows + `SERVE-POOLING-ENDPOINTS` note + engine Serving/Total rollup (Serving 21→22/ACTIVE 6→7, Total 130→131/ACTIVE 47→48) + `scripts/check-agent-record.py` ENGINE 130→131; the record surfaces (this row, `roadmap_v1.md` gap #2, `docs/STATUS.md`, `docs/BENCHMARKS.md`, `feature-matrix.md` MODEL-POOLING note, `parity-ledger.md`, `state.md`). **NON-COLLISION:** additive NEW files only — the sole edits to existing compiled headers are ADDITIVE (methods.h defaulted virtuals, pooling_metadata.h new fields); ZERO edits to any existing production forward/runner path; NO pooling MODEL row created (concrete embedding model + real-oracle cosine gate is the named W3-model residual), so README/Metal/model-matrix rows untouched. | `ACTIVE` | 2026-07-29 — **W2 + W3 LANDED + CPU-GATED (foreground, NOT pushed).** `test_pooler_heads` 27/27 (240 asserts, Embedding/Classifier heads + SequencePooler + DispatchPooler incl. mixed embed+classify batch + ctor validation) and `test_pooling_runner` 5/5 (14 asserts, runner path + STRUCTURAL cosine-parity gate vs double-precision LAST+normalize ref) — plus W1 `test_pooler` 17/17 unchanged. RED-first proven: disable matryoshka slice + logit_mean → 8 cases/50 asserts fail (heads); CLS-instead-of-LAST drops cosine <0.5 + disable normalize → 2 unit-L2 asserts fail (runner). Clean CPU `-Wall -Wextra -Werror` 0-warn full-library build. **HONEST RESIDUAL:** the cosine gate is STRUCTURAL (synthetic weights) — the real-model `vllm.LLM(task="embed").encode` oracle cosine gate needs a registered concrete embedding model forward (W3-model, no number fabricated). Residuals (spec §Work breakdown): concrete pooling MODEL + real-oracle cosine gate (W3-model), endpoints /v1/embeddings+score+rerank+classify (W4), tokwise AllPool/StepPool (W5). Prior 2026-07-28 — W0 spike + W1 pooler OP LANDED + CPU-GATED: `test_pooler` 17/17 (50 asserts) vs double-precision refs, RED-first proven. | | `CLAIM-DSV4-GGUF-LOADER` | `QUANT-GGUF-IQ2_XXS` (INVENTORIED→ACTIVE), `QUANT-GGUF-Q2_K` (INVENTORIED→ACTIVE); cross-refs `MODEL-TEXT-deepseek-v4-deepseek-v4-for-causal-lm` (stays `SPIKE`, owned by `CLAIM-DEEPSEEK-V4-IMPL`) | Claude Code (opus-4-8) | isolated worktree `.claude/worktrees/gguf-iquant-dsv4` (CPU-only `build-cpu` `-DVLLM_CPP_CUDA=OFF`; NO GPU, NO 90 GB download — the dequant unit gate uses known packed bytes; the GGUF header was HTTP-range-read, no download) | branch `feat/gguf-iquant-dsv4`, base `main` `4d1be010` (confirmed via `git rev-parse HEAD`) | GGUF IQ2_XXS + Q2_K dequant, the DeepSeek-V4-Flash single-Spark GGUF quant-path brick (W1). Owns ONLY the GGUF/quant PATH (NOT the forward — the forward TUs stay owned by `CLAIM-DEEPSEEK-V4-*`): the two `dequantize_row_*` decoders + grid/sign tables in `src/vt/cpu/cpu_quant_dequant.cpp`; the `kQ2_K`/`kIQ2_XXS` vt block dtype registration in `src/vt/dtype.{h,cpp}` + `src/vt/ops.cpp`; the id-16 reader trait in `gguf_reader.cpp` + ids-10/16 dispatch in `gguf_dequant.cpp`; `tests/vllm/test_gguf_dequant.cpp` + `tests/vt/test_ops_quant_traits.cpp`; the two `QUANT-GGUF-*` rows; NEW `.agents/specs/gguf-iquant-dsv4.md`; the V4 GGUF-loadable note on the model-matrix V4 row (row stays SPIKE); the record surfaces. **NON-COLLISION:** additive within the existing GGUF dequant switch + vt block table — does NOT touch any DeepSeek-V4 forward TU (`deepseek_v4.{cpp,h}`/`_dsa`/`_weights`), README, or Metal; the k-quant/NVFP4 decoders are byte-unchanged. | `ACTIVE` | 2026-07-29 — **W1 LANDED + CPU-GATED (foreground, NOT pushed).** IQ2_XXS (id 16, codebook `iq2xxs_grid`+signs+4-bit scale) + Q2_K (id 10, nibble sub-scale/min) ported 1:1 from llama.cpp `ggml-quants.c` `237ad9b96`; both DEQUANT-ONLY (no vec_dot ⇒ route to expand-bf16). `test_gguf_dequant` **15/15·480** (hand-derived literals: IQ2_XXS grid[1] byte0=0x2b→5.375, ksigns[1] flips j=0,7→±3.0, db 0.125/0.375; Q2_K 5.75/-0.25/2.5/0.25) + `test_ops_quant_traits` **9/9·5643** (dequant-only contract). All 7 changed TUs clean under full `-Werror`; the `voxtral.cpp` GCC-13 `-Werror=array-bounds` FP PROVEN pre-existing (fails at base with this diff's `dtype.h` reverted), neutralized only to link the test binaries. **W2 (V4-GGUF loader) DERIVED not landed:** HTTP-range-read the real `UD-IQ2_XXS` header — `general.architecture=deepseek4`, `general.file_type=19` (=IQ2_XXS), `split.tensors.count=1328`, full `deepseek4.*` config-KV schema; the tensor NAME manifest is beyond the CDN range cap + uncached ⇒ the V4 registry GGUF reject STAYS. Residuals: V4 forward (W3-W8, multi-Spark) + the V4-GGUF name map (W2, manifest-blocked) + a vec_dot perf leaf. | +| `CLAIM-EMBEDDINGS-ONE-SURFACE` | `ENG-POOLING-RUNNER` (live engine-step invocation), `SERVE-POOLING-ENDPOINTS` (SPIKE→ACTIVE, `/v1/embeddings`), `MODEL-EMBED-llama-llama-for-causal-lm` (INVENTORIED→ACTIVE; PARTIAL on merge — only the `LlamaModel` membership registered); cross-refs `ENG-POOLER-SEQ` (stays `CLAIM-POOLING`, ops untouched) | Claude Code (fable-5) helper, task #285 | isolated worktree `/home/mudler/_git/vllm.cpp-embeddings-one-surface` (CPU-only; lean per-target builds under disk pressure) | branch `row/EMBEDDINGS-ONE-SURFACE`, base `main` `b44ad337`, DRAFT PR #137 (the reservation) | ARCH-ONE-SURFACE fold ROW 6: embeddings/pooling through the ONE surface. Owns: NEW `src/vllm/model_executor/models/llama_embedding_registry.cpp` + `LoadLlamaModelEmbeddingWeights` (llama_weights.cpp) + `Qwen3DenseModel::ForwardHidden` (qwen3.{h,cpp} additive tail); the ADDITIVE task-gated pooling plumb (`LoadedModel::pooler()`, runner `pooling_runner_`+`pool_tokens`, `Request/EngineCoreRequest::pooling_params`, `ModelRunnerOutput::pooler_output`, scheduler pooling stop, `EngineCoreOutput/RequestOutput::pooling_output`, `LLMEngine::add_pooling_request/embed`, `ResolveAsyncEnabled(is_pooling_model)`); `vllm_embed`/`vllm_embedding_result_free` ABI v15 (vllm.h + vllm_c.cpp incl. the refuse-both-directions guards + the v13 `vllm_complete_tokens` missing-guard fix); `handle_embeddings` + `set_embedder` + task-conditional route (api_server.{h,cpp}) + server main pooling dispatch; NEW fixture `tests/vllm/models/fixtures/llama_embed_e2e` + `scripts/mm/llama_embed_fixture_gen.py` + `tests/vllm/models/test_llama_embedding_fold.cpp`; test/guard updates (test_capi v15 section + floor pin >= 15, test_dlopen symbols, c_header_compile.c, test_api_server embeddings section, test_model_registry/gguf arch pins, check-supported-models ARCH_TOKEN_RE); allowlist row removal + FEATURES/STATUS/BENCHMARKS rows + matrices + specs. **NON-COLLISION:** every engine hook is task-gated on `is_pooling_model`/`pooling_params` (nullopt/false = byte-identical text path); no SACRED path rewritten; no example added. | `ACTIVE` | 2026-08-08 — CPU-LANDED on the branch: fold gate `test_llama_embedding_fold` 4/4-231 (engine path == direct registry path + f64 LAST+normalize ref + chunked is_valid arm), `test_capi` 48/48-462, `test_dlopen` 30/30, server suite 50/50, registry 24/24-820, engine suites green (scheduler 423, llm_engine 204, engine_core 44, output_processor 77, qwen3_forward 1557, async_llm 342, llama_forward 509); 9 mutation kills (floor pin, refuse both directions, route gating both ways, engine-step invocation, scheduler stop, registry info pin, async-off wire). RESIDUAL: real embedding checkpoint + `LLM(task="embed")` oracle cosine. | + diff --git a/.agents/engine-matrix.md b/.agents/engine-matrix.md index 07c09522f..82a97d9b3 100644 --- a/.agents/engine-matrix.md +++ b/.agents/engine-matrix.md @@ -42,11 +42,11 @@ forensics: roadmap_v1.md and the parity ledger. | Sampling and generation | 15 | 4 | 2 | 0 | 0 | 3 | 0 | 1 | 5 | | Structured output and tools | 7 | 0 | 3 | 0 | 0 | 2 | 0 | 0 | 2 | | Speculative decoding | 21 | 0 | 0 | 1 | 0 | 4 | 0 | 4 | 11 | -| Serving, API, CLI, library | 25 | 10 | 2 | 2 | 0 | 4 | 2 | 1 | 4 | +| Serving, API, CLI, library | 25 | 10 | 2 | 1 | 0 | 5 | 2 | 1 | 4 | | LoRA and adapters | 2 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | | Long context and attention | 10 | 5 | 0 | 0 | 1 | 0 | 1 | 0 | 3 | | Loading, tokenizer, config | 9 | 3 | 3 | 0 | 0 | 0 | 1 | 1 | 1 | -| **Total** | **143** | **36** | **16** | **5** | **7** | **20** | **8** | **9** | **41** | +| **Total** | **143** | **36** | **16** | **4** | **7** | **21** | **8** | **9** | **41** | ## Engine core and scheduling @@ -209,9 +209,9 @@ claims it. | `SERVE-E2E-NIGHTLY` | Server conformance and real-model nightly suites for all release gates | T0 | `tests/entrypoints/openai/`; `tests/v1/e2e/`; `.buildkite/test-pipeline.yaml` | current unit/conformance tests only; no scheduled DGX suite | `tests/vllm/entrypoints/openai/test_conformance.cpp:1`; `tests/parity/test_qwen36_paged_engine.cpp:78`; `tests/parity/test_qwen27_paged_engine.cpp:110` | `planned: specs/server-e2e-nightly.md` | `INVENTORIED` | - | | `ENG-RELEASE-BINARIES` | Downloadable host-ABI-specific `vllm-server` bundles: adaptive CPU and fat CUDA primary artifacts, optional per-SM diagnostics, and literal-static feasibility boundary | T0 | vLLM release lanes `.buildkite/release-pipeline.yaml:1-18,34-170` @ `555967922`; release-image dependency boundary `docker/Dockerfile.cpu:262-290` | server target only `examples/CMakeLists.txt:54-64`; CPU per-TU/runtime-dispatch baseline `CMakeLists.txt:870-890`, `src/vt/cpu/cpu_matmul_elem.cpp:553-612`, `src/vt/cpu/cpu_quant_dot_arm.cpp:39-77`; cross-family CUDA fat/per-source-gencode and multi-SM AOT gaps remain; no install/archive/publish implementation | help smoke only `examples/CMakeLists.txt:59-63`; issue `#117`; user-reviewed fat-CUDA/adaptive-CPU matrix and gates in [release-binary-matrix.md](specs/release-binary-matrix.md) | [release-binary-matrix.md](specs/release-binary-matrix.md) | `SPIKE` | `CLAIM-ENG-RELEASE-BINARIES-SPIKE` | | `SERVE-CLI-CHAT` | Interactive `chat` and `complete` commands against a running OpenAI-compatible server, plus preservation of the existing local-model completion invocation | T1 | registration `vllm/entrypoints/cli/main.py:17-37,73-98`; client/model resolution + stream shaping `vllm/entrypoints/cli/openai.py:30-100`; chat `:155-234`; complete `:237-312` at `5559679229` | current in-process completion only `examples/cli/main.cpp:1-207`; remote command implementation absent | C-ABI stream baseline `tests/capi/test_capi.cpp:567-711`; chat-template baseline `tests/capi/test_chat_prompt.cpp:37-89`; command/fake-server tests absent | [cli-chat-complete.md](specs/cli-chat-complete.md) | `ANCHOR-BACKFILL` | `CLAIM-SERVE-CLI-CHAT-SPIKE` | -| `SERVE-POOLING-ENDPOINTS` | Embeddings, pooling, score, rerank, classify HTTP surface (`/v1/embeddings`, `/pooling`, `/score`, `/rerank`, `/classify`). **SPIKED 2026-07-28 (`CLAIM-POOLING`):** the whole pooling task class is scoped in [pooling-task-class.md](specs/pooling-task-class.md) — endpoints depend on the pooling RUNNER (`ENG-POOLING-RUNNER`, W3 LANDED 2026-07-29 — `PoolingRunner` returns pooled data via the model `Pooler`) and a concrete pooling model. Endpoint protocol + handler port is the W4 brick | T2 | `vllm/entrypoints/pooling/embed/api_router.py:28`; `vllm/entrypoints/pooling/scoring/api_router.py:37,71`; `vllm/entrypoints/pooling/classify/api_router.py:26` | - | - | [pooling-task-class.md](specs/pooling-task-class.md) | `SPIKE` | `CLAIM-POOLING` | +| `SERVE-POOLING-ENDPOINTS` | Embeddings, pooling, score, rerank, classify HTTP surface (`/v1/embeddings`, `/pooling`, `/score`, `/rerank`, `/classify`). **SPIKED 2026-07-28 (`CLAIM-POOLING`):** the whole pooling task class is scoped in [pooling-task-class.md](specs/pooling-task-class.md). **`/v1/embeddings` LIVE 2026-08-08 (ARCH-ONE-SURFACE ROW 6, `CLAIM-EMBEDDINGS-ONE-SURFACE`):** task-conditional registration (embed/api_router.py:22-28 mirror; the route exists ONLY on a pooling-model server, and the generate routes do not — both directions socket-404-pinned), OpenAI request/response shape (string-or-array input; `dimensions`/base64/token-arrays are named-residual 400s), handler drives the ONE engine path (`LoadedEngine -> LLMEngine::embed -> registry forward -> PoolingRunner`) — the same path `vllm_embed` (ABI v15) drives. RESIDUALS: `/pooling`, `/score`, `/rerank`, `/classify` (need a classify arch) | T2 | `vllm/entrypoints/pooling/embed/api_router.py:28`; `vllm/entrypoints/pooling/embed/protocol.py:34,173-185`; `vllm/entrypoints/pooling/scoring/api_router.py:37,71`; `vllm/entrypoints/pooling/classify/api_router.py:26` | `src/vllm/entrypoints/openai/api_server.cpp` `handle_embeddings` + the `if (embedder_)` route gate; `examples/server/main.cpp` pooling task dispatch | `tests/vllm/entrypoints/openai/test_api_server.cpp` embeddings section (dispatch shape + socket smoke + BOTH-direction 404 pins) | [embeddings-one-surface.md](specs/embeddings-one-surface.md) | `ACTIVE` | `CLAIM-EMBEDDINGS-ONE-SURFACE` | | `ENG-POOLER-SEQ` | The non-generative POOLER OP — turn hidden states into a pooled embedding/logit row instead of a sampled token. **W1 LANDED + CPU-GATED 2026-07-28 (`CLAIM-POOLING`, NOT pushed):** the sequence pooling methods `CLSPool`/`LastPool`/`MeanPool` (+ `GetSeqPoolingMethod` factory) over a packed `[num_tokens, hidden]` CPU buffer keyed by a minimal `PoolingCursor` (CLS/MEAN reject partial prefill, LAST allows it, MeanPool upcasts to float32) and the activation heads `PoolerIdentity`/`PoolerNormalize` (L2 `F.normalize`)/`PoolerMultiLabelClassify` (sigmoid)/`PoolerClassify` (sigmoid if `num_labels<2` else `softmax`). Unit-gated vs DOUBLE-PRECISION references, RED-first. **W2 LANDED + CPU-GATED 2026-07-29 (`CLAIM-POOLING`, NOT pushed):** the pooler HEADS composite (`EmbeddingPoolerHead` = projector→matryoshka→normalize; `ClassifierPoolerHead` = classifier→`(logit-mean)/sigma`→activation), the `SequencePooler` (method∩head task intersection) + `PoolerForEmbed`/`PoolerForClassify` factories, the `DispatchPooler` groupby-task routing (`ForEmbedding`/`ForSeqCls` + a mixed embed+classify batch + ctor task-support validation), and the `PoolerConfig`/`PoolingParams`/`PoolingParamsUpdate` structs; `test_pooler_heads` 27/27 (240 asserts) vs double-precision refs, RED-first (disable matryoshka slice + logit_mean calibration → 8 cases / 50 asserts fail). RESIDUALS (named, spec §Work breakdown): the endpoints (W4), tokwise `AllPool`/`StepPool` (W5), a concrete pooling MODEL + real-oracle cosine gate (W3-model — see `ENG-POOLING-RUNNER`) | T2 | `vllm/model_executor/layers/pooler/seqwise/methods.py:35-121`; `vllm/model_executor/layers/pooler/activations.py:106-158`; `vllm/model_executor/layers/pooler/seqwise/heads.py:19-196`; `vllm/model_executor/layers/pooler/seqwise/poolers.py:41-138`; `vllm/model_executor/layers/pooler/special.py:23-140`; `vllm/model_executor/layers/pooler/common.py:12-30`; `vllm/pooling_params.py:35-70`; `vllm/config/pooler.py:16-90`; `vllm/v1/pool/metadata.py:13-71`; `tests/model_executor/layers/test_pooler_methods.py`, `tests/model_executor/layers/test_pooler_activations.py`, `tests/model_executor/layers/test_pooler_heads.py` | `include/vllm/model_executor/layers/pooler/{methods,activations,pooling_metadata,common,pooling_params,pooler_config,heads,poolers,dispatch_pooler}.h` + `src/vllm/model_executor/layers/pooler/{methods,activations,heads,poolers,dispatch_pooler}.cpp` — anchor `src/vllm/model_executor/layers/pooler/dispatch_pooler.cpp:13` | `tests/vllm/model_executor/layers/pooler/test_pooler.cpp` (CLS/LAST/MEAN + factory + activations, 50 asserts) + `test_pooler_heads.cpp` (Embedding/Classifier heads + SequencePooler + DispatchPooler, 240 asserts) — anchor `tests/vllm/model_executor/layers/pooler/test_pooler.cpp:81` | [pooling-task-class.md](specs/pooling-task-class.md) | `ACTIVE` | `CLAIM-POOLING` | -| `ENG-POOLING-RUNNER` | The pooling RUNNER path — where the generation runner SAMPLES a token, the pooling runner applies the model's `Pooler` to the last hidden state and returns the POOLED DATA (embedding vector / classification logit row). **W3 LANDED + CPU-GATED 2026-07-29 (`CLAIM-POOLING`, NOT pushed):** `PoolingRunner` over a packed `[num_tokens, hidden]` last-hidden-state buffer + a `PoolingMetadata` — `Pool()` delegates to the model pooler (`DispatchPooler.ForEmbedding`), `GetSupportedTasks()`, `ComputeValid()` (`seq_lens==prompt_len`). GATE: a STRUCTURAL cosine-parity gate — the runner's embedding vs an independent double-precision LAST+normalize reference is cosine≈1 (5 cases / 14 asserts), RED-first (CLS-instead-of-LAST drops cosine <0.5; disable normalize → 2 unit-L2 asserts fail). GENERALIZATION DEVIATION: upstream `pooling_runner.py` hardcodes LAST+normalize; we route through the model `Pooler` (the general bert.py path), strictly more capable. HONEST RESIDUAL (named): the REAL-model oracle cosine gate (`vllm.LLM(task="embed").encode`) needs a registered concrete embedding model's forward — no such model is registered yet (W3-model), so no cosine-vs-oracle number is fabricated. The InputBatch→PoolingMetadata construction (logits_indices gather) rides the endpoint brick (W4) | T2 | `vllm/v1/worker/gpu/pool/pooling_runner.py:18-46`; `vllm/tasks.py:10`; `tests/models/language/pooling/test_embedding.py` (real-oracle gate, DEFERRED) | `include/vllm/v1/worker/gpu/pool/pooling_runner.h` + `src/vllm/v1/worker/gpu/pool/pooling_runner.cpp` — anchor `src/vllm/v1/worker/gpu/pool/pooling_runner.cpp:11` | `tests/vllm/v1/worker/gpu/pool/test_pooling_runner.cpp` (runner path + structural cosine gate, 14 asserts, RED-first) — anchor `tests/vllm/v1/worker/gpu/pool/test_pooling_runner.cpp:136` | [pooling-task-class.md](specs/pooling-task-class.md) | `ACTIVE` | `CLAIM-POOLING` | +| `ENG-POOLING-RUNNER` | The pooling RUNNER path — where the generation runner SAMPLES a token, the pooling runner applies the model's `Pooler` to the last hidden state and returns the POOLED DATA (embedding vector / classification logit row). **W3 LANDED + CPU-GATED 2026-07-29 (`CLAIM-POOLING`, NOT pushed):** `PoolingRunner` over a packed `[num_tokens, hidden]` last-hidden-state buffer + a `PoolingMetadata` — `Pool()` delegates to the model pooler (`DispatchPooler.ForEmbedding`), `GetSupportedTasks()`, `ComputeValid()` (`seq_lens==prompt_len`). GATE: a STRUCTURAL cosine-parity gate — the runner's embedding vs an independent double-precision LAST+normalize reference is cosine≈1 (5 cases / 14 asserts), RED-first (CLS-instead-of-LAST drops cosine <0.5; disable normalize → 2 unit-L2 asserts fail). GENERALIZATION DEVIATION: upstream `pooling_runner.py` hardcodes LAST+normalize; we route through the model `Pooler` (the general bert.py path), strictly more capable. HONEST RESIDUAL (named): the REAL-model oracle cosine gate (`vllm.LLM(task="embed").encode`) needs a registered concrete embedding model's forward — no such model is registered yet (W3-model), so no cosine-vs-oracle number is fabricated. **LIVE IN THE ENGINE STEP 2026-08-08 (ARCH-ONE-SURFACE ROW 6, `CLAIM-EMBEDDINGS-ONE-SURFACE`):** `GPUModelRunner` builds a `PoolingRunner` iff the loaded model registration declares `is_pooling_model` (gpu/model_runner.py:368-369 mirror) and `sample_tokens` routes to `pool_tokens()` — pooled data instead of sampled tokens (model_runner.py:1586-1607), validity = the discard predicate (`seq_len < num_tokens` == upstream is_valid, pooling_runner.py:40-41); the scheduler finishes a pooling request on pooled output (scheduler.py:1718-1721) and `EngineCoreOutput.pooling_output` carries it out; async scheduling resolves OFF for pooling models (config/vllm.py:1068-1073, the landed ResolveAsyncScheduling arm now WIRED at model_loader.cpp). First registered pooling arch: `LlamaModel` (`MODEL-EMBED-llama-llama-for-causal-lm`). The fold gate re-anchors the lane's cosine gate THROUGH the registry/runner path: engine path == direct `ModelRegistry::Forward`+`PoolingRunner` path, identical vectors + f64 LAST+normalize reference + chunked-prefill arm (`test_llama_embedding_fold` 4/4-231). REMAINING RESIDUAL: the REAL-model `vllm.LLM(task="embed").encode` oracle cosine (synthetic fixture only — no number fabricated) | T2 | `vllm/v1/worker/gpu/pool/pooling_runner.py:18-46`; `vllm/v1/worker/gpu/model_runner.py:368-369,1586-1607`; `vllm/v1/core/sched/scheduler.py:1718-1721,1837`; `vllm/tasks.py:10`; `tests/models/language/pooling/test_embedding.py` (real-oracle gate, DEFERRED) | `include/vllm/v1/worker/gpu/pool/pooling_runner.h` + `src/vllm/v1/worker/gpu/pool/pooling_runner.cpp:11`; live invocation `src/vllm/v1/worker/gpu/runner.cpp` `pool_tokens` + the `pooling_runner_` ctor gate; scheduler stop `src/vllm/v1/core/sched/scheduler.cpp` pooling elif | `tests/vllm/v1/worker/gpu/pool/test_pooling_runner.cpp:136` (structural cosine gate) + `tests/vllm/models/test_llama_embedding_fold.cpp:206` (registry/engine-path arm, 4/4-231, mutation-killed x9) | [pooling-task-class.md](specs/pooling-task-class.md) + [embeddings-one-surface.md](specs/embeddings-one-surface.md) | `ACTIVE` | `CLAIM-EMBEDDINGS-ONE-SURFACE` | | `SERVE-RESPONSES-MESSAGES` | Responses, Anthropic messages, audio | T2 | `vllm/entrypoints/openai/responses/api_router.py:48`; `vllm/entrypoints/anthropic/api_router.py:49`; `vllm/entrypoints/speech_to_text/transcription/api_router.py:1` | - | - | `planned: specs/responses-messages-endpoints.md` | `INVENTORIED` | - | | `SERVE-ADMIN` | Abort-requests, sleep, pause/resume, profiling, RL weight updates. **`/abort_requests` LANDED + CPU-GATED 2026-07-28 (`CLAIM-C8-SERVE-ENDPOINTS`, NOT pushed):** `POST /abort_requests` (from the dev/rlhf admin router) parses `{request_ids:[...]}` and aborts exactly those (external) ids via an injected abort callback wired to the engine abort path (`AsyncLLM::abort`); an empty/missing list means "abort all in-flight" (the callback decides). Response `{"status":"aborted","aborted":}`; malformed JSON → 400 `{"detail":"Invalid JSON format"}`; abort failure → 500 `{"error":...}` — all three shapes mirror the upstream router verbatim. ADDITIVE + opt-in (route registered only when the abort callback is attached → 404 otherwise). **PRODUCTION `main.cpp` WIRING LANDED + CPU-GATED 2026-07-28 (`CLAIM-C8-SERVE-PROD-WIRING`, NOT pushed):** the shipped `vllm-server` binary now wires `/abort_requests` to the LIVE `AsyncLLM::abort` through the shared `ConfigureUtilityEndpoints` seam, DEV-mode gated behind the new `--enable-server-dev-mode` flag — mirroring vLLM registering the dev/rlhf router only under `if envs.VLLM_SERVER_DEV_MODE` (api_server.py:238; envs.py:157 default 0). Explicit-id abort tears the request down and reports the exact drop in unfinished requests (before−after); empty `request_ids` (abort-ALL) reports 0 — NAMED RESIDUAL (AsyncLLM exposes no active-request-id accessor). RESIDUAL: the abort-ALL enumeration (missing `AsyncLLM::active_request_ids()`); `/sleep`/`/wake_up`/`/is_sleeping`, `/pause`/`/resume`, `/start_profile`/`/stop_profile`, weight-update/EP endpoints still INVENTORIED | T2/T3 | `vllm/entrypoints/serve/dev/rlhf/api_router.py:94-138` (abort_requests); dev-mode gate `vllm/entrypoints/openai/api_server.py:238-240`, `vllm/entrypoints/serve/__init__.py:35`, `vllm/envs.py:157`; `vllm/entrypoints/serve/dev/sleep/api_router.py:21`; `vllm/entrypoints/serve/dev/rlhf/api_router.py:29,74,136`; `vllm/entrypoints/serve/profile/api_router.py:21` | handler `src/vllm/entrypoints/openai/api_server.cpp:488` (`handle_abort_requests`); opt-in setter `include/vllm/entrypoints/openai/api_server.h:156` (`set_abort_requests`); production seam `src/vllm/entrypoints/openai/api_server.cpp` (`ConfigureUtilityEndpoints`, before/after delta-count) + `examples/server/main.cpp` (`--enable-server-dev-mode`); engine abort path `include/vllm/v1/engine/async_llm.h:115` (`abort`) | `tests/vllm/entrypoints/openai/test_api_server.cpp:1104` (shape + callback wiring: explicit ids passthrough, empty→abort-all branch, malformed→400),`:1143` (aborts an in-flight AsyncLLM request → `has_unfinished_requests()` false),`:1250` (opt-in route gate: 404 no-callback → 200 attached, RED-first),`:1319` (**production seam: dev-mode gate 404→200, live abort exact delta-count==1, empty→0**) — in the 32/32 / 420-assertion suite | [admin-endpoints.md](specs/admin-endpoints.md) | `ANCHOR-BACKFILL` | `CLAIM-C8-SERVE-PROD-WIRING` | | `SERVE-VIDEOS-OAI` | `/v1/videos` in OpenAI's Sora WIRE SHAPE, over the vLLM-Omni-derived job endpoints. **CPU-LANDED + GATED 2026-08-06 (`CLAIM-SERVE-VIDEOS-OAI`):** the OpenAI request spellings (`model`, `size` "WxH", `seconds` as a number OR the string enum OpenAI actually types) parse as ALIASES onto the existing native members, NATIVE-wins precedence applied PER-AXIS, both spellings validated either way so a malformed alias is a 400 even when overridden; an unserved `model` is a job `warning` echoed for the job's whole life, never a rejection (a Sora client cannot know the local model's name); and `GET /v1/videos/{id}/content` serves the finished MP4 (404 unknown / 409 unfinished / 500 failed / 500 vanished), without which a caller could start and poll a job but never fetch the result over HTTP. All four routes still register ONLY with a `VideoRunner` attached, now gated over a REAL socket. RESIDUALS (named): OpenAI's status vocabulary/id shape is not mirrored; reference conditioning (`input_reference`, the `metadata` video/audio references) is a stacked follow-up row; the real-weights leg rides the H3 GB10/disk window. | T2 | OpenAI Sora video API (`POST /v1/videos`, `GET /v1/videos/{video_id}/content`); vLLM-Omni `vllm/entrypoints/openai/video/api_router.py` (the async/sync job pair we already mirror) | `include/vllm/entrypoints/openai/video_api.h:31`; `src/vllm/entrypoints/openai/video_api.cpp:98`; `src/vllm/entrypoints/openai/api_server.cpp:279` | `tests/vllm/entrypoints/openai/test_video_api.cpp:64`; `tests/vllm/entrypoints/openai/test_api_server.cpp:1751` | [minimax-h3.md §9](specs/minimax-h3.md) | `ACTIVE` | `CLAIM-SERVE-VIDEOS-OAI` | diff --git a/.agents/model-matrix.md b/.agents/model-matrix.md index 4bd380985..cc9d8d732 100644 --- a/.agents/model-matrix.md +++ b/.agents/model-matrix.md @@ -43,9 +43,9 @@ Rollup by lifecycle state (must equal the detailed per-state row counts): | State | Rows | |---|---| -| INVENTORIED | 315 | +| INVENTORIED | 314 | | PARTIAL | 19 | -| ACTIVE | 9 | +| ACTIVE | 10 | | SPIKE | 6 | | BLOCKED | 5 | | DONE | 3 | @@ -53,7 +53,7 @@ Rollup by lifecycle state (must equal the detailed per-state row counts): | GATING | 1 | | **Total** | **361** | -Engaged architectures (the 47 non-`INVENTORIED` rows): +Engaged architectures (the 48 non-`INVENTORIED` rows): | Support | Architecture | Family / example | Status | Row | |---|---|---|---|---| @@ -67,6 +67,7 @@ Engaged architectures (the 47 non-`INVENTORIED` rows): | 🚧 | `ParakeetForRNNT` / `ParakeetForTDT` (transducer heads) | Parakeet RNN-T and TDT ASR: the decode half of the same family, likewise an audio COMPONENT and not a registry arch (vLLM has no transducer call site at all) | P6 LANDED 2026-08-07 on CPU: LSTM prediction network, joint network, TDT duration head and the greedy transducer loop, gated against dumped HuggingFace `ParakeetForRNNT` / `ParakeetForTDT` oracles from transformers `main` with the emitted sequence and per-step durations EXACT, plus an independent in-test LSTM reference. Real transcripts verified on `nvidia/parakeet-rnnt-0.6b` and `-tdt-0.6b-v3`, token ids EXACT vs HF `generate()` end to end. **Corrects the P4 record**, which called the transducer unmirrored off the locally installed transformers 5.3.0. No CUDA, no GPU suite, no aarch64, no speed number; `.nemo`-only checkpoints out of reach. **ONE-SURFACE ROW 1 (2026-08-07): `ParakeetForRNNT`/`ParakeetForTDT` registered** (BEYOND-PIN, transcription-only refuse-by-task) and served through the same `vllm_transcribe` / `/v1/audio/transcriptions` / thin-client surface as the CTC head | `MODEL-AUDIO-PARAKEET-TRANSDUCER` | | ✅ | `OPTForCausalLM` | OPT-125m | STRICT token-exact 6/6 vs vLLM 0.25.0; speed pending | `MODEL-TEXT-opt-optfor-causal-lm` | | ✅ | `DeepseekV2ForCausalLM` | DeepSeek-V2-Lite (MLA) | SACRED gate 8/8 token-exact vs vLLM 0.25.0; speed short (attributed, W9) | `MODEL-TEXT-deepseek-v2-deepseek-v2-for-causal-lm` | +| 🚧 | `LlamaModel` (embedding conversion) | The first POOLING arch (ARCH-ONE-SURFACE ROW 6): upstream `_EMBEDDING_MODELS` maps `LlamaModel` onto the Llama backbone via `as_embedding_model` (registry.py:230 + adapters.py:230) | LIVE 2026-08-08 on `row/EMBEDDINGS-ONE-SURFACE` (PR #137): `is_pooling_model=true`, text paths refuse by task; `vllm_embed` (ABI v15) + task-conditional `/v1/embeddings`; fold gate 4/4-231 on the committed synthetic fixture (engine path == direct registry path + f64 LAST+normalize ref). Residual: real embedding checkpoint + `LLM(task="embed")` oracle cosine; the other 7 memberships unregistered | `MODEL-EMBED-llama-llama-for-causal-lm` | | ✅ | `LlamaForCausalLM` | Llama-3.2-1B dense (+ Yi + `InternLM3ForCausalLM` aliases) | STRICT token-exact 16/16 vs vLLM 0.25.0; speed pending. Llama-alias checkpoints gated 2026-07-26: Yi (`01-ai/Yi-Coder-1.5B-Chat`, arch=LlamaForCausalLM, zero delta) 16/16; InternLM3 (`internlm3-8b-instruct`, one alias line, plain-Llama+dynamic rope) 16/16 — CLOSES the recent-dense TEXT tier | `MODEL-TEXT-llama-llama-for-causal-lm` | | ✅ | `MistralForCausalLM` | Mistral-7B-v0.3 dense | full paged-engine SACRED gate 16/16 vs vLLM 0.25.0; speed pending | `MODEL-TEXT-mistral-mistral-for-causal-lm` | | ✅ | `Qwen3_5MTP` | Qwen3.5 MTP draft (spec-decode) | `DONE` 2026-07-26: k=1 MTP spec-decode e2e on the 27B GDN hybrid — three-way token-exact at c1 (our-ON == vLLM `--speculative-config mtp` == our-OFF, acceptance 16/16), c1 above vLLM every-axis + c2-c8 on-par-or-above, mixed-batch concurrency bit-exact, server/CLI/C-ABI `--speculative-config`; spec-OFF byte-identical | `MODEL-SPEC-qwen3-5-mtp-qwen3-5-mtp` | @@ -292,7 +293,7 @@ Transformers compatibility is capability-driven and excluded from finite counts. | `MODEL-EMBED-bert-with-rope-gte-new-model` | `GteNewModel` | `registry.py:221`; `vllm/model_executor/models/bert_with_rope.py::GteNewModel` | embedding / text | encoder attention; pooler; FusedMoE/grouped GEMM | ☐ required | `INVENTORIED` | none | unassigned | | `MODEL-EMBED-jina-jina-embeddings-v5-model` | `JinaEmbeddingsV5Model` | `registry.py:222`; `vllm/model_executor/models/jina.py::JinaEmbeddingsV5Model` | embedding / text | encoder attention; pooler | ☐ required | `INVENTORIED` | none | unassigned | | `MODEL-EMBED-llama-llama-bidirectional-model` | `LlamaBidirectionalModel` | `registry.py:223`; `vllm/model_executor/models/llama.py::LlamaBidirectionalModel` | embedding / text | encoder attention; pooler; sliding-window attention | ☐ required | `INVENTORIED` | none | unassigned | -| `MODEL-EMBED-llama-llama-for-causal-lm` | `LlamaModel`, `CwmForCausalLM`, `InternLM3ForCausalLM`, `IQuestCoderForCausalLM`, `LlamaForCausalLM`, `LLaMAForCausalLM`, `TeleChat3ForCausalLM`, `MistralModel` | `registry.py:224-231`; `vllm/model_executor/models/llama.py::LlamaForCausalLM` | embedding / text | encoder attention; pooler; sliding-window attention | ☐ required | `INVENTORIED` | none | unassigned | +| `MODEL-EMBED-llama-llama-for-causal-lm` | `LlamaModel`, `CwmForCausalLM`, `InternLM3ForCausalLM`, `IQuestCoderForCausalLM`, `LlamaForCausalLM`, `LLaMAForCausalLM`, `TeleChat3ForCausalLM`, `MistralModel` | `registry.py:224-231`; `vllm/model_executor/models/llama.py::LlamaForCausalLM` | embedding / text | encoder attention; pooler; sliding-window attention | [embeddings-one-surface](specs/embeddings-one-surface.md) | `ACTIVE` | ARCH-ONE-SURFACE ROW 6 (2026-08-08, in flight on `row/EMBEDDINGS-ONE-SURFACE` PR #137): the `LlamaModel` membership is REGISTERED + LIVE (`as_embedding_model` mirror, adapters.py:230 — `is_pooling_model=true`, bare-prefix loader, pooling forward `Qwen3DenseModel::ForwardHidden`, engine-step `pool_tokens`, `vllm_embed` ABI v15 + `/v1/embeddings`); registration `src/vllm/model_executor/models/llama_embedding_registry.cpp:132`; fold gate `tests/vllm/models/test_llama_embedding_fold.cpp:206` 4/4-231 on the committed synthetic fixture. RESIDUALS: the other 7 memberships (incl. `MistralModel`) unregistered; REAL-checkpoint (e5-mistral class) + `LLM(task="embed")` oracle cosine gate not run (synthetic-fixture arm only; no cosine-vs-oracle number fabricated) | `CLAIM-EMBEDDINGS-ONE-SURFACE` | | `MODEL-EMBED-modernbert-modern-bert-model` | `ModernBertModel` | `registry.py:232`; `vllm/model_executor/models/modernbert.py::ModernBertModel` | embedding / text | encoder attention; pooler; sliding-window attention | ☐ required | `INVENTORIED` | none | unassigned | | `MODEL-EMBED-bert-with-rope-nomic-bert-model` | `NomicBertModel` | `registry.py:233`; `vllm/model_executor/models/bert_with_rope.py::NomicBertModel` | embedding / text | encoder attention; pooler; FusedMoE/grouped GEMM | ☐ required | `INVENTORIED` | none | unassigned | | `MODEL-EMBED-phi3-phi3-for-causal-lm` | `Phi3ForCausalLM` | `registry.py:234`; `vllm/model_executor/models/phi3.py::Phi3ForCausalLM` | embedding / text | encoder attention; pooler | ☐ required | `INVENTORIED` | none | unassigned | diff --git a/.agents/specs/embeddings-one-surface.md b/.agents/specs/embeddings-one-surface.md new file mode 100644 index 000000000..03ad5705c --- /dev/null +++ b/.agents/specs/embeddings-one-surface.md @@ -0,0 +1,180 @@ +# Embeddings/pooling through the ONE surface (`ARCH-ONE-SURFACE` fold ROW 6) + +Row: `ARCH-ONE-SURFACE` leaf, branch `row/EMBEDDINGS-ONE-SURFACE` (task #285). +The audit found the engine-side pooler EXISTS (`ENG-POOLER-SEQ` W1/W2 ops + +`ENG-POOLING-RUNNER` W3 `PoolingRunner`) but is never invoked live: every +registered arch has `is_pooling_model=false`, `/v1/embeddings` is an explicit +residual, and the ABI has no embed entry. This row folds it live: +registry arch -> engine-step invocation -> `vllm_embed` (ABI v15) -> +live `/v1/embeddings`. + +## Scope + +Rows: `ENG-POOLING-RUNNER` (live engine-step invocation), +`SERVE-POOLING-ENDPOINTS` (`/v1/embeddings`), +`MODEL-EMBED-llama-llama-for-causal-lm` (the `LlamaModel` membership), +cross-ref `ENG-POOLER-SEQ` (landed ops, untouched). + +- **In.** (W1) Register the smallest upstream-mirrored embedding arch the + existing code supports: **`LlamaModel`** — upstream `_EMBEDDING_MODELS` + maps `"LlamaModel": ("llama", "LlamaForCausalLM")` + (`vllm/model_executor/models/registry.py:230`) and converts via + `as_embedding_model` (`adapters.py:230`): the CAUSAL backbone forward with + the lm_head removed + `DispatchPooler.for_embedding` (LAST pooling default, + `interfaces_base.py:160`; normalize head). NO new model is built — the + registered forward IS the shared `Qwen3DenseModel` machinery Llama already + routes (`llama.h:39-40`), minus lm_head, plus the landed `DispatchPooler`. + (W1) Invoke the landed `PoolingRunner` in the ENGINE STEP for pooling-task + requests: the runner builds a `PoolingRunner` iff the loaded model is a + pooling model (mirror `gpu/model_runner.py:368-369`) and `sample_tokens` + routes to a pooling branch that returns POOLED DATA instead of sampled + tokens (mirror `gpu/model_runner.py:1586-1607` + `pool/pooling_runner.py:29-42`); + the scheduler finishes a pooling request as soon as pooled output exists + (mirror `v1/core/sched/scheduler.py:1718-1721`), the output carries it to the + frontend (`scheduler.py:1837`), and async scheduling resolves OFF for pooling + models (mirror `vllm/config/vllm.py:1068-1073`, our + `SchedulerConfig::ResolveAsyncScheduling` is_pooling_model arm — already + landed, now WIRED). Text-generation on the pooling arch and embed on a text + arch refuse cleanly both directions (the #121 refuse-by-task precedent). + (W2) `vllm_embed` on `include/vllm.h`: engine handle + text(s) in, float + vector(s) out, explicit free, `vllm_last_error`; `VLLM_ABI_VERSION` 14->15, + floor pin advanced. (W3) live `/v1/embeddings` (OpenAI shape), registered + task-conditionally; socket-level 404 pins BOTH directions. (W4) allowlist + row removed, FEATURES row -> reachable, records. +- **Out (named residuals).** A REAL embedding checkpoint (e5-mistral / + Llama-embed class) through the fold on real hardware — not fetchable + CPU-side in this session; the committed synthetic-fixture arm is the gate + (the #121 precedent). `MistralModel`/`Qwen2Model`/`Gemma2Model` etc. + aliases (upstream registry.py:215-260) — additive follow-ups. Matryoshka + `dimensions` + `encoding_format` on the endpoint/ABI; `/pooling`, `/score`, + `/rerank`, `/classify`; tokwise pooling (W5); classify heads live-wiring. + `vllm_embed` batches sequentially through the synchronous engine (no + AsyncLLM fan-out). + +## Upstream chain (pinned `${VLLM_SOURCE}` @ 555967922, 0.26.0.dev0) + +- Task selection: `LLM.embed` -> `pooling_task="embed"` + (`vllm/entrypoints/pooling/offline.py:47-119`); `--runner pooling` + resolution `vllm/config/model.py:1008-1030`; `convert="embed"` default for + pooling runners `model.py:1058-1060`; arch->pooling class + `registry.py:215-260` (`"LlamaModel": ("llama", "LlamaForCausalLM")` :230); + `as_embedding_model` wrap `adapters.py:230-261` (pooler = + `DispatchPooler.for_embedding`, :257), lm_head replaced by a missing-layer + stage (`adapters.py:135-151`), checkpoint loadable from BOTH `*ForCausalLM` + and bare `*Model` prefixes (`adapters.py:178-181` candidate_prefixes + `["", "model."]`), LAST default (`interfaces_base.py:160`). +- Engine step: `PoolingRunner` built iff pooling model + (`v1/worker/gpu/model_runner.py:368-369`); pool-instead-of-sample + (`model_runner.py:1586-1607`); `pool()` = gather at `logits_indices` + + normalize, `is_valid = seq_lens == prompt_len` + (`pool/pooling_runner.py:29-42`). +- Scheduler: pooling stops as soon as there is output + (`v1/core/sched/scheduler.py:1718-1721`), `pooling_output` on the + EngineCoreOutput (:1837), emit condition includes pooler output (:1792); + `check_stop` asserts non-pooling (`sched/utils.py:95`). +- Config: async scheduling disabled for pooling (`config/vllm.py:1068-1073`); + decoder+LAST pooling SUPPORTS chunked prefill and prefix caching + (`config/model.py:1883-1902,1929-1948`) so those defaults stay untouched. +- Endpoint: `POST /v1/embeddings` (`entrypoints/pooling/embed/api_router.py:28-43`), + handler absent => "does not support" (:22-25); response shape + `entrypoints/pooling/embed/protocol.py` (OpenAI list/data/usage). + +## Our baseline + +- Landed, test-only: `layers/pooler/*` (methods/activations/heads/poolers/ + dispatch_pooler), `v1/worker/gpu/pool/pooling_runner.{h,cpp}`, + `tests/vllm/v1/worker/gpu/pool/test_pooling_runner.cpp` (STRUCTURAL + double-precision LAST+normalize cosine gate — NOT recorded-oracle fixtures; + the qwen36_embed goldens are token-embedding LOOKUP goldens, unrelated). +- `ModelInfo.is_pooling_model` exists (all false); + `SchedulerConfig::ResolveAsyncScheduling(is_pooling_model)` arm landed but + passed `false` at the only call site (`model_loader.cpp:552`). +- Engine plumb points already marked: `scheduler.cpp` "DEFERRED: pooling + stop.", `output_processor.cpp:216,433` pooling-deferred markers, + `input_batch.h:66`, `llm_engine.h` deferred list. +- Precedents: #121 (Parakeet: refuse-by-task registry + capi guard + + task-conditional route + socket 404 pins + committed tiny fixtures), #123 + route-table 404s, #136 append-only ABI field growth. + +## Port map + +| Upstream | Local | Notes | +|---|---|---| +| `adapters.py:230-261` + `registry.py:230` | `src/vllm/model_executor/models/llama_embedding_registry.cpp` (NEW TU) | `REGISTER_VLLM_MODEL(.., "LlamaModel", ..)`, `is_pooling_model=true`, `is_text_generation_model=false`; loader accepts both name prefixes, never loads lm_head; LoadedModel owns `DispatchPooler::ForEmbedding(cfg, kLast)` | +| model returns hidden states (no lm_head) | `Qwen3DenseModel::ForwardHidden` (additive in `qwen3.h`/`qwen3.cpp`) | shared ForwardLayers tail gains a return-hidden arm; text callers byte-identical | +| `VllmModelForPooling.pooler` | `LoadedModel::pooler()` virtual (default nullptr) | additive on the type-erased base | +| `model_runner.py:368-369,1586-1607` | `GPUModelRunner::pooling_runner_` + pool branch in `sample_tokens` (`runner.cpp`) | ADDITIVE + model-task-gated; text path untouched | +| `outputs.py ModelRunnerOutput.pooler_output` | `ModelRunnerOutput::pooler_output` (`v1/engine/types.h`) | `vector>>`, empty on generation steps | +| `request.py pooling_params` | `Request::pooling_params` + `EngineCoreRequest::pooling_params` | `optional`, nullopt = generation, byte-identical | +| `scheduler.py:1718-1721,1792,1837` | the marked elif in `Scheduler::update_from_output` | pooling stop + `EngineCoreOutput::pooling_output` | +| `output_processor.py` pooling branch | `RequestState::make_request_output` + `process_outputs` pooling arm | `RequestOutput::pooling_output` (optional vector; deviation: no separate PoolingRequestOutput class, recorded) | +| `offline.py LLM.embed` | `LLMEngine::add_pooling_request` + `embed()` driver | tokens overload + step loop | +| `config/vllm.py:1068-1073` | pass `info.is_pooling_model` at `model_loader.cpp:552` | wires the landed arm | +| no upstream C ABI (llama.h idiom) | `vllm_embed` / `vllm_embedding_result(_free)`, ABI v15 | `src/capi/vllm_c.cpp`; refuse both directions | +| `pooling/embed/api_router.py:28` + protocol | `handle_embeddings` in `api_server.{h,cpp}` + `set_embedding`; server main task dispatch | task-conditional route registration, 404 both ways | + +## Tests to port + +- `tests/models/language/pooling/test_embedding.py` (real-oracle cosine) -> + the registry-anchored fold arm below; the REAL-checkpoint leg stays the named + residual (no cosine-vs-oracle number fabricated). +- `tests/entrypoints/pooling/embed/*` (endpoint shape) -> + `tests/vllm/entrypoints/openai/test_api_server.cpp` embeddings section. + +## Gates + +- Fold gate `tests/vllm/models/test_llama_embedding_fold.cpp`: committed tiny + synthetic `LlamaModel` checkpoint fixture (deterministic generator script, + #121 precedent) driven through `LoadedEngine::FromModelDir` -> + `add_pooling_request` -> `step` (the REGISTRY/RUNNER path), asserting + (a) IDENTICAL vectors vs the direct `ModelRegistry::Forward`+`PoolingRunner` + path, (b) the pooling lane's double-precision LAST+normalize reference + (cosine ~= 1, unit L2) — the lane's cosine gate now RUNS THROUGH the + registry/runner; (c) refuse-both-directions. +- `test_pooling_runner.cpp` continues to pass unchanged (the op-level gate). +- `test_capi`: v15 floor pin (>= 15), `vllm_embed` bad-path contract + the + REAL fixture-checkpoint smoke through `vllm_engine_load`+`vllm_embed`, + refuse-both-directions pins. +- `test_openai_api_server`: `/v1/embeddings` handler shape + socket-level 404 + pins BOTH directions (route absent on text servers; generate routes absent + on embedding servers). +- `vllm_capi_c_check` (c_header_compile.c references the new symbols). +- Full CPU `-Werror` build; `scripts/agent-preflight.sh` EXIT=0; + mutation-verify every pin RED-first. + +## Dependencies + +- Landed pooling ops (`ENG-POOLER-SEQ` W1/W2) + `PoolingRunner` (W3); the + shared dense backbone (`LlamaModel == Qwen3DenseModel`); the registry, capi + and server seams; the #121/#123/#136 fold precedents. + +## Risks / decisions + +- Depends only on landed code (pooler ops, runner, registry, capi, server). +- Risk: the pooling branch must not perturb the SACRED text path — every hook + is task-gated on `is_pooling_model` (registration-time constant) or + `pooling_params.has_value()`; default nullopt/false everywhere. +- Risk: chunked prefill of pooling prompts — handled by the same + discard/is_valid predicate the text path uses (`seq_len < num_tokens`), + mirroring `pooling_runner.py:40-41`; pooled output only when fully + prefilled. +- Recorded deviation: `vllm_embed`/`/v1/embeddings` drive the SYNCHRONOUS + LLMEngine (embed is blocking request/response); AsyncLLM stays + generation-only. + +## Work breakdown + +1. **W1 — registry + runner (landed on this row):** `LlamaModel` arch TU + + bare-prefix loader + `ForwardHidden` pooling forward; `PoolingRunner` + invoked task-gated in the engine step; scheduler pooling stop; refusals + both directions. +2. **W2 — ABI (landed on this row):** `vllm_embed` + `vllm_embedding_result_free`, + VLLM_ABI_VERSION 15, floor pin advanced, strict-C references, dlopen symbols. +3. **W3 — server (landed on this row):** task-conditional `/v1/embeddings` + + both-direction socket 404 pins; server main pooling dispatch. +4. **W4 — guard/records (landed on this row):** allowlist row removed, + FEATURES/STATUS/BENCHMARKS/matrices/specs updated. +5. **W-next (residuals, unclaimed):** real embedding checkpoint + + `LLM(task="embed")` oracle cosine; `MistralModel`/other membership aliases; + `/pooling`+score/rerank/classify; matryoshka `dimensions`/base64/token-array + inputs; tokwise pooling (W5 of the pooling lane). diff --git a/.agents/specs/one-surface-abi.md b/.agents/specs/one-surface-abi.md index 5997426bd..13fdd4a21 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 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; PR #139 follow-up removes PR #136's seven shared CUDA literals by resolving the stable public name through the platform registry and propagating its `DeviceType` (DSR 39 -> 32, `kcuda=0`, baseline/allowlist unchanged). 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.** +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; PR #139 follow-up removes PR #136's seven shared CUDA literals by resolving the stable public name through the platform registry and propagating its `DeviceType` (DSR 39 -> 32, `kcuda=0`, baseline/allowlist unchanged). 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. ROW 6 (embeddings/pooling) LANDED 2026-08-08 (`row/EMBEDDINGS-ONE-SURFACE`, task #285, PR #137): ABI v15 `vllm_embed`/`vllm_embedding_result_free` over the SAME registry-forward + PoolingRunner engine step the live task-conditional `/v1/embeddings` drives — the first registered POOLING arch (`LlamaModel`, the upstream `_EMBEDDING_MODELS` registry.py:230 + `as_embedding_model` adapters.py:230 mirror: bare-prefix loader, NO lm_head, LAST+normalize `DispatchPooler`), `is_pooling_model=true` with refuse-by-task BOTH directions (registry + capi + route-table 404 pins both ways), the landed `PoolingRunner` invoked task-gated where the sampler would run (gpu/model_runner.py:368-369 + 1586-1607 mirror; scheduler stop scheduler.py:1718-1721; async OFF for pooling config/vllm.py:1068-1073), and the pooling lane's cosine gate RE-ANCHORED THROUGH the registry/runner path (`test_llama_embedding_fold` 4/4-231: full-engine path == direct registry path IDENTICAL vectors + f64 LAST+normalize reference + chunked-prefill is_valid arm, on the committed tiny synthetic fixture). abi-capability allowlist 2 -> 1 (mm-input is the last row). NAMED RESIDUALS: real embedding checkpoint (e5-mistral class) + `LLM(task="embed")` oracle cosine; score/rerank/classify; matryoshka/base64/token-array inputs.** ## The defect diff --git a/.agents/specs/surface-coverage-2026-08-07.md b/.agents/specs/surface-coverage-2026-08-07.md index 0d610a588..2adefa17b 100644 --- a/.agents/specs/surface-coverage-2026-08-07.md +++ b/.agents/specs/surface-coverage-2026-08-07.md @@ -29,7 +29,7 @@ The four surfaces, and the public boundary the guard draws: | 3 | **DeepSeek-V4 fast decode** | yes, but forward is a W3 stub | no (stub) | no | `examples/deepseek_v4_gen` (keep-quant GGUF) | | 4 | **Audio transcription** | **CLOSED (ROW 1)**: Parakeet CTC/RNNT/TDT registered (transcription-only; Whisper/Voxtral still off-registry) | **live `/v1/audio/transcriptions`** (task-conditional; the run_batch line stays a residual) | **`vllm_transcribe` (ABI v11)** | library seam `ParakeetTranscriber`; example is a clean ABI client | | 5 | **Kimi-Linear incremental decode** | yes (recompute forward IS shared) | recompute only | no | `examples/kimi_linear_gen` (§18/§19 paged-incremental + resident loader) | -| 6 | **Embeddings / pooling** | NO (all `is_pooling_model=false`) | no (`/v1/embeddings` = residual) | no | engine-side pooler exists (`ENG-POOLER-SEQ`), never invoked live | +| 6 | **Embeddings / pooling** | **CLOSED (ROW 6, 2026-08-08)**: `LlamaModel` registered `is_pooling_model=true`; `PoolingRunner` invoked task-gated in the engine step | **live `/v1/embeddings`** (task-conditional; 404 both directions) | **`vllm_embed` (ABI v15)** | ONE engine path: `LoadedEngine -> LLMEngine::embed -> registry forward -> PoolingRunner` | | 7 | **Multimodal input over HTTP/ABI** | 5 archs `supports_multimodal` | image seam only; tower not run in engine step | no (text-only chat) | `chat_mm.cpp` seam; towers test-only | 21 of 30 registered text archs are fully on-framework (registry + runner + server + ABI): @@ -64,7 +64,7 @@ All three drivers run a PRIVATE host-argmax greedy loop, not the on-GPU sampler. | Parakeet/FastConformer ASR | **YES (ROW 1)**: ParakeetForCTC/RNNT/TDT, `parakeet_registry.cpp` (SupportsTranscription-only; text paths refuse by task) | `parakeet_transcription.cpp` seam composes encoder/transducer/audio-processor; the example's private `ReadWav16BitMono`/`LoadVocab`/`DecodeIds` are DELETED (`vllm::Tokenizer` now decodes Metaspace split=true) | **`/v1/audio/transcriptions`** (task-conditional) | **`vllm_transcribe` (ABI v11)** | `examples/parakeet_transcribe` = thin `vllm.h` client | | Voxtral audio->text | NO (`VoxtralForConditionalGeneration` unregistered) | `voxtral.cpp` (`vllm::multimodal`) | NO (`/v1/audio/transcriptions` = `run_batch.cpp:188` residual) | NO | tests-only reachability | | Whisper audio encoder | NO | `whisper_audio.cpp:174` | NO | NO | tests-only callers | -| Pooling / embeddings | NO (`is_pooling_model=false` in all 27) | `layers/pooler/*.cpp`, `pool/pooling_runner` (`ENG-POOLER-SEQ`) | NO (`/v1/embeddings` = residual) | NO | `PoolingRunner` test-only | +| Pooling / embeddings | **YES (ROW 6)**: `LlamaModel` (`llama_embedding_registry.cpp`, `is_pooling_model=true`; other `_EMBEDDING_MODELS` memberships still off) | `layers/pooler/*.cpp`, `pool/pooling_runner` (`ENG-POOLER-SEQ`) + the live engine-step invocation (`runner.cpp pool_tokens`) | **`/v1/embeddings`** (task-conditional) | **`vllm_embed` (ABI v15)** | fold gate `test_llama_embedding_fold` | | Multimodal INPUT | 5 archs `supports_multimodal` (Gemma4, KimiK3, Qwen3VL, Qwen3.5/-Moe) | towers `qwen3_vl_vision.cpp:374`, `gemma4_vision.cpp:170`; **Gemma-4 AUDIO USM tower is STANDALONE** (Gemma-4 text+image route via `ModelRegistry::Forward`, audio does NOT) | image seam only, raw-RGB, no stream, **tower not run in live step** (`chat_mm.cpp`; runner never consumes `mm_features`) | NO (text-only; `vllm_c.cpp` sets no mm seam) | — | | MTP / DFlash / ngram speculators | NO (sub-config / draft checkpoint; EAGLE unwired) | `spec_decode/{mtp,dflash}/speculator.cpp`, `ngram_proposer.cpp` | via `speculative_config` | via `speculative_config` (`vllm.h:172`) | — | @@ -119,10 +119,11 @@ before it. Bound to `include/vllm.h` by the marked `abi-capability-table` in `docs/FEATURES.md`. The ABI is text-generation-complete (completion, chat, async, structured output, tool + -reasoning parsers, speculative config, custom logits processor — 7 `reachable` rows). 4 -`embedder-unreachable` rows, each tracked in `scripts/abi-capability-allowlist.txt` -against `ARCH-ONE-SURFACE`: embeddings/pooling, audio transcription, video+audio -generation, multimodal input. +reasoning parsers, speculative config, custom logits processor — 7 `reachable` rows). +ROW 1 (audio transcription, v11), ROW 2 (video+audio generation, v12) and ROW 6 +(embeddings/pooling, v15) each flipped their row `reachable`; ONE +`embedder-unreachable` row remains, tracked in `scripts/abi-capability-allowlist.txt` +against `ARCH-ONE-SURFACE`: multimodal input. **Severity note — the ABI happy path is itself untested.** `vllm_engine_load` is never CI-gated on a REAL model load: `tests/capi/test_capi.cpp` covers only the bad-path error @@ -144,7 +145,7 @@ lanes are leaves of `ARCH-ONE-SURFACE` (do not open parallel rows). | 3 | DeepSeek-V4 fast decode | same as (2) for `DeepseekV4ForCausalLM`; real MLA paged KV (retire the W3 stub) | rewrite `deepseek_v4_gen`; delete `DeepseekV4ForwardGguf*` | M | MLA paged-KV topology | | 4 | Audio transcription | **DONE (ROW 1, 2026-08-07)**: `vllm_transcribe` (ABI v11) + live `/v1/audio/transcriptions`; ParakeetForCTC/RNNT/TDT registered (SupportsTranscription mirror, refuse-by-task) | **DONE**: `parakeet_transcribe` rewritten as a `vllm.h` client (byte-identical transcript goldens); route live, task-conditional | M | encoder→text seam (LANDED: `ParakeetTranscriber`) | | 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 | +| 6 | Embeddings/pooling | **DONE (ROW 6, 2026-08-08)**: `vllm_embed`/`vllm_embedding_result_free` (ABI v15) + live task-conditional `/v1/embeddings`; `LlamaModel` registered `is_pooling_model=true` (as_embedding_model mirror); `PoolingRunner` invoked in the step (pool-instead-of-sample + scheduler pooling stop) | **DONE**: no example existed to rewrite (the capability was test-only); fold gate re-anchors the lane's cosine gate through the registry path | M | pooler live-wiring (LANDED) | | 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 | **DONE (ROW 8, 2026-08-08, `row/DEVICE-KNOB`; leakage follow-up PR #139)**: `vllm_model_params.device` (ABI v14: 0=auto/1=cpu/2=cuda, the vLLM `DeviceConfig.device` names, device.py:13) → `EngineParams::device` → `SelectQueue`; the stable public name resolves through `FindPlatformByName` and its registered `DeviceType` is propagated without a shared CUDA literal; explicit cpu never probes, explicit ABSENT cuda fails LOUD before model I/O; DSR 32 / `kcuda=0` | **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` | diff --git a/.agents/state.md b/.agents/state.md index a890b51e1..8bcf8f45b 100644 --- a/.agents/state.md +++ b/.agents/state.md @@ -43085,3 +43085,93 @@ F79-4 remain open on the landed tree; the review's merge-and-fix map is the binding description. Pi concurrency, BF16 GEMM/speed closure (W6) stay open as the lane's own next steps. + +## 2026-08-08 — ARCH-ONE-SURFACE ROW 6: embeddings/pooling LIVE on the ONE surface (`row/EMBEDDINGS-ONE-SURFACE`, task #285, PR #137) + + +**What landed.** The pooling lane's engine-side pooler (`ENG-POOLER-SEQ` ops + +`ENG-POOLING-RUNNER` `PoolingRunner`) is now INVOKED LIVE through one path: +`vllm_engine_load` on a pooling checkpoint -> `LLMEngine::embed` -> +scheduler -> `GPUModelRunner::pool_tokens` -> pooled vector, driven identically +by `vllm_embed` (ABI v15) and the live task-conditional `/v1/embeddings`. + +**W1 registry+runner.** NEW arch `LlamaModel` +(`llama_embedding_registry.cpp:132`, `is_pooling_model=true`, +`is_text_generation_model=false`) — the exact upstream mirror of +`_EMBEDDING_MODELS` `"LlamaModel": ("llama", "LlamaForCausalLM")` +(registry.py:230) + `as_embedding_model` (adapters.py:230): the SHARED dense +backbone run to the post-final-norm hidden with NO lm_head +(`Qwen3DenseModel::ForwardHidden`, an additive tail arm of the shared +ForwardLayers; text callers byte-identical), loader accepts BOTH name layouts +(adapters.py:178-181 candidate_prefixes) and never loads lm_head +(`LoadLlamaModelEmbeddingWeights`). The runner builds a `PoolingRunner` iff the +registration declares pooling (model_runner.py:368-369 mirror) and +`sample_tokens` routes to `pool_tokens()` (model_runner.py:1586-1607): pooled +rows instead of sampled tokens, validity == the existing discard predicate +(pooling_runner.py:40-41), scheduler pooling stop at the marked DEFERRED site +(scheduler.py:1718-1721), `pooling_output` out through +EngineCoreOutput/RequestOutput, async scheduling OFF for pooling models +(config/vllm.py:1068-1073 — the landed `ResolveAsyncScheduling` arm now WIRED). +Every hook is task-gated on `is_pooling_model`/`pooling_params` (default +nullopt/false = byte-identical text path; engine suites re-run green: +scheduler 423, llm_engine 204, engine_core 44, output_processor 77, +qwen3_forward 1557, async_llm 342, llama_forward 509 asserts). + +**W2 ABI.** `vllm_embed` + `vllm_embedding_result_free`, VLLM_ABI_VERSION +14 -> 15, floor pin advanced to `>= 15`; strict-C references in +c_header_compile.c; dlopen symbol resolution; refuse-by-task BOTH directions +(text entry points on a pooling engine name `vllm_embed`; `vllm_embed` on a +text engine names `vllm_complete`); FIXED en route: v13's +`vllm_complete_tokens` shipped without the v11 task guard (null-deref on a +transcription handle) — guard added. + +**W3 server.** `handle_embeddings` (OpenAI shape per embed/protocol.py:34, +173-185; `dimensions`/base64/token-arrays = named-residual 400s) registered +ONLY when an embedder is attached; server main dispatches pooling archs to a +serving-less embedding server. Socket-level 404 pins BOTH directions. + +**W4 guard/records.** `scripts/abi-capability-allowlist.txt` embeddings row +REMOVED (1 row left: mm-input); FEATURES abi-capability row -> reachable + +`LlamaModel` arch row; `check-supported-models.py` ARCH_TOKEN_RE widened to +bare `*Model`; the runner-routing checker gains the explicit POOLING +classification (a pooling registration is a hidden-state producer BY DESIGN, +never the silently-exempt NONE bucket — pinned in its mutation suite); +gate-commands runnable-baseline re-pinned (+2 rows); STATUS/BENCHMARKS keyed +rows; engine/model matrices + coordination claim +`CLAIM-EMBEDDINGS-ONE-SURFACE`; ROW 6 closed in one-surface-abi.md + +surface-coverage-2026-08-07.md. + +**The fold's correctness anchor.** The pooling lane's cosine gate was +STRUCTURAL (synthetic hidden buffer, registry BYPASSED; the brief's "recorded +oracle fixtures" premise was inaccurate — the qwen36_embed goldens are +token-embedding LOOKUP goldens, unrelated). ROW 6 re-anchors it THROUGH the +registry/runner path: `test_llama_embedding_fold` 4/4-231 on the COMMITTED +deterministic fixture (`scripts/mm/llama_embed_fixture_gen.py`, +`tests/vllm/models/fixtures/llama_embed_e2e`, 151 KB) — (a) direct +`ModelRegistry::Forward`+`PoolingRunner` == f64 LAST+normalize reference, +(b) FULL-ENGINE path == direct path, IDENTICAL vectors, (c) chunked-prefill +(max_num_batched_tokens=2) == unchunked (the is_valid arm), plus +`test_capi` 48/48-462 (v15 section incl. the REAL fixture-checkpoint load +through the public ABI), test_dlopen 30/30, server suite 50/50, +registry 24/24-820. + +**Mutation kills (each RED then reverted, 9).** M1 ABI macro left at 14 -> +floor pin RED; M2 embed-on-text refusal dropped -> capi both-directions RED +(3 asserts); M3 generation-on-pooling refusal dropped -> the refusal case +HANGS (SIGTERM, doctest FAILURE); M4 `if (embedder_)` -> `if (true)` -> +text-server 404 pin RED; M5 generate route unconditional -> embedding-server +404 pin RED; M6 engine-step pooling invocation deleted -> fold gate RED +(2 cases); M7 scheduler pooling stop deleted -> fold gate RED; M9 registry +`is_pooling_model=false` -> registry pin + fold RED (819/820 + 6 asserts); +M11 async-off wire dropped -> fold engine arm RED (hang/SIGTERM + +CHECK_FALSE(async) pin). + +**Residuals (honest).** (1) REAL embedding checkpoint (e5-mistral class) +through the fold + the `vllm.LLM(task="embed").encode` oracle cosine — the +committed synthetic-fixture arm is the gate (the #121 precedent); NO +cosine-vs-oracle number fabricated. (2) The other 7 `_EMBEDDING_MODELS` +memberships (incl. `MistralModel`) unregistered. (3) `/pooling`, `/score`, +`/rerank`, `/classify` (need a classify arch). (4) Matryoshka `dimensions`, +`encoding_format: base64`, token-array inputs = 400s naming the residual. +(5) `vllm_embed` batches sequentially through the synchronous engine +(recorded deviation; AsyncLLM stays generation-only). diff --git a/CMakeLists.txt b/CMakeLists.txt index 473e07085..1fd4721f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -599,6 +599,7 @@ add_library(vllm STATIC src/vllm/model_executor/models/opt_weights.cpp src/vllm/model_executor/models/opt.cpp src/vllm/model_executor/models/llama_registry.cpp + src/vllm/model_executor/models/llama_embedding_registry.cpp src/vllm/model_executor/models/llama_weights.cpp src/vllm/model_executor/models/mistral_registry.cpp src/vllm/model_executor/models/mistral_weights.cpp diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index bdbbc4ff2..2a399f608 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -348,6 +348,7 @@ built on it rather than keeping the flattering one. | Vulkan vs llama.cpp Vulkan (`BENCH-VK-LLAMA`) | **NOT APPLICABLE: nothing measured, claimed or owed.** 22 NATIVE kernels (+6 GDN glue, CPU-oracle gated, no speed); 65 host-tier. opt-125m e2e token-exact on llvmpipe. [Detail](../.agents/specs/vulkan-full-support.md) | `VK-C` coopmat A/B on Thor (`VT_VULKAN_COOPMAT=0` A/Bs it): **11.1x-32.9x** vs our UNTILED scalar kernel, not vs a competent GEMM. `VK-E`: llama.cpp `-DGGML_VULKAN=ON` at `237ad9b96` on dgx, same GGUF, three columns | | ROCm (`BACKEND-GATE-ROCM-VLLM` / `-SGLANG`) | **NOT APPLICABLE: no number measured, claimed or owed.** The W0 skeleton registers 1 of 106 ops and its HIP sources have never been compiled by anyone; no AMD hardware here | A contributor's first `-DVLLM_CPP_HIP=ON` build ([#41](https://github.com/mudler/vllm.cpp/issues/41)). Only once a model runs does a same-box vLLM-ROCm oracle become the gate; the floor is vLLM, quant-matched | | SGLang floor arms | Never ran | Both arms of the SGLang comparison | +| Embeddings on the ONE surface (ROW 6, `LlamaModel` + `vllm_embed` + `/v1/embeddings`) | **NO number measured, claimed or owed.** Correctness-gated only, CPU: the 2026-08-08 fold (engine path == direct registry path, f64 LAST+normalize reference on the committed fixture) is plumbing, no speed claim | A REAL embedding checkpoint (e5-mistral class) + a same-box `vllm.LLM(task="embed")` oracle; only then does an embed-throughput bar exist | | Parakeet/FastConformer ASR (P1-P4 + ONE-SURFACE fold ROW 1) | **NO number measured, claimed or owed.** Correctness-gated only, CPU f32; the 2026-08-07 surface fold (`vllm_transcribe`, `/v1/audio/transcriptions`) is transcript-byte-identical plumbing, no speed claim. | Floor is `parakeet.cpp`, same clip and box; needs a CUDA provider and a pretrained checkpoint | | cuBLAS invocation-parity guard | CI guard landed (CPU); `kGemvHeuristicAlgos` refactor build-verify owed | `nvcc` rebuild + SACRED gate on dgx | | Ampere consumer (`sm_86`, RTX 3090 class) | **No number owed; no such board here.** 2026-08-06 build-verify: 7/7 FA2 TUs 0-warn, real `sm_86` SASS. [Detail](../.agents/benchmark-record.md) | External RTX 3090 report. Floor is llama.cpp on that card (GGUF, not our Blackwell-only NVFP4 grid) | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 2c3810c54..1826b94d4 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -121,6 +121,7 @@ speed-pending, which [BENCHMARKS.md](BENCHMARKS.md) tracks. | `LagunaForCausalLM` | poolside/Laguna-S-2.1-NVFP4, GGUF-Q4_K, Laguna-XS | byte-exact near-tie (distributional vs vLLM) | vLLM parity+ 1.03x, default on, via the `laguna-gen` CLI; the registered engine forward VT_CHECKs non-bf16 (`ARCH-ONE-SURFACE` fold) | | `KimiLinearForCausalLM` | Kimi-Linear-48B-A3B (KDA + NoPE-MLA + MoE) | **Folded onto the shared paged runner (ROW 7 §21, #122): engine==CLI 128/128 byte-identical; vs golden 122/128 (the intrinsic near-tie profile); FA2 paged MLA default-ON; SACRED post-fold green** | Served via `vllm_engine_load` + `vllm_complete_tokens` (ABI v13); server 19.0 tok/s wall vs vLLM ~21 (~0.90×), speed residual open | | `KimiK3ForConditionalGeneration` | Kimi-K3 (2.8T MoE) | scaffold: registry+config+enumeration gated, forward refuses | HW-infeasible (~1.56 TB); no run | +| `LlamaModel` | committed tiny synthetic embedding fixture (engine path == direct pooler path, identical vectors; f64 LAST+normalize reference); real checkpoint (e5-mistral class) is a NAMED residual | pooling/embed only, text paths refuse by task; `vllm_embed` + `/v1/embeddings` | n/a (CPU correctness-grade embeddings) | | `ParakeetForCTC`, `ParakeetForRNNT`, `ParakeetForTDT` | nvidia/parakeet-ctc-0.6b/-1.1b, -rnnt-0.6b, -tdt-0.6b-v3 (transcribed, ids exact vs HF `generate()`, P4/P6 2026-08-07; not retained) + committed synthetic fold fixture | ASR transcription-only (`SupportsTranscription` mirror; text paths refuse by task); fold gate byte-identical to the pre-refactor pipeline | n/a (CPU correctness-grade ASR via `vllm_transcribe` + `/v1/audio/transcriptions`) | | `CohereForCausalLM` | Command-R / Cohere (and Cohere2) | scaffold: W0 tiny-random oracle run-verified; real-checkpoint gate blocked | no run | @@ -149,11 +150,12 @@ Enumerated in `.agents/model-matrix.md`, not registered, no runnable GB10 gate: | `GlmMoeDsaForCausalLM` | GLM-5 (DSA) | ~1404 GiB bf16; dep-blocked (GLM-5.x is DeepSeek-V3.2 verbatim) | | `MiniMaxM2ForCausalLM` | MiniMax-M2 | ~230B, ~428 GiB bf16, ~4x over the unified pool | -25 of the 30 registered architectures carry a passing correctness gate today; -the rest are honestly marked scaffold or blocked above. vLLM registers 130+ text -architectures, so this is a curated, gated subset, not a breadth claim. Embedding -and reranking models are not yet registered: the engine-side pooler landed, no -model architecture is wired. +25 of the 30 registered text-generation architectures carry a passing +correctness gate today; the rest are honestly marked scaffold or blocked above. +vLLM registers 130+ text architectures, so this is a curated, gated subset, not +a breadth claim. The first EMBEDDING architecture is registered and live +(`LlamaModel`, task=embed, LAST pooling, the as_embedding_model mirror, gated +on the committed fixture); reranking/classify models are not yet registered. ## Multimodal @@ -229,12 +231,12 @@ Build with `-DVLLM_CPP_VULKAN=ON`; off by default. | Prometheus metrics | ✅ | ✅ | ✅ | ◐ | | Plugin / out-of-tree model registration | ✅ in-tree factory `DONE` + plugin seam | ✅ | ◐ | ☐ | | LoRA adapters | ☐ CPU brick only | ✅ | ✅ | ✅ | -| Embedding / pooling endpoints | ◐ engine only | ✅ | ✅ | ✅ | +| Embedding / pooling endpoints | ◐ `/v1/embeddings` live (task=embed; score/rerank/classify pending) | ✅ | ✅ | ✅ | | OpenAI video generation `/v1/videos` (Sora shape) | ✅ `model`/`size`/`seconds` aliases + `GET /{id}/content`; `input_reference` and the `metadata` video/audio references condition the render | ◐ (vllm-omni, its own request shape) | ☐ | ☐ | | Flat C ABI for embedding in other languages | ✅ versioned | ☐ | ☐ | ✅ | #### C-ABI capability coverage -- Which capabilities an embedder drives through the flat C ABI (`include/vllm.h`, the only installed header), gated by `scripts/check-surface-coverage.py`: a `reachable` row names an entry point that exists; an `embedder-unreachable` row is tracked in `scripts/abi-capability-allowlist.txt` against its fold row (`ARCH-ONE-SURFACE`). The ABI is text-generation-complete; the two `embedder-unreachable` rows are the open capability gaps. +- Which capabilities an embedder drives through the flat C ABI (`include/vllm.h`, the only installed header), gated by `scripts/check-surface-coverage.py`: a `reachable` row names an entry point that exists; an `embedder-unreachable` row is tracked in `scripts/abi-capability-allowlist.txt` against its fold row (`ARCH-ONE-SURFACE`). The ABI is text-generation-complete; the one `embedder-unreachable` row (multimodal input) is the open capability gap. | Capability | C-ABI surface | Embedder-reachable | |---|---|---| @@ -246,7 +248,7 @@ Build with `-DVLLM_CPP_VULKAN=ON`; off by default. | Tool + reasoning parser selection | `tool_parser`, `reasoning_parser` | reachable | | Speculative decoding config | `speculative_config` | reachable | | Custom logits processor | `vllm_logits_processor` | reachable | -| Embeddings / pooling | none | embedder-unreachable | +| Embeddings / pooling (task=embed) | `vllm_embed`, `vllm_embedding_result_free` (ABI v15; pooling checkpoints load via `vllm_engine_load`) | reachable | | 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 | @@ -278,7 +280,7 @@ CPU elementwise GEMM (f32/f16/bf16) runs AVX2 and AVX-512 tiers on x86 where the | Multi-GPU execution | Hardware-blocked | TP proven equal to tp=1 on CPU; no 2-GPU box to run it | | LoRA end to end | CPU brick landed | Unwired standalone; not usable through the server | | Multimodal over HTTP | Architecturally blocked | Vision tower lives outside the registered engine forward | -| Embedding / reranking models | Engine side only | Pooler and runner path landed, no model architecture registered | +| Reranking / classify models | Engine side only | Embeddings are LIVE (`LlamaModel`, `vllm_embed`, `/v1/embeddings`); the classify/score heads are landed ops with no registered arch | | ROCm | W0 skeleton, unbuilt | Backend + platform + 1 op (RmsNorm); the HIP sources have never been compiled by anyone (no AMD board here). Open: [ROCM.md](ROCM.md), [#41](https://github.com/mudler/vllm.cpp/issues/41) | | XPU, TPU | Not started | CUDA, CPU, Metal and Vulkan are the built backends | | Custom logits processors on CUDA | Open, not root-caused | Segfaults in a CUDA build, 232/232 green on CPU | diff --git a/docs/STATUS.md b/docs/STATUS.md index 57b59f9a2..bac250fb2 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -113,7 +113,7 @@ token-for-token correctness against the pinned oracle. | Reasoning parsing (`SAMPLE-REASONING`, ACTIVE, partial coverage) | 9 parsers, streaming | think_auto (auto-detect default: content unless markers appear), deepseek_r1, deepseek_v3 (passthrough) / holo2 (thinking→R1), mistral ([THINK]), minimax_m2 (+append_think), step3, olmo3 - reasoning split engine-side BEFORE tool parsing, streamed as `reasoning` deltas in the chat chunks. Coverage: 9 of upstream's ~28 registered names (remaining text families + engine-backed adapters tracked as W2/W3 in specs/reasoning-parsers.md); each ported parser doctest-gated vs its tests/reasoning case | | Unified streaming parser engine | Core, assembly, serving-SSE dispatch landed, gated; all 10 engine-backed families ported (family parity closed); JSON-schema tool-arg type coercion landed | The vLLM 0.26 declarative `parser/engine/` (shared state machine plus all 10 configs: qwen3, seed_oss, kimi_k2, minimax_m2, glm47_moe, deepseek_v4/v32, nemotron_v3, gemma4, inkling) and assembly layer, gated field-for-field vs vLLM 0.26. An engine-backed `--tool-call-parser` name drives the live chat SSE chunks, off by default. When a request's tools declare typed parameters, the assembled tool-call arguments are coerced to the declared JSON types (int/number/bool/string/array/null) 1:1 with vLLM `_fix_arg_types`, in both streaming and one-shot; no schema means the arguments pass through as strings unchanged. Details: .agents/specs/parser-assembly-c8.md | | OpenAI server | Supported (subset); #129: SPIKE∅ | `/v1/completions`, `/v1/chat/completions`, streaming SSE, `/v1/models`, `/health`, `/version`, `/ping`, `/metrics`, `/tokenize` (raw-`prompt` and chat-`messages`), `/detokenize`, `/tokenizer_info`, `/server_info`, `/reset_prefix_cache`, `/abort_requests`; `/v1/videos` in OpenAI's Sora shape + `GET /v1/videos/{id}/content`, conditioned on an `input_reference` image or the `metadata` video/audio references. `/tokenizer_info` and `/abort_requests` are flag-gated; `/metrics` and `/reset_prefix_cache` have handlers but no live backing on the async path. Endpoint list and flags: docs/USAGE.md. Depth-2 async serving no longer corrupts the host heap under `ignore_eos` | -| Pooling task class (embeddings / classify / score / rerank) | Spiked; pooler op + heads composite + pooling runner path landed (CPU), not yet servable end-to-end (no concrete model / endpoints) | The non-generative task class. W0 spike over the whole vLLM pooling surface (`.agents/specs/pooling-task-class.md`, `CLAIM-POOLING`). W1 landed the pooler OP (CLS/LAST/MEAN + Identity/Normalize/MultiLabelClassify/Classify activations, double-precision-gated). **W2 landed the pooler HEADS composite** — `EmbeddingPoolerHead` (projector→matryoshka→normalize), `ClassifierPoolerHead` (classifier→`(logit-mean)/sigma`→activation), the `SequencePooler` + `PoolerForEmbed`/`PoolerForClassify` factories, the `DispatchPooler` task routing, and the `PoolerConfig`/`PoolingParams` structs (`test_pooler_heads` 27/27, 240 asserts, RED-first). **W3 landed the pooling RUNNER path** — `PoolingRunner` applies the model's `Pooler` to the last hidden state and returns pooled embeddings instead of sampled tokens, gated by a STRUCTURAL cosine-parity check vs a double-precision LAST+normalize reference (`test_pooling_runner` 5/5, 14 asserts, RED-first). NOT yet servable / honest residuals: a concrete pooling MODEL forward + the REAL-model oracle cosine gate (`vllm.LLM(task="embed").encode`) — no cosine-vs-oracle number is fabricated (W3-model); the `/v1/embeddings` + score/rerank/classify endpoints (W4); tokwise AllPool/StepPool (W5). See docs/BENCHMARKS.md | +| Pooling task class (embeddings / classify / score / rerank) | **EMBEDDINGS LIVE ON THE ONE SURFACE (ROW 6)**: `LlamaModel` registered, `PoolingRunner` in the engine step, `vllm_embed` (ABI v15) + live `/v1/embeddings`; classify/score/rerank engine-side only | The non-generative task class. W0 spike over the whole vLLM pooling surface (`.agents/specs/pooling-task-class.md`, `CLAIM-POOLING`). W1 landed the pooler OP (CLS/LAST/MEAN + Identity/Normalize/MultiLabelClassify/Classify activations, double-precision-gated). **W2 landed the pooler HEADS composite** (`EmbeddingPoolerHead`, `ClassifierPoolerHead`, `SequencePooler` + factories, `DispatchPooler` routing, `PoolerConfig`/`PoolingParams`; `test_pooler_heads` 27/27-240, RED-first). **W3 landed the pooling RUNNER path** (`PoolingRunner`: pooled embeddings instead of sampled tokens, structural cosine gate vs an f64 LAST+normalize reference, `test_pooling_runner` 5/5-14, RED-first). **ROW 6 (2026-08-08): embeddings LIVE** — fold gate `test_llama_embedding_fold` 4/4-231 (engine path == direct registry path, f64 LAST+normalize ref, chunked is_valid arm); residuals: REAL checkpoint + `LLM(task="embed")` oracle cosine (no number fabricated), score/rerank/classify endpoints, matryoshka/base64/token-array inputs, tokwise (W5). Detail: `.agents/specs/embeddings-one-surface.md` | | Plugin system (out-of-core registration) | Spiked; first CPU brick landed, not yet wired into any production path | The extensibility-first discovery layer. W0 spike over vLLM's plugin surface (general / platform / io_processor / endpoint groups, the `register_model` an out-of-tree plugin calls, the invocation seams) is committed (`.agents/specs/plugin-system.md`, `ENG-PLUGIN-SYSTEM` ACTIVE, `CLAIM-PLUGIN-SYSTEM`). W1 landed `vllm::plugins::LoadGeneralPlugins()` + the out-of-core general-plugin registration seam (`RegisterGeneralPlugin` / `REGISTER_VLLM_GENERAL_PLUGIN`) over the existing `REGISTER_VLLM_MODEL`-style registries (the in-tree factory `MODEL-FACTORY-registry` is record-repaired `DONE` 2026-08-05: 28 self-registering TUs, dgx debt paid by the 2026-07-23 seven-gate run): a 1:1 mirror of `load_general_plugins` (load-once idempotence, the `VLLM_PLUGINS` allowlist, per-plugin failure isolation). Proven by an out-of-core toy-model plugin that registers a toy architecture through the public `RegisterModel` seam — unit-gated RED-first (`test_plugin_system` 1 case / 29 assertions: the toy arch resolves ONLY after LoadGeneralPlugins runs it, and not under `VLLM_PLUGINS=""`). Python entry points have no C++20 analogue, so discovery is the project's static-init/`dlopen` registration idiom (recorded porting-inventory §9). NOT yet wired: real shared-object `dlopen` + the C-ABI `vllm_plugin_register` entry (W2), the engine/CLI `--load-plugins` wiring that calls LoadGeneralPlugins from the construction paths (W3), the platform/quant plugin kinds (W4), and the io_processor/stat_logger/endpoint groups (W5) are named residuals. See docs/BENCHMARKS.md | | Offline Batch API (JSONL file runner) | Spiked; first CPU brick landed, not yet exposed as a CLI | The offline OpenAI Batch API: read a JSONL of OpenAI-format requests, run each through the engine, write a JSONL of responses. W0 spike over vLLM's `run_batch.py` (schema, endpoint dispatch, run loop, file I/O) is committed (`.agents/specs/batch-api.md`, `SERVE-BATCH-API` ACTIVE, `CLAIM-BATCH-API`). W1 landed `RunBatch` (`RunLine`/`RunLines`/`Run`) + `RunBatchFile` — a pure orchestrator over the existing `OpenAIServingChat::create_chat_completion` (NO reimplemented generation), 1:1 with vLLM's endpoint_registry url→handler map: `/v1/chat/completions` dispatch, the `BatchResponseData`/`BatchRequestOutput` schema (`vllm-` ids, custom_id echoed), the `run_request` AllResponse/ErrorResponse/stream branches, and the unsupported-endpoint/url error rows. Unit-gated RED-first (`test_openai_run_batch` 7 cases / 80 assertions over the synthetic serving engine: ordered rows + custom_id echo + per-line BatchRequestOutput schema round-trip, a malformed line isolated into an error row so the batch continues, dispatch + 404 error rows; dropping the custom_id echo fails 9 assertions). Recorded deviation: a malformed line is isolated (batch continues) where upstream aborts the job. NOT yet exposed: the `vllm run-batch` CLI + `BatchFrontendArgs` (W2), embeddings/score/rerank dispatch (W3, rides pooling endpoints), audio transcription/translation + media fetch (W4), and http(s)/data-URL file I/O + metrics + overlapped `AsyncLLM` submission (W5) are named residuals. See docs/BENCHMARKS.md | | Tokenizers | Supported | Byte-level BPE (Qwen/Llama-3/OPT/GPT-2/DeepSeek/OLMo-2) and SentencePiece BPE (Mistral/Gemma), plus GGUF vocab; added-token `lstrip`/`rstrip` whitespace semantics (e.g. Phi-4-mini's special tokens); byte-exact vs the vLLM oracle | diff --git a/examples/server/main.cpp b/examples/server/main.cpp index a06d944d4..30b4e2424 100644 --- a/examples/server/main.cpp +++ b/examples/server/main.cpp @@ -435,6 +435,77 @@ int main(int argc, char** argv) { transcription_only = false; // unknown arch: the text path diagnoses } } + // ── POOLING TASK DISPATCH (ARCH-ONE-SURFACE ROW 6): a model dir whose + // architectures resolve to a POOLING registration (is_pooling_model, + // e.g. "LlamaModel" — vLLM _EMBEDDING_MODELS registry.py:230) serves + // /v1/embeddings through the ONE engine path (LoadedEngine -> + // LLMEngine::embed -> registry forward -> PoolingRunner) — the same + // path vllm_embed drives — and registers NO generate routes (vLLM's + // task-conditional registration, api_server.py:255-265). ────────────── + bool pooling_model = false; + if (!archs.empty()) { + try { + pooling_model = + vllm::ModelRegistry::Resolve(std::span(archs)) + .info.is_pooling_model; + } catch (const std::exception&) { + pooling_model = false; // unknown arch: the text path diagnoses + } + } + if (pooling_model) { + std::cerr << "server: pooling (embedding) model (" << archs[0] + << "); serving /v1/embeddings\n"; + vllm::entrypoints::EngineParams embed_params; + embed_params.block_size = args.block_size; + embed_params.num_blocks = args.num_blocks; + embed_params.max_model_len = args.max_model_len; + embed_params.max_num_seqs = args.max_num_seqs; + embed_params.max_num_batched_tokens = args.max_num_batched_tokens; + embed_params.enable_prefix_caching = args.enable_prefix_caching; + auto loaded_embed = std::shared_ptr( + vllm::entrypoints::LoadedEngine::FromModelDir(args.model_dir, + embed_params)); + namespace oai = vllm::entrypoints::openai; + oai::OpenAIServingModels embed_models(served_model_name); + oai::ApiServer embed_server(embed_models, vllm::Version()); + auto embed_mutex = std::make_shared(); + auto embed_counter = std::make_shared>(0); + embed_server.set_embedder( + [loaded_embed, embed_mutex, embed_counter]( + const std::vector& inputs) { + // Serialize batches: the pooling path drives the SYNCHRONOUS + // LLMEngine (async scheduling resolves OFF for pooling models). + std::lock_guard lock(*embed_mutex); + oai::ApiServer::EmbeddingBatch batch; + for (const std::string& text : inputs) { + std::vector ids = + loaded_embed->tokenizer().EncodeWithSpecialTokens(text); + if (ids.empty()) { + throw std::runtime_error( + "input tokenized to an empty prompt"); + } + batch.prompt_tokens += static_cast(ids.size()); + vllm::RequestOutput ro = loaded_embed->engine().embed( + std::move(ids), vllm::PoolingParams{}, + "embd-" + std::to_string(embed_counter->fetch_add(1))); + if (!ro.finished || !ro.pooling_output.has_value()) { + throw std::runtime_error( + "engine produced no pooled output"); + } + batch.embeddings.push_back(std::move(*ro.pooling_output)); + } + return batch; + }); + std::cerr << "server: listening on http://" << args.host << ":" + << args.port << "\n"; + if (!embed_server.listen(args.host, args.port)) { + std::cerr << "server: failed to bind " << args.host << ":" + << args.port << "\n"; + return 1; + } + return 0; + } + if (transcription_only) { std::cerr << "server: transcription-only model (" << archs[0] << "); serving /v1/audio/transcriptions\n"; diff --git a/include/vllm.h b/include/vllm.h index a276381cb..5dc219853 100644 --- a/include/vllm.h +++ b/include/vllm.h @@ -118,8 +118,19 @@ extern "C" { * 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 + * the pre-v14 engine byte-identical. + * v15: vllm_embed / vllm_embedding_result(_free) — EMBEDDINGS through the ONE + * surface (ARCH-ONE-SURFACE fold ROW 6). An engine loaded from a POOLING + * (embedding) checkpoint — a directory whose config.json architectures resolve + * to a pooling registration, e.g. "LlamaModel" (the mirror of vLLM's + * _EMBEDDING_MODELS registry.py:230 + as_embedding_model adapters.py:230) — + * embeds text through the SAME registry forward + PoolingRunner engine step + * the server's /v1/embeddings drives. Text and pooling handles refuse each + * other's tasks LOUDLY: vllm_complete/vllm_chat on a pooling engine name + * vllm_embed, and vllm_embed on a text engine names vllm_complete — the + * SupportsTranscription-refusal precedent (v11) applied to the pooling task. + * Purely additive — no struct changed; zero values preserve behaviour. */ +#define VLLM_ABI_VERSION 15 /* ── Export macro ───────────────────────────────────────────────────────────── * Marks the symbols that make up the stable ABI. Default visibility now; Task 3 @@ -583,6 +594,44 @@ VLLM_API vllm_status vllm_transcribe(vllm_engine* engine, VLLM_API void vllm_transcription_free(vllm_transcription* out); +/* ── Embeddings (ABI v15) ───────────────────────────────────────────────────── + * The embeddings/pooling slice of the ONE-SURFACE fold: an engine loaded from + * a POOLING (embedding) checkpoint — config.json architectures resolving to a + * pooling registration such as "LlamaModel" — turns text into L2-normalized + * embedding vectors through the SAME registry forward + PoolingRunner engine + * step the bundled server's /v1/embeddings drives (task=embed, LAST-token + * pooling: the mirror of vLLM's as_embedding_model conversion). Loading such a + * checkpoint uses the ordinary vllm_engine_load; the handle then serves ONLY + * the embedding entry point (the text/chat entry points refuse, naming this + * one, and vice versa on a text handle). */ + +/* One embedding batch result. OWNERSHIP: `values` is library-allocated; free + * via vllm_embedding_result_free(out). Row-major: embedding i occupies + * values[i*dim .. (i+1)*dim). */ +typedef struct vllm_embedding_result { + float* values; /* n_embeddings * dim floats, row-major */ + int32_t n_embeddings; /* == the number of input texts */ + int32_t dim; /* the model's hidden size */ + int32_t prompt_tokens; /* total input tokens (the OpenAI usage mirror) */ +} vllm_embedding_result; + +/* Embed n_texts NUL-terminated UTF-8 strings on a pooling-capable engine + * handle, filling *out (one embedding per text, input order). BLOCKING; the + * texts are tokenized with the checkpoint's tokenizer and each prompt runs one + * engine prefill + pool step. Returns VLLM_OK on success; + * VLLM_ERR_INVALID_ARGUMENT for a text-generation handle (use vllm_complete / + * vllm_chat there), a NULL texts/out, an n_texts <= 0, or a NULL entry in + * texts; VLLM_ERR_RUNTIME when tokenization or the forward fails. On any + * non-OK status *out is zeroed and vllm_last_error() carries the detail. */ +VLLM_API vllm_status vllm_embed(vllm_engine* engine, + const char* const* texts, int32_t n_texts, + vllm_embedding_result* out); + +/* Free the owned members of an embedding result and zero the struct. The + * struct itself is caller storage. NULL is a no-op. */ +VLLM_API void vllm_embedding_result_free(vllm_embedding_result* out); + + /* ── Video+audio generation (ABI v12, MiniMax-H3) ──────────────────────────── * The video slice of the ONE-SURFACE fold: the SAME library pipeline the * bundled server's /v1/videos routes and the minimax-h3-gen example drive diff --git a/include/vllm/entrypoints/model_loader.h b/include/vllm/entrypoints/model_loader.h index a911bf09b..28bcb7e46 100644 --- a/include/vllm/entrypoints/model_loader.h +++ b/include/vllm/entrypoints/model_loader.h @@ -213,6 +213,14 @@ class LoadedEngine { std::optional named_platform_type); vllm::v1::LLMEngine& engine() { return engine_; } + // ARCH-ONE-SURFACE ROW 6: whether the loaded model registration declares the + // POOLING task class (is_pooling_model). The entrypoints dispatch BY TASK on + // this — text-generation refuses on a pooling engine (naming vllm_embed / + // /v1/embeddings) and embed refuses on a text engine — the mirror of vLLM + // validating runner_type against the model class (config/model.py:607-613). + bool is_pooling_model() const { + return model_->registration().info.is_pooling_model; + } // Lazily start W2's EngineCoreProc + output-handler threads. Once created, // online/server callers use this frontend rather than the synchronous // LLMEngine over the same scheduler/executor. @@ -258,8 +266,12 @@ class LoadedEngine { // CPU construction-matrix test can assert it directly over the // runner_supports_async x VT_ASYNC_SCHED matrix without a disk load. Applies // SchedulerConfig::ResolveAsyncScheduling then the VT_ASYNC_SCHED rollback env. + // `is_pooling_model` (ARCH-ONE-SURFACE ROW 6) resolves async OFF for pooling + // models (mirror of vllm/config/vllm.py:1068-1073); default false is the + // byte-identical text path. static bool ResolveAsyncEnabled(const vllm::SchedulerConfig& scheduler_config, - bool runner_supports_async); + bool runner_supports_async, + bool is_pooling_model = false); private: // Type-erased constructor used by FromModelDir and the concrete-weight diff --git a/include/vllm/entrypoints/openai/api_server.h b/include/vllm/entrypoints/openai/api_server.h index 745f18cc9..fd8d73d2e 100644 --- a/include/vllm/entrypoints/openai/api_server.h +++ b/include/vllm/entrypoints/openai/api_server.h @@ -138,6 +138,18 @@ class ApiServer { DispatchResult handle_audio_transcriptions( const std::string& file_bytes, const std::string& response_format) const; + // POST /v1/embeddings (ARCH-ONE-SURFACE ROW 6). Mirror of vLLM's + // pooling/embed/api_router.py:28 `create_embedding` over the + // EmbeddingCompletionRequest shape (embed/protocol.py:34: `model`, `input` + // as ONE string or an ARRAY of strings) and the EmbeddingResponse shape + // (embed/protocol.py:173-185: id "embd-...", object "list", data rows + // {index, object:"embedding", embedding:[...]}, usage prompt/total tokens). + // Token-array inputs, `dimensions` (matryoshka) and `encoding_format: + // "base64"` are NAMED RESIDUALS -> 400. Registered ONLY when an embedder is + // attached (the transcriber precedent), so a text server answers 404 at the + // route table. + DispatchResult handle_embeddings(const std::string& request_body) const; + DispatchResult handle_videos(const std::string& request_body); DispatchResult handle_videos_sync(const std::string& request_body); DispatchResult handle_video_status(const std::string& job_id) const; @@ -207,6 +219,22 @@ class ApiServer { transcriber_ = std::move(transcriber); } + // Attach the embedding seam backing POST /v1/embeddings (ARCH-ONE-SURFACE + // ROW 6). ADDITIVE and OPT-IN like the transcriber above: absent => route + // unregistered => 404, byte-identical to a server without pooling. The + // callback wraps the ONE engine path (LoadedEngine -> LLMEngine::embed -> + // the registry forward + PoolingRunner step) — the SAME path vllm_embed + // drives — so HTTP and FFI cannot drift. Returns one embedding per input + // (input order) + the total prompt token count for the usage block; throws + // to fail the request (-> 500). + struct EmbeddingBatch { + std::vector> embeddings; + int64_t prompt_tokens = 0; + }; + using EmbedFn = + std::function& inputs)>; + void set_embedder(EmbedFn embedder) { embedder_ = std::move(embedder); } + // Attach the tokenizer + max_model_len backing /tokenize and /detokenize // (non-owning; must outlive the server). void set_tokenizer(const vllm::tok::Tokenizer* tokenizer, @@ -269,6 +297,7 @@ class ApiServer { const v1::metrics::PrometheusStatLogger* metrics_ = nullptr; ::vllm::openai::VideoRunner video_runner_; TranscribeFn transcriber_; + EmbedFn embedder_; mutable ::vllm::openai::VideoJobStore video_jobs_; // Background workers for the ASYNC endpoint. Joined in ~ApiServer, which is // why they are joinable threads and not detached: a detached worker would diff --git a/include/vllm/model_executor/models/llama.h b/include/vllm/model_executor/models/llama.h index 20bdbad20..1562cab78 100644 --- a/include/vllm/model_executor/models/llama.h +++ b/include/vllm/model_executor/models/llama.h @@ -50,6 +50,14 @@ using LlamaModel = Qwen3DenseModel; LlamaWeights LoadLlamaForCausalLMWeights(const std::vector& shards, const HfConfig& config); +// `LlamaModel` EMBEDDING checkpoint loader (ARCH-ONE-SURFACE ROW 6): the same +// name map, accepting BOTH the "model."-prefixed and the bare `*Model` tensor +// layouts (vllm/model_executor/models/adapters.py:178-181 candidate_prefixes +// ["", "model."]) and never loading an lm_head (the as_embedding_model +// conversion has no output layer, adapters.py:135-151). +LlamaWeights LoadLlamaModelEmbeddingWeights( + const std::vector& shards, const HfConfig& config); + // Per-family config hook (mirrors ParseQwen3ForCausalLMConfig). LoadHfConfig // already materializes + validates every consumed Llama field, including the // llama3 rope_scaling dictionary; this explicit no-op hook is the family's diff --git a/include/vllm/model_executor/models/model_registry.h b/include/vllm/model_executor/models/model_registry.h index f3afa8f7f..c33beacb3 100644 --- a/include/vllm/model_executor/models/model_registry.h +++ b/include/vllm/model_executor/models/model_registry.h @@ -114,6 +114,15 @@ class LoadedModel { // instances of a W4A4-capable family may contain only BF16 weights. virtual bool uses_nvfp4_w4a4() const { return false; } + // ARCH-ONE-SURFACE ROW 6: the model-owned Pooler of a POOLING model — the + // mirror of upstream `VllmModelForPooling.pooler` (as_embedding_model wires + // `self.pooler = DispatchPooler.for_embedding(...)`, adapters.py:248-257). + // Non-null iff the registration's info.is_pooling_model; the GPU runner + // builds its PoolingRunner over exactly this pooler (the mirror of + // gpu/model_runner.py:368-369 `PoolingRunner(self.model)`). Default null: + // every text-generation model is byte-identical. + virtual const class Pooler* pooler() const { return nullptr; } + // ── SPEC-MTP I5d-pre: typed access to the MTP draft, without breaking the // type-erasure of this base. Only the concrete Qwen3.5 dense/MoE // LoadedModel (which owns the target Qwen3_5DenseWeights/Qwen3_5MoeWeights) diff --git a/include/vllm/model_executor/models/qwen3.h b/include/vllm/model_executor/models/qwen3.h index 1a360f506..0117b4a4c 100644 --- a/include/vllm/model_executor/models/qwen3.h +++ b/include/vllm/model_executor/models/qwen3.h @@ -162,6 +162,22 @@ class Qwen3DenseModel { const std::vector& attn_kv, const Qwen3DenseWeights& weights, const HfConfig& config, vt::Queue& queue, const std::vector& logits_indices = {}); + + // POOLING forward (ARCH-ONE-SURFACE ROW 6): the same embed + layer stack, + // stopping after the final RMSNorm (+ the logits_indices gather) with NO + // lm_head — the forward of an as_embedding_model conversion + // (vllm/model_executor/models/adapters.py:135-151 replaces the output layer + // with a missing-layer stage; the pooler consumes the post-final-norm + // hidden). Returns a HOST ForwardLogits carrier of [n_out, hidden_size] f32 + // rows (`vocab` == hidden_size on this path); the engine's pooling branch + // hands them to the landed PoolingRunner. Additive: no text caller routes + // here, and the lm_head tail above is byte-identical. + static ForwardLogits ForwardHidden( + const std::vector& token_ids, const std::vector& positions, + const v1::CommonAttentionMetadata& attn_meta, + const std::vector& attn_kv, const Qwen3DenseWeights& weights, + const HfConfig& config, vt::Queue& queue, + const std::vector& logits_indices = {}); }; // SHARED pure-dense decode CUDA-graph driver — the sibling of Qwen3MoeDecodeGraph diff --git a/include/vllm/outputs.h b/include/vllm/outputs.h index 2e19a1c28..17598e1bc 100644 --- a/include/vllm/outputs.h +++ b/include/vllm/outputs.h @@ -113,6 +113,13 @@ struct RequestOutput { std::optional prompt_logprobs; // Whether the whole request is finished. bool finished = false; + // pooling_output (ARCH-ONE-SURFACE ROW 6): the pooled vector of a finished + // POOLING-task request (task=embed: the L2-normalized last-token embedding). + // RECORDED DEVIATION: upstream wraps pooled results in a separate + // PoolingRequestOutput/PoolingOutput class pair (vllm/outputs.py); ours rides + // the ONE RequestOutput as an optional field so every existing consumer of + // the generation shape is byte-identical (nullopt there). + std::optional> pooling_output; // Convenience accessor mirroring the `finished` attribute (upstream exposes // the plain attribute; provided here for parity with the *Output helpers). diff --git a/include/vllm/v1/engine/llm_engine.h b/include/vllm/v1/engine/llm_engine.h index b9064f6da..cdc85f984 100644 --- a/include/vllm/v1/engine/llm_engine.h +++ b/include/vllm/v1/engine/llm_engine.h @@ -112,6 +112,21 @@ class LLMEngine { multimodal::MultiModalInputs mm_inputs, SamplingParams params, int priority = 0); + // add_pooling_request (ARCH-ONE-SURFACE ROW 6): the POOLING-task counterpart + // of the tokens add_request — upstream's `params: SamplingParams | + // PoolingParams` union (llm_engine.py add_request) collapses here to an + // explicit pooling entry point. Builds the request via process_inputs_tokens + // with a benign greedy SamplingParams (the sampler is never invoked on a + // pooling model's step) and attaches the PoolingParams; the scheduler + // finishes it as soon as the runner pooled its prompt + // (scheduler.py:1718-1721). Only meaningful on an engine whose model + // registration declares is_pooling_model — on a text model the request would + // never produce pooled data (the entrypoints refuse it before here). + std::string add_pooling_request(const std::string& request_id, + std::vector prompt_token_ids, + PoolingParams pooling_params, + int priority = 0); + // step (llm_engine.py:296): get the EngineCore outputs -> process_outputs -> // abort any reqs the detokenizer stopped -> return the RequestOutputs. std::vector step(); @@ -159,6 +174,15 @@ class LLMEngine { SamplingParams params, const std::string& request_id = "0", int priority = 0); + // embed (ARCH-ONE-SURFACE ROW 6): the single-request pooling driver — the + // mirror of LLM.embed's add-then-run loop (entrypoints/pooling/offline.py: + // 65-119, pooling_task="embed"). Adds the pooling request, then loops step() + // until it finishes; the returned RequestOutput carries the pooled vector in + // pooling_output. + RequestOutput embed(std::vector prompt_token_ids, + PoolingParams pooling_params = {}, + const std::string& request_id = "0", int priority = 0); + // The rolling prefix-cache hit rate (queries/hits in TOKENS over the most // recent 1000 requests). See EngineCore::prefix_cache_metrics. const CachingMetrics& prefix_cache_metrics() const { diff --git a/include/vllm/v1/engine/output_processor.h b/include/vllm/v1/engine/output_processor.h index 4c0c8a7ac..53f29c40a 100644 --- a/include/vllm/v1/engine/output_processor.h +++ b/include/vllm/v1/engine/output_processor.h @@ -129,10 +129,15 @@ class RequestState { // CompletionOutput/RequestOutput honoring output_kind. Returns nullopt when // FINAL_ONLY-and-not-finished or a stream_interval hold-back suppresses this // step's output. kv_transfer_params deferred (see header). + // `pooling_output` (ARCH-ONE-SURFACE ROW 6; upstream make_request_output's + // pooling_output parameter, output_processor.py:272): the finished POOLING + // request's pooled vector, attached to RequestOutput::pooling_output. + // nullopt on every generation output -> byte-identical text path. std::optional make_request_output( const std::vector& new_token_ids, std::optional finish_reason, - std::optional stop_reason); + std::optional stop_reason, + std::optional> pooling_output = std::nullopt); std::string request_id; std::string external_req_id; // == request_id at T0 (see header). diff --git a/include/vllm/v1/engine/types.h b/include/vllm/v1/engine/types.h index 23193e302..fb5c321d2 100644 --- a/include/vllm/v1/engine/types.h +++ b/include/vllm/v1/engine/types.h @@ -13,7 +13,8 @@ // // DEFERRED upstream fields, intentionally omitted — later units slot these in // without reshaping the structs: -// EngineCoreRequest: mm_features (multimodal), pooling_params, lora_request, +// EngineCoreRequest: mm_features (multimodal) and pooling_params are now +// PRESENT (pooling_params since ARCH-ONE-SURFACE ROW 6); lora_request, // cache_salt, data_parallel_rank, prompt_embeds, prompt_is_token_ids, // client_index, current_wave, trace_headers, resumable, // external_req_id, reasoning_ended / reasoning_parser_kwargs, @@ -22,17 +23,17 @@ // SamplerOutput: logprobs_tensors now carries the real LogprobsTensors payload // (vllm/v1/outputs.py, ported at M1.7); the sampler's gather_logprobs fills // it. It stays std::optional (None => no logprobs requested this step). -// ModelRunnerOutput: logprobs (LogprobsLists) and prompt_logprobs_dict are -// now PRESENT (ROAD-V1-C7 SAMPLE-LOGPROBS payload); pooler_output, -// kv_connector_output / ec_connector_output (P/D KV transfer), +// ModelRunnerOutput: logprobs (LogprobsLists), prompt_logprobs_dict (ROAD-V1-C7 +// SAMPLE-LOGPROBS) and pooler_output (ARCH-ONE-SURFACE ROW 6) are now +// PRESENT; kv_connector_output / ec_connector_output (P/D KV transfer), // num_nans_in_logits, cudagraph_stats, routed_experts, and the // with_kv_conn_output_only / EMPTY_MODEL_RUNNER_OUTPUT helpers stay deferred. // EngineCoreOutput: new_logprobs / new_prompt_logprobs_tensors are now PRESENT // (ROAD-V1-C7); events (EngineCoreEvent) is now PRESENT // (SERVE-RESPONSE-METRICS — the per-request QUEUED/SCHEDULED/PREEMPTED -// timing events the scheduler drains onto each output); pooling_output, -// kv_transfer_params, trace_headers, prefill_stats, routed_experts, -// num_nans_in_logits deferred. +// timing events the scheduler drains onto each output); pooling_output is +// now PRESENT (ARCH-ONE-SURFACE ROW 6); kv_transfer_params, trace_headers, +// prefill_stats, routed_experts, num_nans_in_logits deferred. // EngineCoreOutputs: scheduler_stats (SchedulerStats), utility_output, // finished_requests, wave_complete / start_wave (DP wave signalling), and // the __post_init__ monotonic-timestamp default (the frontend stamps it). @@ -57,6 +58,7 @@ #include #include +#include "vllm/model_executor/layers/pooler/pooling_params.h" // PoolingParams (pooling seam) #include "vllm/multimodal/inputs.h" // multimodal::MultiModalFeatureSpec (mm seam) #include "vllm/sampling_params.h" #include "vllm/v1/metrics/stats.h" // vllm::v1::SchedulerStats (per-step stats) @@ -97,6 +99,13 @@ struct EngineCoreRequest { // carried into Request.lora_name for the prefix-cache extra-key path. nullopt // for a base-model request. Full LoRA runtime is LORA-RUNTIME. std::optional lora_name = std::nullopt; + // pooling_params (EngineCoreRequest.pooling_params, ARCH-ONE-SURFACE ROW 6): + // set iff this is a POOLING-task request (upstream `params: SamplingParams | + // PoolingParams` — the union collapses to sampling_params + this optional). + // nullopt on every generation request -> byte-identical text path. Carried + // into Request::pooling_params; the scheduler's pooling stop + // (scheduler.py:1718-1721 mirror) fires only when it is set. + std::optional pooling_params = std::nullopt; }; // SamplerOutput (vllm/v1/outputs.py): the raw sampler result for a step. @@ -139,6 +148,13 @@ struct ModelRunnerOutput { // tensor SOURCE (lm_head over prompt positions) is a runner/prefill addition // (SAMPLE-PROMPT-LOGPROBS); the OUTPUT plumbing below consumes it 1:1. std::map prompt_logprobs_dict; + // pooler_output (ModelRunnerOutput.pooler_output, vllm/v1/outputs.py; + // ARCH-ONE-SURFACE ROW 6): one entry per req in `req_ids` order on a POOLING + // model's step — the pooled vector, or nullopt while the request is still + // consuming prefill chunks (the is_valid=false rows, + // pool/pooling_runner.py:40-41). EMPTY (size 0) on every generation step -> + // byte-identical text path. + std::vector>> pooler_output; }; // DraftTokenIds (vllm/v1/outputs.py:310-315): the drafter's proposal for the @@ -179,6 +195,11 @@ struct EngineCoreOutput { // IterationStats.update_from_events (stats.py:428-450) to fill the request's // queue/prefill/inference timing intervals + the preemption counter. std::optional> events; + // pooling_output (EngineCoreOutput.pooling_output, ARCH-ONE-SURFACE ROW 6): + // the pooled vector of a finished POOLING request (scheduler.py:1837 + // `pooling_output=pooler_output`). nullopt on every generation output -> + // byte-identical text path. + std::optional> pooling_output; // finished (property): a request is finished iff finish_reason is set. bool Finished() const { return finish_reason.has_value(); } diff --git a/include/vllm/v1/request.h b/include/vllm/v1/request.h index 28eafdffe..13196d4a3 100644 --- a/include/vllm/v1/request.h +++ b/include/vllm/v1/request.h @@ -58,6 +58,7 @@ #include #include +#include "vllm/model_executor/layers/pooler/pooling_params.h" // PoolingParams #include "vllm/multimodal/inputs.h" // multimodal::MultiModalFeatureSpec #include "vllm/sampling_params.h" #include "vllm/v1/core/kv_cache_utils.h" // BlockHash, BlockHasher @@ -173,6 +174,14 @@ struct Request { // model's EOS token id (for the stop check) rides on sampling_params, as // sampling_params.eos_token_id — read it there, matching upstream check_stop. SamplingParams sampling_params; + // pooling_params (upstream Request.pooling_params, vllm/v1/request.py; + // ARCH-ONE-SURFACE ROW 6): set iff this is a POOLING-task request. The + // scheduler finishes such a request as soon as the runner produced its + // pooled output (scheduler.py:1718-1721 mirror); nullopt on every generation + // request keeps the text path byte-identical. sampling_params above stays a + // benign greedy default for the pooling case (the InputBatch admit reads it; + // the sampler is never invoked on a pooling model's step). + std::optional pooling_params; // structured_output_request (request.py:87-92): the per-request structured // output state (constraint params + the compiled grammar), or nullopt when the // request has no structured-output constraint. Populated at construction from diff --git a/include/vllm/v1/worker/gpu/runner.h b/include/vllm/v1/worker/gpu/runner.h index 5b79710d9..ed6f9241b 100644 --- a/include/vllm/v1/worker/gpu/runner.h +++ b/include/vllm/v1/worker/gpu/runner.h @@ -79,6 +79,7 @@ #include "vllm/v1/worker/gpu/input_batch.h" #include "vllm/v1/worker/gpu/model_runner_base.h" #include "vllm/v1/worker/gpu/prepare_inputs.h" +#include "vllm/v1/worker/gpu/pool/pooling_runner.h" // PoolingRunner (pooling arch) #include "vt/device.h" #include "vt/tensor.h" @@ -333,6 +334,18 @@ class GPUModelRunner final : public ModelRunnerBase { std::unique_ptr draft_model = nullptr, std::vector draft_kv = {}); + // ARCH-ONE-SURFACE ROW 6: the pooling counterpart of sample_tokens (mirror + // of gpu/model_runner.py:1586-1607 + pool/pooling_runner.py:29-42). Consumes + // the stashed forward result — for the pooling arch those are the + // [rows, hidden] post-final-norm hidden states, NOT vocab logits — applies + // the model's Pooler via pooling_runner_, and returns a ModelRunnerOutput + // whose pooler_output carries one pooled vector per fully-prefilled request + // (nullopt for rows still consuming prefill chunks — the same + // seq_len == prompt_len validity predicate as is_valid, pooling_runner.py: + // 40-41, which our discard mask already computes). sampled_token_ids rows + // stay EMPTY: a pooling step samples nothing. + ModelRunnerOutput pool_tokens(); + // Allocate the per-full-attn-layer paged KV buffers + the per-GDN-layer // persistent mamba ssm/conv buffers from the KVCacheConfig groups. void initialize_kv_cache(const KVCacheConfig& kv_cache_config); @@ -378,6 +391,13 @@ class GPUModelRunner final : public ModelRunnerBase { vt::Queue queue_; InputBatch input_batch_; Sampler sampler_; + // ARCH-ONE-SURFACE ROW 6 (mirror of gpu/model_runner.py:368-369 + // `if self.is_pooling_model ...: self.pooling_runner = PoolingRunner(model)`): + // non-null iff the loaded model's registration declares is_pooling_model and + // the model owns a Pooler. sample_tokens then routes to pool_tokens() — the + // POOLED DATA takes the place of sampled tokens (model_runner.py:1586-1607). + // Null for every text arch: the sampler path below is byte-identical. + std::unique_ptr pooling_runner_; // KV group layout (resolved from the KVCacheConfig). int full_attn_group_id_ = -1; diff --git a/scripts/abi-capability-allowlist.txt b/scripts/abi-capability-allowlist.txt index 98d28c824..bfa4be883 100644 --- a/scripts/abi-capability-allowlist.txt +++ b/scripts/abi-capability-allowlist.txt @@ -19,5 +19,4 @@ # # Format: ` | fold= | `. -embeddings / pooling | fold=ARCH-ONE-SURFACE | Engine-side pooler landed (ENG-POOLER-SEQ: pooler/{dispatch_pooler,methods,heads,poolers}.cpp + pool/pooling_runner) but PoolingRunner is never invoked by the live step, no arch sets is_pooling_model, /v1/embeddings is a run_batch "does not support endpoint" residual, and vllm.h has no embed/pool symbol. Fold: grow ABI pooling entry point, wire the live route, register a pooling arch multimodal input (image/audio/video) | fold=ARCH-ONE-SURFACE | vllm_chat/vllm_chat_stream are text-only (vllm.h:440,451); the C-ABI's EnsureChatServing never sets the mm seam (src/capi/vllm_c.cpp), and even the server's Qwen3-VL image seam does not run the vision tower in the live engine step (MM-SERVE-E2E residual). Fold: grow ABI multimodal-input entry point + the engine mm-forward diff --git a/scripts/check-gate-commands.py b/scripts/check-gate-commands.py index d6c5ac5dc..64e25e6c1 100755 --- a/scripts/check-gate-commands.py +++ b/scripts/check-gate-commands.py @@ -244,6 +244,11 @@ def audit() -> list[dict]: "KERNEL-GEMM-CPU-ELEM", "KV-CHUNKED-LOCAL-SPEC", "KV-SLIDING-LOCAL-SPECS", + # ARCH-ONE-SURFACE ROW 6 (2026-08-08): embeddings-one-surface.md carries a + # runnable Gates section (preflight + the fold/capi/server suites) for the + # two rows it activates. + "MODEL-EMBED-llama-llama-for-causal-lm", + "SERVE-POOLING-ENDPOINTS", "KV-SLIDING-WINDOW-SPEC", "LOAD-SAFETENSORS-DIRECT-DENSE", "MODEL-FACTORY-registry", diff --git a/scripts/check-runner-routing-consistency.py b/scripts/check-runner-routing-consistency.py index 267dacfee..845f165ef 100644 --- a/scripts/check-runner-routing-consistency.py +++ b/scripts/check-runner-routing-consistency.py @@ -303,13 +303,16 @@ def resolve_alias(cls: str, alias: dict[str, str]) -> str: return cls +_IS_POOLING = re.compile(r"\.is_pooling_model\s*=\s*true") + + @dataclass(frozen=True) class ModelRoute: """The decode-routing verdict for one REGISTER_VLLM_MODEL registration.""" name: str # allowlist key (registry stem minus _registry) reg_file: str forward_fn: str - classification: str # DEVICE | HOST | REFUSE | NONE + classification: str # DEVICE | HOST | REFUSE | POOLING | NONE private_generate_loop: bool # invariant (b): ships a *GenerateCore host loop device_source: str = "" # which delegated class supplied the device seam activation: str = "BF16_RESIDENT" # invariant (c): F32_STREAM | BF16_RESIDENT @@ -402,7 +405,17 @@ def scan_registrations( } device_source = "" classification = "NONE" - if classify_with_helpers(body, text) == "DEVICE": + # POOLING registrations (ARCH-ONE-SURFACE ROW 6): a registry TU that + # declares `.is_pooling_model = true` registers a NON-GENERATIVE model - + # its forward is a HIDDEN-STATE producer for the PoolingRunner (the + # engine pools instead of sampling, gpu/model_runner.py:1586-1607), so + # the device-resident-LOGITS seam does not apply BY DESIGN, exactly as + # a refuse-by-name stub decodes nothing. Classified explicitly (never + # the silently-exempt NONE bucket) and excluded from the HOST-drift + # gate; the bf16-activation invariant still applies to its body. + if _IS_POOLING.search(strip_comments(text)): + classification = "POOLING" + elif classify_with_helpers(body, text) == "DEVICE": classification, device_source = "DEVICE", fn for cls in delegated_classes: impl = fd_bodies.get(cls) @@ -411,7 +424,7 @@ def scan_registrations( impl_class = classify_with_helpers(impl[1], file_text.get(impl[0], "")) if impl_class == "DEVICE" and classification != "DEVICE": classification, device_source = "DEVICE", cls - if classification != "DEVICE": + if classification not in ("DEVICE", "POOLING"): # Not device-reachable: rank the delegated ForwardDevice impls, else the # hook body itself, as HOST > REFUSE > NONE. impl_classes = [ @@ -522,6 +535,7 @@ def main() -> int: n_device = sum(1 for r in scanned.values() if r.classification == "DEVICE") n_host = sum(1 for r in scanned.values() if r.classification == "HOST") n_refuse = sum(1 for r in scanned.values() if r.classification == "REFUSE") + n_pooling = sum(1 for r in scanned.values() if r.classification == "POOLING") n_none = sum(1 for r in scanned.values() if r.classification == "NONE") n_f32 = sum(1 for r in scanned.values() if r.activation == "F32_STREAM") n_bf16 = sum( @@ -566,7 +580,9 @@ def main() -> int: f"OK (runner-routing): {len(scanned)} registered model(s); " f"{n_device} return device-resident logits on the runner, " f"{n_host} host-logits off-framework ({len(allowlisted)} allowlisted), " - f"{n_refuse} refuse-by-name stub(s) skipped, {n_none} no-logit-producer." + f"{n_refuse} refuse-by-name stub(s) skipped, " + f"{n_pooling} pooling (hidden-state producer(s) for the PoolingRunner), " + f"{n_none} no-logit-producer." ) # Invariant (c): bf16-resident activations (no hand-rolled f32 host stream). diff --git a/scripts/check-supported-models.py b/scripts/check-supported-models.py index 9fd9c4429..e647a21a5 100644 --- a/scripts/check-supported-models.py +++ b/scripts/check-supported-models.py @@ -58,7 +58,8 @@ # future registered arch stops matching this, the self-check below fails loudly # rather than silently dropping it from the comparison. ARCH_TOKEN_RE = re.compile( - r"`([A-Za-z0-9_]+For(?:CausalLM|ConditionalGeneration|CTC|RNNT|TDT))`" + r"`([A-Za-z0-9_]+(?:For(?:CausalLM|ConditionalGeneration|CTC|RNNT|TDT)" + r"|Model))`" ) diff --git a/scripts/mm/llama_embed_fixture_gen.py b/scripts/mm/llama_embed_fixture_gen.py new file mode 100644 index 000000000..1331b14b7 --- /dev/null +++ b/scripts/mm/llama_embed_fixture_gen.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +"""Generate the committed tiny `LlamaModel` EMBEDDING fixture (ARCH-ONE-SURFACE +ROW 6, the #121 committed-fixture precedent). + +Writes tests/vllm/models/fixtures/llama_embed_e2e/: + config.json — architectures ["LlamaModel"] (the bare *Model arch the + upstream _EMBEDDING_MODELS maps onto the Llama backbone, + vllm/model_executor/models/registry.py:230) + tokenizer.json — minimal Metaspace BPE vocab (tok::Tokenizer-parseable) + model.safetensors — deterministic tiny bf16 backbone in the BARE `*Model` + name layout (embed_tokens.weight / layers.N... / + norm.weight — NO "model." prefix, NO lm_head), the + layout adapters.py:178-181 maps with candidate_prefixes + +Deterministic: fixed seed, no torch. Re-running reproduces byte-identical +files, so the committed fixture is reviewable. + +Usage: python3 scripts/mm/llama_embed_fixture_gen.py +""" + +from __future__ import annotations + +import json +import struct +from pathlib import Path + +OUT = Path(__file__).resolve().parents[2] / "tests/vllm/models/fixtures/llama_embed_e2e" + +HIDDEN = 64 +LAYERS = 2 +HEADS = 4 +KV_HEADS = 2 +HEAD_DIM = 16 +INTERMEDIATE = 128 +VOCAB = 32 + + +def f32_to_bf16_bits(x: float) -> int: + """Round-to-nearest-even f32 -> bf16, matching vt::F32ToBF16.""" + (bits,) = struct.unpack("> 16) & 1 + rounding = 0x7FFF + lsb + return ((bits + rounding) >> 16) & 0xFFFF + + +class Rng: + """Deterministic xorshift32 in [-scale, scale) — no numpy dependency.""" + + def __init__(self, seed: int): + self.state = seed & 0xFFFFFFFF or 1 + + def next_u32(self) -> int: + x = self.state + x ^= (x << 13) & 0xFFFFFFFF + x ^= x >> 17 + x ^= (x << 5) & 0xFFFFFFFF + self.state = x + return x + + def uniform(self, scale: float) -> float: + return (self.next_u32() / 2**32 * 2.0 - 1.0) * scale + + +def bf16_tensor(shape: list[int], seed: int, scale: float = 0.08) -> bytes: + rng = Rng(seed) + numel = 1 + for s in shape: + numel *= s + return b"".join( + struct.pack(" None: + header: dict[str, dict] = {} + offset = 0 + for name, (shape, data) in tensors.items(): + header[name] = { + "dtype": "BF16", + "shape": shape, + "data_offsets": [offset, offset + len(data)], + } + offset += len(data) + hdr = json.dumps(header, sort_keys=True).encode() + with open(path, "wb") as f: + f.write(struct.pack(" None: + OUT.mkdir(parents=True, exist_ok=True) + + config = { + "architectures": ["LlamaModel"], + "model_type": "llama", + "hidden_size": HIDDEN, + "num_hidden_layers": LAYERS, + "num_attention_heads": HEADS, + "num_key_value_heads": KV_HEADS, + "head_dim": HEAD_DIM, + "intermediate_size": INTERMEDIATE, + "rms_norm_eps": 1e-5, + "rope_theta": 500000.0, + "vocab_size": VOCAB, + "max_position_embeddings": 128, + "torch_dtype": "bfloat16", + "tie_word_embeddings": False, # embedding conversion: NO lm_head at all + "attention_bias": False, + } + (OUT / "config.json").write_text(json.dumps(config, indent=1) + "\n") + + # Minimal Metaspace BPE the tok::Tokenizer parses; ids stay < VOCAB. + tokenizer = { + "version": "1.0", + "pre_tokenizer": { + "type": "Metaspace", + "replacement": "▁", + "prepend_scheme": "always", + "split": True, + }, + "decoder": { + "type": "Metaspace", + "replacement": "▁", + "prepend_scheme": "always", + "split": True, + }, + "model": { + "type": "BPE", + "unk_token": None, + # Character-level vocab (BPE with empty merges tokenizes each + # Metaspace pre-token into characters): every lowercase test + # string encodes to in-vocab ids, so EncodeWithSpecialTokens never + # fails and ids stay < vocab_size. + "vocab": {"▁": 0} | {chr(c): i + 1 for i, c in enumerate(range(ord("a"), ord("z") + 1))}, + "merges": [], + }, + "added_tokens": [], + } + (OUT / "tokenizer.json").write_text(json.dumps(tokenizer, indent=1) + "\n") + + qdim = HEADS * HEAD_DIM + kdim = KV_HEADS * HEAD_DIM + tensors: dict[str, tuple[list[int], bytes]] = {} + seed = 1 + # BARE `*Model` layout: no "model." prefix, no lm_head (adapters.py:135-181). + tensors["embed_tokens.weight"] = ([VOCAB, HIDDEN], bf16_tensor([VOCAB, HIDDEN], seed)) + seed += 1 + tensors["norm.weight"] = ([HIDDEN], bf16_tensor([HIDDEN], seed, 0.5)) + seed += 1 + for layer in range(LAYERS): + base = f"layers.{layer}." + tensors[base + "input_layernorm.weight"] = ( + [HIDDEN], bf16_tensor([HIDDEN], seed, 0.5)) + seed += 1 + tensors[base + "post_attention_layernorm.weight"] = ( + [HIDDEN], bf16_tensor([HIDDEN], seed, 0.5)) + seed += 1 + tensors[base + "self_attn.q_proj.weight"] = ( + [qdim, HIDDEN], bf16_tensor([qdim, HIDDEN], seed)) + seed += 1 + tensors[base + "self_attn.k_proj.weight"] = ( + [kdim, HIDDEN], bf16_tensor([kdim, HIDDEN], seed)) + seed += 1 + tensors[base + "self_attn.v_proj.weight"] = ( + [kdim, HIDDEN], bf16_tensor([kdim, HIDDEN], seed)) + seed += 1 + tensors[base + "self_attn.o_proj.weight"] = ( + [HIDDEN, qdim], bf16_tensor([HIDDEN, qdim], seed)) + seed += 1 + tensors[base + "mlp.gate_proj.weight"] = ( + [INTERMEDIATE, HIDDEN], bf16_tensor([INTERMEDIATE, HIDDEN], seed)) + seed += 1 + tensors[base + "mlp.up_proj.weight"] = ( + [INTERMEDIATE, HIDDEN], bf16_tensor([INTERMEDIATE, HIDDEN], seed)) + seed += 1 + tensors[base + "mlp.down_proj.weight"] = ( + [HIDDEN, INTERMEDIATE], bf16_tensor([HIDDEN, INTERMEDIATE], seed)) + seed += 1 + + write_safetensors(OUT / "model.safetensors", tensors) + total = sum(len(d) for _, d in tensors.values()) + print(f"wrote {OUT} (weights {total} bytes, {len(tensors)} tensors)") + + +if __name__ == "__main__": + main() diff --git a/src/capi/vllm_c.cpp b/src/capi/vllm_c.cpp index 00071ea42..b36d61173 100644 --- a/src/capi/vllm_c.cpp +++ b/src/capi/vllm_c.cpp @@ -95,6 +95,9 @@ struct vllm_engine { // the HTTP server's worker pool). Guarded by chat_mutex for the lazy build. std::mutex chat_mutex; std::unique_ptr chat_serving; + // ABI v15: serialize vllm_embed batches per handle (the pooling path drives + // the SYNCHRONOUS LLMEngine, not the AsyncLLM the text entry points share). + std::mutex embed_mutex; }; // One non-blocking callback-delivery request. The AsyncLLM output handler owns @@ -146,9 +149,22 @@ void ClearError() { g_last_error.clear(); } // A transcription-only handle (Parakeet) reports an actionable error instead // of dereferencing the null LoadedEngine — the SupportsTranscription-only // mirror of vLLM excluding "generate" from supported_tasks -// (vllm/model_executor/models/interfaces.py:1118). +// (vllm/model_executor/models/interfaces.py:1118). Since ABI v15 the SAME +// guard also refuses a POOLING (embedding) engine: its model has no +// text-generation path either (is_pooling_model && !is_text_generation_model, +// the mirror of vLLM's runner_type validation, config/model.py:607-613) — +// running generate on it would sample over hidden states. bool RequireTextEngine(const vllm_engine* engine, const char* fn) { - if (engine->loaded != nullptr) return true; + if (engine->loaded != nullptr) { + if (engine->loaded->is_pooling_model()) { + SetError(std::string(fn) + + ": this engine was loaded from a pooling (embedding) " + "checkpoint; it has no text-generation path — use vllm_embed " + "or the server's /v1/embeddings"); + return false; + } + return true; + } SetError(std::string(fn) + ": this engine was loaded from a transcription-only checkpoint " "(Parakeet); it has no text-generation path — use vllm_transcribe"); @@ -784,6 +800,11 @@ VLLM_API vllm_status vllm_complete_tokens( SetError("vllm_complete_tokens: out_tokens is null with max_out_tokens > 0"); return VLLM_ERR_INVALID_ARGUMENT; } + // Refuse-by-task (ABI v15 tightening): v13 shipped this entry point without + // the v11 guard, so a transcription-only handle would deref the null + // LoadedEngine here; the same guard now also refuses pooling engines. + if (!RequireTextEngine(engine, "vllm_complete_tokens")) + return VLLM_ERR_INVALID_ARGUMENT; try { const vllm::SamplingParams sp = ToSamplingParams(*params, vllm::RequestOutputKind::kCumulative); @@ -1227,6 +1248,127 @@ VLLM_API void vllm_transcription_free(vllm_transcription* out) { out->has_text = 0; } +// ── Embeddings (ABI v15, ARCH-ONE-SURFACE ROW 6) ──────────────────────────── +// The pooling slice of the ONE surface: the SAME registry forward + +// PoolingRunner engine step the server's /v1/embeddings drives +// (LLMEngine::embed -> pool_tokens, the mirror of LLM.embed / +// entrypoints/pooling/offline.py:65-119 with pooling_task="embed"). + +VLLM_API vllm_status vllm_embed(vllm_engine* engine, const char* const* texts, + int32_t n_texts, vllm_embedding_result* out) { + if (out == nullptr) { + SetError("vllm_embed: out is null"); + return VLLM_ERR_INVALID_ARGUMENT; + } + out->values = nullptr; + out->n_embeddings = 0; + out->dim = 0; + out->prompt_tokens = 0; + if (engine == nullptr || texts == nullptr) { + SetError("vllm_embed: engine or texts is null"); + return VLLM_ERR_INVALID_ARGUMENT; + } + if (n_texts <= 0) { + SetError("vllm_embed: n_texts must be > 0"); + return VLLM_ERR_INVALID_ARGUMENT; + } + for (int32_t i = 0; i < n_texts; ++i) { + if (texts[i] == nullptr) { + SetError("vllm_embed: texts[" + std::to_string(i) + "] is null"); + return VLLM_ERR_INVALID_ARGUMENT; + } + } + // Refuse-by-task, the other direction of RequireTextEngine: only a POOLING + // (embedding) engine serves this entry point — the mirror of vLLM refusing + // `--runner pooling` on a non-pooling model (config/model.py:612-617). + if (engine->loaded == nullptr) { + SetError( + "vllm_embed: this engine was loaded from a transcription-only " + "checkpoint (Parakeet); use vllm_transcribe"); + return VLLM_ERR_INVALID_ARGUMENT; + } + if (!engine->loaded->is_pooling_model()) { + SetError( + "vllm_embed: this engine was loaded from a text-generation " + "checkpoint; it has no pooling path — use vllm_complete / vllm_chat " + "(embedding checkpoints resolve to a pooling architecture, e.g. " + "LlamaModel)"); + return VLLM_ERR_INVALID_ARGUMENT; + } + try { + // Serialize embed batches per handle: the pooling path drives the + // SYNCHRONOUS LLMEngine step loop (async scheduling resolves OFF for + // pooling models, config/vllm.py:1068-1073 mirror). + std::lock_guard lock(engine->embed_mutex); + const vllm::tok::Tokenizer& tokenizer = engine->loaded->tokenizer(); + vllm::v1::LLMEngine& e = engine->loaded->engine(); + + std::vector> vectors; + vectors.reserve(static_cast(n_texts)); + int64_t total_prompt_tokens = 0; + for (int32_t i = 0; i < n_texts; ++i) { + // The serving tokenization applies the template's special tokens + // (add_special_tokens=True on the OpenAI embedding path). + std::vector ids = tokenizer.EncodeWithSpecialTokens(texts[i]); + if (ids.empty()) { + SetError("vllm_embed: texts[" + std::to_string(i) + + "] tokenized to an empty prompt"); + return VLLM_ERR_INVALID_ARGUMENT; + } + total_prompt_tokens += static_cast(ids.size()); + const std::string request_id = + "embed-" + std::to_string(engine->next_request_id.fetch_add(1)); + vllm::RequestOutput ro = + e.embed(std::move(ids), vllm::PoolingParams{}, request_id); + if (!ro.finished || !ro.pooling_output.has_value()) { + SetError("vllm_embed: engine produced no pooled output"); + return VLLM_ERR_RUNTIME; + } + vectors.push_back(std::move(*ro.pooling_output)); + } + + const size_t dim = vectors.empty() ? 0 : vectors[0].size(); + for (const std::vector& v : vectors) { + if (v.size() != dim || dim == 0) { + SetError("vllm_embed: inconsistent embedding dimensions"); + return VLLM_ERR_RUNTIME; + } + } + float* values = static_cast( + std::malloc(static_cast(n_texts) * dim * sizeof(float))); + if (values == nullptr) { + SetError("vllm_embed: out-of-memory copying embeddings"); + return VLLM_ERR_RUNTIME; + } + for (int32_t i = 0; i < n_texts; ++i) { + std::memcpy(values + static_cast(i) * dim, + vectors[static_cast(i)].data(), + dim * sizeof(float)); + } + out->values = values; + out->n_embeddings = n_texts; + out->dim = static_cast(dim); + out->prompt_tokens = static_cast(total_prompt_tokens); + ClearError(); + return VLLM_OK; + } catch (const std::exception& e) { + SetError(std::string("vllm_embed: ") + e.what()); + return VLLM_ERR_RUNTIME; + } catch (...) { + SetError("vllm_embed: unknown error"); + return VLLM_ERR_UNKNOWN; + } +} + +VLLM_API void vllm_embedding_result_free(vllm_embedding_result* out) { + if (out == nullptr) return; + std::free(out->values); + out->values = nullptr; + out->n_embeddings = 0; + out->dim = 0; + out->prompt_tokens = 0; +} + // ── Video+audio generation (ABI v12, MiniMax-H3) ──────────────────────────── // Thin C wrappers over the ONE library seam // (vllm::multimodal::MiniMaxH3VideoEngine) the server's /v1/videos routes and diff --git a/src/vllm/entrypoints/model_loader.cpp b/src/vllm/entrypoints/model_loader.cpp index 846d77eb6..bbe5249f1 100644 --- a/src/vllm/entrypoints/model_loader.cpp +++ b/src/vllm/entrypoints/model_loader.cpp @@ -553,9 +553,14 @@ vllm::SchedulerConfig LoadedEngine::MakeSchedulerConfig( // ResolveAsyncScheduling(runner_supports_async) yields runner_supports_async // (when otherwise compatible). bool LoadedEngine::ResolveAsyncEnabled( - const vllm::SchedulerConfig& scheduler_config, bool runner_supports_async) { - return vllm::AsyncSchedulingEnabled( - scheduler_config.ResolveAsyncScheduling(runner_supports_async)); + const vllm::SchedulerConfig& scheduler_config, bool runner_supports_async, + bool is_pooling_model) { + // Pooling models resolve async scheduling OFF (the mirror of vLLM disabling + // it by default for pooling models, vllm/config/vllm.py:1068-1073) — the + // landed is_pooling_model arm of ResolveAsyncScheduling, wired here since + // ARCH-ONE-SURFACE ROW 6. false (every text arch) is byte-identical. + return vllm::AsyncSchedulingEnabled(scheduler_config.ResolveAsyncScheduling( + runner_supports_async, is_pooling_model)); } std::unique_ptr LoadedEngine::MakeScheduler( @@ -734,7 +739,8 @@ LoadedEngine::LoadedEngine(HfConfig config, max_model_len_, params.max_num_seqs > 0 ? params.max_num_seqs : 8, max_num_batched_tokens_, params.policy), - runner_.runner_supports_async())), + runner_.runner_supports_async(), + model_->registration().info.is_pooling_model)), max_concurrent_batches_(MakeSchedulerConfig( max_model_len_, params.max_num_seqs > 0 ? params.max_num_seqs diff --git a/src/vllm/entrypoints/openai/api_server.cpp b/src/vllm/entrypoints/openai/api_server.cpp index 624f46b3d..1f692dbe2 100644 --- a/src/vllm/entrypoints/openai/api_server.cpp +++ b/src/vllm/entrypoints/openai/api_server.cpp @@ -3,6 +3,8 @@ // dependency deviation. #include "vllm/entrypoints/openai/api_server.h" +#include +#include #include #include #include @@ -365,6 +367,113 @@ ApiServer::DispatchResult ApiServer::handle_audio_transcriptions( } } +ApiServer::DispatchResult ApiServer::handle_embeddings( + const std::string& request_body) const { + // Mirror of vLLM pooling/embed/api_router.py:28 `create_embedding` over the + // EmbeddingCompletionRequest shape (embed/protocol.py:34: `model` + `input` + // as ONE string or an ARRAY of strings) and the EmbeddingResponse shape + // (embed/protocol.py:173-185). The embedding itself runs through the ONE + // engine path (LLMEngine::embed -> registry forward -> PoolingRunner) — the + // same code path vllm_embed drives, so HTTP and FFI cannot drift. + if (!embedder_) { + // The api_router `if handler is None` mirror (embed/api_router.py:22-25); + // the socket layer never registers the route without an embedder. + return MakeError(500, "InternalServerError", + "The model does not support Embeddings API"); + } + nlohmann::json body; + try { + body = nlohmann::json::parse(request_body); + } catch (const std::exception& e) { + return MakeError(400, "BadRequestError", + std::string("invalid JSON body: ") + e.what()); + } + if (!body.is_object()) { + return MakeError(400, "BadRequestError", "request body must be an object"); + } + // model: honoured like every other serving handler — an unknown name is 404. + if (body.contains("model") && body["model"].is_string() && + !models_.is_base_model(body["model"].get())) { + return MakeError(404, "NotFoundError", + "The model `" + body["model"].get() + + "` does not exist."); + } + // encoding_format: float (the default) only; base64 is a NAMED residual. + if (body.contains("encoding_format") && body["encoding_format"].is_string() && + body["encoding_format"].get() != "float") { + return MakeError(400, "BadRequestError", + "encoding_format '" + + body["encoding_format"].get() + + "' is not supported (supported: float; base64 is a " + "named residual)"); + } + if (body.contains("dimensions") && !body["dimensions"].is_null()) { + // Matryoshka truncation is a NAMED residual of this fold (the pooler op + // supports it; the request plumb does not yet). + return MakeError(400, "BadRequestError", + "dimensions is not supported yet (named residual)"); + } + // input: ONE string or an ARRAY of strings (embed/protocol.py:34 + // EmbeddingCompletionRequest via CompletionRequestMixin). Token-array + // inputs are a NAMED residual. + std::vector inputs; + if (!body.contains("input")) { + return MakeError(400, "BadRequestError", "input is required"); + } + if (body["input"].is_string()) { + inputs.push_back(body["input"].get()); + } else if (body["input"].is_array()) { + for (const nlohmann::json& item : body["input"]) { + if (!item.is_string()) { + return MakeError(400, "BadRequestError", + "input must be a string or an array of strings " + "(token-array inputs are a named residual)"); + } + inputs.push_back(item.get()); + } + if (inputs.empty()) { + return MakeError(400, "BadRequestError", + "input must contain at least one string"); + } + } else { + return MakeError(400, "BadRequestError", + "input must be a string or an array of strings"); + } + + try { + const EmbeddingBatch batch = embedder_(inputs); + if (batch.embeddings.size() != inputs.size()) { + return MakeError(500, "InternalServerError", + "embedder returned a mismatched batch"); + } + nlohmann::json data = nlohmann::json::array(); + for (size_t i = 0; i < batch.embeddings.size(); ++i) { + data.push_back(nlohmann::json{ + {"index", static_cast(i)}, + {"object", "embedding"}, + {"embedding", batch.embeddings[i]}, + }); + } + // id: "embd-" (upstream f"embd-{random_uuid()}", + // embed/protocol.py:180 — the serving_completion.h counter stand-in). + static std::atomic embd_counter{0}; + DispatchResult r; + r.body = nlohmann::json{ + {"id", "embd-" + std::to_string(embd_counter.fetch_add(1))}, + {"object", "list"}, + {"created", static_cast(std::time(nullptr))}, + {"model", models_.model_name()}, + {"data", std::move(data)}, + {"usage", + nlohmann::json{{"prompt_tokens", batch.prompt_tokens}, + {"total_tokens", batch.prompt_tokens}}}, + }.dump(); + return r; + } catch (const std::exception& e) { + return MakeError(500, "InternalServerError", e.what()); + } +} + ApiServer::DispatchResult ApiServer::handle_videos( const std::string& request_body) { // vLLM-Omni's ASYNC video endpoint: validate, enqueue, and return the job id @@ -905,6 +1014,19 @@ void ApiServer::register_routes() { write(handle_server_info(), res); }); + if (embedder_) { + // Embeddings (ARCH-ONE-SURFACE ROW 6). Registered ONLY when an embedder is + // attached (task-conditional, the api_server.py:255-265 supported_tasks + // mirror), so a text server answers 404 at the route table — and an + // embedding server, having no completion_/chat_ handlers, answers 404 on + // the generate routes the same way. + server.Post("/v1/embeddings", + [this, write](const httplib::Request& req, + httplib::Response& res) { + write(handle_embeddings(req.body), res); + }); + } + if (transcriber_) { // Parakeet ASR (ARCH-ONE-SURFACE ROW 1). Registered ONLY when a // transcriber is attached, so a text server answers 404 exactly as before. diff --git a/src/vllm/model_executor/models/llama_embedding_registry.cpp b/src/vllm/model_executor/models/llama_embedding_registry.cpp new file mode 100644 index 000000000..cf1e50b7c --- /dev/null +++ b/src/vllm/model_executor/models/llama_embedding_registry.cpp @@ -0,0 +1,135 @@ +// `LlamaModel` EMBEDDING registry TU — ARCH-ONE-SURFACE fold ROW 6, the first +// live pooling architecture. ADDITIVE self-registration (new TU + one +// REGISTER_VLLM_MODEL line, zero shared-array edits), the parakeet_registry / +// llama_registry precedent. +// +// UPSTREAM MIRROR, exactly: the pinned vLLM's `_EMBEDDING_MODELS` maps +// "LlamaModel": ("llama", "LlamaForCausalLM") +// (vllm/model_executor/models/registry.py:230) and, because LlamaForCausalLM is +// not itself a pooling model, `--runner pooling` resolves `--convert embed` +// (vllm/config/model.py:1058-1060) and wraps the class with `as_embedding_model` +// (adapters.py:230-261): +// * the output layer (lm_head / logits processor) is replaced by a +// missing-layer stage (adapters.py:135-151) — the model FORWARD returns +// hidden states, not logits; +// * `self.pooler = DispatchPooler.for_embedding(pooler_config)` +// (adapters.py:248-257), LAST sequence pooling by default for a +// decoder-only conversion (interfaces_base.py:160 +// `default_seq_pooling_type: ClassVar = "LAST"`); +// * checkpoint weights load from BOTH the `*ForCausalLM` and bare `*Model` +// name layouts (adapters.py:178-181 candidate_prefixes ["", "model."]). +// The registered forward here is therefore the SHARED dense backbone +// (LlamaModel == Qwen3DenseModel, llama.h:39-40) run to the post-final-norm +// hidden (Qwen3DenseModel::ForwardHidden) — no new model was built. +// +// TASK ROUTING (the #121 refuse-by-task precedent, both directions): +// info.is_pooling_model=true + is_text_generation_model=false is the registry +// truth the entrypoints dispatch on — the C ABI refuses vllm_complete/vllm_chat +// on this arch (pointing at vllm_embed / /v1/embeddings) and refuses vllm_embed +// on a text arch; the server registers /v1/embeddings INSTEAD OF the generate +// routes. The engine step routes this model's batches through the landed +// PoolingRunner instead of the sampler (runner.cpp pooling branch, the mirror +// of gpu/model_runner.py:368-369 + 1586-1607). +#include "vllm/model_executor/models/model_registry.h" + +#include +#include +#include +#include + +#include "vllm/model_executor/layers/pooler/dispatch_pooler.h" +#include "vllm/model_executor/layers/pooler/pooler_config.h" +#include "vllm/model_executor/models/llama.h" +#include "vllm/model_executor/models/qwen3_5.h" // ForwardLogits carrier + +namespace vllm { +namespace { + +// registry.py _ModelInfo for the embedding conversion: a POOLING model with NO +// text-generation path (the wrapped class serves task "embed" only — +// pooling_runner.py:22-27 admits exactly ["embed"]). +inline constexpr ModelInfo kLlamaEmbeddingInfo{ + .is_text_generation_model = false, + .is_pooling_model = true, + .is_hybrid = false, + .has_inner_state = false, + .supports_multimodal = false, + .supports_transcription = false, + .supports_transcription_only = false, + .score_type = "bi-encoder", +}; + +// Opaque owned model: the shared dense weight container + the model-owned +// DispatchPooler (the VllmModelForPooling.pooler mirror the PoolingRunner is +// built over, adapters.py:248-257). +class LlamaEmbeddingLoadedModel final : public LoadedModel { + public: + LlamaEmbeddingLoadedModel(const ModelRegistration& registration, + LlamaWeights weights) + : LoadedModel(registration), + weights_(std::move(weights)), + pooler_(DispatchPooler::ForEmbedding(PoolerConfig{}, + SequencePoolingType::kLast)) {} + + const LlamaWeights& weights() const { return weights_; } + const Pooler* pooler() const override { return pooler_.get(); } + + private: + LlamaWeights weights_; + std::unique_ptr pooler_; +}; + +std::unique_ptr LoadLlamaModelEmbedding( + const ModelRegistration& registration, const HfConfig& config, + const ModelSource& source) { + if (source.kind != ModelSource::Kind::kSafetensors) { + throw std::runtime_error( + "Model architecture LlamaModel does not support GGUF weights"); + } + if (source.safetensors == nullptr) { + throw std::runtime_error("safetensors model source is empty"); + } + return std::make_unique( + registration, LoadLlamaModelEmbeddingWeights(*source.safetensors, config)); +} + +void PrepareLlamaModelEmbedding(LoadedModel& model, const HfConfig& config, + vt::Queue& queue) { + (void)model; + (void)config; + (void)queue; +} + +ForwardLogits ForwardLlamaModelEmbedding(LoadedModel& model, + const ModelForwardInput& input) { + // The POOLING forward: shared dense backbone to the post-final-norm hidden, + // NO lm_head (the as_embedding_model missing-layer stage). The returned + // carrier holds [n_out, hidden_size] f32 host rows; the runner's pooling + // branch (never the sampler) consumes them. logits_indices gathers the + // per-request last-token rows exactly as the text path would — which for + // LAST pooling IS upstream's `hidden_states[input_batch.logits_indices]` + // (pooling_runner.py:36). + // (The runner passes empty logits_indices when the gather toggle is off; the + // pooling branch then host-gathers, mirroring the text host path.) + auto& emb = static_cast(model); + return LlamaModel::ForwardHidden(input.token_ids, input.positions, + input.attn_meta, input.attn_kv, + emb.weights(), input.config, input.queue, + input.logits_indices); +} + +const ModelFactory kLlamaEmbeddingFactory{ + .parse_config = &ParseLlamaForCausalLMConfig, + .load_weights = &LoadLlamaModelEmbedding, + .prepare = &PrepareLlamaModelEmbedding, + .forward = &ForwardLlamaModelEmbedding, + .make_kv_cache = &MakeLlamaForCausalLMKVCache, + .is_dense_model = true, +}; + +} // namespace + +REGISTER_VLLM_MODEL(llama_model_embedding, "LlamaModel", kLlamaEmbeddingFactory, + kLlamaEmbeddingInfo) + +} // namespace vllm diff --git a/src/vllm/model_executor/models/llama_weights.cpp b/src/vllm/model_executor/models/llama_weights.cpp index 39e936c53..1c23a63d2 100644 --- a/src/vllm/model_executor/models/llama_weights.cpp +++ b/src/vllm/model_executor/models/llama_weights.cpp @@ -118,4 +118,50 @@ LlamaWeights LoadLlamaForCausalLMWeights( return w; } +LlamaWeights LoadLlamaModelEmbeddingWeights( + const std::vector& shards, const HfConfig& config) { + // ARCH-ONE-SURFACE ROW 6: the `LlamaModel` EMBEDDING checkpoint loader — + // the SAME name map as LoadLlamaForCausalLMWeights (LoadLlamaLayer above is + // the single source of it), with the two as_embedding_model deltas: + // 1. BOTH name layouts load (adapters.py:178-181 candidate_prefixes + // ["", "model."]): a bare `LlamaModel` checkpoint names its tensors + // `embed_tokens.weight` / `layers.N...` / `norm.weight` (no "model." + // prefix); a `*ForCausalLM`-layout export keeps the prefix. The + // resolver maps the canonical "model."-prefixed ask onto whichever + // layout the shards actually carry. + // 2. NO lm_head, ever (adapters.py:135-151 replaces the output layer with + // a missing-layer stage): tie_word_embeddings is forced true so the + // pooling forward — which never multiplies by an output layer — has a + // well-formed container, and a checkpoint lm_head.weight is ignored. + std::unordered_map where; + for (const SafetensorsFile& shard : shards) + for (const std::string& name : shard.Names()) where[name] = &shard; + const TensorResolver get = + [&where](const std::string& name) -> const StTensor& { + std::string key = name; + auto it = where.find(key); + if (it == where.end() && key.rfind("model.", 0) == 0) { + key = key.substr(6); // the bare `*Model` layout + it = where.find(key); + } + VT_CHECK(it != where.end(), "llama embedding: tensor not found: " + name); + return it->second->Get(key); + }; + + VT_CHECK(config.num_hidden_layers > 0, + "llama embedding: num_hidden_layers must be positive"); + + LlamaWeights w; + w.tie_word_embeddings = true; // no output layer on the pooling forward + w.attention_bias = RawBool(config.raw, "attention_bias", false); + + w.embed_tokens = LoadBf16Direct(get, "model.embed_tokens.weight"); + w.final_norm = LoadBf16Direct(get, "model.norm.weight"); + + w.layers.reserve(static_cast(config.num_hidden_layers)); + for (int64_t l = 0; l < config.num_hidden_layers; ++l) + w.layers.push_back(LoadLlamaLayer(get, l, w.attention_bias)); + return w; +} + } // namespace vllm diff --git a/src/vllm/model_executor/models/qwen3.cpp b/src/vllm/model_executor/models/qwen3.cpp index 014d212c3..169ebc340 100644 --- a/src/vllm/model_executor/models/qwen3.cpp +++ b/src/vllm/model_executor/models/qwen3.cpp @@ -215,12 +215,20 @@ void EmbedInto(Dev d, DBuf& hidden, const std::vector& token_ids, // `hidden` DBuf reassignment (RunLayer's `hidden = MlpBlock(...)`) never disturbs // the persistent embedding — the copy is a pure device->device data move, so the // layer sequence and its output are BYTE-IDENTICAL to the pre-split forward. +// `return_hidden` (ARCH-ONE-SURFACE ROW 6, default false = byte-identical +// text path): when true, STOP after the final RMSNorm (+ the logits_indices +// gather) and return the [n_out, H] hidden rows upcast to f32 — the pooling +// forward of an embedding conversion, whose model has NO lm_head at all +// (adapters.py:135-151: as_embedding_model replaces the output layer with a +// missing-layer stage; the pooler consumes the post-final-norm hidden). Every +// existing caller leaves the default, so the lm_head tail is untouched. DBuf ForwardLayers(Dev d, const Tensor& hidden_in, const std::vector& positions, const CommonAttentionMetadata& attn_meta, const std::vector& attn_kv, const Qwen3DenseWeights& weights, const HfConfig& config, - const std::vector& logits_indices) { + const std::vector& logits_indices, + bool return_hidden = false) { const int64_t T = hidden_in.shape[0]; const int64_t H = config.hidden_size; const int64_t vocab = config.vocab_size; @@ -254,13 +262,6 @@ DBuf ForwardLayers(Dev d, const Tensor& hidden_in, vt::RmsNorm(d.q, dnorm.t(), hidden.t(), w_fn, vt::RmsNormArgs{eps, false}, &res.t()); } - // lm_head. Tied (Qwen3-0.6B): logits = hidden @ embed_tokens^T via MatmulBT - // over the [vocab,H] embed table (== [N=vocab,K=H]). Untied: the loaded - // Matmul-B [H,vocab] lm_head via vt::Matmul. - const bool tied = weights.tie_word_embeddings || weights.lm_head.Empty(); - Tensor lm = tied ? ResidentWeight(d, weights.embed_tokens, {vocab, H}) - : ResidentWeight(d, weights.lm_head); - const bool do_gather = !logits_indices.empty() && static_cast(logits_indices.size()) < T; Tensor src = dnorm.t(); @@ -272,6 +273,23 @@ DBuf ForwardLayers(Dev d, const Tensor& hidden_in, src = dgather.t(); } const int64_t n_out = src.shape[0]; + + // ARCH-ONE-SURFACE ROW 6 pooling tail: the post-final-norm hidden rows, + // upcast bf16 -> f32 (vt::CastF32), with NO lm_head — an embedding-converted + // checkpoint has no output layer to multiply by. Never taken by any text + // caller (return_hidden defaults false). + if (return_hidden) { + DBuf dhid(d, DType::kF32, {n_out, H}); + vt::CastF32(d.q, dhid.t(), src); + return dhid; + } + + // lm_head. Tied (Qwen3-0.6B): logits = hidden @ embed_tokens^T via MatmulBT + // over the [vocab,H] embed table (== [N=vocab,K=H]). Untied: the loaded + // Matmul-B [H,vocab] lm_head via vt::Matmul. + const bool tied = weights.tie_word_embeddings || weights.lm_head.Empty(); + Tensor lm = tied ? ResidentWeight(d, weights.embed_tokens, {vocab, H}) + : ResidentWeight(d, weights.lm_head); DBuf logits(d, DType::kF32, {n_out, vocab}); if (tied) vt::MatmulBT(d.q, logits.t(), src, lm); @@ -289,12 +307,13 @@ DBuf ForwardBody(Dev d, const std::vector& token_ids, const CommonAttentionMetadata& attn_meta, const std::vector& attn_kv, const Qwen3DenseWeights& weights, const HfConfig& config, - const std::vector& logits_indices) { + const std::vector& logits_indices, + bool return_hidden = false) { const int64_t T = static_cast(token_ids.size()); DBuf hidden(d, DType::kBF16, {T, config.hidden_size}); EmbedInto(d, hidden, token_ids, weights, config); return ForwardLayers(d, hidden.t(), positions, attn_meta, attn_kv, weights, config, - logits_indices); + logits_indices, return_hidden); } ForwardLogits WrapDeviceLogits(Dev d, DBuf&& dlogits, int64_t rows, int64_t vocab) { @@ -412,6 +431,30 @@ ForwardLogits Qwen3DenseModel::ForwardDevice( return WrapDeviceLogits(d, std::move(dlogits), n_out, config.vocab_size); } +ForwardLogits Qwen3DenseModel::ForwardHidden( + const std::vector& token_ids, const std::vector& positions, + const CommonAttentionMetadata& attn_meta, const std::vector& attn_kv, + const Qwen3DenseWeights& weights, const HfConfig& config, vt::Queue& queue, + const std::vector& logits_indices) { + // ARCH-ONE-SURFACE ROW 6: the POOLING forward — the same embed + layer stack + // as Forward/ForwardDevice, stopping after the final RMSNorm (+ gather) with + // NO lm_head, mirroring an as_embedding_model conversion whose output layer + // is a missing-layer stage (adapters.py:135-151). The [n_out, H] f32 rows are + // downloaded to the host carrier: the landed pooler ops are host-side, and an + // embedding batch is one prefill (no per-step decode loop to keep resident). + Dev d{vt::GetBackend(queue.device.type), queue}; + DBuf dhidden = ForwardBody(d, token_ids, positions, attn_meta, attn_kv, weights, + config, logits_indices, /*return_hidden=*/true); + const int64_t n_out = dhidden.t().shape[0]; + const int64_t H = config.hidden_size; + ForwardLogits fl; + fl.rows = n_out; + fl.vocab = H; // the carrier's row width IS the hidden size on this path + fl.host.resize(static_cast(n_out) * static_cast(H)); + dhidden.Download(d, fl.host.data()); + return fl; +} + // ─── Qwen3DenseDecodeGraph (shared pure-dense decode CUDA-graph driver) ─────── // The pure-dense sibling of Qwen3MoeDecodeGraph (qwen3_moe.cpp) — SAME cold -> // warm -> capture -> replay state machine, SAME padded-batch capture set diff --git a/src/vllm/v1/core/sched/scheduler.cpp b/src/vllm/v1/core/sched/scheduler.cpp index d4db8b6f0..dce84ecfe 100644 --- a/src/vllm/v1/core/sched/scheduler.cpp +++ b/src/vllm/v1/core/sched/scheduler.cpp @@ -775,7 +775,24 @@ EngineCoreOutputs Scheduler::update_from_output( new_token_ids = std::move(result.first); stopped = result.second; } - // DEFERRED: pooling stop. + // Pooling stop (ARCH-ONE-SURFACE ROW 6; scheduler.py:1718-1721 `elif + // request.pooling_params and pooler_output is not None`): a POOLING request + // finishes as soon as the runner produced its pooled output. The runner + // reports nullopt for a row still consuming prefill chunks (the + // is_valid=false rows, pooling_runner.py:40-41), so such a request keeps + // running. pooler_output is EMPTY on every generation step -> the text path + // above is byte-identical. + std::optional> pooler_output; + if (!model_runner_output.pooler_output.empty() && + req_index < static_cast(model_runner_output.pooler_output.size())) { + pooler_output = + model_runner_output.pooler_output[static_cast(req_index)]; + } + if (new_token_ids.empty() && request->pooling_params.has_value() && + pooler_output.has_value()) { + request->status = RequestStatus::kFinishedStopped; + stopped = true; + } // scheduler.py:1636-1651: advance the structured-output FSM by the sampled // tokens. Only when the request produced tokens and the manager says the FSM @@ -838,11 +855,14 @@ EngineCoreOutputs Scheduler::update_from_output( // (upstream's `if new_token_ids or ... or stopped`). A partial-prefill // request that produced neither is skipped: "EngineCore returns no partial // prefill outputs". - if (!new_token_ids.empty() || stopped) { + if (!new_token_ids.empty() || pooler_output.has_value() || stopped) { EngineCoreOutput out; out.request_id = req_id; out.new_token_ids = new_token_ids; out.finish_reason = finish_reason; + // Pooled data rides the output to the frontend (scheduler.py:1837 + // `pooling_output=pooler_output`); nullopt on every generation output. + out.pooling_output = std::move(pooler_output); out.new_logprobs = std::move(new_logprobs); out.new_prompt_logprobs_tensors = std::move(new_prompt_logprobs_tensors); // stop_reason is int|str|None upstream; our EngineCoreOutput carries an diff --git a/src/vllm/v1/engine/llm_engine.cpp b/src/vllm/v1/engine/llm_engine.cpp index 80667ba6e..1d3e43a28 100644 --- a/src/vllm/v1/engine/llm_engine.cpp +++ b/src/vllm/v1/engine/llm_engine.cpp @@ -113,6 +113,41 @@ std::string LLMEngine::add_request(const std::string& request_id, return req_id; } +std::string LLMEngine::add_pooling_request(const std::string& request_id, + std::vector prompt_token_ids, + PoolingParams pooling_params, + int priority) { + // ARCH-ONE-SURFACE ROW 6 — the POOLING-task add. Mirrors the tokens + // add_request step-for-step; the SamplingParams are a benign greedy default + // (temperature 0, max_tokens 1) because the InputBatch admit reads them but + // the sampler is NEVER invoked on a pooling model's step (the runner routes + // to pool_tokens, model_runner.py:1586-1607 mirror). The PoolingParams ride + // the EngineCoreRequest into Request::pooling_params, which arms the + // scheduler's pooling stop (scheduler.py:1718-1721). + SamplingParams greedy; + greedy.temperature = 0.0; + greedy.max_tokens = 1; + EngineCoreRequest request = input_processor_.process_inputs_tokens( + request_id, std::move(prompt_token_ids), std::move(greedy), + /*arrival_time=*/std::nullopt, priority); + if (!pooling_params.task.has_value()) { + pooling_params.task = PoolingTask::kEmbed; + } + if (!pooling_params.use_activation.has_value()) { + pooling_params.use_activation = true; // pooling_runner.py:38 F.normalize + } + request.pooling_params = std::move(pooling_params); + const std::string req_id = request.request_id; + + output_processor_.add_request(request, /*prompt=*/std::nullopt, + /*request_index=*/0); + + auto req = std::make_unique( + Request::FromEngineCoreRequest(request, block_hasher_)); + engine_core_.add_request(std::move(req)); + return req_id; +} + void LLMEngine::FanOutParallelSampling(const EngineCoreRequest& request, std::optional prompt) { // llm_engine.py:280-291. Build the shared ParentRequest, then register n child @@ -227,6 +262,26 @@ RequestOutput LLMEngine::generate(std::vector prompt_token_ids, return result; } +RequestOutput LLMEngine::embed(std::vector prompt_token_ids, + PoolingParams pooling_params, + const std::string& request_id, int priority) { + // The single-request pooling driver (LLM.embed / offline.py:65-119 mirror): + // add the pooling request, then loop step() until it finishes. The finished + // RequestOutput carries the pooled vector in pooling_output. + add_pooling_request(request_id, std::move(prompt_token_ids), + std::move(pooling_params), priority); + RequestOutput result; + while (has_unfinished_requests()) { + std::vector step_outputs = step(); + for (RequestOutput& out : step_outputs) { + if (out.finished) { + result = std::move(out); + } + } + } + return result; +} + RequestOutput LLMEngine::generate(multimodal::MultiModalInputs mm_inputs, SamplingParams params, const std::string& request_id, int priority) { diff --git a/src/vllm/v1/engine/output_processor.cpp b/src/vllm/v1/engine/output_processor.cpp index 3d2ba3b95..f9b845632 100644 --- a/src/vllm/v1/engine/output_processor.cpp +++ b/src/vllm/v1/engine/output_processor.cpp @@ -182,8 +182,10 @@ RequestState RequestState::FromNewRequest(const tok::Tokenizer* tokenizer, std::optional RequestState::make_request_output( const std::vector& new_token_ids, std::optional finish_reason, - std::optional stop_reason) { - // output_processor.py:272-331 (text path; pooling / parent_req deferred). + std::optional stop_reason, + std::optional> pooling_output) { + // output_processor.py:272-331 (text + pooling; parent_req deferred for + // pooling — a pooling request is always n==1). const bool finished = finish_reason.has_value(); const bool final_only = output_kind == RequestOutputKind::kFinalOnly; @@ -240,7 +242,13 @@ std::optional RequestState::make_request_output( out_external_req_id = parent_req->external_req_id(); } - return NewRequestOutput(out_external_req_id, std::move(outputs), out_finished); + RequestOutput ro = + NewRequestOutput(out_external_req_id, std::move(outputs), out_finished); + // ARCH-ONE-SURFACE ROW 6 (output_processor.py:319 pooling branch; recorded + // deviation: upstream returns a separate PoolingRequestOutput class — ours + // carries the pooled vector as an optional field on the ONE RequestOutput). + ro.pooling_output = std::move(pooling_output); + return ro; } RequestOutput RequestState::NewRequestOutput( @@ -449,9 +457,10 @@ OutputProcessorOutput OutputProcessor::process_outputs( req_state.logprobs_processor->update_from_output(eco); } - // 4) Create and handle the RequestOutput (:650-666). + // 4) Create and handle the RequestOutput (:650-666). The pooled vector of + // a finished pooling request rides through (ARCH-ONE-SURFACE ROW 6). std::optional request_output = req_state.make_request_output( - new_token_ids, finish_reason, stop_reason); + new_token_ids, finish_reason, stop_reason, eco.pooling_output); if (request_output.has_value()) { // streaming_input deferred (false) -> no finished=false override. if (req_state.queue != nullptr) { diff --git a/src/vllm/v1/request.cpp b/src/vllm/v1/request.cpp index a725695f4..1a700d157 100644 --- a/src/vllm/v1/request.cpp +++ b/src/vllm/v1/request.cpp @@ -97,6 +97,9 @@ Request Request::FromEngineCoreRequest(const EngineCoreRequest& request, // ordinary text path -> byte-identical hashes. req.cache_salt = request.cache_salt; req.lora_name = request.lora_name; + // Pooling-task marker (ARCH-ONE-SURFACE ROW 6; upstream Request.pooling_params, + // vllm/v1/request.py). nullopt on every generation request -> byte-identical. + req.pooling_params = request.pooling_params; // Now install the hasher and compute the initial block hashes over the fully // populated request. req.block_hasher_ = std::move(block_hasher); diff --git a/src/vllm/v1/worker/gpu/runner.cpp b/src/vllm/v1/worker/gpu/runner.cpp index a62024b74..dd3ea42bd 100644 --- a/src/vllm/v1/worker/gpu/runner.cpp +++ b/src/vllm/v1/worker/gpu/runner.cpp @@ -320,6 +320,13 @@ GPUModelRunner::GPUModelRunner( // so force the sync host input path here. Byte-identical for non-spec // (spec_config_ is nullopt there, so this is AsyncRunnerEnvDefault()). async_input_combine_ = AsyncRunnerEnvDefault() && !spec_config_.has_value(); + // ARCH-ONE-SURFACE ROW 6 (mirror gpu/model_runner.py:368-369): a POOLING + // model's runner pools instead of sampling — build the PoolingRunner over + // the model-owned Pooler. Null for every text arch (byte-identical). + if (model_->registration().info.is_pooling_model && + model_->pooler() != nullptr) { + pooling_runner_ = std::make_unique(*model_->pooler()); + } initialize_kv_cache(kv_cache_config); ModelRegistry::Prepare(*model_, config_, queue_); } @@ -353,6 +360,13 @@ GPUModelRunner::GPUModelRunner( // so force the sync host input path here. Byte-identical for non-spec // (spec_config_ is nullopt there, so this is AsyncRunnerEnvDefault()). async_input_combine_ = AsyncRunnerEnvDefault() && !spec_config_.has_value(); + // ARCH-ONE-SURFACE ROW 6 (mirror gpu/model_runner.py:368-369): a POOLING + // model's runner pools instead of sampling — build the PoolingRunner over + // the model-owned Pooler. Null for every text arch (byte-identical). + if (model_->registration().info.is_pooling_model && + model_->pooler() != nullptr) { + pooling_runner_ = std::make_unique(*model_->pooler()); + } initialize_kv_cache(kv_cache_config); ModelRegistry::Prepare(*model_, config_, queue_); } @@ -1470,6 +1484,87 @@ ModelRunnerOutput GPUModelRunner::sample_tokens_with_rejection(vt::Tensor& logit return out; } +// ARCH-ONE-SURFACE ROW 6: the pooling counterpart of sample_tokens. Mirror of +// gpu/model_runner.py:1586-1607 (pool instead of sample) over the landed +// PoolingRunner (pool/pooling_runner.py:29-42). The stashed forward result of +// the pooling arch is the [rows, hidden] POST-FINAL-NORM HIDDEN (the model has +// no lm_head — adapters.py:135-151), already gathered at logits_indices on the +// default path, which for LAST pooling IS upstream's +// `hidden_states[input_batch.logits_indices]` (pooling_runner.py:36). +ModelRunnerOutput GPUModelRunner::pool_tokens() { + const int num_reqs = exec_state_.num_reqs; + const int64_t hidden = exec_state_.logits.vocab; // == hidden_size here + ForwardLogits& fl = exec_state_.logits; + VT_CHECK(!fl.on_device(), + "pool_tokens: the pooling forward returns a HOST hidden carrier"); + + // One hidden row per request. Default (gather ON): the forward already + // gathered the per-request last-token rows. VT_LOGITS_GATHER=0: re-gather on + // host from the full [num_actual_tokens, hidden] rows, exactly as the text + // host path re-gathers logits. + std::vector gathered; + const float* rows = nullptr; + if (fl.rows == num_reqs) { + rows = fl.host.data(); + } else { + gathered.resize(static_cast(num_reqs) * static_cast(hidden)); + for (int i = 0; i < num_reqs; ++i) { + const int row = exec_state_.step.logits_indices[static_cast(i)]; + std::memcpy(gathered.data() + static_cast(i) * + static_cast(hidden), + fl.host.data() + static_cast(row) * + static_cast(hidden), + static_cast(hidden) * sizeof(float)); + } + rows = gathered.data(); + } + vt::Tensor hidden_rows = vt::Tensor::Contiguous( + const_cast(rows), vt::DType::kF32, vt::Device{vt::DeviceType::kCPU, 0}, + {static_cast(num_reqs), hidden}); + + // PoolingMetadata over the GATHERED buffer: one row per sequence (first == + // last == i), task embed, activation ON — the unconditional L2 normalize of + // pooling_runner.py:38 (a per-request use_activation knob is the matryoshka/ + // dimensions residual, named in the row spec). + vllm::PoolingMetadata md; + for (int i = 0; i < num_reqs; ++i) { + md.pooling_cursor.first_token_indices.push_back(i); + md.pooling_cursor.last_token_indices.push_back(i); + md.pooling_cursor.prompt_lens.push_back(1); + md.pooling_cursor.seq_lens.push_back(1); + md.pooling_cursor.num_scheduled_tokens.push_back(1); + vllm::PoolingParams pp; + pp.task = vllm::PoolingTask::kEmbed; + pp.use_activation = true; + md.pooling_params.push_back(pp); + md.tasks.push_back(vllm::PoolingTask::kEmbed); + } + vllm::PoolerOutput pooled = pooling_runner_->Pool(hidden_rows, md); + VT_CHECK(static_cast(pooled.size()) == num_reqs, + "pool_tokens: pooler must return one vector per request"); + + // Validity = the request's whole prompt has been seen (seq_lens == prompt_len, + // pooling_runner.py:40-41). Our discard mask is the SAME predicate + // (step.seq_lens[i] < num_tokens_no_spec[i] == still consuming prefill), so a + // chunked-prefill row reports nullopt and the request keeps running. + ModelRunnerOutput out; + out.req_ids.reserve(static_cast(num_reqs)); + out.sampled_token_ids.reserve(static_cast(num_reqs)); + out.pooler_output.reserve(static_cast(num_reqs)); + for (int i = 0; i < num_reqs; ++i) { + const std::string& req_id = exec_state_.req_ids[static_cast(i)]; + out.req_ids.push_back(req_id); + out.req_id_to_index[req_id] = i; + out.sampled_token_ids.push_back({}); // a pooling step samples NOTHING + if (exec_state_.discard[static_cast(i)] != 0) { + out.pooler_output.push_back(std::nullopt); + } else { + out.pooler_output.push_back(std::move(pooled[static_cast(i)])); + } + } + return out; +} + ModelRunnerOutput GPUModelRunner::sample_tokens( const std::optional& grammar_output) { ModelRunnerOutput out; @@ -1478,6 +1573,15 @@ ModelRunnerOutput GPUModelRunner::sample_tokens( return out; // 0-token flush step (nothing sampled). } + // POOLING ROUTING (ARCH-ONE-SURFACE ROW 6), mirroring the model-level task + // split of gpu/model_runner.py:1586-1607: on a POOLING model the step's + // output is the POOLED DATA, never a sampled token. pooling_runner_ is set + // iff the registration declares is_pooling_model (ctor), so every text arch + // takes the sampler path below byte-identically. + if (pooling_runner_ != nullptr) { + return pool_tokens(); + } + std::vector sampled_logits; // host buffer; outlives the sampler when used vt::Tensor logits = assemble_sample_logits(grammar_output, sampled_logits); @@ -2165,6 +2269,13 @@ void GPUModelRunner::replay_last_sampled_ops(AsyncDeviceInputs& dev) { std::unique_ptr GPUModelRunner::sample_tokens_async( const std::optional& grammar_output) { + // ARCH-ONE-SURFACE ROW 6: pooling models resolve async scheduling OFF + // (config/vllm.py:1068-1073 mirror in LoadedEngine::ResolveAsyncEnabled), so + // the depth-2 async sampler must never see one — refuse loudly rather than + // run the device sampler over hidden states. + VT_CHECK(pooling_runner_ == nullptr, + "sample_tokens_async: pooling models use the synchronous scheduler " + "(async scheduling is disabled for pooling, config/vllm.py:1068)"); // When async is NOT engaged (production default), degenerate to the byte- // identical synchronous path wrapped as a ready output — so a caller in the // depth-2 loop can always call sample_tokens_async without branching, yet the diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7b075be42..939b14356 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -80,6 +80,12 @@ vllm_cpp_add_test(test_qwen3_dflash_forward vllm/models/test_qwen3_dflash_forwar target_include_directories(test_qwen3_dflash_forward PRIVATE ${CMAKE_SOURCE_DIR}/src) vllm_cpp_add_test(test_llama_forward vllm/models/test_llama_forward.cpp) target_include_directories(test_llama_forward PRIVATE ${CMAKE_SOURCE_DIR}/src) +# ARCH-ONE-SURFACE fold ROW 6: embeddings through the registry/runner path on +# the committed tiny LlamaModel fixture (see scripts/mm/llama_embed_fixture_gen.py). +vllm_cpp_add_test(test_llama_embedding_fold + vllm/models/test_llama_embedding_fold.cpp) +target_compile_definitions(test_llama_embedding_fold PRIVATE + LLAMA_EMBED_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/vllm/models/fixtures/llama_embed_e2e") vllm_cpp_add_test(test_mistral_forward vllm/models/test_mistral_forward.cpp) target_include_directories(test_mistral_forward PRIVATE ${CMAKE_SOURCE_DIR}/src) vllm_cpp_add_test(test_gemma3_load vllm/models/test_gemma3_load.cpp) @@ -647,7 +653,11 @@ if(VLLM_CPP_SERVER) # library transcription seam on the committed parakeet_e2e fixture # (ARCH-ONE-SURFACE ROW 1). target_compile_definitions(test_openai_api_server PRIVATE - PARAKEET_E2E_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/vllm/models/fixtures/parakeet_e2e") + PARAKEET_E2E_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/vllm/models/fixtures/parakeet_e2e" + # /v1/embeddings dispatch + socket smoke run against the REAL engine path + # (LoadedEngine -> PoolingRunner) on the committed llama_embed_e2e fixture + # (ARCH-ONE-SURFACE ROW 6). + LLAMA_EMBED_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/vllm/models/fixtures/llama_embed_e2e") # M3.6: the OpenAI server CONFORMANCE suite — the full API contract exercised # end to end over the REAL cpp-httplib server on an ephemeral port. vllm_cpp_add_test(test_openai_conformance vllm/entrypoints/openai/test_conformance.cpp) @@ -677,7 +687,8 @@ vllm_cpp_add_test(test_capi capi/test_capi.cpp) target_include_directories(test_capi PRIVATE ${CMAKE_SOURCE_DIR}/src) target_compile_definitions(test_capi PRIVATE PARAKEET_E2E_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/vllm/models/fixtures/parakeet_e2e" - MINIMAX_H3_VIDEO_FOLD_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/vllm/models/fixtures/minimax_h3_video_fold") + MINIMAX_H3_VIDEO_FOLD_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/vllm/models/fixtures/minimax_h3_video_fold" + LLAMA_EMBED_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/vllm/models/fixtures/llama_embed_e2e") vllm_cpp_add_test(test_chat_prompt capi/test_chat_prompt.cpp) target_include_directories(test_chat_prompt PRIVATE ${CMAKE_SOURCE_DIR}/src) diff --git a/tests/capi/c_header_compile.c b/tests/capi/c_header_compile.c index 669e4f013..d02acc6e8 100644 --- a/tests/capi/c_header_compile.c +++ b/tests/capi/c_header_compile.c @@ -70,6 +70,14 @@ int vllm_capi_c_header_check(vllm_engine* eng, const char* prompt) { vllm_transcription_free(&transcript); } + /* Embeddings (ABI v15). */ + { + const char* texts[1] = {"strict-C embed reference"}; + vllm_embedding_result emb; + st = vllm_embed(eng, texts, 1, &emb); + vllm_embedding_result_free(&emb); + } + vllm_engine_free(eng); } diff --git a/tests/capi/test_capi.cpp b/tests/capi/test_capi.cpp index a241c933b..c677cf21d 100644 --- a/tests/capi/test_capi.cpp +++ b/tests/capi/test_capi.cpp @@ -12,6 +12,7 @@ #include +#include #include #include #include @@ -1296,11 +1297,12 @@ TEST_CASE("capi: version and abi-version are exposed") { // transcription slice (vllm_transcribe) is ABI v11; the video-generation // 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 + // (vllm_model_params.device) is ABI v14; the embeddings slice (vllm_embed / + // vllm_embedding_result_free) is ABI v15. 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); + CHECK(vllm_abi_version() >= 15); } // ─── ABI v11: audio transcription (ARCH-ONE-SURFACE ROW 1) ─────────────────── @@ -1839,3 +1841,123 @@ TEST_CASE("capi v14: explicit cpu forces the CPU queue at the EngineParams seam" std::runtime_error); } } + +// ─── ABI v15: embeddings (ARCH-ONE-SURFACE ROW 6) ──────────────────────────── +// The embeddings slice gated THROUGH the public ABI on the committed tiny +// LlamaModel fixture (tests/vllm/models/fixtures/llama_embed_e2e): a REAL +// checkpoint-directory load through vllm_engine_load, then vllm_embed through +// the SAME registry forward + PoolingRunner engine step the fold gate +// (test_llama_embedding_fold) anchors. Plus the argument contract and the +// refuse-by-task pins in BOTH directions (the v11 precedent applied to the +// pooling task). + +namespace { +std::string LlamaEmbedFixture() { return std::string(LLAMA_EMBED_FIXTURE_DIR); } +} // namespace + +TEST_CASE("capi v15: vllm_embed embeds through the public ABI (fixture load)") { + vllm_model_params mp = vllm_model_params_default(); + const std::string dir = LlamaEmbedFixture(); + mp.model_path = dir.c_str(); + vllm_engine* eng = nullptr; + REQUIRE(vllm_engine_load(&mp, &eng) == VLLM_OK); + REQUIRE(eng != nullptr); + + const char* texts[2] = {"the quick brown fox", "the lazy dog"}; + vllm_embedding_result out; + REQUIRE(vllm_embed(eng, texts, 2, &out) == VLLM_OK); + REQUIRE(out.values != nullptr); + CHECK(out.n_embeddings == 2); + CHECK(out.dim == 64); // the fixture's hidden_size + CHECK(out.prompt_tokens > 0); + // Each embedding is unit-L2 (the pooling normalize ran) and the two DIFFER + // (different prompts pool different last-token hiddens). + double delta = 0.0; + for (int32_t r = 0; r < out.n_embeddings; ++r) { + double l2 = 0.0; + for (int32_t c = 0; c < out.dim; ++c) { + const double v = out.values[r * out.dim + c]; + l2 += v * v; + } + CHECK(std::sqrt(l2) == doctest::Approx(1.0).epsilon(1e-5)); + } + for (int32_t c = 0; c < out.dim; ++c) { + delta += std::abs(static_cast(out.values[c]) - + static_cast(out.values[out.dim + c])); + } + CHECK(delta > 1e-3); + + vllm_embedding_result_free(&out); + CHECK(out.values == nullptr); // zeroed after free + CHECK(out.n_embeddings == 0); + vllm_embedding_result_free(&out); // double-free is a safe no-op + vllm_embedding_result_free(nullptr); + vllm_engine_free(eng); +} + +TEST_CASE("capi v15: refuse-by-task in both directions (pooling vs text)") { + // Pooling handle: every text entry point refuses with the actionable message + // instead of driving generation over hidden states. + vllm_model_params mp = vllm_model_params_default(); + const std::string dir = LlamaEmbedFixture(); + mp.model_path = dir.c_str(); + vllm_engine* emb = nullptr; + REQUIRE(vllm_engine_load(&mp, &emb) == VLLM_OK); + + vllm_sampling_params sp = vllm_sampling_params_default(); + vllm_completion comp; + CHECK(vllm_complete(emb, "hello", &sp, &comp) == VLLM_ERR_INVALID_ARGUMENT); + CHECK(std::string(vllm_last_error()).find("pooling (embedding)") != + std::string::npos); + CHECK(std::string(vllm_last_error()).find("vllm_embed") != std::string::npos); + char* chat_out = nullptr; + CHECK(vllm_chat(emb, "{\"messages\":[]}", &chat_out) == + VLLM_ERR_INVALID_ARGUMENT); + CHECK(std::string(vllm_last_error()).find("vllm_embed") != std::string::npos); + { + const int32_t prompt_ids[1] = {0}; + int32_t out_ids[4]; + int32_t n_out = 0; + CHECK(vllm_complete_tokens(emb, prompt_ids, 1, &sp, out_ids, 4, &n_out, + nullptr) == VLLM_ERR_INVALID_ARGUMENT); + CHECK(std::string(vllm_last_error()).find("vllm_embed") != + std::string::npos); + } + vllm_engine_free(emb); + + // Text handle: vllm_embed refuses symmetrically, naming the text entry + // points. + vllm_engine* text = MakeSyntheticEngine(); + REQUIRE(text != nullptr); + const char* texts[1] = {"hello"}; + vllm_embedding_result out; + CHECK(vllm_embed(text, texts, 1, &out) == VLLM_ERR_INVALID_ARGUMENT); + CHECK(std::string(vllm_last_error()).find("text-generation") != + std::string::npos); + CHECK(std::string(vllm_last_error()).find("vllm_complete") != + std::string::npos); + vllm_engine_free(text); +} + +TEST_CASE("capi v15: vllm_embed argument contract") { + vllm_model_params mp = vllm_model_params_default(); + const std::string dir = LlamaEmbedFixture(); + mp.model_path = dir.c_str(); + vllm_engine* eng = nullptr; + REQUIRE(vllm_engine_load(&mp, &eng) == VLLM_OK); + + const char* texts[2] = {"the fox", nullptr}; + vllm_embedding_result out; + // Null engine / texts / out; non-positive n_texts; a NULL texts entry. + CHECK(vllm_embed(nullptr, texts, 1, &out) == VLLM_ERR_INVALID_ARGUMENT); + CHECK(vllm_embed(eng, nullptr, 1, &out) == VLLM_ERR_INVALID_ARGUMENT); + CHECK(vllm_embed(eng, texts, 1, nullptr) == VLLM_ERR_INVALID_ARGUMENT); + CHECK(vllm_embed(eng, texts, 0, &out) == VLLM_ERR_INVALID_ARGUMENT); + CHECK(vllm_embed(eng, texts, -3, &out) == VLLM_ERR_INVALID_ARGUMENT); + CHECK(vllm_embed(eng, texts, 2, &out) == VLLM_ERR_INVALID_ARGUMENT); + CHECK(std::string(vllm_last_error()).find("texts[1]") != std::string::npos); + // On every refused call *out stays zeroed. + CHECK(out.values == nullptr); + CHECK(out.n_embeddings == 0); + vllm_engine_free(eng); +} diff --git a/tests/capi/test_dlopen.cpp b/tests/capi/test_dlopen.cpp index b50e4dccf..584f3563d 100644 --- a/tests/capi/test_dlopen.cpp +++ b/tests/capi/test_dlopen.cpp @@ -30,6 +30,9 @@ namespace { // declarations in vllm.h; a header-less consumer would type them by hand. using fn_version = const char* (*)(void); using fn_abi_version = int32_t (*)(void); +using fn_embed = vllm_status (*)(vllm_engine*, const char* const*, int32_t, + vllm_embedding_result*); +using fn_embed_free = void (*)(vllm_embedding_result*); using fn_model_params_default = vllm_model_params (*)(void); using fn_sampling_params_default = vllm_sampling_params (*)(void); using fn_engine_load = vllm_status (*)(const vllm_model_params*, vllm_engine**); @@ -94,6 +97,12 @@ TEST_CASE("dlopen: libvllm.so resolves the whole C ABI by name and drives it") { auto p_request_error = Sym(lib, "vllm_request_error"); auto p_request_free = Sym(lib, "vllm_request_free"); auto p_string_free = Sym(lib, "vllm_string_free"); + // ABI v15 (ARCH-ONE-SURFACE ROW 6): the embeddings entry points resolve. + auto p_embed = Sym(lib, "vllm_embed"); + auto p_embed_free = Sym(lib, "vllm_embedding_result_free"); + (void)p_embed; + // A NULL free is the documented no-op — drivable with no model loaded. + p_embed_free(nullptr); auto p_completion_free = Sym(lib, "vllm_completion_free"); auto p_last_error = Sym(lib, "vllm_last_error"); diff --git a/tests/scripts/test_check_runner_routing_consistency.py b/tests/scripts/test_check_runner_routing_consistency.py index 91a3b7102..1b88a9e51 100644 --- a/tests/scripts/test_check_runner_routing_consistency.py +++ b/tests/scripts/test_check_runner_routing_consistency.py @@ -139,6 +139,12 @@ def test_private_device_wrapper_classifies_device(self) -> None: # tree's live case. scanned = mod.scan_registrations(mod.MODELS_DIR, mod.INCLUDE_DIR) self.assertEqual(scanned["deepseek_v4"].classification, "DEVICE") + # ARCH-ONE-SURFACE ROW 6: a registry TU declaring `.is_pooling_model = + # true` is a NON-GENERATIVE registration — a hidden-state producer for + # the PoolingRunner, classified POOLING explicitly (deleting the + # checker's pooling arm would drop it into NONE and red the bucket pin + # below). + self.assertEqual(scanned["llama_embedding"].classification, "POOLING") # No registered model may sit in the silently-exempt NONE bucket at all. self.assertEqual( sorted(n for n, r in scanned.items() if r.classification == "NONE"), [] diff --git a/tests/scripts/test_check_supported_models.py b/tests/scripts/test_check_supported_models.py index f1f4a1feb..19267d914 100644 --- a/tests/scripts/test_check_supported_models.py +++ b/tests/scripts/test_check_supported_models.py @@ -106,10 +106,14 @@ def test_empty_registry_fails(self) -> None: def test_unrepresentable_registered_arch_fails_the_self_check(self) -> None: # A registered string the FEATURES arch-token pattern cannot express must # surface as an error, never be silently excluded from the comparison. - registered = REGISTERED | {"WeirdModel"} + # (ARCH-ONE-SURFACE ROW 6 widened the pattern to bare `*Model` archs — + # the upstream _EMBEDDING_MODELS naming, e.g. `LlamaModel` — so the + # unrepresentable example is now a suffix the pattern still cannot + # express, not a `*Model` name.) + registered = REGISTERED | {"WeirdArchitecture"} errors = chk.supported_models_errors(registered, _features(TWO)) self.assertTrue(any("do not match the FEATURES arch-token pattern" in e for e in errors), errors) - self.assertTrue(any("WeirdModel" in e for e in errors), errors) + self.assertTrue(any("WeirdArchitecture" in e for e in errors), errors) class ScopingTests(unittest.TestCase): diff --git a/tests/vllm/entrypoints/openai/test_api_server.cpp b/tests/vllm/entrypoints/openai/test_api_server.cpp index 7a02c86ad..e5548ba79 100644 --- a/tests/vllm/entrypoints/openai/test_api_server.cpp +++ b/tests/vllm/entrypoints/openai/test_api_server.cpp @@ -18,6 +18,8 @@ #include #include +#include +#include #include #include #include @@ -2353,3 +2355,184 @@ TEST_CASE("api_server: an explicit-cpu device-selected engine serves /v1/complet CHECK(j.at("choices").at(0).at("finish_reason") == "length"); CHECK(j.at("usage").at("completion_tokens") == 5); } + +// ─── /v1/embeddings (ARCH-ONE-SURFACE ROW 6) ───────────────────────────────── +// Task-conditional like /v1/audio/transcriptions: a TEXT server never +// registers the route; a pooling (embedding) server registers it and NOT the +// generate routes — vLLM's supported_tasks-conditional registration +// (api_server.py:255-265) + pooling/embed/api_router.py:28 semantics. The +// embedder wraps the REAL engine path (LoadedEngine::FromModelDir on the +// committed llama_embed_e2e fixture -> LLMEngine::embed -> the registry +// forward + PoolingRunner step), the SAME path vllm_embed drives. + +namespace { + +struct EmbedHarness { + vllm::entrypoints::openai::OpenAIServingModels models{"llama-embed-fixture"}; + ApiServer server{models, "test-version"}; + std::shared_ptr loaded; + std::shared_ptr mutex = std::make_shared(); + + EmbedHarness() { + vllm::entrypoints::EngineParams params; + params.max_model_len = 64; + loaded = std::shared_ptr( + vllm::entrypoints::LoadedEngine::FromModelDir( + std::string(LLAMA_EMBED_FIXTURE_DIR), params)); + auto engine = loaded; + auto mu = mutex; + auto counter = std::make_shared>(0); + server.set_embedder( + [engine, mu, counter](const std::vector& inputs) { + std::lock_guard lock(*mu); + ApiServer::EmbeddingBatch batch; + for (const std::string& text : inputs) { + std::vector ids = + engine->tokenizer().EncodeWithSpecialTokens(text); + REQUIRE(!ids.empty()); + batch.prompt_tokens += static_cast(ids.size()); + vllm::RequestOutput ro = engine->engine().embed( + std::move(ids), vllm::PoolingParams{}, + "embd-" + std::to_string(counter->fetch_add(1))); + REQUIRE(ro.finished); + REQUIRE(ro.pooling_output.has_value()); + batch.embeddings.push_back(std::move(*ro.pooling_output)); + } + return batch; + }); + } +}; + +} // namespace + +TEST_CASE("api_server: embeddings dispatch — OpenAI shape over the engine path") { + EmbedHarness h; + + // ONE string input. + ApiServer::DispatchResult r = h.server.handle_embeddings( + R"({"model":"llama-embed-fixture","input":"the quick brown fox"})"); + CHECK(r.status == 200); + json j = json::parse(r.body); + CHECK(j.at("object") == "list"); + CHECK(j.at("model") == "llama-embed-fixture"); + CHECK(std::string(j.at("id")).rfind("embd-", 0) == 0); + REQUIRE(j.at("data").size() == 1); + CHECK(j.at("data").at(0).at("object") == "embedding"); + CHECK(j.at("data").at(0).at("index") == 0); + REQUIRE(j.at("data").at(0).at("embedding").is_array()); + CHECK(j.at("data").at(0).at("embedding").size() == 64); // hidden_size + // Unit L2: the pooling normalize ran. + double l2 = 0.0; + for (const auto& v : j.at("data").at(0).at("embedding")) + l2 += v.get() * v.get(); + CHECK(std::sqrt(l2) == doctest::Approx(1.0).epsilon(1e-5)); + CHECK(j.at("usage").at("prompt_tokens").get() > 0); + CHECK(j.at("usage").at("total_tokens") == j.at("usage").at("prompt_tokens")); + + // ARRAY input: one embedding per string, input order. + r = h.server.handle_embeddings( + R"({"input":["the quick brown fox","the lazy dog"]})"); + CHECK(r.status == 200); + j = json::parse(r.body); + REQUIRE(j.at("data").size() == 2); + CHECK(j.at("data").at(1).at("index") == 1); + + // Malformed / unsupported requests. + CHECK(h.server.handle_embeddings("not json").status == 400); + CHECK(h.server.handle_embeddings(R"({"model":"x"})").status == 404); + CHECK(h.server.handle_embeddings(R"({"input":42})").status == 400); + CHECK(h.server.handle_embeddings(R"({"input":[]})").status == 400); + CHECK(h.server.handle_embeddings(R"({"input":[[1,2]]})").status == 400); + CHECK(h.server + .handle_embeddings( + R"({"input":"x","encoding_format":"base64"})") + .status == 400); + CHECK(h.server.handle_embeddings(R"({"input":"x","dimensions":16})").status == + 400); +} + +TEST_CASE("api_server: embeddings without an embedder is a 500, not a crash") { + vllm::entrypoints::openai::OpenAIServingModels models{"m"}; + ApiServer server{models, "test-version"}; + ApiServer::DispatchResult r = server.handle_embeddings(R"({"input":"x"})"); + CHECK(r.status == 500); + CHECK(json::parse(r.body).at("error").at("message") == + "The model does not support Embeddings API"); +} + +TEST_CASE("api_server: embeddings socket smoke; generate routes 404 on the " + "embedding server") { + EmbedHarness h; + const int port = h.server.bind_to_any_port("127.0.0.1"); + REQUIRE(port > 0); + std::thread server_thread([&h]() { h.server.serve(); }); + for (int i = 0; i < 500 && !h.server.is_running(); ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + REQUIRE(h.server.is_running()); + + { + httplib::Client client("127.0.0.1", port); + client.set_connection_timeout(5, 0); + client.set_read_timeout(15, 0); + + auto res = client.Post("/v1/embeddings", + R"({"input":"the quick brown fox"})", + "application/json"); + REQUIRE(res); + CHECK(res->status == 200); + CHECK(json::parse(res->body).at("data").at(0).at("embedding").size() == 64); + + // The generate routes are NOT registered on an embedding server (the + // task-conditional registration, both directions). + auto completions = client.Post("/v1/completions", "{}", "application/json"); + REQUIRE(completions); + CHECK(completions->status == 404); + auto chat = client.Post("/v1/chat/completions", "{}", "application/json"); + REQUIRE(chat); + CHECK(chat->status == 404); + + // Liveness + discovery still serve. + auto health = client.Get("/health"); + REQUIRE(health); + CHECK(health->status == 200); + auto models_res = client.Get("/v1/models"); + REQUIRE(models_res); + CHECK(json::parse(models_res->body).at("data").at(0).at("id") == + "llama-embed-fixture"); + } + + h.server.stop(); + server_thread.join(); +} + +TEST_CASE("api_server: /v1/embeddings does not exist on a TEXT server") { + // The reverse pin, the exact twin of "the audio routes do not exist on a + // TEXT server": task-conditional registration means a TEXT-engine server (no + // embedder attached) must answer 404 from the ROUTE TABLE — a well-formed + // request that the handler WOULD accept proves the route was never + // registered (an `if (true)` registration mutation answers 200/400 from the + // handler instead and only THIS test reds). + const HfConfig c = MakeConfig(); + const Qwen3_5MoeWeights w = MakeWeights(c); + ServerHarness h(c, w, Fixture()); + + const int port = h.server.bind_to_any_port("127.0.0.1"); + REQUIRE(port > 0); + std::thread server_thread([&h]() { h.server.serve(); }); + for (int i = 0; i < 500 && !h.server.is_running(); ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + REQUIRE(h.server.is_running()); + + { + httplib::Client client("127.0.0.1", port); + client.set_connection_timeout(5, 0); + client.set_read_timeout(15, 0); + auto res = client.Post("/v1/embeddings", R"({"input":"hello"})", + "application/json"); + REQUIRE(res); + CHECK(res->status == 404); + } + + h.server.stop(); + server_thread.join(); +} diff --git a/tests/vllm/models/fixtures/llama_embed_e2e/config.json b/tests/vllm/models/fixtures/llama_embed_e2e/config.json new file mode 100644 index 000000000..b3c9a8af9 --- /dev/null +++ b/tests/vllm/models/fixtures/llama_embed_e2e/config.json @@ -0,0 +1,19 @@ +{ + "architectures": [ + "LlamaModel" + ], + "model_type": "llama", + "hidden_size": 64, + "num_hidden_layers": 2, + "num_attention_heads": 4, + "num_key_value_heads": 2, + "head_dim": 16, + "intermediate_size": 128, + "rms_norm_eps": 1e-05, + "rope_theta": 500000.0, + "vocab_size": 32, + "max_position_embeddings": 128, + "torch_dtype": "bfloat16", + "tie_word_embeddings": false, + "attention_bias": false +} diff --git a/tests/vllm/models/fixtures/llama_embed_e2e/model.safetensors b/tests/vllm/models/fixtures/llama_embed_e2e/model.safetensors new file mode 100644 index 0000000000000000000000000000000000000000..c7766b2a228e105cb666cbdb972bb75e2c1c6211 GIT binary patch literal 154259 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