feat(LTX25-RES2S-LOOP): the res_2s loop, its second evaluation, and the phi that must not be fixed (#921) - #1101
feat(LTX25-RES2S-LOOP): the res_2s loop, its second evaluation, and the phi that must not be fixed (#921)#1101localai-bot wants to merge 4 commits into
Conversation
… fixed (#921) `TI2VidTwoStagesHQPipeline` cannot be served because the res_2s sampler is unported. What exists is one substep's SDE arithmetic; the sampler is `res2s_audio_video_denoising_loop` (`ltx-pipelines utils/samplers.py:208-447` @ `fd4ded7f`), and the sampler IS the HQ variant. Serving `LTX_2_3_HQ_PARAMS` on the Euler loop renders a plausible clip at half the model evaluations the preset was tuned for, and no shape check, frame count or rendered pixel can tell the two apart. Two findings the spec commits to before any code. `DiffusionStage.__call__` passes the loop SIX keyword arguments and no others (`utils/blocks.py:566-573`), so every remaining knob takes its declared default on the shipped arm. Two of those defaults contradict the already-ported ancestral arm: `noise_seed` is the constant `-1` rather than the request seed, and `new_noise_fn` is `_get_new_noise`, which NORMALIZES, against the ancestral loop's `_get_plain_noise` two lines away. `phi` (`res2s.py:4-22`) is a cancellation cliff, not a series expansion. Its guard is `abs(z) < 1e-10`, and outside it upstream evaluates `(exp(z) - remainder) / z^j` directly, so upstream's own `phi(2, -1e-10)` is `0.0` and `phi(2, -1e-8)` is `1.1102230246251563`. A port that used a Taylor series near zero — the numerically BETTER implementation — would return `0.5` and be wrong for this port's purpose. The goldens pin the cancelled values. The gate is an exact DiT-evaluation COUNT, `2 * n_full_steps + 1` when the schedule ends at 0, measured against upstream's own loop: 9 against the Euler arm's 4. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…he phi that must not be fixed (#921) `TI2VidTwoStagesHQPipeline` can now be served, as `pipeline_kind res2s_two_stage`. What was missing was not arithmetic around the edges: the res_2s SAMPLER is what makes the HQ preset high quality, and serving `LTX_2_3_HQ_PARAMS` on the Euler loop renders a finished, correctly sized, plausible clip at half the model evaluations the preset was tuned for. No shape, frame count, sample rate or pixel says so. So the gate is a COUNT. `Ltx2Res2sDenoisingLoop` evaluates the transformer at `sigmas[i]`, again at `sqrt(sigma * sigma_next)`, and once more at a terminal sigma the schedule injects, which is `2 * steps + 1` against the first-order arm's `steps`. Measured end to end through `VideoEngine::Generate`: 7 and 11 forwards at 3 and 5 steps on `res2s_two_stage`, against 3 and 5 on `one_stage`, both read off real renders. Three upstream facts this port would have got wrong by analogy, each verified at Lightricks/LTX-2 `fd4ded7f` rather than inherited. `DiffusionStage.__call__` hands the loop SIX keyword arguments and no others (`utils/blocks.py:566-573`), so every remaining knob takes its declared default. Two of those contradict the already-shipped ancestral arm: `noise_seed` is the constant -1 rather than the request's seed, and `new_noise_fn` is `_get_new_noise`, which NORMALIZES, against the ancestral loop's `_get_plain_noise` ten lines away. `phi` (`res2s.py:4-22`) is a cancellation cliff, not a series expansion. Its guard is `abs(z) < 1e-10` and outside it upstream evaluates the quotient directly, so upstream's own `phi(2, -1e-10)` is `0.0` and `phi(2, -1e-8)` is `1.1102230246251563`. A Taylor series near zero, the numerically better implementation, returns `0.5` at both and diverges from the model's own runtime. The goldens pin the cancelled values exactly, and the header says not to fix it. The SDE coefficients run at TWO widths because upstream hands them two: float64 from the substep's `[sigma, sub_sigma]` pair (`samplers.py:342`) and float32 from the loop's own schedule at step level (`:415`). One templated implementation, three instantiations, no second copy of the formula. Every golden came out of upstream's own executing code, imported from the checkout and run: `phi`, `get_res2s_coefficients`, `Res2sDiffusionStep`, `post_process_latent` and the loop itself. Video latents come back BIT-EXACT on three of five fixtures and one ulp on the other two. Twenty mutations over five rounds, eighteen detected. Four survivors were real holes and are closed: the loop tolerance was `kRoundOff` and could not see the precision split (now one ulp), no fixture ran at eta != 0.5 so the pinned substep eta was invisible (now an `Eta1` fixture), the engine's call to the noise normalization was gated by nothing (now `res2s_noise_moment_error`), and the fixture's noise hook was stateless so swapping the video and audio draws changed nothing (now stateful, as upstream's generator is). Two survivors stand and are recorded rather than papered over: the bong guard's strict `sigma > 0.03` cannot be reached through a float32 schedule, on either side, and the `h * a21` association differs by less than one ulp. Four mutations could not fail and each was printed rather than counted as a pass. `M6a` through `M6d` removed one of two idempotent normalize calls; `M6b` ran a truncated `-tc` filter that matched ZERO cases and printed `SUCCESS!` with exit 0, which only the case-count column caught; `M18a` targeted the wrong file and reported `ANCHOR NOT FOUND`. Reachability: `vllm_video_generate` to `VideoEngine::Generate` to the phase loop's `kRes2s` dispatch. Deleting that dispatch reds the end-to-end case. `--pipeline-kind` passes the string through with no allowlist, so `ltx2-gen`, the C ABI and the server all reach the arm. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…ded denoiser, and the substep conversion no gate could see (#921, #1092) `daeff67f2` (#1092) landed the guided video denoiser into the same phase-loop region this row edits, and the merge is not textual. This row's `Evaluate` called a bare `Ltx2DitForward`; `main` calls `Ltx2GuidedDenoise`. Keeping this row's version would have made the HQ preset the only unguided video arm in the tree, at cfg 1.0 where `ti2vid_two_stages_hq.py:271-281` runs a `GuidedDenoiser` at 3.0 / 7.0 with rescale 0.45 — and invisible to every gate this row had, because the evaluation count is the sampler's factor and guidance is the denoiser's. So `Evaluate` builds the `Ltx2X0Model` lambda and calls `Ltx2GuidedDenoise`, and both samplers reach it. `Ltx2ConditioningTrace::dit_forwards` is the second counter: `dit_evaluations` counts denoiser calls and stays at 2n+1 whether or not the arm is guided, while this one counts actual DiT forwards and is three times that on the HQ stage 1 (cond + uncond + mod). An unguided arm makes them equal, and the new end-to-end case asserts both exactly on two step counts. The res_2s substep evaluation passes a literal `step_index = 0`, mirroring `samplers.py:385`. It is read by `should_skip_step` (guiders.py:287-291), so the substep is unskippable at any `skip_step` — inert on the HQ preset's own 0 and live for a `video_skip_step` request. `Ltx2Res2sLoopStats::eval_step_indices` gates the sequence against upstream's own loop. THE SUBSTEP'S x0 CONVERSION HAD NO OBSERVABLE, and the reviewer's mutation for it survived. The substep runs over `x_mid` (samplers.py:369-378), a state that never becomes the stream's own latent, so `to_denoised` there must use the latent that evaluation was handed. MEASURED: with it reading `video.latent` instead, `test_ltx2_video` stayed GREEN at 74 cases and 2234 assertions. The loop's own arithmetic is gated with a fixture denoiser that never performs this conversion. `res2s_substep_*` records what the second evaluation was handed and returned, and the new case asserts `cond == latent - timesteps * velocity` over those four vectors with the midpoint displacement as its non-vacuity bound. The engine's `VT_CHECK` compared `stats.evaluations` against `stats.full_steps` — two fields of one struct, where `2n + 1 > n` holds for every n — while its comment claimed it compared two counters. It now compares the engine's own `dit_evaluations` delta against the loop's count. Mutation M8 applies the under-counting defect beside the old form and the suite is GREEN at exit 0. Two claims pointed at files that did not exist. Section 5 named a golden generator `gen_goldens.py` and section 8 named a mutation harness `mutate.py`, and neither was in the spec, in `scripts/`, or anywhere in the tree. `scripts/gen-ltx2-res2s-goldens.py` reproduces the committed `ltx2_res2s_goldens.inc` BYTE FOR BYTE at the pin, apart from the header line naming it and the `EvalStepIndices` arrays this change adds — which is the evidence the goldens are what upstream produced. `scripts/mutation-harness.py` refuses a dirty tree, refuses an absent or ambiguous anchor, runs the whole binary rather than a `--test-case` filter, and refuses to score a survivor on a zero case or assertion count. "Exactly three things" was false and load bearing, because it was the argument for what this row had to port. Diffing the two upstream pipelines at `fd4ded7f` shows at least seven differences; corrected in the spec, `ltx2_samplers.h`, the `Res2sTwoStageRecipe` comment and `docs/USAGE.md`, each with the disposition of the four that are not this row's. `docs/USAGE.md` also counted stage 1 alone at 31 evaluations where a full render is 38 denoiser calls and 100 transformer forwards, and four upstream anchors pointed one line or one file off. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
|
Superseded by #1125. This branch carried It could not be repaired in place: #1125 is the same tree — verified:
All the review's substance rode across unchanged: the res_2s arm routed through |
…he guidance the naive merge would have dropped (#921) (#1125) `pipeline_kind=res2s_two_stage` serves upstream's `TI2VidTwoStagesHQPipeline` -- the high-quality arm. New TU `ltx2_samplers.{h,cpp}`, mirroring upstream's own `utils/` partition: a stepper advances a substep, a sampler decides how many there are. Ported at `fd4ded7f`: `Ltx2Phi` (`utils/res2s.py:4-22`), `Ltx2GetRes2sCoefficients` + `Ltx2PhiCache` (`:25-62`), `Ltx2Res2sNormalizeNoise` (`utils/samplers.py:160-170`), `Ltx2Res2sDenoisingLoop` (`:208-447`), and `Res2sTwoStageRecipe` (`ti2vid_two_stages_hq.py:59-340` + `utils/constants.py:95-115`). THE SAMPLER IS THE HQ VARIANT. Serving the HQ preset on the Euler loop renders a plausible clip that is quietly not HQ, at roughly half the model evaluations the preset was tuned for, and no shape or token gate can see it. The gate is therefore an exact DiT-evaluation count -- `2n+1` when the schedule ends at 0, else `2n` -- asserted with the eval-sigma sequence beside it, so two forwards at the same sigma also fails. End to end: 7 and 11 evaluations at 3 and 5 steps on `res2s_two_stage`, against 3 and 5 on `one_stage`. `phi` is a CANCELLATION CLIFF, not a series expansion. Upstream guards only `abs(z) < 1e-10` and otherwise evaluates the quotient directly, so its own `phi(2,-1e-10)` is 0.0 and `phi(2,-1e-8)` is 1.1102230246251563. A Taylor expansion near zero -- the numerically BETTER port -- returns 0.5 and diverges from what the model actually ran. Pinned with `==`. GUIDANCE, which the merge nearly dropped. `daeff67f2` (#1092) landed the guided video denoiser into the same phase loop. A textual resolution keeping `Evaluate` as a bare `Ltx2DitForward` would have made the HQ preset the only unguided video arm in the tree -- upstream's stage 1 runs a `GuidedDenoiser` at cfg 3.0/7.0 and rescale 0.45 (`ti2vid_two_stages_hq.py:271-281`) -- and the evaluation count cannot see it, because a denoiser call is one evaluation guided or not. So `Evaluate` builds the `Ltx2X0Model` lambda and calls `Ltx2GuidedDenoise`, and a SECOND counter was added: `dit_forwards` counts actual `Ltx2DitForward` calls and is `3 * (2n+1)` on HQ stage 1 (cond + uncond + modality), asserted exactly at two step counts with `forwards != evaluations` as its own assertion. Stripping guidance from the res_2s arm alone is RED. `step_index` mirrors upstream literally: `step_idx` at the first evaluation (`samplers.py:301`), a literal 0 at the substep beside its one-element schedule (`:385`), `n_full_steps` at the terminal one (`:437`). Since `should_skip_step` is `step % (skip_step + 1) != 0`, the literal 0 makes the substep unskippable at any `skip_step` -- inert on the HQ preset's own `skip_step = 0`, live for a request override. A mutation survivor the review did not list: the substep's x0 conversion must use the latent THAT EVALUATION was handed, because the substep runs over `x_mid`. Reading the stream latent moves the whole substep prediction and nothing could see it, since the loop's arithmetic is gated with a fixture denoiser that performs no conversion. Now gated and RED. The engine's `VT_CHECK` was a tautology -- both operands came from the same `stats` object and `2n+1 > n` holds for every n -- and is now the trace delta against `stats.evaluations`. The argument is executable: the same under-counting defect beside the restored old check is GREEN. Both generator scripts are committed rather than described. `scripts/gen-ltx2-res2s-goldens.py` imports upstream's own `phi`, `get_res2s_coefficients`, `Res2sDiffusionStep`, `post_process_latent`, `_channelwise_normalize` and `res2s_audio_video_denoising_loop` at the pin and reproduces `ltx2_res2s_goldens.inc` byte for byte -- which is the evidence the goldens are upstream's and not this port's. Supersedes #1101, whose branch carried a merge commit with a bare subject and no trailer block. `check-commit-trailers.py` walks merges, that commit was a first-parent ancestor of every candidate head, and repairing it would have needed a force-push. Same tree, one commit, block intact. Owed, not claimed: no render on real weights, and no oracle-run comparison -- everything is gated against upstream SOURCE at `fd4ded7f`. The HQ preset is host-only here, because its `modality_scale = 3.0` asks for the isolated-modality pass and `Ltx2DitForwardDevice` takes no perturbations; that is #1092's owed device work, not newly incurred. Closes #921. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code] Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Closes #921. Row
LTX25-RES2S-LOOP, specltx25-res2s-loop.md,committed in
de8bd2b2cbefore any implementation code.TI2VidTwoStagesHQPipelinecan now be served, aspipeline_kind res2s_two_stage. What was missing was not arithmetic around the edges: theres_2s SAMPLER is what makes the high-quality preset high quality, and serving
LTX_2_3_HQ_PARAMSon the Euler loop renders a finished, correctly sized,plausible clip at half the model evaluations the preset was tuned for. No
shape, frame count, sample rate or pixel says so.
So the gate is a COUNT.
Ltx2Res2sDenoisingLoopevaluates the transformer atsigmas[i], again atsqrt(sigma * sigma_next), and once more at a terminalsigma the schedule injects, which is
2 * steps + 1against the first-orderarm's
steps. Measured end to end throughVideoEngine::Generate: 7 and 11forwards at 3 and 5 steps on
res2s_two_stageagainst 3 and 5 onone_stage,all four read off real renders rather than one computed from another.
Three upstream facts this port would have got wrong by analogy
Each verified at Lightricks/LTX-2
fd4ded7frather than inherited from theissue.
The loop runs on its own defaults.
DiffusionStage.__call__hands it SIXkeyword arguments and no others (
utils/blocks.py:566-573), so every remainingknob takes its declared default on the shipped arm. Two of those contradict the
already-shipped ancestral arm:
noise_seedis the constant-1rather than therequest's seed, so the res_2s SDE injections do not depend on the seed at all;
and
new_noise_fnis_get_new_noise, which NORMALIZES, against the ancestralloop's
_get_plain_noiseten lines away in the same file.phiis a cancellation cliff, not a series expansion. Its guard isabs(z) < 1e-10and outside it upstream evaluates(exp(z) - remainder) / z^jdirectly, so upstream's own
phi(2, -1e-10)is0.0andphi(2, -1e-8)is1.1102230246251563. A Taylor series near zero, which is the numerically betterimplementation, returns
0.5at both and diverges from the model's own runtime.The goldens pin the cancelled values with
==, not a tolerance, and the headersays not to fix it.
The SDE coefficients run at TWO widths, because upstream hands them two:
float64 from the substep's
[sigma, sub_sigma]pair (samplers.py:342) andfloat32 from the loop's own schedule at step level (
:415). One templatedimplementation, three instantiations, no second copy of the formula.
Evidence
Every golden came out of upstream's own executing code, imported from the
checkout and run:
phi,get_res2s_coefficients,Res2sDiffusionStep,post_process_latentand the loop itself. Only the denoiser, the noise draw(
torch.randn, whose stream this port does not have) and two media-IO modulesare substituted. Video latents come back BIT-EXACT on three of five fixtures and
one ulp on the other two.
Twenty mutations over five rounds, eighteen detected. Each row records
git diff --stat, whether it BUILT with its compile-error count, the exit codecaptured directly, and the doctest case and assertion counts.
Four survivors were real holes and are closed. The loop tolerance was this
file's
kRoundOffof 5e-6 and could not see the precision split, so it is nowone ulp. No fixture ran at eta != 0.5, so the pinned substep eta was invisible,
and an
Eta1fixture generated from upstream now separates them. The engine'scall to the noise normalization was gated by nothing, because normalization
changes no count, so
Ltx2ConditioningTrace::res2s_noise_moment_errornowobserves it. The fixture's noise hook was stateless and handed both modalities
the same values, so swapping the video and audio draws changed nothing; it is
now stateful, as upstream's generator is.
Two survivors stand and are recorded rather than papered over. The bong
guard's strict
sigma > 0.03cannot be reached through a float32 schedule oneither side, because
0.03fwidens to0.029999999329447746; the test commentthat claimed to pin it has been replaced by that derivation. And the
h * a21association differs by less than one ulp at this fixture's scale.Four mutations could not fail, and each was printed rather than counted as a
pass.
M6athroughM6deach removed one of two idempotent normalize calls.M6bran a truncated-tcfilter that matched ZERO cases and printedSUCCESS!with exit 0 over a clean build and a correct diff, which only thecase-count column caught.
M18atargeted the wrong file and reportedANCHOR NOT FOUND.Reachability
vllm_video_generatetoVideoEngine::GeneratetoLtx2VideoEngine::Generateto the phase loop's
kRes2sdispatch. Deleting that dispatch reds theend-to-end case (exit 1, 6 failed assertions).
--pipeline-kindpasses thestring through with no allowlist (
examples/ltx2_gen/main.cpp:239), soltx2-gen, the C ABI and--video-extra pipeline_kind=res2s_two_stageon theserver all reach the arm.
The engine's per-evaluation body is hoisted into one
Evaluatelambda that bothsamplers call, rather than a second forward path written by hand, so the
keyframe-marker guards, the frozen scalar sigma and the device/host split are
reached identically from both and
dit_evaluationsis a single increment no armcan bypass.
Gate
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DVLLM_CPP_CUDA=OFFCONFIGURE_EXIT=0,BUILD_EXIT=0,: error:count 0,ctest -N498,CTEST_EXIT=0, "100% tests passed, 0 tests failed out of 498" in 177.81 s,on the merged tree at
da54d350e. Load average 117.09 at the start of that run;free disk 13 G of 447 G at 98%.
No space leftandBFD assertionare 0 inboth the build and ctest logs, with the greps positive-controlled (each returns
1 on a synthetic line carrying the string).
An earlier run of the same tree failed
test_serve_low_tools, which passed solowith exit 0 at 19.74 s. That is the load-dependent flake
#428, not this change.
Not claimed
No render on real weights.
dgx.casais contended and OOM-reboots under asecond job, and a sampler is exactly the thing CPU goldens gate well; the
real-checkpoint HQ render is owed under the spec's
## Owed. The loop's SDEnoise is drawn from this port's
SplitMixGaussianrather than upstream's seededtorch.randn, so a render is not bit-comparable with Lightricks' — the samelimit the shipped ancestral arm already carries.
legacy_mode=Falseis notbuilt: nothing upstream reaches it from any pipeline in scope, so a selection
surface for it would be invented here.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]