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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/issue-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ rather than merged. `scripts/check-agent-record.py` gates both.
| [#1150](https://github.com/mudler/vllm.cpp/issues/1150) | `LTX25-TI2VID-RECIPE` | The sigma SHIFT is derived from the target latent on every arm, where six of upstream's seven `LTX2Scheduler.execute` call sites pass NO latent and take `default_number_of_tokens` = `MAX_SHIFT_ANCHOR` = 4096 (`schedulers.py:11,:29,:31`). `grep -rn '\.execute(' packages/ltx-pipelines/src/ltx_pipelines/` at `fd4ded7f` returns seven and that grep is the whole population: only `ti2vid_two_stages_hq.py:267` passes `latent=empty_latent`. This engine passes `target_tokens` at `src/vllm/multimodal/ltx2_video.cpp:3442-3443`, so it mirrors the exception and diverges from the rule. Correct today: `t2a_one_stage` (passes 0 at `src/vllm/model_executor/models/ltx2_t2a.cpp:178`) and `res2s_two_stage`. DIVERGENT: `one_stage` at four version keys (`ti2vid_one_stage.py:207`), `a2vid_two_stage` stage 1 (`a2vid_two_stage.py:226`) and `retake`'s non-distilled arm (`retake.py:287`). Recipes carrying explicit `sigmas` never reach the derivation and are unaffected. Not a rounding difference: at the recipe default geometry the target latent is 6144 tokens, giving `sigma_shift` 2.78 against upstream's 2.05, so every sigma moves. Invisible because the trajectory changes while the frame count, shapes, sample rate and errors do not, and our goldens were captured from this engine so they PIN it rather than detect it. `.agents/specs/ltx25-res2s-loop.md:80-88` saw the HQ/plain split and concluded the divergence was on the plain two-stage arm alone; that is right about HQ and wrong about the blast radius. Found by row `LTX25-TI2VID-RECIPE`, which added the seam — `Ltx2PhaseRecipe::schedule_tokens`, defaulted to today's behaviour so nothing moves — and set it on the one phase it ships. Not fixed in flow because flipping the other three re-samples five shipped, gated arms and rewrites their goldens, which needs its own spec and fresh review. Listed under `## Owed` in [`ltx25-ti2vid-recipe.md`](specs/ltx25-ti2vid-recipe.md) | bug |
| [#1151](https://github.com/mudler/vllm.cpp/issues/1151) | `LTX25-TI2VID-RECIPE` | The `requires_distilled_lora` refusal still advertised [#1118](https://github.com/mudler/vllm.cpp/issues/1118) as OPEN, and cited `a2vid_two_stage.py`'s line numbers to every other pipeline. #1118 closed at `4ae0f54ab` (row `LTX25-PHASE-LORA`, PR [#1140](https://github.com/mudler/vllm.cpp/pull/1140)), which added `Ltx2PhaseRecipe::loras` and `Ltx2RebindDitLoras`; the message at `src/vllm/multimodal/ltx2_video.cpp:1039-1041` still ended "upstream fuses that adapter into stage 2 ALONE and this engine fuses once at load, so stage 1 sees it too", every clause of which had become false, and the comment above the refusal said the same. `ltx25-phase-lora.md` repaired the REFERENCE-CONDITIONING refusal, which carried the identical claim ~1100 lines away, and named only that one in its port map, so this site and the `ltx2-gen --help` text (`examples/ltx2_gen/main.cpp:210-212`) were both missed. Second defect at the same site: the refusal is deliberately keyed on the FLAG rather than on the kind string so the next recipe inherits it (the comment names #1093 and #1096 as waiting), yet its body interpolated `im.pipeline_kind` into the first sentence and hard-coded a2vid's `:164`, `:114`, `:107` into the rest — so the first arm to inherit it would be told its own name and then a different pipeline's source lines. `--distilled-lora required=True` lives on `default_2_stage_arg_parser` (`utils/args.py:1123`, `:1140-1155`), which all of these pipelines select, and that shared anchor is what the message now cites. Found and fixed IN FLOW by row `LTX25-TI2VID-RECIPE`, the second user of the flag; `tests/vllm/multimodal/test_ltx2_video.cpp` asserted the string `1118` was PRESENT and now asserts it is absent | bug |
| [#1152](https://github.com/mudler/vllm.cpp/issues/1152) | `LTX25-TI2VID-RECIPE` | `Ltx2PipelineRecipe::allow_request_latents` (`include/vllm/model_executor/models/ltx2_pipeline.h:705`) is WRITTEN by every recipe and READ by nothing — the "a parameter no caller passes" shape AGENTS.md `## Nothing lands dead` names. Measured at `c83b96934`: `grep -rn allow_request_latents src include examples` minus the declaration returns FIVE lines and all five are assignments (`ltx2_pipeline.cpp:1264` false, `:1345` false, `:1473` true, `:1604` false). Positive control, the field declared one line above and set in the same blocks: `allow_request_sigmas` returns its assignments PLUS a real reader at `src/vllm/multimodal/ltx2_video.cpp:3476`, so the grep is well-formed and the absence is the finding. Consequence: `Res2sTwoStageRecipe` carries `true` where every other recipe carries `false` and nothing can tell the difference — no upstream `__call__` among these pipelines takes an initial-latent parameter, so `false` is what the signatures support and the `true` looks like an oversight, but it is unfalsifiable while nothing reads the field. The tests assert the VALUES, so they gate the record against itself and cannot see that nothing consumes it, which is the tautology shape [#911](https://github.com/mudler/vllm.cpp/issues/911) recorded on the anchor checker. Two closes: give it a reader (a refusal on a request supplying a latent to a recipe whose upstream signature has none) or delete it and its assertions — deleting is defensible, since no request surface carries a latent at all so the refusal could never fire either. Found by row `LTX25-TI2VID-RECIPE` while deriving the same field for a sixth recipe; not fixed in flow because both closes touch five landed recipes and one deletes gated assertions. Listed under `## Owed` in [`ltx25-ti2vid-recipe.md`](specs/ltx25-ti2vid-recipe.md) | bug |
| [#1160](https://github.com/mudler/vllm.cpp/issues/1160) | `SPEC-MTP` | `--speculative-config` read five keys (`src/vllm/config/speculative.cpp:14-131` @ `affc2a7fd`) and DROPPED every other one, so `{"method":"dspark","model":"...","num_speculative_tokens":7,"draft_sample_method":"probabilistic"}` started a server that drafted GREEDY, logged nothing and exited 0, and a misspelled `"num_speculatve_tokens"` silently took the resolved default. Upstream gets this refusal free: `SpeculativeConfig` carries `extra="forbid"` through its `@config` decorator (`vllm/config/speculative.py:81-83` @ `555967922`), and the hand-written C++ parser reproduced the reads without the guard. The method value WAS validated (`speculative.cpp:43,48-54`), so the strictness existed for one field and was absent for the object. Not merely an inert flag: draft sampling and verify are greedy here (`include/vllm/v1/worker/gpu/spec_decode/dspark/speculator.h:36-38`, `include/vllm/v1/spec_decode/rejection_sampler.h:53-57`), so a dropped `probabilistic` yields a DETERMINISTIC run when a sampled draft was requested, and a deterministic run is adjudicable by the token-exact greedy gate while the requested configuration is not, which lets a parity or benchmark number be taken under a configuration nobody chose. Fixed by admitting the object key by key against the `SpeculativeConfig` field set at the pin (`speculative.py:85-283`), in three classes. The five honoured keys pass. `draft_sample_method` and `rejection_sample_method` pass at their upstream defaults `greedy` and `standard`, which are what this engine implements, and any other value names row `SPEC-ACCEPT-VARIANTS`. Every other name is refused, worded differently for a declared vLLM field than for a typo. Scope item 2 of the issue, probabilistic draft sampling itself, stays owned by `SPEC-ACCEPT-VARIANTS` (`.agents/engine-matrix.md:191`, `INVENTORIED`) and is NOT in this change. Gated by `tests/vllm/config/test_speculative_unknown_keys.cpp` (9/9, 63 assertions, RED-first at 6/9 failing) plus three black-box server-CLI cases in `examples/CMakeLists.txt`, which are the reach proof: the unit file calls the parser directly and stays GREEN with the server call site deleted, while the CLI cases go RED | bug |
| [#1165](https://github.com/mudler/vllm.cpp/issues/1165) | `FIX-GPU-MEM-UTIL-INERT` | `--gpu-memory-utilization` is parsed (`src/vllm/entrypoints/openai/server_main.cpp:440-441`), threaded to both engines (`:952`, `:1039`), carried on the C ABI (`include/vllm.h:486`, `src/capi/vllm_c.cpp:577-580`), spelled by `examples/cli/main.cpp:118-119`, defaulted to 0.92 (`include/vllm/entrypoints/model_loader.h:90`) and then read by NOTHING: `LoadedEngine::ResolveNumBlocks` falls through knob 1 (`num_blocks`) and knob 2 (`kv_cache_memory_bytes`) to a bare `return 256` under a `TODO(ROAD-V1-MEM M3)` (`src/vllm/entrypoints/model_loader.cpp:954-959`), so a user who passes `--gpu-memory-utilization 0.85` believes they sized the KV pool and sized nothing. DISTINCT from [#83](https://github.com/mudler/vllm.cpp/issues/83), which owns IMPLEMENTING the utilization path (`ROAD-V1-MEM` M3, dgx-gated on a profile run and an oracle-matched pool). This row owns not lying about it: accept the flag, keeping vLLM's exact name and fraction semantics per `.agents/roadmap_v1.md:71`, and emit one notice per engine load when the caller set it explicitly AND the utilization path is the one that resolved the pool. Fixed in flow. Spec [`gpu-mem-util-inert.md`](specs/gpu-mem-util-inert.md) | bug |
| [#1166](https://github.com/mudler/vllm.cpp/issues/1166) | `FIX-FP8-BLOCKWISE-REFUSAL` | `Qwen/Qwen3.8-27B-FP8` is block-wise (fine-grained 128x128) FP8 and this tree implements per-tensor FP8 only, so the load stops on a message that names the wrong thing. Measured live at revision `017b9c7af6b5689d5dd426a76e0bc077eb5ca20a` on 2026-08-17: the config declares `quant_method` `fp8`, `weight_block_size` `[128, 128]` and `activation_scheme` `dynamic`, and the safetensors header of `layers-3.safetensors`, read by RANGE REQUEST rather than downloaded, gives `self_attn.q_proj.weight` `F8_E4M3` `[12288, 5120]` beside `self_attn.q_proj.weight_scale_inv` `BF16` `[96, 40]`, which is exactly `[12288/128, 5120/128]`, with ZERO `input_scale` tensors in the shard. `LoadAttnDense` branches on the weight dtype alone (`qwen3_5_dense_weights.cpp:479`) so the block-wise projection enters the per-tensor arm at `:480`, and `LoadFp8Raw` (`qwen3_5_weights.cpp:449`) asks for `<proj>.weight_scale` at `:458`, which this checkpoint spells `weight_scale_inv`, so the resolver at `qwen3_5_dense_weights.cpp:682` raises `tensor not found: ...q_proj.weight_scale`. Nothing is missing from the checkpoint. The reader is sent after a tensor upstream never writes in this mode instead of being told the fine-grained arm is absent. NOT the silently-wrong-numerics case, and the check that rules it out is recorded rather than assumed: `ReadF32Scalar` (`qwen3_5_weights.cpp:312`) bounds its input with `t.nbytes >= sizeof(float)`, a LOWER bound, so a `[96, 40]` scale would pass and read as block `(0,0)`, but the NAME miss stops the load before that scalar read, and upstream makes the spelling strictly conditional on block quant (`weight_scale_inv if self.block_quant else weight_scale`, `fp8.py:511` at pin `555967922`), so no upstream block-wise checkpoint reaches it. FIXED IN FLOW as a named refusal at `ModelRegistry::Load`; the block-wise arm itself stays owed | bug |
| [#1161](https://github.com/mudler/vllm.cpp/issues/1161) | `ENG-CUDAGRAPH` | SGLang landed **Breakable CUDA Graph (BCG)** and the whole surface was absent from `.agents/sglang-matrix.md`, while `ENG-CUDAGRAPH` carried no verdict on it. Grounded at pin `v0.5.15`/`f63458b5be`: BCG captures one forward as a SEQUENCE of `torch.cuda.CUDAGraph` segments split at eager break points on the attention and recurrent calls, sharing one mempool with weak-ref'd intermediates, with **no `torch.compile`** (`breakable_cuda_graph.py:204-241,244-260,14-23,156-169`; `breakable_cuda_graph_backend.py:14-17`; break points `radix_attention.py:256`, `forward_mla.py:1092`, `radix_linear_attention.py:159`, `nemotron_h.py:1240`, `dsa_indexer.py:2414`). **The public priority claim is narrower than it reads:** vLLM's v1 default is ALREADY `FULL_AND_PIECEWISE`, splitting at `splitting_ops` (`vllm/config/compilation.py:60-63,517,615,630` @ `555967922`), so the capability has been in our benchmark denominator all along — what SGLang landed first is the CONSTRUCTION (piecewise coverage from runtime stream capture plus a decorator, no Dynamo/Inductor/FX), which matters to us only because we have no compiler either. **The load-bearing outcome is a REFUTATION:** prefill graph capture is not a lever on our CUDA gate models, because GB10 2026-07-09 measured steady-state prefill GPU-idle-between-launches at **3.8%** with GPU-busy >96% on both arms, and attributed the 27B prefill gap to **92.5% non-GEMM glue GPU work** with the dominant GEMM the same symbol at the same call count (+0.17%) and attention AHEAD. There are no launch bubbles in our prefill for a graph to collapse, and vLLM's piecewise prefill graph is already inside the denominator we lose to, so it does not explain the gap either. Decode is a separate and already-banked case (conc-64 launch overhead was ~24%, already one `cudaGraphLaunch`/step). FIXED IN FLOW: `SGLANG-BCG` row added under a new "Graph capture and runtime execution" section of `sglang-matrix.md`, the refutation recorded on `ENG-CUDAGRAPH` (which stays `PARTIAL`, because nothing shipped), roadmap track `C12` added, and spec [`sglang-breakable-cuda-graph.md`](specs/sglang-breakable-cuda-graph.md) written to carry the analysis. NOT fixed here and owned by the three issues below: no engine code, no measurement taken, no oracle run, and no advance of the SGLang pin | record |
Expand Down
33 changes: 33 additions & 0 deletions docs/SPECULATIVE-DECODING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,39 @@ the list of accepted ones (`src/vllm/config/speculative.cpp`).
MTP and DFlash are the two with binding numbers behind them; the per-method
detail below and in [BENCHMARKS.md](BENCHMARKS.md) says which is which.

## Which keys the JSON accepts

The object is checked key by key and **nothing is dropped**. A name this engine
cannot honour is refused at startup, by name, before a byte of weights is read.
Until [#1160](https://github.com/mudler/vllm.cpp/issues/1160) an unrecognised key
was silently discarded, so `"draft_sample_method":"probabilistic"` started a
server that drafted greedily and reported nothing, and a typo such as
`"num_speculatve_tokens"` quietly took the resolved default instead of the value
that was typed.

| Key | Accepted |
|---|---|
| `method` | `mtp`, `dflash`, `dspark`, `ngram`, `draft_model` |
| `num_speculative_tokens` | a positive integer, or absent for the method's own default |
| `model` | the draft checkpoint path or HF repo id |
| `prompt_lookup_min`, `prompt_lookup_max` | an integer of at least 1, `ngram` only |
| `draft_sample_method` | `greedy` only, which is upstream's default and what this engine does |
| `rejection_sample_method` | `standard` only, which is upstream's default and what this engine does |

There are two kinds of refusal, worded differently on purpose. A name vLLM's own
`SpeculativeConfig` declares, such as `quantization` or `max_model_len`, is
reported as a real vLLM field this engine does not implement. Any other name is
reported as unknown, together with the list above, because that one is usually a
typo. `draft_sample_method: probabilistic` and the `synthetic` and `block`
acceptance variants name row `SPEC-ACCEPT-VARIANTS`, which owes them.

The distinction matters beyond ergonomics. Draft sampling and verify are greedy
here, so a dropped `probabilistic` produced a **deterministic** run when a sampled
draft was asked for, and a deterministic run is adjudicable by the token-exact
greedy gate while the configuration the user actually requested is not. A
silently downgraded flag therefore lets a parity or benchmark number be taken
under a configuration nobody chose.

## MTP

- **Models:** the Qwen3.5 / 3.6 gate checkpoints that ship an `mtp.*` draft head
Expand Down
Loading
Loading