Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9368ece
works i think
zucchini-nlp Apr 7, 2026
513035c
more models
zucchini-nlp Apr 14, 2026
0bb610a
like this
zucchini-nlp Apr 15, 2026
d527a15
pi0
zucchini-nlp Apr 15, 2026
fe59952
Merge branch 'main' into split-out-gemma-style-mask
zucchini-nlp Apr 16, 2026
1099438
Merge branch 'main' into split-out-gemma-style-mask
zucchini-nlp Apr 17, 2026
6fa84ea
fix some
zucchini-nlp Apr 17, 2026
19c3066
fix more
zucchini-nlp Apr 17, 2026
e8f06b2
.
zucchini-nlp Apr 17, 2026
0e17f9a
Merge branch 'main' into split-out-gemma-style-mask
zucchini-nlp Apr 20, 2026
3617c36
for now
zucchini-nlp Apr 20, 2026
96ef824
assumes always prefix, unless type ids are passed
zucchini-nlp Apr 23, 2026
add5818
Merge remote-tracking branch 'upstream/main' into split-out-gemma-sty…
zucchini-nlp Apr 23, 2026
7286713
ig it is sdpa choosing a different backend
zucchini-nlp Apr 23, 2026
87b8df0
again sdpa choosing different backend in PG
zucchini-nlp Apr 24, 2026
281e967
better docs
zucchini-nlp Apr 24, 2026
9555c59
comments: pad and move around
zucchini-nlp Apr 27, 2026
ef1c767
forward in generation
zucchini-nlp Apr 27, 2026
9b9706c
fix repo
zucchini-nlp Apr 27, 2026
c1178aa
prepare token types when sampling as well
zucchini-nlp Apr 27, 2026
d46668a
consume as unused kwags
zucchini-nlp Apr 27, 2026
c8fb8fd
mirror padding mask and add kv-offset
zucchini-nlp Apr 28, 2026
be84c3a
oops
zucchini-nlp Apr 28, 2026
89ababc
Merge branch 'main' into split-out-gemma-style-mask
zucchini-nlp Apr 28, 2026
82cf4d0
wait no
zucchini-nlp Apr 28, 2026
c4c5ce1
Merge branch 'main' into split-out-gemma-style-mask
zucchini-nlp Apr 28, 2026
11ffad6
split to a fn and add a tiny comment
zucchini-nlp May 5, 2026
75ffa0e
Merge branch 'main' into split-out-gemma-style-mask
zucchini-nlp May 5, 2026
e873189
Merge branch 'main' into split-out-gemma-style-mask
zucchini-nlp May 5, 2026
40bdc43
fix for last, drop labels because we test inference!
zucchini-nlp May 6, 2026
76e206b
Merge branch 'main' into split-out-gemma-style-mask
zucchini-nlp May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/transformers/generation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ def prepare_inputs_for_generation(
attention_mask=attention_mask,
past_key_values=model_inputs.get("past_key_values"),
position_ids=model_inputs.get(position_ids_key),
block_sequence_ids=model_inputs.get("block_sequence_ids"),
# The following kwargs are not used in the main function - only on a few models with overloaded `create_masks_for_generate`
token_type_ids=model_inputs.get("token_type_ids"),
mm_token_type_ids=model_inputs.get("mm_token_type_ids"),
Expand Down
61 changes: 59 additions & 2 deletions src/transformers/masking_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ def inner_mask(batch_idx: int, head_idx: int, q_idx: int, kv_idx: int) -> bool:
return inner_mask


def blockwise_overlay(block_sequence_ids: torch.Tensor) -> Callable:
"""
This is an overlay depicting a blockwise masking pattern. Instead of a single
token, each block consists of arbitrary length tokens. In causal setup, each block
can attend to prev block causally and can't attend to future blocks. Within one block
the attention is always bidirectional.
Mostly used in MLLMs when non-text data attends bidirectionally to itself.
"""

def inner_mask(batch_idx: int, head_idx: int, q_idx: int, kv_idx: int) -> bool:
# Unmask if the q and kv come from same group which is not -1 (i.e. non-text)
q_group = block_sequence_ids[batch_idx, q_idx]
kv_group = block_sequence_ids[batch_idx, kv_idx]
return (q_group == kv_group) & (q_group >= 0)

return inner_mask


def sliding_window_causal_mask_function(sliding_window: int) -> Callable:
"""
This return the mask_function function to create a sliding window mask.
Expand Down Expand Up @@ -194,6 +212,19 @@ def prepare_padding_mask(attention_mask: torch.Tensor | None, kv_length: int, kv
return local_padding_mask


def maybe_pad_block_sequence_ids(
block_sequence_ids: torch.Tensor, attention_mask: torch.Tensor | None, kv_length: int, kv_offset: int
) -> torch.Tensor:
"""
Pads the `block_sequence_ids` in case the total length is less than `kv_length`.
Usually that happens with `StaticCache` generation or generating without cache.
Pads to the right with `-1`.
"""
if (padding_length := kv_length + kv_offset - block_sequence_ids.shape[-1]) > 0:
block_sequence_ids = F.pad(block_sequence_ids, pad=(0, padding_length), value=-1)
return block_sequence_ids


def _can_skip_causal_mask_xpu(
padding_mask: torch.Tensor | None,
query_length: int,
Expand Down Expand Up @@ -889,6 +920,7 @@ def create_causal_mask(
position_ids: torch.Tensor | None = None,
or_mask_function: Callable | None = None,
and_mask_function: Callable | None = None,
block_sequence_ids: torch.Tensor | None = None,
) -> torch.Tensor | BlockMask | None:
"""
Create a standard causal mask based on the attention implementation used (stored in the config). If `past_key_values`
Expand Down Expand Up @@ -916,6 +948,10 @@ def create_causal_mask(
and_mask_function (`Callable`, optional):
An optional mask function to combine with the causal mask function (by doing the intersection of both). This is
useful to easily overlay another mask on top of the causal one, for example for image tokens handling.
block_sequence_ids (`torch.Tensor`, *optional*):
A tensor of same shape as input IDs indicating to which block or group each token belongs to. Tokens from
the same block will keep a bidirectional mask within the block, attending causally to the past. Index `-1`
can be used for blocks that have to keep complete causality within itself.
"""
# Power feature: if `is_causal` is False, then fallback to bi-directional mask for bi-directional attention.
# It allows to use decoder-only models with bi-directional attention as well
Expand Down Expand Up @@ -974,10 +1010,14 @@ def create_causal_mask(
allow_is_causal_skip = False
use_vmap = True

# If we detected packing format
# If we detected packing format or blockwise overlay
if packed_sequence_mask is not None:
mask_factory_function = and_masks(mask_factory_function, packed_sequence_mask_function(packed_sequence_mask))
allow_is_causal_skip = False
if block_sequence_ids is not None:
block_sequence_ids = maybe_pad_block_sequence_ids(block_sequence_ids, attention_mask, kv_length, kv_offset)
mask_factory_function = or_masks(mask_factory_function, blockwise_overlay(block_sequence_ids))
allow_is_causal_skip = False
Comment thread
zucchini-nlp marked this conversation as resolved.

# We now create the mask
causal_mask = mask_interface(
Expand Down Expand Up @@ -1006,6 +1046,7 @@ def create_bidirectional_mask(
past_key_values: Cache | None = None,
or_mask_function: Callable | None = None,
and_mask_function: Callable | None = None,
**kwargs,
) -> torch.Tensor | BlockMask | None:
"""
Create a standard bidirectional mask based on the attention implementation used (stored in the config).
Expand Down Expand Up @@ -1098,6 +1139,7 @@ def create_sliding_window_causal_mask(
position_ids: torch.Tensor | None = None,
or_mask_function: Callable | None = None,
and_mask_function: Callable | None = None,
block_sequence_ids: torch.Tensor | None = None,
) -> torch.Tensor | BlockMask | None:
"""
Create a sliding window causal mask based on the attention implementation used (stored in the config). This type
Expand Down Expand Up @@ -1126,6 +1168,10 @@ def create_sliding_window_causal_mask(
and_mask_function (`Callable`, optional):
An optional mask function to combine with the sliding causal mask function (by doing the intersection of both). This is
useful to easily overlay another mask on top of the sliding causal one, for example for image tokens handling.
block_sequence_ids (`torch.Tensor`, *optional*):
A tensor of same shape as input IDs indicating to which block or group each token belongs to. Tokens from
the same block will keep a bidirectional mask within the block, attending causally to the past. Index `-1`
can be used for blocks that have to keep complete causality within itself.
"""
# Power feature: if `is_causal` is False, then fallback to bi-directional mask for bi-directional attention
# It allows to use decoder-only models with bi-directional attention as well
Expand Down Expand Up @@ -1183,10 +1229,14 @@ def create_sliding_window_causal_mask(
allow_is_causal_skip = False
use_vmap = True

# If we detected packing format
# If we detected packing format or blockwise overlay
if packed_sequence_mask is not None:
mask_factory_function = and_masks(mask_factory_function, packed_sequence_mask_function(packed_sequence_mask))
allow_is_causal_skip = False
if block_sequence_ids is not None:
block_sequence_ids = maybe_pad_block_sequence_ids(block_sequence_ids, attention_mask, kv_length, kv_offset)
mask_factory_function = or_masks(mask_factory_function, blockwise_overlay(block_sequence_ids))
allow_is_causal_skip = False

# We now create the mask
causal_mask = mask_interface(
Expand Down Expand Up @@ -1216,6 +1266,7 @@ def create_bidirectional_sliding_window_mask(
past_key_values: Cache | None = None,
or_mask_function: Callable | None = None,
and_mask_function: Callable | None = None,
**kwargs,
) -> torch.Tensor | BlockMask | None:
"""
Create a standard bidirectional sliding window mask based on the attention implementation used (stored in the config).
Expand Down Expand Up @@ -1433,6 +1484,7 @@ def create_masks_for_generate(
position_ids: torch.Tensor | None = None,
or_mask_function: Callable | None = None,
and_mask_function: Callable | None = None,
block_sequence_ids: torch.Tensor | None = None,
**kwargs,
):
"""
Expand All @@ -1458,6 +1510,10 @@ def create_masks_for_generate(
and_mask_function (`Callable`, optional):
An optional mask function to combine with the other mask function (by doing the intersection of both). This is
useful to easily overlay another mask on top of the causal one, for example for image tokens handling.
block_sequence_ids (`torch.Tensor`, *optional*):
A tensor of same shape as input IDs indicating to which block or group each token belongs to. Tokens from
the same block will keep a bidirectional mask within the block, attending causally to the past. Index `-1`
can be used for blocks that have to keep complete causality within itself.
"""
# The attribute reside in the text config for composite models
effective_config = config.get_text_config()
Expand All @@ -1470,6 +1526,7 @@ def create_masks_for_generate(
"position_ids": position_ids,
"or_mask_function": or_mask_function,
"and_mask_function": and_mask_function,
"block_sequence_ids": block_sequence_ids,
Comment thread
zucchini-nlp marked this conversation as resolved.
}

# If the attribute exist, we need several masks
Expand Down
126 changes: 42 additions & 84 deletions src/transformers/models/gemma3/modeling_gemma3.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,71 +702,15 @@ def forward(self, vision_outputs: torch.Tensor):
return projected_vision_outputs.type_as(vision_outputs)


def token_type_ids_mask_function(group_ids: torch.Tensor) -> Callable:
"""
This function adds the correct offsets to the `q_idx` and `kv_idx` as the torch API can only accept lengths,
not start and end indices.
Args:
group_ids (`torch.Tensor`):
A tensor of shape `(bs, len)` assigning each token to a vision group. Tokens with the same group
come from the same input image. Text is denoted by `-1`.
"""

def inner_mask(batch_idx: int, head_idx: int, q_idx: int, kv_idx: int) -> bool:
seq_length = group_ids.shape[-1]

# clamp indices because with static cache they can go beyond `group_ids.shape[-1]`
q_idx_clamped = q_idx.clamp(max=seq_length - 1)
kv_idx_clamped = kv_idx.clamp(max=seq_length - 1)

# Unmask if the q and kv come from same group which is not -1 (i.e. non-text)
q_group = group_ids[batch_idx, q_idx_clamped]
kv_group = group_ids[batch_idx, kv_idx_clamped]
q_group = torch.where(q_idx < seq_length, q_group, -1)
kv_group = torch.where(kv_idx < seq_length, kv_group, -1)
return (q_group == kv_group) & (q_group >= 0)

return inner_mask


@deprecate_kwarg("input_embeds", version="5.6.0", new_name="inputs_embeds")
def create_causal_mask_mapping(
config: PreTrainedConfig,
inputs_embeds: torch.Tensor,
attention_mask: torch.Tensor | None,
past_key_values: Cache | None,
position_ids: torch.Tensor | None,
token_type_ids: torch.Tensor | None = None,
is_first_iteration: bool | None = None,
**kwargs,
) -> dict:
"""
Overwrites the base `create_masks_for_generate` with `token_type_ids` masking to create the causal mask mapping
for all kinds of forward passes. Gemma3 uses a bidirectional mask for images.

Uses `pixel_values` as an optional input to disambiguate edge cases.
"""
mask_kwargs = {
"config": config.get_text_config(),
"inputs_embeds": inputs_embeds,
"attention_mask": attention_mask,
"past_key_values": past_key_values,
"position_ids": position_ids,
}
if token_type_ids is not None:
# We need to pass an additional mask function to account for token type ids, and it needs to be an `or` (to
# undo the causal masking)

# First find where a new image block starts: 1 if image and previous not image
# The images cannot attend to future images, but can attend to all prev images and to itself bidirectionally
is_image = (token_type_ids == 1).to(inputs_embeds.device)
is_previous_image = nn.functional.pad(is_image, (1, 0), value=0)[:, :-1]
new_image_start = is_image & ~is_previous_image
group_ids = torch.cumsum(new_image_start.int(), dim=1) - 1
group_ids = torch.where(is_image, group_ids, -1)
mask_kwargs["or_mask_function"] = token_type_ids_mask_function(group_ids)

return create_masks_for_generate(**mask_kwargs)
def get_block_sequence_ids_for_mask(token_type_ids: torch.Tensor, device: torch.device | None = None) -> torch.Tensor:
# First find where a new image block starts: 1 if image and previous not image
# The images cannot attend to future images, but can attend to all prev images and to itself bidirectionally
is_image = (token_type_ids == 1).to(device=device)
is_previous_image = nn.functional.pad(is_image, (1, 0), value=0)[:, :-1]
new_image_start = is_image & ~is_previous_image
group_ids = torch.cumsum(new_image_start.int(), dim=1) - 1
block_sequence_ids = torch.where(is_image, group_ids, -1)
return block_sequence_ids


@auto_docstring(
Expand Down Expand Up @@ -898,14 +842,25 @@ def forward(

# It may already have been prepared by e.g. `generate`
if not isinstance(causal_mask_mapping := attention_mask, dict):
causal_mask_mapping = create_causal_mask_mapping(
self.config,
inputs_embeds=inputs_embeds,
attention_mask=attention_mask,
past_key_values=past_key_values,
position_ids=position_ids,
token_type_ids=token_type_ids,
)
mask_kwargs = {
"config": self.config.get_text_config(),
"inputs_embeds": inputs_embeds,
"attention_mask": attention_mask,
"past_key_values": past_key_values,
"position_ids": position_ids,
}

if token_type_ids is not None:
mask_kwargs["block_sequence_ids"] = get_block_sequence_ids_for_mask(
token_type_ids, device=inputs_embeds.device
)

# Create the masks
sliding_mask_kwargs = mask_kwargs.copy()
causal_mask_mapping = {
"full_attention": create_causal_mask(**mask_kwargs),
"sliding_attention": create_sliding_window_causal_mask(**sliding_mask_kwargs),
}

outputs = self.language_model(
attention_mask=causal_mask_mapping,
Expand Down Expand Up @@ -1111,17 +1066,20 @@ def create_masks_for_generate(
is_first_iteration: bool | None = False,
**kwargs,
) -> dict:
# Uses the overwritten `create_masks_for_generate` with `token_type_ids` masking
return create_causal_mask_mapping(
config,
inputs_embeds,
attention_mask,
past_key_values,
position_ids,
token_type_ids,
is_first_iteration=is_first_iteration,
**{k: v for k, v in kwargs.items() if k != "pixel_values"},
)
mask_kwargs = {
"config": config.get_text_config(),
"inputs_embeds": inputs_embeds,
"attention_mask": attention_mask,
"past_key_values": past_key_values,
"position_ids": position_ids,
}

if token_type_ids is not None:
mask_kwargs["block_sequence_ids"] = get_block_sequence_ids_for_mask(
token_type_ids, device=inputs_embeds.device
)

return create_masks_for_generate(**mask_kwargs)


class Gemma3ForSequenceClassification(Gemma3PreTrainedModel):
Expand Down
Loading
Loading