InternVL3-9B-CVRR / source_splitting.py
dmis-lab's picture
Add files using upload-large-folder tool
a381a62 verified
Raw
History Blame Contribute Delete
18.9 kB
"""Layer-split forward for Qwen2.5-VL and Qwen3-VL.
Method §3.2 needs ``F = F_{>l*} o F_{<=l*}`` as two separately runnable halves so
that the multimodal branch can be cut off at ``l*`` and replaced by the
workspace. §3.1 needs the same split to read and patch activations at a chosen
depth.
This module reimplements the prologue of the native Qwen VL text-model forward
-- embedding merge, M-RoPE index, causal mask, rotary embeddings -- as a
reusable :class:`SplitContext`, then exposes the decoder-layer loop as a range
you can run piecewise. Qwen3-VL additionally injects three ``DeepStack``
vision features after language layers 0--2; those tensors are carried in the
split context and applied at the identical layer boundaries. It deliberately
mirrors transformers 4.57.6 rather than monkeypatching it;
``tests/test_split_equivalence.py`` asserts the composed halves reproduce the
stock forward bit-for-bit, which is what makes the mirroring safe to rely on.
Shapes use ``B`` batch, ``L`` sequence, ``d`` backbone width (3584 on the 7B),
``N_v`` visual tokens, ``N_q`` question tokens.
"""
from __future__ import annotations
from dataclasses import dataclass, replace
from typing import Any
import torch
from transformers.cache_utils import Cache
from transformers.masking_utils import (
create_causal_mask,
create_sliding_window_causal_mask,
)
@dataclass
class SplitContext:
"""Per-forward state shared by every decoder layer.
Computed once by :func:`make_split_context` so that layer ranges can be run
independently without recomputing masks or rotary tables.
"""
hidden_states: torch.Tensor # [B, L, d] - mutated as layers run
position_ids: torch.Tensor # [3, B, L] - M-RoPE (t, h, w)
position_embeddings: tuple[torch.Tensor, torch.Tensor] # (cos, sin) [B, L, head_dim]
causal_mask_mapping: dict[str, torch.Tensor | None]
cache_position: torch.Tensor # [L]
text_position_ids: torch.Tensor | None # [B, L] only when packed
past_key_values: Cache | None
# Original 2-D key-padding mask. ``create_causal_mask`` is allowed to
# return ``None`` for SDPA and delegate causality to ``is_causal``; keeping
# this tensor lets counterfactual branches materialize the equivalent mask
# before removing a precisely selected set of attention edges.
attention_mask: torch.Tensor | None = None # [B, L_kv]
# Qwen3-VL only. DeepStack adds one visual feature tensor after each of
# the first three language layers. They stay ``None`` for Qwen2.5-VL and
# for every text-only branch.
visual_pos_masks: torch.Tensor | None = None # [B, L] bool
deepstack_visual_embeds: list[torch.Tensor] | None = None
def clone_at(self, hidden_states: torch.Tensor) -> "SplitContext":
"""Same context, different hidden states (for patched re-runs)."""
return SplitContext(
hidden_states=hidden_states,
position_ids=self.position_ids,
position_embeddings=self.position_embeddings,
causal_mask_mapping=self.causal_mask_mapping,
cache_position=self.cache_position,
text_position_ids=self.text_position_ids,
past_key_values=self.past_key_values,
attention_mask=self.attention_mask,
visual_pos_masks=self.visual_pos_masks,
deepstack_visual_embeds=self.deepstack_visual_embeds,
)
# ---------------------------------------------------------------------------
# embedding / position construction
# ---------------------------------------------------------------------------
def embed_multimodal(
vl_model,
input_ids: torch.LongTensor, # [B, L]
pixel_values: torch.Tensor | None = None,
image_grid_thw: torch.LongTensor | None = None,
attention_mask: torch.Tensor | None = None,
*,
return_deepstack: bool = False,
) -> (
tuple[torch.Tensor, torch.Tensor]
| tuple[
torch.Tensor,
torch.Tensor,
torch.Tensor | None,
list[torch.Tensor] | None,
]
):
"""Token embeddings with image features scattered in, plus M-RoPE indices.
Mirrors the prefill path of ``Qwen2_5_VLModel.forward``. Pass
``pixel_values=None`` to get the text-only branch used for ``Q*``.
Args:
vl_model: a native ``Qwen2_5_VLModel`` or ``Qwen3VLModel`` (i.e.
``model.model``, not the ``...ForConditionalGeneration`` wrapper).
return_deepstack: also return Qwen3-VL's visual-position mask and
DeepStack features. The default two-value return keeps all
Qwen2.5 callers backward compatible.
Returns:
``(inputs_embeds [B, L, d], position_ids [3, B, L])`` and optionally
``(visual_pos_masks, deepstack_visual_embeds)``.
"""
inputs_embeds = vl_model.get_input_embeddings()(input_ids) # [B, L, d]
model_type = str(getattr(vl_model.config, "model_type", ""))
# A freshly wrapped model exposes the native qwen3_vl config here.
# Reloading a fully saved CLOSE checkpoint reconstructs the nested native
# backbone from the wrapper config, so its model view carries
# close_qwen3_vl instead. Both use the tuple-returning Qwen3 feature API.
is_qwen3_vl = model_type in {"qwen3_vl", "close_qwen3_vl"}
visual_pos_masks = None
deepstack_visual_embeds = None
if pixel_values is not None:
image_features = vl_model.get_image_features(pixel_values, image_grid_thw)
if is_qwen3_vl:
image_embeds, deepstack_visual_embeds = image_features
else:
image_embeds = image_features
image_embeds = torch.cat(image_embeds, dim=0).to(
inputs_embeds.device, inputs_embeds.dtype
) # [N_v_total, d]
image_mask, _ = vl_model.get_placeholder_mask(
input_ids, inputs_embeds=inputs_embeds, image_features=image_embeds
)
inputs_embeds = inputs_embeds.masked_scatter(image_mask, image_embeds)
if is_qwen3_vl:
visual_pos_masks = image_mask[..., 0]
if is_qwen3_vl:
position_ids, _ = vl_model.get_rope_index(
input_ids,
image_grid_thw,
None, # video_grid_thw
attention_mask=attention_mask,
)
else:
position_ids, _ = vl_model.get_rope_index(
input_ids,
image_grid_thw,
None, # video_grid_thw
second_per_grid_ts=None,
attention_mask=attention_mask,
)
if return_deepstack:
return (
inputs_embeds,
position_ids,
visual_pos_masks,
deepstack_visual_embeds,
)
return inputs_embeds, position_ids
def make_split_context(
text_model,
inputs_embeds: torch.Tensor, # [B, L, d]
position_ids: torch.Tensor, # [3, B, L]
attention_mask: torch.Tensor | None = None,
past_key_values: Cache | None = None,
cache_position: torch.Tensor | None = None,
visual_pos_masks: torch.Tensor | None = None,
deepstack_visual_embeds: list[torch.Tensor] | None = None,
) -> SplitContext:
"""Build masks and rotary embeddings once, as the stock forward does.
Args:
text_model: ``Qwen2_5_VLTextModel`` (``vl_model.language_model``).
"""
if cache_position is None:
past_seen = past_key_values.get_seq_length() if past_key_values is not None else 0
cache_position = torch.arange(
past_seen, past_seen + inputs_embeds.shape[1], device=inputs_embeds.device
) # [L]
if position_ids.ndim == 2:
position_ids = position_ids[None, ...].expand(3, position_ids.shape[0], -1)
model_type = str(getattr(text_model.config, "model_type", ""))
# Packed-sequence convention: a leading text-only row makes it [4, B, L].
if position_ids.ndim == 3 and position_ids.shape[0] == 4:
text_position_ids = position_ids[0] # [B, L]
position_ids = position_ids[1:] # [3, B, L]
elif model_type == "qwen3_vl_text":
# Qwen3-VL always passes the temporal M-RoPE row to both the causal-mask
# builder and decoder layers, even for ordinary (non-packed) inputs.
text_position_ids = position_ids[0]
else:
text_position_ids = None
mask_kwargs: dict[str, Any] = {
"config": text_model.config,
"input_embeds": inputs_embeds,
"attention_mask": attention_mask,
"cache_position": cache_position,
"past_key_values": past_key_values,
"position_ids": text_position_ids,
}
causal_mask_mapping = {"full_attention": create_causal_mask(**mask_kwargs)}
if getattr(text_model, "has_sliding_layers", False):
causal_mask_mapping["sliding_attention"] = create_sliding_window_causal_mask(
**mask_kwargs
)
position_embeddings = text_model.rotary_emb(inputs_embeds, position_ids)
return SplitContext(
hidden_states=inputs_embeds,
position_ids=position_ids,
position_embeddings=position_embeddings,
causal_mask_mapping=causal_mask_mapping,
cache_position=cache_position,
text_position_ids=text_position_ids,
past_key_values=past_key_values,
attention_mask=attention_mask,
visual_pos_masks=visual_pos_masks,
deepstack_visual_embeds=deepstack_visual_embeds,
)
def block_attention_edges(
ctx: SplitContext,
query_mask: torch.Tensor,
key_mask: torch.Tensor,
) -> SplitContext:
"""Return ``ctx`` with selected query-to-key attention edges removed.
``query_mask`` and ``key_mask`` are boolean ``[B, L]`` supports in the
current (cache-free) sequence. Every ordinary causal/padding constraint is
preserved; only their Cartesian product is additionally masked. Both
boolean SDPA masks (``True`` means visible) and additive eager masks
(``0``/negative infinity) are supported.
The helper deliberately rejects cached contexts. Its intended use is a
counterfactual recurrent layer evaluation, never autoregressive decoding,
and silently guessing the key offset of a populated cache would invalidate
the causal comparison.
"""
if ctx.past_key_values is not None and ctx.past_key_values.get_seq_length() > 0:
raise ValueError("block_attention_edges requires a cache-free context")
if query_mask.dtype != torch.bool or key_mask.dtype != torch.bool:
raise TypeError("query_mask and key_mask must be boolean tensors")
if query_mask.shape != key_mask.shape or query_mask.ndim != 2:
raise ValueError(
"query_mask and key_mask must have the same [B, L] shape, got "
f"{tuple(query_mask.shape)} and {tuple(key_mask.shape)}"
)
batch_size, seq_len = query_mask.shape
if ctx.hidden_states.shape[:2] != (batch_size, seq_len):
raise ValueError(
"edge masks must match the SplitContext sequence, got "
f"{tuple(query_mask.shape)} for {tuple(ctx.hidden_states.shape[:2])}"
)
blocked = query_mask[:, None, :, None] & key_mask[:, None, None, :]
updated: dict[str, torch.Tensor] = {}
for attention_type, base_mask in ctx.causal_mask_mapping.items():
if base_mask is None:
# SDPA may omit an all-valid causal mask. Materialize exactly that
# lower triangle, then reapply key padding before deleting edges.
q_positions = ctx.cache_position
if q_positions.numel() != seq_len:
raise ValueError(
"cache-free context must have one cache position per row"
)
key_positions = torch.arange(seq_len, device=query_mask.device)
visible = key_positions[None, :] <= q_positions[:, None]
visible = visible[None, None].expand(batch_size, 1, -1, -1)
if ctx.attention_mask is not None:
if ctx.attention_mask.shape != (batch_size, seq_len):
raise ValueError(
"counterfactual edge masking expects a 2-D [B, L] "
"attention mask"
)
visible = visible & ctx.attention_mask[:, None, None, :].bool()
updated[attention_type] = visible & ~blocked
continue
if not isinstance(base_mask, torch.Tensor) or base_mask.ndim != 4:
raise TypeError(
"counterfactual edge masking supports tensor 4-D attention "
f"masks, got {type(base_mask)!r}"
)
if base_mask.shape[0] not in (1, batch_size):
raise ValueError("attention-mask batch dimension is incompatible")
if base_mask.shape[-2:] != (seq_len, seq_len):
raise ValueError(
"counterfactual edge masking expects a square cache-free mask, "
f"got {tuple(base_mask.shape)}"
)
if base_mask.dtype == torch.bool:
updated[attention_type] = base_mask & ~blocked
elif base_mask.is_floating_point():
updated[attention_type] = base_mask.masked_fill(
blocked, torch.finfo(base_mask.dtype).min
)
else:
raise TypeError(
f"unsupported attention mask dtype {base_mask.dtype}"
)
return replace(ctx, causal_mask_mapping=updated)
# ---------------------------------------------------------------------------
# running layer ranges
# ---------------------------------------------------------------------------
def run_layer_range(
text_model,
ctx: SplitContext,
start: int,
stop: int | None = None,
use_cache: bool = False,
hidden_states: torch.Tensor | None = None,
collect: bool = False,
) -> torch.Tensor | tuple[torch.Tensor, list[torch.Tensor]]:
"""Run ``text_model.layers[start:stop]`` on ``ctx``.
``self.norm`` is *not* applied -- it belongs to the very top of the stack.
Call :func:`final_norm` after the last range.
Args:
hidden_states: override the context's states (leave ``None`` to chain).
collect: also return the input hidden states of every layer in the range
plus the range output, i.e. ``stop - start + 1`` tensors.
Returns:
``[B, L, d]``, or ``(output, collected)`` when ``collect``.
"""
layers = text_model.layers
stop = len(layers) if stop is None else stop
h = ctx.hidden_states if hidden_states is None else hidden_states
collected: list[torch.Tensor] = []
for layer_index, layer in enumerate(layers[start:stop], start=start):
if collect:
collected.append(h)
attention_type = getattr(layer, "attention_type", "full_attention")
h = layer(
h,
attention_mask=ctx.causal_mask_mapping[attention_type],
position_ids=ctx.text_position_ids,
past_key_values=ctx.past_key_values,
use_cache=use_cache,
cache_position=ctx.cache_position,
position_embeddings=ctx.position_embeddings,
)
# 4.57 decoder layers return a bare tensor; older ones returned a tuple.
if isinstance(h, tuple):
h = h[0]
if (
ctx.deepstack_visual_embeds is not None
and layer_index < len(ctx.deepstack_visual_embeds)
):
if ctx.visual_pos_masks is None:
raise ValueError("DeepStack features require visual_pos_masks")
h = text_model._deepstack_process(
h,
ctx.visual_pos_masks,
ctx.deepstack_visual_embeds[layer_index],
)
if collect:
collected.append(h)
return h, collected
return h
def final_norm(text_model, hidden_states: torch.Tensor) -> torch.Tensor:
"""Apply the stack's final RMSNorm. ``[B, L, d] -> [B, L, d]``."""
return text_model.norm(hidden_states)
# ---------------------------------------------------------------------------
# token selection operators (Pi_img / Pi_q in §3.2)
# ---------------------------------------------------------------------------
def image_token_mask(input_ids: torch.LongTensor, image_token_id: int) -> torch.Tensor:
"""``Pi_img`` support: ``[B, L]`` bool, True at image placeholder positions."""
return input_ids == image_token_id
def vision_span_mask(input_ids: torch.LongTensor, config) -> torch.Tensor:
"""``[B, L]`` bool covering ``<|vision_start|>``, image pads, ``<|vision_end|>``.
Use this (not :func:`image_token_mask`) when *removing* the visual segment to
build the text-only branch, so the delimiters do not survive as orphans.
"""
ids = {
config.vision_start_token_id,
config.vision_end_token_id,
config.image_token_id,
config.video_token_id,
}
mask = torch.zeros_like(input_ids, dtype=torch.bool)
for tid in ids:
mask |= input_ids == tid
return mask
def select_tokens(
hidden_states: torch.Tensor, # [B, L, d]
mask: torch.Tensor, # [B, L] bool
) -> torch.Tensor:
"""Gather masked positions. Requires an equal count per batch element.
Returns ``[B, N, d]`` where ``N`` is that per-element count.
"""
counts = mask.sum(dim=1)
if counts.numel() > 1 and not bool((counts == counts[0]).all()):
raise ValueError(
f"select_tokens needs the same number of selected tokens per batch "
f"element, got {counts.tolist()}. Bucket by visual-token count or "
f"gather per-example instead."
)
n = int(counts[0])
b, _, d = hidden_states.shape
return hidden_states[mask].view(b, n, d)
def select_tokens_padded(
hidden_states: torch.Tensor, # [B, L, d]
mask: torch.Tensor, # [B, L] bool
) -> tuple[torch.Tensor, torch.Tensor]:
"""Gather masked positions, right-padded to the batch maximum.
Qwen2.5-VL uses dynamic resolution, so ``N_v`` differs across a batch. §3.1's
patching genuinely needs equal counts (it transplants position by position),
but ``r_theta`` only cross-attends over ``V*`` -- a variable-length memory is
exactly what a key-padding mask is for.
Returns ``(padded [B, N_max, d], key_padding_mask [B, N_max])`` where the
mask is ``True`` at padding, matching ``nn.MultiheadAttention``.
"""
counts = mask.sum(dim=1)
n_max = int(counts.max())
b, _, d = hidden_states.shape
out = hidden_states.new_zeros((b, n_max, d))
pad = torch.ones((b, n_max), dtype=torch.bool, device=hidden_states.device)
for i in range(b):
n = int(counts[i])
out[i, :n] = hidden_states[i][mask[i]]
pad[i, :n] = False
return out, pad
__all__ = [
"SplitContext",
"embed_multimodal",
"make_split_context",
"block_attention_edges",
"run_layer_range",
"final_norm",
"image_token_mask",
"vision_span_mask",
"select_tokens",
"select_tokens_padded",
]