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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .agents/claims/CLAIM-SERVE-RECIPE-ARGS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# CLAIM-SERVE-RECIPE-ARGS

| Claim | Row IDs | Agent | Worktree / remote dir | Branch | Owned scope | State | Last update |
|---|---|---|---|---|---|---|---|
| `CLAIM-SERVE-RECIPE-ARGS` | `SERVE-RECIPE-ARGS` (`SPIKE`) | Claude Code (opus-5), operator role — spec only; implementation goes to a fresh implementer | isolated worktree; no GPU, no oracle run, no publication | `row/serve-recipe-args`, issue [#606](https://github.com/mudler/vllm.cpp/issues/606) | Owns ONLY: NEW `.agents/specs/serve-recipe-args.md`; the NEW `SERVE-RECIPE-ARGS` row plus the two summary counts it moves in `.agents/engine-matrix.md`; and, at implementation time, the accepted-and-inert table in `src/vllm/entrypoints/openai/server_main.cpp`, its `docs/USAGE.md` entries, and its RED-first tests. EXCLUDES `--language-model-only` (#607, a real capability gap), the `--tool-call-parser` default (`hermes` vs upstream `None`, pre-existing and out of scope), and every flag that is inert because we lack the capability — TP, EP, `--mm-encoder-tp-mode` — which must keep aborting | `SPIKE` | 2026-08-13 — spec committed; implementation not started |
5 changes: 3 additions & 2 deletions .agents/engine-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ forensics: roadmap_v1.md and the parity ledger.
| Sampling and generation | 15 | 4 | 2 | 0 | 0 | 4 | 0 | 1 | 4 |
| Structured output and tools | 7 | 0 | 3 | 0 | 0 | 2 | 0 | 0 | 2 |
| Speculative decoding | 21 | 0 | 0 | 1 | 0 | 5 | 0 | 4 | 10 |
| Serving, API, CLI, library | 32 | 10 | 2 | 0 | 2 | 9 | 2 | 3 | 4 |
| Serving, API, CLI, library | 33 | 10 | 2 | 1 | 2 | 9 | 2 | 3 | 4 |
| LoRA and adapters | 2 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
| Long context and attention | 10 | 5 | 0 | 0 | 1 | 0 | 1 | 0 | 3 |
| Loading, tokenizer, config | 10 | 3 | 3 | 0 | 0 | 1 | 1 | 1 | 1 |
| **Total** | **152** | **35** | **16** | **4** | **9** | **29** | **8** | **11** | **39** |
| **Total** | **153** | **35** | **16** | **5** | **9** | **29** | **8** | **11** | **39** |

## Engine core and scheduling

Expand Down Expand Up @@ -219,6 +219,7 @@ claims it.
| `ENG-FORGE-COAUTHOR` | The forbidden-AI-trailer rule was catching ATTRIBUTION rather than an authorship claim, which is why bot-opened PRs red `main` on merge. GitHub composes the squash message itself and appends the account that opened the PR — `Co-authored-by: localai-org-maint-bot <...@users.noreply.github.com>` — and most PRs here are opened by a bot, so nearly every squash trips the AI-identity check. Real instance `f64f2b71`, invisible until #406 repaired the parse, which is why it reads as a new failure and is not one. The rule exists so an AI cannot claim it WROTE the code, and that stays; GitHub is recording who pressed the button, and the AI-involvement claim is already carried separately by `AI-Assisted` and `Assisted-by` in the same block. FIX: accept a `Co-authored-by` at a GitHub account noreply address even when the name matches an AI identity token, keyed on the FORGE'S OWN DOMAIN rather than the name so the exemption cannot be borrowed. A hand-written `Co-authored-by: Claude <claude@anthropic.com>` still fails; `Signed-off-by` is excluded from the exemption entirely, because a sign-off is a legal assertion about provenance rather than attribution. `AGENTS.md` records the same distinction in the same change so prose and checker cannot drift | T0 | NO vLLM analogue -- local protocol machinery, so the mirror rule does not apply and there is no upstream `file:line` to port from. Governed by `AGENTS.md` §"Changing the rules or a checker" | `scripts/check-commit-trailers.py:38` (`FORGE_ACCOUNT_EMAIL` and the forbidden-trailer skip) | `tests/scripts/test_check_commit_trailers.py:1` 25 cases -- the RED-BEFORE forge-bot case plus THREE guards that matter more than the relaxation because this LOOSENS a rule: a hand-written AI co-author still fails, `Signed-off-by` at the same noreply address still fails, and a human co-author still passes; all three green before and after. Real commit `f64f2b71` re-verified per commit | [forge-coauthor-attribution.md](specs/forge-coauthor-attribution.md); issue [#418](https://github.com/mudler/vllm.cpp/issues/418) | `ACTIVE` | `CLAIM-ENG-FORGE-COAUTHOR` |
| `ENG-NOW-DERIVED` | W1-W5 remove the per-row `.agents/NOW.md` write: each moved row's own spec carries `## Now`, `scripts/now.py` renders the live roster offline-first, and the digest cannot regrow a row table. Implementation merge `dbd0d51c`; progressive legacy-spec backfill is the selected compatibility policy, not remaining work. Runtime/performance/parity are `VOID` because this is local protocol machinery | T0 | No vLLM analogue; governed by `AGENTS.md` §Changing the rules or a checker | `scripts/now.py:163`; `scripts/check-doc-checkpoint.py:181`; `scripts/check-now-current.py:57` | `tests/scripts/test_doc_checkpoint.py:142`; `tests/scripts/test_now_render.py:34`; `tests/scripts/test_check_now_current.py:1`; closure [parity-ledger.md#L939](parity-ledger.md#L939) | [now-derived.md](specs/now-derived.md); issue [#374](https://github.com/mudler/vllm.cpp/issues/374) | `DONE` | `dbd0d51c` |
| `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-RECIPE-ARGS` | Accepted-and-inert serve arguments: an enumerated table of flags that published recipes pass, that are no-ops for this engine, and that must therefore not abort argument parsing. Not a catch-all — anything unlisted still aborts, and mirrored validation still fires. Found by the 2026-08-13 recipe-surface sweep: `vllm-serve` rejects unknown arguments (`src/vllm/entrypoints/openai/server_main.cpp:440`), so `--enable-auto-tool-choice` (89/157 official recipes) and `--trust-remote-code` (82/157) stop the server before model load even though neither means anything here — including for models we ship token-exact and gated | T1 | `vllm/entrypoints/openai/cli_args.py:105` (`enable_auto_tool_choice` default), `:395` (requires `--tool-call-parser`, a `TypeError` otherwise — mirrored, not dropped); threading `vllm/entrypoints/openai/api_server.py:426,441,529,544` at `5559679229` | absent — `server_main.cpp:440` aborts on any unrecognized flag; no accepted-and-inert seam exists | none yet; RED-first cases named in the spec, including the mutation that must turn the "unlisted flag still aborts" test RED | [serve-recipe-args.md](specs/serve-recipe-args.md) | `SPIKE` | `CLAIM-SERVE-RECIPE-ARGS` |
| `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. **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` |
Expand Down
115 changes: 115 additions & 0 deletions .agents/specs/serve-recipe-args.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# SERVE-RECIPE-ARGS — a published recipe command must reach model load

Issue: [#606](https://github.com/mudler/vllm.cpp/issues/606)
Row: `SERVE-RECIPE-ARGS` ([engine-matrix.md](../engine-matrix.md))
Sweep that found it: `roadmap_v1.md` § Recipe-surface sweep (2026-08-13)

## The defect

`vllm-serve` rejects any argument it does not recognise
(`src/vllm/entrypoints/openai/server_main.cpp:440`):

```cpp
std::cerr << "server: unknown argument '" << flag << "'\n";
```

That is the right default. It is also why two flags that mean *nothing* to us stop
the server before it loads a model. Measured over `vllm-project/recipes`
@ `86c7777aa699482ef1ebd0c5da9fc540ccc00a40`, 157 official model recipes:

| Flag | Recipes passing it | Why it is inert for us |
|---|---:|---|
| `--enable-auto-tool-choice` | **89 / 157** | we parse tool calls whenever `--tool-call-parser` resolves; there is no second gate to open |
| `--trust-remote-code` | **82 / 157** | authorizes executing Python from the checkpoint; we have no Python, so there is nothing to authorize |

The reproducer is a recipe's own copy-paste block —
`recipes/models/Qwen/Qwen3.5-27B.yaml`, `features.tool_calling`:

```bash
vllm serve Qwen/Qwen3.5-27B-FP8 --enable-auto-tool-choice --tool-call-parser qwen3_coder
# ^ server: unknown argument '--enable-auto-tool-choice'
```

Qwen3.5-27B is a model we ship **token-exact and gated**. The kernels are not the
thing standing between a user and a running server; argument parsing is.

## Upstream anchors

- `vllm/entrypoints/openai/cli_args.py:105` — `enable_auto_tool_choice: bool = False`.
- `vllm/entrypoints/openai/cli_args.py:395` — **`--enable-auto-tool-choice`
without `--tool-call-parser` is a `TypeError`.** This validation is part of the
behaviour and must be mirrored, not dropped.
- `vllm/entrypoints/openai/api_server.py:426,441,529,544` — threaded onward as
`enable_auto_tools`.

## Design

Add an **accepted-and-inert** table to `server_main.cpp`. Not a catch-all: a fixed
list, one entry per flag, each carrying the reason it is inert and whether it takes
a value.

```
{"--enable-auto-tool-choice", kNoValue, "tool parsing is already unconditional once --tool-call-parser resolves"},
{"--trust-remote-code", kNoValue, "no Python runtime: there is no remote code to trust"},
```

Three rules the table must obey:

1. **A flag not in the table still aborts.** Silently swallowing
`--tensor-parallel-size` would let a user believe they got tensor parallelism.
The whole value of the seam is that it is enumerated.
2. **Accepting is announced.** On use, emit one notice per accepted flag naming it
and its reason, so a user reading the log learns the flag did nothing rather
than inferring that it worked.
3. **Mirrored validation still fires.** `--enable-auto-tool-choice` with
`--tool-call-parser none` must fail exactly as `cli_args.py:395` does. Inert is
not the same as unvalidated.

### The one place we knowingly differ

Upstream defaults `--tool-call-parser` to `None`; we default it to `hermes`
(`docs/USAGE.md:878`). So upstream's flag genuinely gates something and ours cannot.
This spec does **not** change that default — it is pre-existing, out of scope here,
and worth its own issue if a reviewer wants it reconciled. The notice text must not
imply our behaviour matches upstream's when the parser is unset.

## Tests

RED-first, in `tests/vllm/entrypoints/openai/`:

| Case | Asserts |
|---|---|
| each listed flag starts the server | argument parsing succeeds and the engine reaches load |
| an unlisted unknown flag | still aborts with the existing message — the guard is not widened |
| `--enable-auto-tool-choice` + `--tool-call-parser none` | fails, mirroring `cli_args.py:395` |
| notice emission | each accepted flag names itself and its reason on stderr |

The second row is the one that matters. A mutation that turns the table into a
catch-all must turn that test RED; if it does not, the test is not defending the
guarantee. Prove it by mutating in a scratch copy, then restore the tree
byte-for-byte.

## Gates

Focused: the new test file. Full: `scripts/agent-preflight.sh --staged` plus the
serve conformance suite. No GPU, no oracle run — this is an argument-parsing
change and does not touch a forward pass.

## Risks / decisions

- **Risk**: the table becomes a dumping ground for anything that fails to parse.
Mitigated by rule 1 plus the per-entry reason string — an entry with no honest
reason cannot be written.
- **Decision**: `--trust-remote-code` is accepted rather than rejected-with-advice.
Rejecting it with "we don't need this" would still abort the recipe command,
which is the entire defect.
- **Decision**: flags that are inert *because we lack the capability* — TP, EP,
`--mm-encoder-tp-mode` — are **NOT** in this table. They keep aborting. Accepting
them would be the failure mode this seam exists to prevent.
- **Out of scope**: `--language-model-only` (#607) is a real capability gap, not an
inert flag, and must not be quietly added here.

## Now

`SPIKE` — spec committed, implementation not started. Next: a fresh implementer
takes the table, the four tests, and the mutation proof.
Loading
Loading