Skip to content

feat(LTX25-RES2S-LOOP): the res_2s loop, its second evaluation, and the phi that must not be fixed (#921) - #1101

Closed
localai-bot wants to merge 4 commits into
mainfrom
row/LTX25-RES2S-LOOP
Closed

feat(LTX25-RES2S-LOOP): the res_2s loop, its second evaluation, and the phi that must not be fixed (#921)#1101
localai-bot wants to merge 4 commits into
mainfrom
row/LTX25-RES2S-LOOP

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

Closes #921. Row
LTX25-RES2S-LOOP, spec
ltx25-res2s-loop.md,
committed in de8bd2b2c before any implementation code.

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 high-quality 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,
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 fd4ded7f rather than inherited from the
issue.

The loop runs on its own defaults. DiffusionStage.__call__ hands it 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 contradict the
already-shipped ancestral arm: noise_seed is the constant -1 rather than the
request's seed, so the res_2s SDE injections do not depend on the seed at all;
and new_noise_fn is _get_new_noise, which NORMALIZES, against the ancestral
loop's _get_plain_noise ten lines away in the same file.

phi 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 Taylor series near zero, which is the numerically better
implementation, returns 0.5 at both and diverges from the model's own runtime.
The goldens pin the cancelled values with ==, not a tolerance, 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.

Evidence

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. Only the denoiser, the noise draw
(torch.randn, whose stream this port does not have) and two media-IO modules
are 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 code
captured directly, and the doctest case and assertion counts.

Four survivors were real holes and are closed. The loop tolerance was this
file's kRoundOff of 5e-6 and could not see the precision split, so it is now
one ulp. No fixture ran at eta != 0.5, so the pinned substep eta was invisible,
and an Eta1 fixture generated from upstream now separates them. The engine's
call to the noise normalization was gated by nothing, because normalization
changes no count, so Ltx2ConditioningTrace::res2s_noise_moment_error now
observes 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.03 cannot be reached through a float32 schedule on
either side, because 0.03f widens to 0.029999999329447746; the test comment
that claimed to pin it has been replaced by that derivation. And the
h * a21 association 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.
M6a through M6d each removed one of two idempotent normalize calls.
M6b ran a truncated -tc filter that matched ZERO cases and printed
SUCCESS! with exit 0 over a clean build and a correct diff, 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 Ltx2VideoEngine::Generate
to the phase loop's kRes2s dispatch. Deleting that dispatch reds the
end-to-end case (exit 1, 6 failed assertions). --pipeline-kind passes the
string through with no allowlist (examples/ltx2_gen/main.cpp:239), so
ltx2-gen, the C ABI and --video-extra pipeline_kind=res2s_two_stage on the
server all reach the arm.

The engine's per-evaluation body is hoisted into one Evaluate lambda that both
samplers 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_evaluations is a single increment no arm
can bypass.

Gate

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DVLLM_CPP_CUDA=OFF

CONFIGURE_EXIT=0, BUILD_EXIT=0, : error: count 0, ctest -N 498,
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 left and BFD assertion are 0 in
both 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 solo
with exit 0 at 19.74 s. That is the load-dependent flake
#428, not this change.

Not claimed

No render on real weights. dgx.casa is contended and OOM-reboots under a
second 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 SDE
noise is drawn from this port's SplitMixGaussian rather than upstream's seeded
torch.randn, so a render is not bit-comparable with Lightricks' — the same
limit the shipped ancestral arm already carries. legacy_mode=False is not
built: 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]

mudler added 4 commits August 17, 2026 01:27
… 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]
@localai-bot

Copy link
Copy Markdown
Collaborator Author

Superseded by #1125.

This branch carried da54d350e161, a git merge origin/main commit with a bare subject and no body. scripts/check-commit-trailers.py:336 builds its range with rev-list and no --no-merges, so merge commits need the block too — it reported three findings on that one commit, and commit-protocol-tag was red at every head.

It could not be repaired in place: da54d350e161 is a first-parent ancestor of every candidate head, so rev-list includes it whatever the merge base becomes. Fixing it means rewriting history, which means a force-push, which this project does not do. The fresh implementer correctly stopped rather than improvising, and left the call to the operator.

#1125 is the same tree — verified: git rev-parse HEAD^{tree} on the new branch equals the tree of 3307b6d96, so nothing was lost in the recreation — collapsed to one commit with the block intact, then merged with 589abad12 (#1115) which landed during the recreation.

check-commit-trailers now exits 0 on the new branch, with this branch's head exiting 1 as the armed control. Full gate on the merged tree: CONFIGURE_EXIT=0, BUILD_EXIT=0, 0 compile errors, 504 targets linked, ctest -N 502, 502 of 502 passed, CTEST_EXIT=0, with No space left and BFD both 0 against positive controls that return 1.

All the review's substance rode across unchanged: the res_2s arm routed through Ltx2GuidedDenoise rather than resolved textually, the dit_forwards counter that makes an unguided HQ arm RED, the substep-latent survivor closed, the tautological VT_CHECK replaced with the trace delta, and both generator scripts committed.

localai-bot added a commit that referenced this pull request Aug 17, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LTX-2.5: the res_2s DENOISING LOOP is unported, so TI2VidTwoStagesHQPipeline cannot be served — only its per-step arithmetic exists

2 participants