Skip to content

feat(LTX25-GUIDED-VIDEO): the guided video denoiser, and the sentence in ltx2.h that was wrong (#1092) - #1102

Merged
localai-bot merged 9 commits into
mainfrom
row/LTX25-GUIDED-VIDEO
Aug 17, 2026
Merged

feat(LTX25-GUIDED-VIDEO): the guided video denoiser, and the sentence in ltx2.h that was wrong (#1092)#1102
localai-bot merged 9 commits into
mainfrom
row/LTX25-GUIDED-VIDEO

Conversation

@localai-bot

@localai-bot localai-bot commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

The LTX-2.5 video denoise loop ran ONE unguided forward per step. Every recipe
resolved a video_guidance that nothing read, so a pipeline_kind = one_stage
render ignored cfg_scale = 3.0, stg_scale = 1.0, rescale_scale = 0.7 and
modality_scale = 3.0 and denoised along a different trajectory than
ti2vid_one_stage.py:221-226 @ fd4ded7f, which builds a
FactoryGuidedDenoiser from exactly those.

Issue #1092. Spec
.agents/specs/ltx25-guided-video.md,
committed at 36510ec2d, before any code.

_guided_denoise (ltx-pipelines utils/denoisers.py:61-211) is now ported, in
its own translation unit because upstream has its own file. It assembles the four
passes -- cond, uncond, perturbed, isolated-modality -- from the UNION of what
the two guiders want, runs each through the caller's X0Model, and combines each
modality with its own guider over the same splits. Four unported pipelines
(a2vid_two_stage.py:230, ti2vid_two_stages.py:248,
ti2vid_two_stages_hq.py:271, keyframe_interpolation.py:232) were each blocked
on this one piece; none is any more.

The conversion lives in the caller's lambda, not in the seam

DiffusionStage never hands the loop a velocity model: it hands
X0Model(builder.build(...)) (utils/blocks.py:480-482, forward at
model.py:590-604). The seam therefore takes a callable and can only ever
receive denoised tensors. Combining velocities and converting once afterwards is
a different function wherever rescale_scale != 0, which is 0.7 on every video
row of the params table. That defect shipped on the audio arm of this tree and is
#1039.

post_process_latent stays OUT of the denoiser. Upstream applies it in the LOOP,
to the guider's result (utils/samplers.py:35, :484), and the difference is
not cosmetic: the rescale is a scalar over the whole tensor, so it multiplies the
conditioned tokens too, and the after-guider application is what pins them back
to clean.

The NOT-PORTED note in ltx2.h was wrong, not stale

It refused SKIP_A2V_CROSS_ATTN and SKIP_V2A_CROSS_ATTN because "nothing
upstream that this port serves constructs them -- STG is built from stg_blocks
and reaches the SELF-attention types alone". STG does. The isolated-modality pass
does not: _guided_denoise builds BOTH cross types with blocks=None whenever
either guider has modality_scale != 1.0 (denoisers.py:125-138), and every
VIDEO row of the params table sets it to 3.0. The sentence was true of
text-to-audio, which pins the field to 1.0 (t2a_one_stage.py:202), and it
was written while text-to-audio was the only guided path here. Both directions
are now cross_attn_skip_all booleans on Ltx2DitPerturbation, gating the A2V
and V2A branches exactly as transformer.py:335 and :367 do.

The same shape appears once more and is corrected the same way. negative_prompt
and the five audio_* guider knobs were refused on every non-t2a engine, on the
same reading of upstream. default_1_stage_arg_parser carries the whole audio
guider row beside the video one (utils/args.py:947-1066, the audio row opening at :1008) and
TI2VidOneStagePipeline consumes both (ti2vid_one_stage.py:211-218). That
refusal is gone; the direction that survives refuses a knob describing a PICTURE
on a pipeline that renders none. The case that asserted the old behaviour is
rewritten rather than deleted, because "this used to be refused" is what a later
reader needs.

What a caller gains, and what is refused

Seven per-generation extras mirror default_1_stage_arg_parser, reaching
ltx2-gen and the C ABI: --video-cfg-guidance-scale,
--video-stg-guidance-scale, --video-rescale-scale, --video-skip-step,
--video-stg-blocks, --a2v-guidance-scale, --v2a-guidance-scale. Every one is
refused whole on a phase whose recipe sets allow_guidance_override = false --
the distilled and retake recipes, whose guidance is distilled into the weights.
That field had never been read.

The unconditional forward needs a negative conditioning. With a tower it is the
second half of the encode GenerateAudioOnly already performed and discarded;
without one, negative_prompt_embeds_path and
negative_audio_prompt_embeds_path are the negative half of the existing embeds
fallback. That pair is a LOCAL ADAPTATION and is recorded as one in the spec:
upstream has no embeds surface at all. With neither, a cfg_scale other than 1.0
is refused by name rather than served the positive context twice, which would
leave the whole classifier-free term at exactly zero.

Two further refusals exist because the alternative renders. An stg_blocks list
naming no block this checkpoint has would perturb nothing under upstream's
membership test, leaving stg_scale * (cond - perturbed) at zero. And the
perturbed or isolated-modality pass on the device arm cannot run at all:
Ltx2DitForwardDevice takes no perturbations argument, so it is refused by
name rather than served an unperturbed forward. That arm is OWED and the spec
lists it under ## Owed.

What is unchanged

distilled_two_stage, dfr, retake and dmd2 keep the guider they always
had, which is Ltx2MultiModalGuiderParams's own default construction and is
upstream's _POSITIVE_ONLY_GUIDER (denoisers.py:25-28). They issue one forward
per step through the new seam, and no golden in the suite moved.

Reachability

Entry point: vllm_video_generate -> VideoEngine::Generate on an engine loaded
with pipeline_kind = one_stage, a documented value of a documented load extra
that needs no other flag. Every gate case enters there; nothing constructs a
guider, a DiT, a modality or a perturbation by hand.

Deleting the production call site -- the Ltx2GuidedDenoise line in the phase
loop, replaced by the single unguided forward this change removes -- turns the
suite RED at 3 failed cases and 13 failed assertions (M11 below).

The gate

test_ltx2_video: 71 cases, 2145 assertions, exit 0.

It asserts x0 == latent - sigma*velocity per token on ALL FOUR arms, with the
PER-TOKEN sigma rather than the schedule scalar: exact in x0 space, off by the
whole sample in velocity space. Non-vacuity is stated twice, once for a zero
sample and once per arm for a zero velocity. Each arm is also asserted to DIFFER
from the conditional arm, so a pass whose context or perturbation never reached
the forward fails rather than passing as a perfectly converted copy.

It then replays Ltx2MultiModalGuidance over the recorded arms, and again over
arms REBUILT FROM THE RAW VELOCITIES, and recovers the Euler step's input from
the latent the sampler wrote. A seam-level control puts the two spaces apart only
at a non-zero rescale, with the modality term present, which the T2A control
could not carry.

A second case runs the same guided configuration WITH an image conditioning,
because post_process_latent is a literal no-op without one.

Mutations

Twelve, each reporting three facts, because two of them are how a mutation lies:
that it applied (git diff --stat), that it BUILT (compile-error count), and the
exit code captured directly rather than after a pipe.

# Mutation Applied Built Exit Result
M1 cond pass left in VELOCITY space 1 file, +1 yes, 0 errors 1 RED
M2 uncond pass left in VELOCITY space 1 file, +1 yes, 0 errors 1 RED
M3 perturbed pass left in VELOCITY space 1 file, +1 yes, 0 errors 1 RED
M4 modality pass left in VELOCITY space 1 file, +1 yes, 0 errors 1 RED
M5 second ToDenoised BELOW the step-0 record 1 file, +3/-1 yes, 0 errors 1 RED
M6 second ToDenoised ABOVE the step-0 record 1 file, +3/-1 yes, 0 errors 1 RED
M7 uncond pass given the POSITIVE context 1 file, +1/-3 yes, 0 errors 1 RED
M8 modality pass given NO cross-attn perturbation 1 file, +2/-2 yes, 0 errors 1 RED
M9 video self-attn perturbation dropped in the DiT 1 file, +1/-1 yes, 0 errors 1 RED
M10a post_process_latent ADDED per arm 1 file, +3/-2 yes, 0 errors 0 GREEN, an IDENTITY -- see below
M10b post_process_latent MOVED per arm 1 file, +4/-3 yes, 0 errors 1 RED
M11 REACHABILITY: the Ltx2GuidedDenoise call site deleted 1 file, +12/-1 yes, 0 errors 1 RED (3 cases, 13 assertions)

Every restore was verified with a scoped git diff --stat reporting clean, and
every restored file had its mtime bumped before the rebuild, because a restored
file with an older mtime lets ninja skip and the NEXT measurement runs the
PREVIOUS mutation's binary.

M10a is a no-op, and saying so took a measurement. post_process_latent is
x*mask + clean*(1-mask), so it can only touch a mask-0 token; such a token's
per-token sigma is 0, so X0Model returns latent - 0*v, which is latent; and
a conditioned token's latent IS its clean value. Every arm already equals what
post-processing would write. The first response to the green was to strengthen
the gate -- that is where the rebuilt-from-velocities replay came from -- and
when that did not move it either, the conditioned case was written to state the
identity in an assertion. M10b, the placement that actually changes the render,
is RED against it.

Gate numbers

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DVLLM_CPP_CUDA=OFF
cmake --build build -j6 && ctest --test-dir build -j4 --output-on-failure
head 420f6b4741ef8b5faae0874ccfb2d622d7c4a7d6, remote-verified with git ls-remote
CONFIGURE_EXIT 0
BUILD_EXIT 0, : error: count 0
ctest -N Total Tests: 498
CTEST_EXIT 0
result 100% tests passed, 0 failed out of 498; 2 skipped (test_modelopt_mixed_precision_checkpoint, test_voxtral_e2e)
No space left / BFD 0 in both logs; positive control on the same file, Passed matches 496 lines
disk 13 G free, 98% used
load 1.63 at start, 38.57 at end (another agent building)

What this does NOT claim

No number here comes from a running oracle. vLLM-Omni is UNPINNED
(#633) and carries no LTX-2.5
recipe; no LTX-2.5 checkpoint on this host has a recorded sha256
(#1048). The guidance is gated
against upstream SOURCE at fd4ded7f, not upstream OUTPUT, and the spec's
honesty statement says so.

#1049 is PARTLY retired:
Ltx2BatchedPerturbationConfig now has a product caller. Ltx2Guidance,
Ltx2CfgDelta and Ltx2StgDelta stay dead, and the spec argues why routing
through them would mean inventing a kind dispatch upstream does not have rather
than closing a wiring gap.

The fresh review, and what it moved

The review returned CHANGES REQUESTED on 420f6b474: one blocking finding and
ten non-blocking. It reproduced the core exactly -- the seam, the x0 space, all
four arms, the reachability, the gate -- and independently confirmed the live
defect: at the merge base video_guidance had exactly two hits, a declaration
and a write with no reader, against a positive control where audio_guidance
finds its T2A consumer. None of that is revisited below.

B1, the blocking one: the two cross booleans were gated together

Ltx2DitPerturbation's two flags reached Ltx2BlockArgs and the gate could not
tell which of them the DiT applied. Three mutations were GREEN over the whole
binary: the DiT ignoring video_cross_attn_skip_all (M12), ignoring
audio_cross_attn_skip_all (M13), and swapping which flag gates which
direction (M15). Only ignoring both (M14) was caught. A build that applies one
direction, or applies both to the wrong ones, renders -- on the DEFAULT video
arm, whose modality_scale is 3.0 -- with the isolated-modality term half wrong.

Two things made the shipped-path case blind to it. The end-to-end
MaxAbsDiffOf(video_first_modality, video_first_cond) still fires with one
direction applied, because the modality pass still differs from cond. And
Ltx2ConditioningTrace::video_modality_skipped_{a2v,v2a} is assigned from the
perturbation struct the seam built (ltx2_denoisers.cpp:315-316), so it
records what was handed over and nothing about what the DiT did with it -- while
its message claimed the latter. That message is corrected rather than left to
overstate what it measures.

The repair is test-only, and the separation comes from upstream's own
predicates.
run_a2v needs the VIDEO stream enabled and the audio stream
merely PRESENT; run_v2a needs the reverse (transformer.py:265-269). So a
forward with audio->enabled = false runs A2V alone, and one with
video->enabled = false runs V2A alone -- the configuration ltx2.h already
documents as rendering rather than failing. Each row asserts BOTH halves: the
flag for that direction MOVES the stream it writes, and the flag for the other
direction leaves it BIT-IDENTICAL. The second half is what detects the swap.
Doing this on a both-enabled forward is impossible on this fixture, because block
1's V2A reads what block 0's A2V wrote.

ltx2.h's disclosure that called M15 untestable is retired. Its premise was
true -- both directions are off together on the shipped path -- and its
conclusion did not follow, because nothing obliges the separating test to use the
shipped combination.

# Mutation of ltx2_dit.cpp git diff --stat Built Exit Result Fails at
M12 DiT ignores video_cross_attn_skip_all 1 file, +1/-1 yes, 0 errors 1 RED moved(a2v_off.video, base.video)
M13 DiT ignores audio_cross_attn_skip_all 1 file, +1/-1 yes, 0 errors 1 RED moved(v2a_off.audio, base.audio)
M14 DiT ignores BOTH 1 file, +2/-2 yes, 0 errors 1 RED both of the above, the both-flags subcase, and the shipped-path mod != cond
M15 DiT SWAPS the two flags 1 file, +2/-2 yes, 0 errors 1 RED all four directional checks, including both "must NOT move" ones

Every run was the WHOLE binary rather than a --test-case filter, because
several case names here contain commas and doctest splits -tc on them; a
truncated filter matches zero cases and prints SUCCESS! with exit 0. Each run
is recorded with its case and assertion counts so a zero-count run cannot pass
for a green one: baseline 72 cases / 2182 assertions / exit 0, and each
mutation ran the same 72 and 2182 with 1, 1, 2 and 1 cases failing respectively.
Every restore was byte-verified and mtime-bumped before the rebuild.

B3, the other finding with product code behind it

The empty-stg_blocks refusal is a real divergence and the recorded reason was
wrong. Measured at fd4ded7f:

Evidence Where
"Set to [] to disable STG", in the same table and idiom as stg_scale -> 0.0 and cfg_scale -> 1.0 ltx-pipelines/docs/multimodal-guidance.md:13
MultiModalGuiderParams.stg_blocks DEFAULTS to [] guiders.py:204
the flags are nargs="*", so [] has a CLI spelling; nargs="+" was the one-character way to forbid it args.py:979-985, :1039-1045, :1107-1113
LTX_2_3_HQ_PARAMS SHIPS stg_blocks=[] on both modalities constants.py:105, :113
no validation of stg_blocks anywhere in that tree whole-tree search, null results recorded

blocks=None means EVERY block and blocks=[] means NO block
(perturbations.py:26-33), and ApplyStgBlocksExtra exists to keep
PRESENT-and-empty distinct from ABSENT -- which the refusal then made
unreachable. Dropped in ApplyGuidanceOverrides, exempted in
check_reaches_a_block. The out-of-range refusal stays, because that is a
request disagreeing with the CHECKPOINT rather than a caller asking for nothing,
and upstream never meets it (48-block checkpoints only).

Upstream does not skip the pass either: do_perturbed_generation reads
stg_scale alone (guiders.py:279-281). The new case asserts the pass RAN,
perturbed no block, and returned cond bit for bit, with a named-block
control beside it so the case is about emptiness rather than about the extra
being read at all.

One sub-claim is REJECTED on evidence. The finding argued that
audio_stg_blocks="" is still accepted on t2a_one_stage because that path
returns before ApplyGuidanceOverrides. It does return there, and the request is
still refused -- by ltx2_t2a.cpp:203-214, which builds the block mask and fails
when no bit is set, and which git log -S puts on main at 0b0b8900f with
#1032, not on this branch. So
there is no asymmetry today: both arms refuse and both diverge from upstream.
Fixing the video half creates one, which is why
#1111 is filed, indexed, and
listed under ## Owed. It is not fixed in flow because it changes a landed row's
gated behaviour and one of its cases.

B6, the anchors

Re-derived against fd4ded7f from the sentence making each claim, never by
reading text out of the cited span. _guided_denoise is 61-211, not 62-207.
enabled = not skip is at 158, 168; 151 and 161 are the = None
initializers. The V2A guard is 367; 366 is blank. The batched config is built
at 182-187; 172-176 is a comment plus the per-sample replication at :175.
The partial blend is 572-573. The one PromptEncoder call is 166-174.
default_1_stage_arg_parser is 930-1067 with its guider flags at
947-1066. The two --*-stg-blocks flags open at 979-985 and
1039-1045. cross_attn_skip_all is DECLARED at transformer_args.py:70;
118 is a call site. modality_scale = 3.0 is at constants.py:54, :64 and
_PARAMS_SINCE_VERSION at 130-133, so the cited 40-80 covered neither.
CFGGuider and STGGuider are 11-27 and 56-74. The perturbations
ARGUMENT is model.py:493; 492 is the def.

No gate protects a spec anchor
(#632), so the 43 replacements
were applied by a script that asserts the expected hit count per edit and refuses
the whole run on a mismatch. Two were caught that way and re-derived.

The rest

Finding Disposition
B2 docs/FEATURES.md still called T2A "the only GUIDED arm", made false by this PR's own row two lines below. Corrected inside the existing cell at 202 of 220 chars; the page's prose-paragraph count is unchanged at 21 of 21, because adding a paragraph there re-reds main for the whole repo (#1055). check-public-doc-tables.py green.
B4 INFO("arm = " << arm.name) printed arm = 1, doctest stringifying a const char* through its bool overload, so M1-M4 produced byte-identical failure context. Wrapped in std::string at all three sites in the file.
B5 The rescale control's modality claim is structurally true and numerically inert. The case now MEASURES it: 4.054e-01 at modality_scale 3.0 against 4.118e-01 at 1.0, asserted to agree within a factor of two. Restated in the case title, the comment and spec 7.2, so a later reader cannot lean on this control for modality coverage -- the modality arm's gate is the per-arm invariant, which M4 turns red.
B7 "the seam cannot be handed a velocity" is caller discipline, not a type guarantee: Ltx2X0Outputs carries the velocity beside the prediction, so a lambda that swaps them compiles and renders. The claim is restated and the code left alone, because dropping the velocity would delete what the invariant is checked against. M1-M4 are the real gate.
B8 origin/main merged in twice (it moved during the repair) and the gate rerun on the merged tree. Spec 1 now carries #1093-#1096 from 281e6a120. .agents/issue-index.md was rebuilt both times by taking origin/main wholesale and re-appending this branch's rows: 320-line prefix byte-identical, 304 rows, 304 distinct ids.
B10 The new docs/USAGE.md section gains the /v1/videos caveat its two siblings carry, a flag-to-extra table with the raw key spellings an ABI caller needs, the audio row's spellings, the load-extra status of the two negative-embeds keys, and the empty-list behaviour B3 decided. The rows are placed in that section rather than in the retake-scoped table at :3128, where they would be filed under the wrong pipeline.
B9, B11 recorded by the reviewer as not this repair's.

Gate numbers, on the merged head

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DVLLM_CPP_CUDA=OFF
cmake --build build -j6 && ctest --test-dir build -j4 --output-on-failure
head 8e0f19650dffa5b4daff6ec16ca9e27a49dc8508, remote-verified with git ls-remote
CONFIGURE_EXIT 0
BUILD_EXIT 0, : error: count 0
ctest -N Total Tests: 499
CTEST_EXIT 0
result 100% tests passed, 0 failed out of 499; 2 skipped (test_modelopt_mixed_precision_checkpoint, test_voxtral_e2e)
an earlier run of the same gate at 09aee8cc2, under load 134-168 from other agents on the box, test_serve_low_tools (#428) and test_engine_core_proc (#1052) failed and both Passed on a serial re-run at load 71, exit 0. Both are green in the run above
test_ltx2_video Passed, 204.30 s; standalone 72 cases / 2182 assertions / exit 0
No space left / BFD 0 in both logs, with positive controls on the same files: 495 Linking lines in the build log, 497 Passed lines in the ctest log
disk 68 G free, 85% used at the gate; it reached 23 M free / 100% earlier in this session and check-test-registration failed with "Cannot open file for write" plus CMake's misreported "Inappropriate ioctl for device" -- an ENOSPC wearing a verdict about the code, confirmed by dd writing 22 of 64 MB and by the same gate passing once space returned
load 9.58 at the gate's start, 58.34 at the end (other agents on the box)

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]

mudler added 8 commits August 17, 2026 01:36
…r pipelines wait on the same seam (#1092)

`Ltx2PhaseRecipe::video_guidance` is set by every recipe and read by nothing, so
a `pipeline_kind = one_stage` render ignores the `cfg_scale = 3.0`,
`stg_scale = 1.0`, `rescale_scale = 0.7` and `modality_scale = 3.0` its own
recipe resolved. Upstream's `ti2vid_one_stage.py:221-226` @ `fd4ded7f` builds a
`FactoryGuidedDenoiser` from exactly those.

The spec commits before the implementation on the same pull request, which is
this repository's default shape. It records three missing parts rather than one:
the denoiser itself (`utils/denoisers.py:62-207`), the negative conditioning the
video path never encodes, and the `SKIP_A2V_CROSS_ATTN` / `SKIP_V2A_CROSS_ATTN`
perturbations without which `modality_scale = 3.0` — the default on every video
row — cannot run. `ltx2.h:41-49` records those last two as not ported because
"nothing upstream that this port serves constructs them"; that sentence was true
for text-to-audio, which pins `modality_scale = 1.0`, and is false for every
video pipeline.

It also states what the row cannot claim: there is no oracle run behind any
number in it. vLLM-Omni is UNPINNED (#633) and carries no LTX-2.5 recipe, and no
LTX-2.5 checkpoint here has a recorded sha256 (#1048), so the guidance
arithmetic is gated against upstream SOURCE and not upstream OUTPUT.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
… in ltx2.h that was wrong (#1092)

The LTX-2.5 video denoise loop ran ONE unguided forward per step. Every recipe
resolved a `video_guidance` that nothing read, so a `pipeline_kind = one_stage`
render ignored `cfg_scale = 3.0`, `stg_scale = 1.0`, `rescale_scale = 0.7` and
`modality_scale = 3.0` and denoised along a different trajectory than
`ti2vid_one_stage.py:221-226` @ `fd4ded7f`, which builds a
`FactoryGuidedDenoiser` from exactly those.

`_guided_denoise` (`ltx-pipelines utils/denoisers.py:62-207`) is now ported, in
its own translation unit because upstream has its own file. It assembles the
four passes -- cond, uncond, perturbed, isolated-modality -- from the UNION of
what the two guiders want, runs each through the caller's `X0Model`, and
combines each modality with its own guider over the same splits. Four unported
pipelines (`a2vid_two_stage.py:230`, `ti2vid_two_stages.py:248`,
`ti2vid_two_stages_hq.py:271`, `keyframe_interpolation.py:232`) were each
blocked on this one piece.

## The conversion lives in the caller's lambda, not in the seam

`DiffusionStage` never hands the loop a velocity model: it hands
`X0Model(builder.build(...))` (`utils/blocks.py:480-482`, forward at
`model.py:590-604`). So the seam takes a callable and can only ever receive
denoised tensors. Combining velocities and converting once afterwards is a
different function wherever `rescale_scale != 0`, which is 0.7 on every video
row of the params table. That defect shipped on the audio arm of this tree and
is #1039.

`post_process_latent` therefore stays OUT of the denoiser. Upstream applies it in
the LOOP, to the guider's result (`utils/samplers.py:35`, `:484`). Applying it
per arm would make every arm agree on exactly the conditioned tokens, which
zeroes the guidance delta precisely where a keyframe or a reference clip is
conditioning.

## The NOT-PORTED note in ltx2.h was wrong, not stale

It refused `SKIP_A2V_CROSS_ATTN` and `SKIP_V2A_CROSS_ATTN` because "nothing
upstream that this port serves constructs them -- STG is built from `stg_blocks`
and reaches the SELF-attention types alone". STG does. The isolated-modality pass
does not: `_guided_denoise` builds BOTH cross types with `blocks=None` whenever
either guider has `modality_scale != 1.0` (`denoisers.py:121-137`), and every
VIDEO row of the params table sets it to 3.0. The sentence was true of
text-to-audio, which pins the field to 1.0 (`t2a_one_stage.py:200-202`), and it
was written while text-to-audio was the only guided path here. Both directions
are now `cross_attn_skip_all` booleans on `Ltx2DitPerturbation`, gating the A2V
and V2A branches exactly as `transformer.py:335` and `:366` do.

The same shape appears once more, and is corrected the same way: `negative_prompt`
and the five `audio_*` guider knobs were refused on every non-t2a engine, on the
same reading. `default_1_stage_arg_parser` carries the whole audio guider row
beside the video one (`utils/args.py:1011-1075`) and
`TI2VidOneStagePipeline` consumes both. That refusal is gone; the direction that
survives is the one that refuses a knob describing a PICTURE on a pipeline that
renders none.

## What a caller gains, and what is refused

Seven per-generation extras mirror `default_1_stage_arg_parser`, reaching
`ltx2-gen` and the C ABI. Every one is refused whole on a phase whose recipe sets
`allow_guidance_override = false` -- the distilled and retake recipes, whose
guidance is distilled into the weights. That field had never been read before.

The unconditional forward needs a negative conditioning. With a tower it is the
second half of the encode `GenerateAudioOnly` already performed and discarded;
without one, `negative_prompt_embeds_path` and
`negative_audio_prompt_embeds_path` are the negative half of the existing embeds
fallback, and are recorded as a local adaptation rather than an upstream
surface. With neither, a `cfg_scale` other than 1.0 is refused by name rather
than served the positive context twice, which would leave the whole
classifier-free term at exactly zero.

Two more refusals exist because the alternative renders: an `stg_blocks` list
that names no block this checkpoint has (upstream's membership test would perturb
nothing, leaving `stg_scale * (cond - perturbed)` at zero), and the perturbed or
isolated-modality pass on the device arm, where `Ltx2DitForwardDevice` takes no
`perturbations` argument.

## What is unchanged

`distilled_two_stage`, `dfr`, `retake` and `dmd2` keep the guider they always
had, which is `Ltx2MultiModalGuiderParams`'s own default construction and is
upstream's `_POSITIVE_ONLY_GUIDER` (`denoisers.py:25-28`). They issue one forward
per step through the new seam and their goldens did not move.

## Evidence

Spec [`.agents/specs/ltx25-guided-video.md`](.agents/specs/ltx25-guided-video.md),
committed at `36510ec2d`, before this change.

The gate asserts `x0 == latent - sigma*velocity` per token on ALL FOUR arms --
exact in x0 space, off by the whole sample in velocity space -- with the
non-vacuity guard stated twice, once for a zero sample and once per arm for a
zero velocity. It then replays `Ltx2MultiModalGuidance` over the recorded arms,
and again over arms REBUILT FROM THE RAW VELOCITIES, which is the only check that
can see something applied to every arm alike. It recovers the Euler step's input
from the latent the sampler wrote. A seam-level control puts the two spaces
1.5e-07 apart at `rescale_scale = 0` and far apart at 0.7, with the modality term
present, which the T2A control could not carry.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
`origin/main` advanced to `0bac476b7` while this row was in progress. Two record
surfaces touched both sides and each was verified by hand rather than taken from
the driver.

`.agents/issue-index.md` is append-only under `merge=union`: the merged file's
first 309 lines are BYTE-IDENTICAL to `origin/main`'s whole file, it carries
exactly one extra line -- this row's `#1092` -- appended at the end, and its 292
row keys are unique.

`docs/FEATURES.md` took main's MUSIC3 edit unchanged; this row's three LTX-2.5
rows sit elsewhere in the same table and neither side removed a row.

No source file was touched by both sides.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
…ity, and proving it took a conditioned render (#1092)

The mutation pass on this row ran twelve mutations. Eleven went RED. The twelfth
-- `post_process_latent` ADDED to each guidance arm, with the after-guider
application left in place -- stayed green at 71 cases / 2145 assertions / exit 0,
and this commit is what came of chasing it.

The first reading was that the gate could not see it, so the gate grew a check
that replays `Ltx2MultiModalGuidance` over arms REBUILT from `latent` and the
four RAW velocities. That replay is independent of anything applied to every arm
alike, which is exactly the shape of the mutation. It did not move the green
either, and that was the clue.

The second reading is that the mutation changes nothing. `post_process_latent` is
`x*mask + clean*(1-mask)` (`utils/helpers.py:462-464`), so it can only touch a
token whose denoise mask is 0. Such a token's per-token sigma is 0
(`timesteps_from_mask`, `:494-503`), so `X0Model` returns `latent - 0*v`, which
is `latent`; and a conditioned token's `latent` IS its clean value, which the
conditioner writes and the Euler step preserves. Every arm already equals what
post-processing would write.

## The tempting next sentence is false, and that is the test this adds

"So it does not matter where it is applied" does not follow. The guider's rescale
(`guiders.py:268-271`) is a scalar over the WHOLE tensor, so it multiplies the
conditioned tokens too: every guidance term is zero on a token where all four
arms agree, leaving `pred = latent * factor` there. The after-guider
`post_process_latent` is what pins those tokens back to `clean`. Remove it, or
move it down into the denoiser, and the conditioned tokens leave the step scaled
by a number nobody asked for, on a render that finishes.

The unconditioned case could not see any of this: with every mask entry at 1,
`post_process_latent` is a literal no-op and every placement of it passes. So
this adds a CONDITIONED `one_stage` case, on the same guided configuration with
an image conditioning, which asserts both halves -- that no arm was touched, and
that the guider's result was, on exactly the mask-0 tokens -- behind a positive
control that the post-process moved something at all.

MEASURED: the MOVE mutation is RED against it (71 cases / 2133 assertions /
exit 1, 0 compile errors, `1 file changed, 4 insertions(+), 3 deletions(-)`). The
ADD mutation stays green, and the arm assertion now states why in an executable
form rather than leaving a green unexplained.

## And one guard at the call rather than at the recipe

`Ltx2DitForwardDevice` has no `perturbations` parameter, so the argument list
silently drops one. The pre-loop refusal is a statement about the RECIPE; this
adds the statement about the CALL, in the `X0Model` lambda itself, because the
two are not the same check and a pass that reached the device arm with a
perturbation would leave the STG and modality terms at exactly zero with nothing
in the frames, the shapes or the counts to show for it.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
Bring the branch up to origin/main at 281e6a1 before re-gating the
review repairs. Both sides touch docs/FEATURES.md and docs/USAGE.md, so a
clean merge-tree is not a built one and the full gate is rerun on the
merged tree.

.agents/issue-index.md is a keyed append-only record, so the auto-merge
result was discarded: origin/main's version was taken wholesale and this
branch's single row re-appended. The 319-line prefix is byte-identical to
origin/main, 302 rows carry no duplicate issue id, and this branch's row
is byte-identical to the one it committed.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
… three mutations proved it (#1092)

The fresh review returned CHANGES REQUESTED on `420f6b474`: one blocking finding
and ten non-blocking ones. It reproduced the core of the row exactly — the seam,
the x0 space, all four arms, the reachability, the gate — and independently
confirmed the live defect the row fixes, so none of that is revisited here.

B1, THE BLOCKING ONE. `Ltx2DitPerturbation`'s two cross booleans were gated
together and not per direction. Three mutations of `ltx2_dit.cpp`, each built
clean with zero compile errors and each exit status captured directly, were GREEN
over the whole `test_ltx2_video` binary: the DiT ignoring
`video_cross_attn_skip_all` (M12), ignoring `audio_cross_attn_skip_all` (M13),
and SWAPPING which flag gates which direction (M15). Only ignoring BOTH (M14) was
caught. A build that applies exactly one direction, or applies both to the wrong
ones, renders — on the DEFAULT video arm, whose `modality_scale` is 3.0 — with
the isolated-modality term half wrong.

Two things made the shipped-path case blind to it. The end-to-end
`MaxAbsDiffOf(video_first_modality, video_first_cond)` still fires with one
direction applied, because the modality pass still differs from `cond`. And
`Ltx2ConditioningTrace::video_modality_skipped_{a2v,v2a}` is assigned from the
perturbation struct THE SEAM BUILT (`ltx2_denoisers.cpp:315-316`), so it records
what was handed over and nothing about what the DiT did with it — while its
message claimed the latter. That message is corrected rather than left to
overstate.

The repair is test-only. Separation comes from upstream's own predicates
(`transformer.py:265-269`): `run_a2v` needs the VIDEO stream enabled and the
audio stream merely PRESENT, and `run_v2a` the reverse, so a forward with
`audio->enabled = false` runs A2V alone and one with `video->enabled = false`
runs V2A alone. Each row asserts BOTH halves — the flag for that direction moves
the written stream, and the flag for the other direction leaves it bit-identical
— which is what makes the swap detectable and not only the omission. The
`ltx2.h` disclosure that called M15 untestable is retired: its premise (both
directions are off together on the shipped path) was true and its conclusion did
not follow, because nothing obliges the separating test to use the shipped
combination. M12, M13, M14 and M15 are now all RED.

B3, the other finding with product code behind it. The EMPTY `stg_blocks`
refusal is a real divergence and its recorded reason was wrong. Measured at
Lightricks/LTX-2 `fd4ded7f`: `ltx-pipelines/docs/multimodal-guidance.md:13`
documents "Set to `[]` to disable STG" in the same table and idiom as
`stg_scale` -> 0.0; the field DEFAULTS to `[]` (`guiders.py:204`); the flags are
`nargs="*"` (`args.py:979-985`, `:1039-1045`, `:1107-1113`) so the empty list has
a CLI spelling, and `nargs="+"` was the one-character way to forbid it;
`LTX_2_3_HQ_PARAMS` SHIPS it on both modalities (`constants.py:105`, `:113`); and
a whole-tree search found no validation of `stg_blocks` at all, with the null
results recorded. `blocks=None` means EVERY block and `blocks=[]` means NO block
(`perturbations.py:26-33`), and `ApplyStgBlocksExtra` exists to keep
PRESENT-and-empty distinct from ABSENT — which the refusal then made
unreachable. Dropped in `ApplyGuidanceOverrides` and exempted in
`check_reaches_a_block`; the out-of-range refusal stays, because that is a
request disagreeing with the CHECKPOINT rather than a caller asking for nothing.
Upstream does not skip the pass either (`do_perturbed_generation` reads
`stg_scale` alone, `guiders.py:279-281`), so the new case asserts the pass RAN,
perturbed no block, and returned `cond` bit for bit.

One sub-claim of B3 is REJECTED on evidence. It argued that
`audio_stg_blocks=""` is still accepted on `t2a_one_stage`. That path does return
before `ApplyGuidanceOverrides`, and the request is still refused — by
`ltx2_t2a.cpp:203-214`, which `git log -S` puts on `main` at `0b0b8900f` with
#1032, not on this branch. So there is no asymmetry today; both arms refuse and
both diverge from upstream. Fixing the video half creates one, which is why
#1111 is filed, indexed, and listed under `## Owed` rather than left implied. It
is not fixed in flow because it changes a landed row's gated behaviour and one of
its cases.

B6, the anchors, re-derived against `fd4ded7f` from the sentence making each
claim rather than by reading text out of the cited span. `_guided_denoise` is
61-211 and not 62-207; `enabled = not skip` is at 158 and 168, where 151 and 161
are the `= None` initializers; the V2A guard is 367 and 366 is blank; the batched
config is built at 182-187, where 172-176 is a comment plus the per-sample
replication; the partial blend is 572-573; the one `PromptEncoder` call is
166-174; `default_1_stage_arg_parser` is 930-1067 with its guider flags at
947-1066; `cross_attn_skip_all` is DECLARED at `transformer_args.py:70` and 118
is a call site; `modality_scale = 3.0` is at `constants.py:54, :64` with
`_PARAMS_SINCE_VERSION` at 130-133, and the cited 40-80 covers neither;
`CFGGuider` and `STGGuider` are 11-27 and 56-74; the `perturbations` ARGUMENT is
`model.py:493` and 492 is the `def`. No gate protects a spec anchor (#632), so
the 43 replacements were applied by a script that asserts the expected hit count
per edit and refuses the whole run on a mismatch; two were caught that way and
re-derived.

The rest. B2: `docs/FEATURES.md` still called T2A "the only GUIDED arm", which
this row's own new row two lines below made false — corrected inside the existing
cell at 202 of 220 chars, with the page's prose-paragraph count unchanged at 21
of 21, because adding a paragraph there re-reds `main` for the whole repo
(#1055). B4: `INFO("arm = " << arm.name)` printed `arm = 1`, doctest
stringifying a `const char*` through its bool overload, so a single-arm
regression could not be attributed from the output — wrapped in `std::string` at
all three sites in the file. B5: the rescale control's modality claim is
structurally true and numerically inert, and the case now MEASURES that
(`4.054e-01` at `modality_scale` 3.0 against `4.118e-01` at 1.0) instead of
implying coverage it does not provide; the modality arm's gate is the per-arm
invariant, which M4 turns red. B7: "the seam cannot be handed a velocity" is
caller discipline and not a type guarantee, since `Ltx2X0Outputs` carries the
velocity beside the prediction — the claim is restated and the code left alone,
because dropping the velocity would delete what the invariant is checked
against. B10: the new `docs/USAGE.md` section gains the `/v1/videos` caveat its
two siblings carry, a flag-to-extra table with the raw key spellings an ABI
caller needs, and the empty-list behaviour B3 decided. B8: `origin/main` is
merged in and the gate rerun on the merged tree.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
Second merge on this branch: origin/main advanced to 8fa405b (#595, the
doc-checkpoint feature trigger) while the review repairs were being gated, so the
gate reruns on the merged tree rather than on a tree that only merges cleanly.

.agents/issue-index.md is a keyed append-only record, so the auto-merge result
was discarded again: origin/main's version was taken wholesale and this branch's
two rows re-appended in the order it committed them. The 320-line prefix is
byte-identical to origin/main, and the file carries 304 rows with 304 distinct
issue ids.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
…te measures (#1092)

`docs/FEATURES.md`'s evidence cell for the cross-attention perturbations read
"asserts the isolated-modality pass reached the DiT with both directions off and
moved the tensor", which is exactly the claim the review found overstated: that
assertion is satisfied by a build applying one direction, or the wrong two.

The cell now names what the gate actually does — each direction alone, on a
forward where one stream is PRESENT but DISABLED so only that direction runs, and
with the other flag required to leave the stream bit-identical, which is the half
that detects a swap.

Cell measured at 218 of 220 chars and the page's prose-paragraph count is
unchanged at 21 of 21, so nothing else on the page has to move.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
@localai-bot
localai-bot marked this pull request as ready for review August 17, 2026 08:58
Third merge on this branch: origin/main advanced to 9b3317c while the repair
was being gated. The incoming change is `qwen3_5_weights.cpp` and touches nothing
this row owns, but the trailer and commit-style gates decline to report at all on
a branch that is behind, so the merge is what makes them say something about this
tree.

.agents/issue-index.md is a keyed append-only record, so origin/main's version
was taken wholesale again and this branch's two rows re-appended in commit order.
The prefix is byte-identical to origin/main and the file carries 305 rows with
305 distinct issue ids.

FOLLOWING_AGENTS_PROTOCOL

Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]
@localai-bot
localai-bot merged commit daeff67 into main Aug 17, 2026
21 of 27 checks passed
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.

2 participants