Ltx2TextFeatureExtractorForward's Linear is a scalar, single-threaded, double-accumulating triple loop. On the full LTX-2.5 text tower's caption projection it dominates wall time before generation starts, and it is the reason a full-model render appears to hang between "engine loaded" and the first denoise step.
Where
src/vllm/model_executor/models/ltx2_text_encoder.cpp:60-71:
for (int64_t r = 0; r < rows; ++r) {
const float* xr = x.data() + r * w.in_features;
for (int64_t o = 0; o < w.out_features; ++o) {
const float* wr = w.weight.data() + o * w.in_features;
double acc = has_bias ? static_cast<double>(w.bias[o]) : 0.0;
for (int64_t i = 0; i < w.in_features; ++i)
acc += static_cast<double>(xr[i]) * static_cast<double>(wr[i]);
out[r * w.out_features + o] = static_cast<float>(acc);
}
}
One thread, no blocking, no SIMD. The accumulator is double and both operands are widened per multiply, so it cannot vectorise into the f32 FMA path even if the compiler tried.
Observed
On dgx (GB10, 20 cores) running one_stage on the full/dev transformer: after "engine loaded", resident memory went flat at t≈150 s and stayed byte-identical for 1073 s with no progress output, in a phase whose stacks land in the text feature extractor. ltx2-gen prints nothing between load and completion, so this presents to a user as a hang rather than as slow arithmetic.
The caption projection is the large one (in_features on the order of 1.9e5), and cost here is rows * out_features * in_features — the shape where a scalar loop is least affordable.
Two separate problems
1. Execution strategy. This is a plain GEMM. It should route through the existing vt:: GEMM seam like every other projection in the tree, not be hand-rolled. Same defect class as #1202 (Ltx2FuseLoraIntoTensor), and the two together are most of the pre-generation wall on a full-model render.
2. The double accumulator is not a mirror of upstream. torch.nn.functional.linear on f32 inputs accumulates in f32, not f64. The comment directly above this function cites F.linear as the reference, so widening the accumulator diverges from the oracle it names. It is not obviously wrong — wider is usually closer to exact — but it means this path cannot be bit-compared against upstream, and it hides reduction-order differences that a f32 accumulator would expose. Whichever way it is resolved, it should be a stated decision with a reason beside it, per the dtype-polarity rule in AGENTS.md.
Suggested resolution
Route through the vt:: GEMM seam, and decide the accumulator question explicitly against the pinned oracle. If f64 is kept deliberately, annotate it with the reason; if it moves to f32, the existing goldens will say whether anything shifts.
Blast radius
Every LTX-2.5 pipeline kind goes through the text tower, so this is on the critical path of all of them, including the only full-model arm that currently runs (one_stage).
Ltx2TextFeatureExtractorForward'sLinearis a scalar, single-threaded, double-accumulating triple loop. On the full LTX-2.5 text tower's caption projection it dominates wall time before generation starts, and it is the reason a full-model render appears to hang between "engine loaded" and the first denoise step.Where
src/vllm/model_executor/models/ltx2_text_encoder.cpp:60-71:One thread, no blocking, no SIMD. The accumulator is
doubleand both operands are widened per multiply, so it cannot vectorise into the f32 FMA path even if the compiler tried.Observed
On
dgx(GB10, 20 cores) runningone_stageon the full/dev transformer: after "engine loaded", resident memory went flat at t≈150 s and stayed byte-identical for 1073 s with no progress output, in a phase whose stacks land in the text feature extractor.ltx2-genprints nothing between load and completion, so this presents to a user as a hang rather than as slow arithmetic.The caption projection is the large one (in_features on the order of 1.9e5), and cost here is
rows * out_features * in_features— the shape where a scalar loop is least affordable.Two separate problems
1. Execution strategy. This is a plain GEMM. It should route through the existing
vt::GEMM seam like every other projection in the tree, not be hand-rolled. Same defect class as #1202 (Ltx2FuseLoraIntoTensor), and the two together are most of the pre-generation wall on a full-model render.2. The
doubleaccumulator is not a mirror of upstream.torch.nn.functional.linearon f32 inputs accumulates in f32, not f64. The comment directly above this function citesF.linearas the reference, so widening the accumulator diverges from the oracle it names. It is not obviously wrong — wider is usually closer to exact — but it means this path cannot be bit-compared against upstream, and it hides reduction-order differences that a f32 accumulator would expose. Whichever way it is resolved, it should be a stated decision with a reason beside it, per the dtype-polarity rule in AGENTS.md.Suggested resolution
Route through the
vt::GEMM seam, and decide the accumulator question explicitly against the pinned oracle. If f64 is kept deliberately, annotate it with the reason; if it moves to f32, the existing goldens will say whether anything shifts.Blast radius
Every LTX-2.5 pipeline kind goes through the text tower, so this is on the critical path of all of them, including the only full-model arm that currently runs (
one_stage).