Skip to content

LTX-2.5 text-tower Linear is a scalar single-threaded double-accumulating loop; dominates pre-generation wall and reads as a hang #1208

Description

@localai-bot

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions