Skip to content

Commit c43b5fa

Browse files
committed
merge: L10 into the campaign -- the last row, and 8 wrong anchors it swept out
Reviewed PASS at 131328e after three FAIL rounds. The last one is the reason to read this: told to fix ONE wrong file:line, the implementer swept every citation the branch adds -- 19 of them -- and found EIGHT wrong at six sites. Fixing the instance would have left seven. Three of the eight are one anchor repeated. modeling_rope_utils.py:187-245 was cited three times as the authority for that function's ZERO PADDING, which is at :246 -- one line past the cited end, so the range stops inside the torch.cat( argument list immediately before the argument that IS the whole claim. Our own gemma4.cpp:300-312 has the identical shape: it closes on `} else {`, leaving the vt::CastF32 pair at :313-314 that the sentence charges outside the range. A range that stops just short of its own subject is worse than a missing citation, because it looks checked. The reviewer then re-extracted the citations independently and confirmed the sweep missed none, resolved all eight corrections itself at the pins, verified the transformers files are md5-identical between the venv and the clone so the numbering is not install-specific, and proved code identity by comment-stripping and hashing rather than by eye. Three conflicts, none of them a union. docs/USAGE.md's first: the base says "A typed prompt works" because L13 landed it; L10 still opened "There is no prompt", stale on arrival. Took the base's framing but KEPT L10's tokenization-divergence paragraph, which is unique to it and still true -- upstream tokenizes through __call__ with add_special_tokens defaulting True and so runs the post_processor, while this port calls plain encode and prepends BOS; identical only because the shipped post_processor's special-token map is EMPTY, measured on the file rather than assumed. Its second and docs/FEATURES.md both took the base wholesale: the base's parity paragraph is a strict superset carrying anchors, the ltx2.h refusal list and a provenance note, and L10's row still said first-party NVFP4 does not load (L9A landed it) and described an encoder_path refusal L13 lifted. Issue: #435, #604 FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code]
2 parents 14efa53 + 131328e commit c43b5fa

10 files changed

Lines changed: 2811 additions & 527 deletions

File tree

docs/FEATURES.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,13 @@ speed-pending, which [BENCHMARKS.md](BENCHMARKS.md) tracks.
147147
### Standalone and non-registered lanes
148148

149149
These run through dedicated forwards, not the `REGISTER_VLLM_MODEL` registry, so
150-
they sit outside the gated list above.
150+
they sit outside the gated list above. One caveat the LTX-2.5 row is too narrow
151+
to carry: its text tower's prompt tokenization mirrors upstream only while the
152+
checkpoint's tokenizer `post_processor` adds nothing. The shipped one is MEASURED
153+
empty, so this port's plain encode plus an explicit BOS prepend matches
154+
upstream's `add_special_tokens=True` today; a checkpoint with a non-empty
155+
`post_processor` would tokenize differently here, and `Ltx2TokenizeGemmaPrompt`
156+
in `ltx2_text_encoder.cpp` is the call that would have to change.
151157

152158
| Lane | Tested checkpoint(s) | Correctness gate | Speed vs reference |
153159
|---|---|---|---|

docs/USAGE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,15 @@ them to 4096 and 2048, and passes both streams through the embeddings connector
408408
before cross-attention. The tower is ~24 GB of host bf16 and stays resident,
409409
because a prompt arrives per request.
410410

411+
One tokenization detail is a KNOWN DIVERGENCE rather than a mirror, and it is
412+
checkpoint-conditional: upstream tokenizes through the HuggingFace `__call__`
413+
with its default `add_special_tokens=True`, so it runs the tokenizer's
414+
post_processor, while this port calls the plain encode and prepends BOS by hand.
415+
On the shipped checkpoint the two are identical — its post_processor declares an
416+
EMPTY special-token map, measured on the shipped file rather than assumed — so
417+
nothing is lost today. A checkpoint whose post_processor DID add tokens would
418+
tokenize differently here.
419+
411420
`--encoder-config` supplies the Gemma config, and it is required for the only
412421
shipped encoder: `vonkaiser`'s
413422
`gemma4-12b-with-proj-nvfp4-torchao.safetensors` carries no `__metadata__` at

include/vllm/model_executor/models/gemma4.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,30 @@ std::vector<int32_t> Gemma4GenerateGreedyViaRegistry(
251251
vt::Queue& queue, int max_new_tokens,
252252
std::vector<float>* out_margins = nullptr);
253253

254+
// The FULL-attention layers' "proportional" rope cos|sin table, on host in f32 —
255+
// the exact values the forward builds (BuildProportionalRopeCache rounds them to
256+
// bf16 to match the q/k it rotates; nothing else differs). Returns
257+
// [max_pos + 1, head_dim]: the first head_dim/2 columns are cos and the second
258+
// half sin, over the head_dim/2 DISTINCT angle pairs, mirroring upstream's
259+
// `emb = cat((freqs, freqs))` with each angle stored once
260+
// (`Gemma4UnifiedTextRotaryEmbedding.forward`, modeling_gemma4_unified.py:259-275
261+
// — the `cat` itself is :271; the inv_freq it consumes comes from
262+
// modeling_rope_utils.py:187-254).
263+
//
264+
// This is a GATE SURFACE, and it exists because of a measurement rather than a
265+
// preference. `partial_rotary_factor` decides how many angle pairs are rotated
266+
// and how many are zero-padded to identity, and it is the one field on this path
267+
// that the tower's hidden states cannot resolve: on the reduced LTX tower
268+
// fixture, forcing it from the config's 0.25 to 1.0 displaces the worst hidden
269+
// state by 1.09e-01 against that state's measured bf16 noise floor of 9.99e-02
270+
// — a ratio of 1.09, inside the tolerance the states are gated at — and a LARGER
271+
// fixture makes it worse, not better (0.65 at head_dim 16/32, seq 32), because
272+
// bf16 accumulation noise grows at least as fast as the rope contribution. So
273+
// the states are the wrong instrument and this table is the right one: f32, no
274+
// accumulation, compared element-wise against the oracle's own rotary embedding.
275+
std::vector<float> Gemma4ProportionalRopeCosSin(const HfConfig& config,
276+
int64_t head_dim, int64_t max_pos);
277+
254278
// Wrap already-loaded Gemma-4 weights in the registered LoadedModel so a caller
255279
// that owns the weights (the mm e2e gate) can drive ModelRegistry::Forward without
256280
// re-reading the checkpoint. `Make` OWNS the moved weights; `Borrow` does NOT own

include/vllm/model_executor/models/ltx2_text_encoder.h

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -450,19 +450,29 @@ struct Ltx2GemmaPromptTokens {
450450
// tokenizer.py:31-59, mirrored including the parts that look like details:
451451
//
452452
// * `text.strip()` first (:33). diffusers strips too (pipeline_ltx2.py:333).
453-
// * encode with NO special tokens added by the post_processor. The shipped
454-
// tokenizer's `post_processor` is a TemplateProcessing whose `special_tokens`
455-
// map is EMPTY, so it would add nothing anyway — measured on the shipped
456-
// file, not assumed.
457-
// * then PREPEND BOS unconditionally if it is not already first (:44-46).
458-
// THIS IS WHERE THE TWO REFERENCES DISAGREE and upstream is followed:
459-
// `ltx_core` prepends explicitly and says why — "Gemma 3 already emits it
460-
// via post_processor; Gemma 4 does not, so we prepend" (tokenizer.py:12-15)
461-
// — while diffusers passes `add_special_tokens=True` and relies on the
462-
// post_processor (pipeline_ltx2.py:339), which for THIS tokenizer.json adds
463-
// nothing. Following diffusers would drop token 0 of every prompt. Recorded
464-
// rather than silently resolved: `ltx_core` is the model author's own
465-
// runtime and is explicit about the case.
453+
// * encode, then PREPEND BOS if it is not already first — CONDITIONAL, on
454+
// upstream's own `if not input_ids or input_ids[0] != bos_id` guard
455+
// (:44-46). A port that prepends unconditionally doubles the BOS.
456+
//
457+
// Two things about that, and the first one is a KNOWN DIVERGENCE rather than
458+
// a mirrored default. Upstream calls `self.tokenizer(text, ...)` — `__call__`
459+
// with its default `add_special_tokens=True` (tokenizer.py:37-43) — so
460+
// upstream DOES run the post_processor and we call plain `Encode`, which
461+
// does not. On THIS checkpoint the two are identical, because the shipped
462+
// `post_processor` is a TemplateProcessing whose `special_tokens` map is
463+
// EMPTY and whose template is the bare sequence, so it has nothing to add:
464+
// measured on the shipped file, not assumed. If a future checkpoint ships a
465+
// post_processor that DOES add something, upstream would emit it and we
466+
// would not — so this is the line to change, not a property to rely on.
467+
//
468+
// What the two references actually disagree about is narrower than "one
469+
// runs the post-processor": both let it run. `ltx_core` ALSO prepends BOS
470+
// explicitly and says why — "Gemma 3 already emits it via post_processor;
471+
// Gemma 4 does not, so we prepend" (tokenizer.py:12-15) — while diffusers
472+
// relies on the post_processor alone (pipeline_ltx2.py:339), which for this
473+
// tokenizer.json adds nothing, so following diffusers would drop token 0 of
474+
// every prompt. `ltx_core` is the model author's own runtime and is explicit
475+
// about the case, so it is the one followed.
466476
// * EOS is never appended (:14).
467477
// * truncation happens BEFORE the BOS prepend and again after (:41, :46), so a
468478
// maximal prompt loses its LAST token to make room for BOS rather than

scripts/gen-ltx2-gemma-tower-goldens.py

Lines changed: 120 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,28 @@
3535
12B tower. Our port runs only the VALID tokens at their ORIGINAL absolute
3636
positions. That is equivalent -- pads are masked out of attention and are
3737
causally before every valid token, and the feature extractor zeroes their
38-
rows anyway -- but "is equivalent" is a claim, so section 3 emits the full
38+
rows anyway -- but "is equivalent" is a claim, so section 4 emits the full
3939
left-padded oracle run and the C++ suite holds the short run's valid rows
4040
to the padded run's valid rows. If the equivalence is ever false, that gate
4141
is what says so, and a 100x cost claim stops resting on an argument.
4242
4343
* ONE arithmetic width per state, both ways round. Section 2 is the oracle in
44-
float32 and section 4 the SAME oracle in bfloat16. Our forward carries the
45-
stream in bf16 and widens only on the way out (gemma4.h,
46-
Gemma4HiddenStatesResult), so bf16 is the dtype-MATCHED arm and f32 is the
47-
arm that would catch a reduction-order defect a bf16 store absorbs. Gating
48-
only one of them has burned this project before.
44+
float32 and section 3 the SAME oracle in bfloat16; sections 4 and 4b are
45+
that pair again for the left-padded run. Our forward carries the stream in
46+
bf16 and widens only on the way out (gemma4.h, Gemma4HiddenStatesResult),
47+
so bf16 is the dtype-MATCHED arm and f32 is the arm that would catch a
48+
reduction-order defect a bf16 store absorbs. Gating only one of them has
49+
burned this project before.
50+
51+
Every leg runs on a DEEP COPY of the module. `nn.Module.to(dtype)` converts
52+
in place and bf16 rounding is destructive, so before that fix the two
53+
"float32" legs after the bf16 one were executing over bf16-ROUNDED weights
54+
-- MEASURED at up to 3.90e-02 per state, of the same order as the noise
55+
floor the tolerance itself is derived from.
56+
57+
* The ROPE TABLES, in f32 (section 6), because `partial_rotary_factor` is not
58+
resolvable from the hidden states at any fixture size this generator can
59+
build. Section 6's note carries the measurement that establishes it.
4960
5061
Both sides rebuild every weight from one deterministic FNV-1a + splitmix64
5162
stream keyed by the parameter's own HuggingFace NAME, exactly as
@@ -84,6 +95,7 @@
8495
from __future__ import annotations
8596

8697
import argparse
98+
import copy
8799
import json
88100
import os
89101
import subprocess
@@ -317,7 +329,20 @@ def build_tower(real_config: dict):
317329

318330

319331
def run_tower(inner, ids, mask, dtype, positions=None):
320-
m = inner.to(dtype).eval()
332+
"""One oracle leg, on a DEEP COPY of the module.
333+
334+
`nn.Module.to(dtype)` converts parameters and buffers IN PLACE, so the
335+
obvious `inner.to(dtype)` / `inner.to(torch.float32)` round trip does not
336+
restore anything: bf16 rounding is destructive, and every leg after the
337+
first bf16 one would run over bf16-ROUNDED weights while calling itself
338+
float32. MEASURED on this fixture before the fix: re-running the identical
339+
f32 call after the bf16 leg moved a state by up to 3.90e-02, which is
340+
~40x the f32 legs' own round-off and of the same order as the bf16 noise
341+
floor the gate is calibrated on. Sections 4 and 5 were silently
342+
order-dependent because of it. Copying is cheap here -- the reduced tower
343+
is a few hundred KB -- and it makes every leg independent of leg order.
344+
"""
345+
m = copy.deepcopy(inner).to(dtype).eval()
321346
kwargs = {}
322347
if positions is not None:
323348
# The absolute positions the tokens occupy in the padded batch. Left
@@ -332,9 +357,45 @@ def run_tower(inner, ids, mask, dtype, positions=None):
332357
output_hidden_states=True,
333358
**kwargs,
334359
)
335-
states = [h[0].to(torch.float32).contiguous().numpy() for h in out.hidden_states]
336-
inner.to(torch.float32)
337-
return states
360+
return [h[0].to(torch.float32).contiguous().numpy() for h in out.hidden_states]
361+
362+
363+
def rope_cos_sin(config, layer_type: str, head_dim: int, positions) -> np.ndarray:
364+
"""The oracle's OWN cos|sin table for one layer type, in float32.
365+
366+
Why this is emitted at all: `partial_rotary_factor` is NOT resolvable from
367+
the hidden states. MEASURED on this fixture, the whole difference between
368+
the config's 0.25 and a port that ignored it and rotated fully is 1.09e-01
369+
at the worst state against a bf16 noise floor of 9.99e-02 -- a ratio of
370+
1.09, i.e. inside the tolerance the same states are gated at. Enlarging the
371+
fixture does not rescue it: at (head_dim 16/32, seq 32) the ratio FALLS to
372+
0.65, because bf16 accumulation noise grows at least as fast as the rope
373+
contribution does. So the end-to-end states are the wrong instrument, and
374+
the right one is the table itself, in f32, with no accumulation in it.
375+
376+
Built by the real `Gemma4UnifiedTextRotaryEmbedding`, which is what routes
377+
`rope_type: "proportional"` to `_compute_proportional_rope_parameters`
378+
(modeling_gemma4_unified.py:206-218 -- the ROPE_INIT_FUNCTIONS lookup at :207
379+
and the call at :218; modeling_rope_utils.py:187-254) and so is the only
380+
thing that decides how many angle pairs are zero-padded.
381+
382+
Returns [len(positions), head_dim]: the first head_dim/2 columns are cos
383+
over the distinct angle pairs and the second half is sin, which is the
384+
layout `BuildProportionalRopeCache` writes.
385+
"""
386+
from transformers.models.gemma4_unified.modeling_gemma4_unified import ( # noqa: PLC0415
387+
Gemma4UnifiedTextRotaryEmbedding,
388+
)
389+
390+
rope = Gemma4UnifiedTextRotaryEmbedding(config)
391+
pos = torch.tensor([list(positions)], dtype=torch.long)
392+
x = torch.zeros(1, len(positions), head_dim, dtype=torch.float32)
393+
cos, sin = rope(x, pos, layer_type=layer_type)
394+
pairs = head_dim // 2
395+
# Upstream duplicates each angle (`emb = cat((freqs, freqs))`) so cos/sin are
396+
# head_dim wide over head_dim/2 DISTINCT angles; take one copy of each.
397+
table = torch.cat((cos[0, :, :pairs], sin[0, :, :pairs]), dim=-1)
398+
return table.to(torch.float32).contiguous().numpy()
338399

339400

340401
# ---------------------------------------------------------------------------
@@ -408,6 +469,11 @@ def main() -> int:
408469
plain_f32 = run_tower(inner, TOKENS, [1] * SEQ, torch.float32)
409470
plain_bf16 = run_tower(inner, TOKENS, [1] * SEQ, torch.bfloat16)
410471
padded_f32 = run_tower(inner, padded_ids, padded_mask, torch.float32)
472+
# The padded run at the SHIPPED dtype too. Without it the left-padded rows
473+
# have no dtype-matched oracle, and anything gated against them -- the
474+
# prompt-to-conditioning path, which is left-padded by construction -- has
475+
# no measured floor to be held to, only a borrowed one.
476+
padded_bf16 = run_tower(inner, padded_ids, padded_mask, torch.bfloat16)
411477
# The equivalence claim, isolated INSIDE the oracle and in f32 so no dtype
412478
# noise is mixed into it: the same valid tokens, told their absolute
413479
# positions, run WITHOUT the pads. If upstream's own two answers agree, the
@@ -523,19 +589,60 @@ def main() -> int:
523589
"// [state][padded_seq * hidden]. Rows 0..kLtxTowerNumPad-1 are pad rows and\n"
524590
"// their contents are upstream's garbage-but-masked values; the gate reads\n"
525591
"// only the VALID tail and holds section 2 to it.\n"
592+
"// Every leg runs on a DEEP COPY of the module, so this one is not\n"
593+
"// downstream of the bf16 leg's rounding and the sections are independent\n"
594+
"// of the order they are produced in.\n"
526595
)
527596
for i, s in enumerate(padded_f32):
528597
emit_f32(out, f"kLtxTowerPaddedStateF32_{i}", s)
529598

599+
out.write(
600+
"// --- section 4b: the LEFT-PADDED run, BFLOAT16 (the SHIPPED dtype) ---\n"
601+
"// The dtype-MATCHED arm for anything gated on the left-padded rows, and\n"
602+
"// with section 4 it is also the padded run's own measured noise floor.\n"
603+
)
604+
for i, s in enumerate(padded_bf16):
605+
emit_f32(out, f"kLtxTowerPaddedStateBf16_{i}", s)
606+
530607
out.write(
531608
"// --- section 5: the equivalence, measured INSIDE the oracle ---\n"
532609
"// Per state, max|short-run-at-absolute-positions - padded-run's valid rows|,\n"
533-
"// both f32, so this number carries NO dtype noise. It is upstream's own\n"
534-
"// answer to 'is dropping the pads free?'. Our port inherits it; the C++\n"
535-
"// gate checks the two claims separately so a failure says which one broke.\n"
610+
"// both f32 and both on independently deep-copied modules, so this number\n"
611+
"// carries NO dtype noise. It is upstream's own answer to 'is dropping the\n"
612+
"// pads free?'. Our port inherits it; the C++ gate checks the two claims\n"
613+
"// separately so a failure says which one broke.\n"
536614
)
537615
emit_f32(out, "kLtxTowerPadEquivalence", equivalence)
538616

617+
# SECTION 6 -- the rope table, because the hidden states cannot see it.
618+
#
619+
# `partial_rotary_factor` decides how many of the full-attention layers'
620+
# angle pairs are rotated and how many are zero-padded to identity. It is
621+
# config-carried, shape-invisible, and -- MEASURED, not assumed -- also
622+
# invisible in the gated hidden states: setting it to 1.0 displaces the
623+
# worst state by 1.09e-01 against a bf16 floor of 9.99e-02 (ratio 1.09),
624+
# and a larger fixture makes that WORSE, not better (0.65 at head_dim
625+
# 16/32, seq 32), because bf16 accumulation noise grows at least as fast.
626+
# A gate whose tolerance is bf16 noise cannot resolve it, so the table
627+
# itself is emitted and compared in f32.
628+
full_head_dim = GLOBAL_HEAD_DIM
629+
rope_positions = list(range(PADDED_SEQ))
630+
rope_full = rope_cos_sin(config.text_config, "full_attention",
631+
full_head_dim, rope_positions)
632+
out.write(
633+
"// --- section 6: the ORACLE's FULL-attention rope cos|sin table, f32 ---\n"
634+
"// [position][global_head_dim]: first half cos, second half sin, over the\n"
635+
"// global_head_dim/2 DISTINCT angle pairs (upstream stores each twice).\n"
636+
"// `rope_type: proportional` at theta 1e6 and partial_rotary_factor 0.25,\n"
637+
"// so the trailing pairs are cos=1, sin=0. Emitted because the partial\n"
638+
"// factor is NOT resolvable from the hidden states -- see the note above --\n"
639+
"// and because a table that silently rotated every pair would still produce\n"
640+
"// 13 finite, plausibly-scaled states.\n"
641+
)
642+
emit_scalar(out, "kLtxTowerRopePositions", len(rope_positions))
643+
out.write("\n")
644+
emit_f32(out, "kLtxTowerRopeFullCosSin", rope_full)
645+
539646
out.write("} // namespace vllm_test\n")
540647

541648
sys.stderr.write(

scripts/gen-ltx2-prompt-tokens-goldens.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,13 @@ def tokenize_with_weights(tok, text: str, bos_id: int, pad_id: int):
8383
only thing borrowed is the algorithm, and every step cites its line.
8484
"""
8585
text = text.strip() # :33
86-
ids = list(tok.encode(text, add_special_tokens=False).ids) # :38-43
86+
# :37-43 is `self.tokenizer(text, ...)` — `__call__` with its DEFAULT
87+
# `add_special_tokens=True`, so upstream runs the post_processor. This passes
88+
# False deliberately, to transcribe what the C++ `Encode` does; the two agree
89+
# only because the measured post_processor here has an EMPTY `special_tokens`
90+
# map (asserted into the emitted header below). On a checkpoint where it added
91+
# something, THIS line and the C++ call would both have to change.
92+
ids = list(tok.encode(text, add_special_tokens=False).ids) # :37-43
8793
if len(ids) > MAX_LENGTH: # truncation=True
8894
ids = ids[:MAX_LENGTH]
8995
if not ids or ids[0] != bos_id: # :44-46

0 commit comments

Comments
 (0)