Skip to content
372 changes: 372 additions & 0 deletions .agents/specs/ltx25-upsampler-arms.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ in `ltx2_text_encoder.cpp` is the call that would have to change.
| LTX-2.5 T2A guidance space | LTX-2.5 DiT (T2A arm) | `test_ltx2_video` "the guider is handed x0 predictions" through `Generate`, on all 3 arms plus the guider output and the Euler input; a seam case puts the two spaces 1.5e-07 apart at rescale 0 and 0.352 at 0.7 | Combines **denoised (x0)**, mirroring `X0Model` (`model.py:590-604`). Was velocity space, which agrees only at rescale 0 ([#1039](https://github.com/mudler/vllm.cpp/issues/1039)) |
| LTX-2.5 VIDEO guidance | LTX-2.5 DiT, joint video+audio | `test_ltx2_video`'s `ltx2 one_stage:` cases through `Generate`; all FOUR arms carry the x0 invariant and the guider output replays EXACTLY | `--pipeline-kind one_stage` runs `_guided_denoise`: 4 forwards/step, combined per modality in **x0**. Was ONE unguided forward, every `video_guidance` field dead ([#1092](https://github.com/mudler/vllm.cpp/issues/1092)) |
| LTX-2.5 cross-attention perturbations | LTX-2.5 DiT | `test_ltx2_video` and `test_ltx2_device` each gate one direction ALONE, on a forward where the other stream is PRESENT but DISABLED. Swapping the two flags is RED on both arms | `SKIP_A2V_CROSS_ATTN` / `SKIP_V2A_CROSS_ATTN`, which `modality_scale = 3.0` selects on every video row. On the DEVICE forward too since 2026-08-19 ([#1092](https://github.com/mudler/vllm.cpp/issues/1092)) |
| LTX-2.5 latent upsampler `dims=2` arm | LTX-2.5 spatial upsampler checkpoint whose config sets `dims: 2` | `ltx2 the latent upsampler reproduces upstream on the dims=2 arm` (`test_ltx2_pipeline`) against the EXECUTED upstream module, plus `a dims=2 upsampler checkpoint RENDERS` (`test_ltx2_video`) through `LoadVideoEngine`/`Generate`; removing the fold reds the golden at max|diff| 0.68 while the render stays green, so the value gate and not a shape check is what holds it | No flag: `dims` is read off the checkpoint's own config, so a 2-D upsampler at `--upsampler` just works. Conv2d for `initial_conv`, both ResBlock stacks and `final_conv` (model.py:47); the frame axis folds into the batch (:86), which makes GroupNorm PER FRAME. Frame count preserved, H and W doubled, which is what both spatial call sites require. Two 2-D contradictions upstream cannot run are refused by name and EXECUTED against the real module by the golden generator: `temporal_upsample` (a Conv3d handed a folded 4-D tensor) and `rational_resampler` (five-value unpack at spatial_rational_resampler.py:41) |
| LTX-2.5 audio-to-video (`A2VidPipelineTwoStage`) | LTX-2.5 DiT + audio VAE encoder + spatial upsampler | `test_ltx2_pipeline` and `test_ltx2_video`'s `ltx2 a2vid:` cases through `LoadVideoEngine`+`Generate`; the take's latent is bit-identical across SEEDS and moves with the WINDOW | `--pipeline-kind a2vid_two_stage`. Guided half-res stage 1, DERIVED schedule, plain Euler; distilled 3-sigma stage 2. `--audio-path` and `--lora` REQUIRED; the distilled adapter rides stage 2 ALONE (#1118) |
| LTX-2.5 keyframe interpolation (`KeyframeInterpolationPipeline`) | LTX-2.5 DiT + spatial upsampler | `ltx2 keyframe:` cases in `test_ltx2_pipeline` / `test_ltx2_video` via `LoadVideoEngine`+`Generate`: frame 0 APPENDS against a `ti2vid_two_stage` control, the x0 invariant on four arms, the 4096 anchor | `--pipeline-kind keyframe_interpolation`. No frame-0 special case, so `--first-frame` is guidance to interpolate FROM; stage 2's audio leaves. `--lora` REQUIRED. `--last-frame` new (#1191). CPU fixtures |
| LTX-2.5 two-stage text/image-to-video (`TI2VidTwoStagesPipeline`) | LTX-2.5 DiT + spatial upsampler | `test_ltx2_pipeline` and `test_ltx2_video` `ltx2 ti2vid:` cases through `LoadVideoEngine`+`Generate`; the x0 invariant on all FOUR arms, and the 4096 anchor read at two geometries against a res_2s control that moves | `--pipeline-kind ti2vid_two_stage`. Guided half-res stage 1 on the UNADAPTED model, plain Euler; distilled 3-sigma stage 2. `--lora` REQUIRED, no `--audio-path`; stage 1's audio leaves. CPU fixtures, Full-model run owed |
Expand Down Expand Up @@ -391,7 +392,7 @@ CPU elementwise GEMM (f32/f16/bf16) runs AVX2 and AVX-512 tiers on x86 where the
| Kimi-Linear-48B-A3B (KDA + NoPE-MLA + MoE hybrid) | **Runner fold LANDS (ROW 7 §21, #122): the ENGINE/SERVER surface serves Kimi at the 122/128 golden profile (engine==CLI 128/128); STRICT stays closed (intrinsic p7 near-tie)** | server 19.0 tok/s wall / CLI 18.9 vs vLLM ~21 (~0.90×), speed residual named (§21) |
| Muse Glimmer 30B (Meta) | Text gated at **reduced depth 4/52** only; vision wired but never reference-checked | [spec](../.agents/specs/muse-glimmer.md) / [#268](https://github.com/mudler/vllm.cpp/issues/268). Full depth, multi-step decode, image/video, server path and parser scoping open. vLLM speed OPEN GAP; llama.cpp bar #333 |
| LTX-2.5 AUTO duration (the duration head) | Brick ported, never constructed | `duration_head_path` is REFUSED by name rather than accepted-and-ignored ([#611](https://github.com/mudler/vllm.cpp/issues/611)); supplying a head cannot load one. Give `num_frames` or `duration` |
| LTX-2.5 arms a request CAN reach | Refused by name at the call site | The spatiotemporal latent upsampler (both flags set). Supplying that checkpoint names that arm, not the temporal one. The temporal-only x2 arm is ported, not refused |
| LTX-2.5 arms a request CAN reach | Refused by name at the call site | The spatiotemporal latent upsampler (both flags set). Supplying that checkpoint names that arm, not the temporal one. Its operator is small; what keeps it out is that it returns `[c, 2f-1, 2h, 2w]` while both spatial call sites require the frame count back unchanged, so porting it would trade a named refusal for a shape complaint ([#2584](https://github.com/mudler/vllm.cpp/issues/2584)). The temporal-only x2 arm is ported, not refused, and so is `dims=2`; what stays refused in 2-D are its two contradictions, `temporal_upsample` and `rational_resampler`, neither of which upstream can run either |
| LTX-2.5 resolution | Off-grid sizes refused, naming the offending axis and a size you can actually pass; frames still round | `--width`/`--height` must divide 64 (two-stage) or 32 (one-stage), from the VAE factor times the phase downscale ([#919](https://github.com/mudler/vllm.cpp/issues/919)). `--frames` rounds to `8k + 1`. No size cap |
| LTX-2.5 arms nothing can request | Declared, not requestable | `int8-convrot` (ComfyUI-only), single-node multi-GPU, `BetaScheduler` (upstream selects no scheduler either). No flag or extra asks for these. `multishot` was RETIRED (absent upstream) and `kLoraFusion` too (now served) |
| Qwen3.8-27B quantized arms (Q4_K_M GGUF, its `clip` mmproj, the `unsloth` "NVFP4" = `mixed-precision`) | All three files ACCOUNTED against committed manifests (866 / 334 / 1968 names). **Q4_K_M RUNS on CPU and its token gate vs llama.cpp `b10451` FAILED** (2026-08-23): tokenizer exact 6/6, generation divergent 5/6, every divergence a rank-2 loss under 0.18 logits over 288 steps, so a precision difference and not a wiring defect ([evidence](bench-evidence/qwen38-27b-q4km-token-gate-20260823.md)). No speed or memory number is admissible. NVFP4 still un-run (#1632) | BF16 gated (#915). `--mmproj` (ABI v22) loads a `clip` GGUF; a load refuses a file with a tensor no loader reads. "NVFP4" @`7d6f8d4d`: W4A4 loads, FP8 refused ([spec](../.agents/specs/qwen38-27b-quant-arms.md)) |
Expand Down
2 changes: 1 addition & 1 deletion include/vllm/model_executor/models/ltx2_pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -1176,7 +1176,7 @@ Ltx2PipelineRecipe ResolveLtx2PipelineRecipe(const std::string& pipeline_kind,
// TWO KINDS live here, and conflating them overstated what this port refuses:
//
// REACHABLE REFUSAL — a product path constructs the condition and throws, so a
// caller CAN trip it. `kSpatiotemporalUpsampler` (ltx2_upsampler.cpp:465) is
// caller CAN trip it. `kSpatiotemporalUpsampler` (ltx2_upsampler.cpp:497) is
// the ONE. `ltx2_video.cpp` reaches it through `Ltx2UpsampleVideoLatent` when a
// phase asks for the spatial-upsample transform. The TEMPORAL-ONLY x2
// upsampler is NOT among them: it is ported (`2e9d95e74`, spec
Expand Down
74 changes: 58 additions & 16 deletions include/vllm/model_executor/models/ltx2_upsampler.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,28 @@
// Ltx2RationalForScale <- spatial_rational_resampler.py:10-14
// Ltx2UpsampleVideoLatent <- model.py:129-143 (upsample_video)
//
// ─── WHAT THE TEMPORAL ARM IS REACHABLE FROM ─────────────────────────────────
// NOTHING, today, and that is stated here rather than only in the spec because a
// header is what the next reader opens. It is ported and gated against executed
// upstream at reduced dimensions, and `Ltx2ParseUpsamplerConfig`
// (ltx2_loader.cpp:1431-1444) reads `temporal_upsample` off a checkpoint. But the
// engine's ONE upsampler call site is the `kSpatialUpsample` phase input
// transform (multimodal/ltx2_video.cpp:1408-1466), which shape-checks the result
// against a SPATIALLY doubled latent and fails otherwise; and upstream's only
// consumer is `DFRPipeline`'s rounds loop (ltx-pipelines/dfr_pipeline.py:235-245,
// 402-407), which is not ported. The shipped temporal checkpoint
// ─── WHAT EACH ARM IS REACHABLE FROM ─────────────────────────────────────────
// Stated here rather than only in the spec because a header is what the next
// reader opens. `Ltx2UpsampleVideoLatent` has THREE product call sites, all in
// multimodal/ltx2_video.cpp, and each one pins the frame axis:
//
// :3521 the video latent, the `kSpatialUpsample` phase input transform.
// Requires `up.frames == vshape.frames` at :3525-3531.
// :3548 the generated keyframe slots, which take the SAME spatial upsampler
// (dfr_pipeline.py:348). Requires `slot_positions.size()` at :3552-3563.
// :5058 DFR's temporal-refinement rounds, the TEMPORAL arm. Reached through
// the `temporal_upsample_rounds` load extra, and instrumented by
// `trace.temporal_upsample_calls` (multimodal/ltx2_video.h:741).
//
// This paragraph said "NOTHING, today" of the temporal arm and "the engine's ONE
// upsampler call site" of the spatial one. Both were false by the time they were
// read: site :5058 drives the temporal arm and is not a phase input transform at
// all. Corrected under issue #2580; the count is what the dims=2 port's
// reachability argument rests on, so it is derived here rather than remembered.
// The shipped temporal checkpoint
// (`ltx-2.5-latent-temporal-upscaler-x2-bf16-1.0.safetensors`,
// ltx-pipelines/docs/pipelines.md:176) is not on the NAS either, so no
// real-weight result exists.
// ltx-pipelines/docs/pipelines.md:176) is not on the NAS, so no real-weight
// result exists for that arm.
//
// ─── WHAT SEPARATES THIS FROM THE VAE'S CONVOLUTIONS ─────────────────────────
// These are plain `torch.nn.Conv3d`/`Conv2d` with `padding=1` — ZERO padding on
Expand All @@ -55,13 +64,46 @@
// * The temporal arm DROPS THE FIRST FRAME after the shuffle (model.py:109-113),
// so `f` frames in produce `2f - 1` out and not `2f`.
//
// ─── THE dims=2 ARM (model.py:47, :85-100) ───────────────────────────────────
// PORTED. `conv = torch.nn.Conv2d if dims == 2 else torch.nn.Conv3d` (:47)
// reaches four parameter groups — `initial_conv`, both ResBlock stacks and
// `final_conv` — so each is a 4-D kernel where the 3-D arms build a 5-D one. The
// `upsampler` branch (:55-72) never reads `dims` and keeps its rank.
//
// The forward folds the frame axis into the BATCH (:86) and unfolds at :100,
// which has one consequence a shape check cannot see: GroupNorm normalises PER
// FRAME. This port reproduces the fold by running one frame at a time, so the
// existing reduction over `frames * height * width` gives the per-frame
// statistic without a second normaliser. Gated by "reproduces upstream on the
// dims=2 arm" (test_ltx2_pipeline) against the executed module, and reached
// end-to-end by "a dims=2 upsampler checkpoint RENDERS" (test_ltx2_video).
//
// Any `dims` that is not 2 builds Conv3d, which is upstream's own `else` at :47
// and is mirrored rather than narrowed to a refusal upstream does not raise.
//
// ─── NOT PORTED, refused by name ─────────────────────────────────────────────
// * `spatial_upsample AND temporal_upsample` (model.py:55-59) — a DIFFERENT
// operator from the temporal-only arm: `Conv3d(mid, 8*mid)` + PixelShuffleND(3).
// Asking for it throws.
// * `dims == 2` (model.py:85-100) — a checkpoint that sets it wants Conv2d
// everywhere, i.e. no temporal convolution at all. LTX-2.5's upsampler is
// dims=3; the 2-D arm is refused rather than approximated by the 3-D one.
// Asking for it throws. Its operator is small; what keeps it out is that it
// returns `[c, 2f-1, 2h, 2w]` and BOTH spatial call sites above require the
// frame count back unchanged. Owed, with the measurement, in
// .agents/specs/ltx25-upsampler-arms.md.
// * `dims == 2` WITH `temporal_upsample` — not an arm but a contradiction:
// upstream builds that upsampler as a Conv3d (:68-71) and the 2-D forward
// hands it a 4-D tensor. It raises on the CHANNEL COUNT and not on the rank:
// `Conv3d` reads a 4-D input as an unbatched 5-D one, so at 3 frames against
// `mid_channels` 32 it reports "to have 32 channels, but got 3". At
// `frames == mid_channels` the conv passes and `PixelShuffleND(1)` fails
// instead. Two mechanisms, one contradiction — refused by name once.
// * `dims == 2` WITH `rational_resampler` — the sibling, and the dangerous one:
// every operator in that branch is per-frame, so this port would compute a
// finite, plausible latent no shape check could fault. Upstream raises
// `not enough values to unpack (expected 5, got 4)` at
// spatial_rational_resampler.py:41. Refused by name.
//
// Both are EXECUTED against the real module by
// scripts/gen-ltx2-pipeline-goldens.py, which asserts each raises and what it
// says, so neither refusal can drift into one upstream would serve.
//
// ─── DTYPE ───────────────────────────────────────────────────────────────────
// f32, because this is the CPU REFERENCE arm and the gate compares the ALGORITHM
Expand Down
Loading