huggingface/transformers · #49753
Keep image inputs on the first chunk of a chunked prefill
src/transformers/generation/utils.py75 + / 7 −
@@ -174,7 +174,6 @@ "patches_grid", "image_token_pooling", "image_grids",- "image_num_crops", "video_token_pooling", "video_grids", )@@ -701,11 +700,12 @@ def prepare_inputs_for_generation( position_ids_key = "decoder_position_ids" if self.config.is_encoder_decoder else "position_ids" if (position_ids := kwargs.pop(position_ids_key, None)) is not None: model_inputs[position_ids_key] = position_ids- if (token_type_ids := kwargs.pop("token_type_ids", None)) is not None:- model_inputs["token_type_ids"] = token_type_ids+ for token_type_key in ("token_type_ids", "mm_token_type_ids", "moe_mm_token_type_ids"):+ if (token_type := kwargs.pop(token_type_key, None)) is not None:+ model_inputs[token_type_key] = token_type # 3. Slice model inputs if it's an input that should have the same length as `input_ids`- for model_input_name in [position_ids_key, "token_type_ids", "mm_token_type_ids"]:+ for model_input_name in [position_ids_key, "token_type_ids", "mm_token_type_ids", "moe_mm_token_type_ids"]: model_input = model_inputs.get(model_input_name) if model_input is not None and model_input.shape[-1] != sequence_length: # Input can be 2D or 3D, and we always slice on `seq-length` (last dim)@@ -4312,7 +4312,18 @@ def _prefill( chunk_size = generation_config.prefill_chunk_size # In case we are restarting from non-empty Cache and the input_ids also contain the old ones, we need to slice next_sequence_length = 0 if next_sequence_length is None else next_sequence_length- input_chunks = torch.split(input_ids[:, -next_sequence_length:], chunk_size, dim=-1)+ prefill_ids = input_ids[:, -next_sequence_length:]+ input_chunks = torch.split(prefill_ids, chunk_size, dim=-1)+ raw_mm_keys = [+ key+ for key in MULTIMODAL_INPUTS_TO_DROP_OUTSIDE_PREFILL+ if key != "mm_encoder_outputs" and model_kwargs.get(key) is not None+ ]+ if is_first_iteration and len(input_chunks) > 1 and raw_mm_keys:+ raise ValueError(+ f"Chunked prefill cannot split the raw multimodal inputs {raw_mm_keys} into chunks, so pass "+ "`prefill_chunk_size=None` or use a model that pre-encodes them."+ ) model_forward = ( self.get_compiled_call(generation_config.compile_config)@@ -4322,15 +4333,47 @@ def _prefill( attention_mask = model_kwargs.pop("attention_mask", None) position_ids = model_kwargs.pop("position_ids", None)+ token_types = {+ key: model_kwargs.pop(key)+ for key in ("token_type_ids", "mm_token_type_ids", "moe_mm_token_type_ids")+ if model_kwargs.get(key) is not None+ }+ mm_encoder_outputs = model_kwargs.pop("mm_encoder_outputs", None)++ # Pre-encoded features are consumed in placeholder order over (batch, seq): feature row `i` of a modality+ # fills its `i`-th placeholder token, so each chunk gets the rows of the placeholders it contains+ chunk_mm_encoder_outputs = [{} for _ in input_chunks]+ if is_first_iteration and mm_encoder_outputs:+ for modality, output in mm_encoder_outputs.items():+ if output is None:+ continue+ mask = prefill_ids == getattr(self.config, f"{modality}_token_id")+ feature_rows = mask.flatten().cumsum(0).view_as(mask) - 1+ for chunk_idx, (chunk_mask, chunk_rows) in enumerate(+ zip(mask.split(chunk_size, dim=-1), feature_rows.split(chunk_size, dim=-1))+ ):+ if chunk_mask.any():+ chunk_mm_encoder_outputs[chunk_idx][modality] = _slice_mm_encoder_output(+ output, chunk_rows[chunk_mask]+ )+ past_length = cache.get_seq_length()- for input_chunk in input_chunks:+ for chunk_idx, input_chunk in enumerate(input_chunks): current_length = past_length + input_chunk.shape[-1] if attention_mask is not None: model_kwargs["attention_mask"] = attention_mask[:, :current_length] if position_ids is not None: # Always slice on `seq-length` (last dim) model_kwargs["position_ids"] = position_ids[..., past_length:current_length]- model_inputs = self.prepare_inputs_for_generation(input_chunk, **model_kwargs)+ # Up to the chunk end like the attention mask, for the models that read them by position+ for key, token_type in token_types.items():+ model_kwargs[key] = token_type[..., :current_length]+ model_inputs = self.prepare_inputs_for_generation(+ input_chunk, is_first_iteration=is_first_iteration, **model_kwargs+ )++ if chunk_mm_encoder_outputs[chunk_idx]:+ model_inputs["mm_encoder_outputs"] = chunk_mm_encoder_outputs[chunk_idx] outputs = model_forward(**model_inputs, return_dict=True) @@ -4340,11 +4383,36 @@ def _prefill( # Recreate the kwargs based on the full length model_kwargs["attention_mask"] = attention_mask model_kwargs["position_ids"] = position_ids+ model_kwargs.update(token_types)+ if mm_encoder_outputs is not None:+ model_kwargs["mm_encoder_outputs"] = mm_encoder_outputs # Latest outputs contain next token logits return outputs +def _slice_mm_encoder_output(output: ModelOutput, rows: torch.LongTensor) -> ModelOutput:+ """+ Keeps the given rows of the flattened `(num_placeholder_tokens, hidden_size)` features of a pre-encoded multimodal+ output, in the container type the model expects: a single-item list, or a tensor with a batch of one. Only the+ pooled and deepstack features are kept, as those are what the models read from pre-encoded outputs.+ """++ def slice_features(features):+ rows_features = torch.cat([feature.reshape(-1, feature.shape[-1]) for feature in features])[rows]+ if isinstance(features, torch.Tensor):+ return rows_features.unsqueeze(0)+ return [rows_features]++ sliced = {"pooler_output": slice_features(output["pooler_output"])}+ deepstack_features = output.get("deepstack_features")+ if deepstack_features is not None:+ if isinstance(deepstack_features[0][0], torch.Tensor):+ deepstack_features = [slice_features(layer_features) for layer_features in deepstack_features]+ sliced["deepstack_features"] = deepstack_features+ return type(output)(**sliced)++ def _speculative_sampling( candidate_input_ids, candidate_logits,src/transformers/models/gemma3/modeling_gemma3.py4 + / 1 −
@@ -1034,7 +1034,10 @@ def prepare_inputs_for_generation(self, input_ids, use_cache=True, is_first_iter model_inputs = super().prepare_inputs_for_generation( input_ids, use_cache=use_cache, is_first_iteration=is_first_iteration, **kwargs )- if not (is_first_iteration or not use_cache):+ if is_first_iteration or not use_cache:+ # The bidirectional mask reads them by position, so a prefill chunk needs them from the start of the sequence+ model_inputs["token_type_ids"] = kwargs.get("token_type_ids")+ else: # Don't pass to not apply bidirectional mask on top model_inputs["token_type_ids"] = None src/transformers/models/gemma3/modular_gemma3.py4 + / 1 −
@@ -858,7 +858,10 @@ def prepare_inputs_for_generation(self, input_ids, use_cache=True, is_first_iter model_inputs = super().prepare_inputs_for_generation( input_ids, use_cache=use_cache, is_first_iteration=is_first_iteration, **kwargs )- if not (is_first_iteration or not use_cache):+ if is_first_iteration or not use_cache:+ # The bidirectional mask reads them by position, so a prefill chunk needs them from the start of the sequence+ model_inputs["token_type_ids"] = kwargs.get("token_type_ids")+ else: # Don't pass to not apply bidirectional mask on top model_inputs["token_type_ids"] = None src/transformers/models/gemma4/modeling_gemma4.py4 + / 1 −
@@ -2677,7 +2677,10 @@ def prepare_inputs_for_generation(self, input_ids, use_cache=True, is_first_iter model_inputs = super().prepare_inputs_for_generation( input_ids, use_cache=use_cache, is_first_iteration=is_first_iteration, **kwargs )- if not (is_first_iteration or not use_cache):+ if is_first_iteration or not use_cache:+ # The bidirectional mask reads them by position, so a prefill chunk needs them from the start of the sequence+ model_inputs["mm_token_type_ids"] = kwargs.get("mm_token_type_ids")+ else: # Don't pass to not apply bidirectional mask on top model_inputs["mm_token_type_ids"] = None # If `per_layer_inputs` was provided along with `inputs_embeds` for first forward, drop it for subsequent forwardssrc/transformers/models/gemma4/modular_gemma4.py4 + / 1 −
@@ -2349,7 +2349,10 @@ def prepare_inputs_for_generation(self, input_ids, use_cache=True, is_first_iter model_inputs = super().prepare_inputs_for_generation( input_ids, use_cache=use_cache, is_first_iteration=is_first_iteration, **kwargs )- if not (is_first_iteration or not use_cache):+ if is_first_iteration or not use_cache:+ # The bidirectional mask reads them by position, so a prefill chunk needs them from the start of the sequence+ model_inputs["mm_token_type_ids"] = kwargs.get("mm_token_type_ids")+ else: # Don't pass to not apply bidirectional mask on top model_inputs["mm_token_type_ids"] = None # If `per_layer_inputs` was provided along with `inputs_embeds` for first forward, drop it for subsequent forwardssrc/transformers/models/gemma4_unified/modeling_gemma4_unified.py4 + / 1 −
@@ -1414,7 +1414,10 @@ def prepare_inputs_for_generation(self, input_ids, use_cache=True, is_first_iter model_inputs = super().prepare_inputs_for_generation( input_ids, use_cache=use_cache, is_first_iteration=is_first_iteration, **kwargs )- if not (is_first_iteration or not use_cache):+ if is_first_iteration or not use_cache:+ # The bidirectional mask reads them by position, so a prefill chunk needs them from the start of the sequence+ model_inputs["mm_token_type_ids"] = kwargs.get("mm_token_type_ids")+ else: # Don't pass to not apply bidirectional mask on top model_inputs["mm_token_type_ids"] = None src/transformers/models/gemma4_unified/modular_gemma4_unified.py4 + / 1 −
@@ -1312,7 +1312,10 @@ def prepare_inputs_for_generation(self, input_ids, use_cache=True, is_first_iter model_inputs = super().prepare_inputs_for_generation( input_ids, use_cache=use_cache, is_first_iteration=is_first_iteration, **kwargs )- if not (is_first_iteration or not use_cache):+ if is_first_iteration or not use_cache:+ # The bidirectional mask reads them by position, so a prefill chunk needs them from the start of the sequence+ model_inputs["mm_token_type_ids"] = kwargs.get("mm_token_type_ids")+ else: # Don't pass to not apply bidirectional mask on top model_inputs["mm_token_type_ids"] = None src/transformers/models/molmo2/modeling_molmo2.py17 + / 0 −
@@ -1035,6 +1035,23 @@ def forward( image_hidden_states=outputs.image_hidden_states, ) + def prepare_inputs_for_generation(+ self, input_ids, inputs_embeds=None, use_cache=True, is_first_iteration=False, **kwargs+ ):+ model_inputs = super().prepare_inputs_for_generation(+ input_ids,+ inputs_embeds=inputs_embeds,+ use_cache=use_cache,+ is_first_iteration=is_first_iteration,+ **kwargs,+ )+ if is_first_iteration or not use_cache:+ # The block mask reads them by position, so a prefill chunk needs them from the start of the sequence+ model_inputs["mm_token_type_ids"] = kwargs.get("mm_token_type_ids")+ else:+ model_inputs["mm_token_type_ids"] = None+ return model_inputs+ @staticmethod def create_masks_for_generate( config: PreTrainedConfig,src/transformers/models/molmo2/modular_molmo2.py17 + / 0 −
@@ -1781,6 +1781,23 @@ def forward( image_hidden_states=outputs.image_hidden_states, ) + def prepare_inputs_for_generation(+ self, input_ids, inputs_embeds=None, use_cache=True, is_first_iteration=False, **kwargs+ ):+ model_inputs = super().prepare_inputs_for_generation(+ input_ids,+ inputs_embeds=inputs_embeds,+ use_cache=use_cache,+ is_first_iteration=is_first_iteration,+ **kwargs,+ )+ if is_first_iteration or not use_cache:+ # The block mask reads them by position, so a prefill chunk needs them from the start of the sequence+ model_inputs["mm_token_type_ids"] = kwargs.get("mm_token_type_ids")+ else:+ model_inputs["mm_token_type_ids"] = None+ return model_inputs+ @staticmethod def create_masks_for_generate( config: PreTrainedConfig,tests/utils/test_cache_utils.py296 + / 0 −
@@ -48,19 +48,33 @@ AutoModelForCausalLM, AutoTokenizer, Cache,+ CLIPVisionConfig,+ CTRLConfig,+ CTRLLMHeadModel, DynamicCache,+ Ernie4_5_VLMoeConfig,+ Ernie4_5_VLMoeForConditionalGeneration, Gemma2Config,+ Gemma3Config,+ Gemma3ForConditionalGeneration,+ Gemma3TextConfig,+ Gemma4Config, Gemma4ForCausalLM,+ Gemma4ForConditionalGeneration, Gemma4TextConfig,+ Gemma4VisionConfig, GenerationConfig, LlamaConfig, LlamaForCausalLM,+ LlavaConfig,+ LlavaForConditionalGeneration, MixtralConfig, MixtralForCausalLM, PreTrainedConfig, QuantizedCache, Qwen3VLConfig, Qwen3VLForConditionalGeneration,+ SiglipVisionConfig, StaticCache, convert_and_export_with_cache, pipeline,@@ -519,6 +533,288 @@ def test_chunked_prefill_cache_continuation(self, cache_implementation): self.assertTrue(torch.equal(no_chunked_only_new_ids, chunked_only_new_ids)) self.assertTrue(torch.equal(chunked_only_new_ids, chunked_full_ids[:, -chunked_only_new_ids.shape[-1] :])) + @staticmethod+ def _get_tiny_llava():+ set_seed(3)+ config = LlavaConfig(+ vision_config=CLIPVisionConfig(+ hidden_size=16,+ intermediate_size=32,+ num_hidden_layers=1,+ num_attention_heads=2,+ image_size=8,+ patch_size=4,+ projection_dim=16,+ ),+ text_config=LlamaConfig(+ vocab_size=64,+ hidden_size=16,+ intermediate_size=32,+ num_hidden_layers=1,+ num_attention_heads=2,+ num_key_value_heads=2,+ max_position_embeddings=64,+ pad_token_id=0,+ ),+ image_token_index=63,+ vision_feature_layer=-1,+ vision_feature_select_strategy="full",+ )+ return LlavaForConditionalGeneration(config).to(torch_device).eval()++ def test_chunked_prefill_keeps_image_inputs(self):+ """+ A chunked prefill must pass the image inputs to its first forward, as the unchunked prefill does. They were+ dropped from every chunk, so a vision-language model generated without the image.+ """+ model = self._get_tiny_llava()+ # 5 image tokens (4 patches + CLS) in the first chunk of 6, then 4 text tokens+ input_ids = torch.tensor([[1] + [63] * 5 + [5, 6, 7, 8]], device=torch_device)+ image_a = torch.randn(1, 3, 8, 8, device=torch_device)+ image_b = torch.randn(1, 3, 8, 8, device=torch_device) * 5+ generation_kwargs = {+ "max_new_tokens": 6,+ "do_sample": False,+ "output_scores": True,+ "return_dict_in_generate": True,+ }++ expected = model.generate(input_ids, pixel_values=image_a, **generation_kwargs)+ out = model.generate(input_ids, pixel_values=image_a, **generation_kwargs, prefill_chunk_size=6)+ out_b = model.generate(input_ids, pixel_values=image_b, **generation_kwargs, prefill_chunk_size=6)++ self.assertTrue(torch.equal(out.sequences, expected.sequences))+ torch.testing.assert_close(torch.stack(out.scores), torch.stack(expected.scores))+ self.assertFalse(torch.equal(torch.stack(out.scores), torch.stack(out_b.scores)))++ def test_chunked_prefill_splits_image_features(self):+ """+ A chunk boundary may fall inside a run of image tokens: each chunk must get the image features of its own image+ tokens, so that any `prefill_chunk_size` gives the same output as the unchunked prefill.+ """+ model = self._get_tiny_llava()+ generation_kwargs = {+ "max_new_tokens": 6,+ "do_sample": False,+ "output_scores": True,+ "return_dict_in_generate": True,+ }+ one_image = torch.tensor([[1] + [63] * 5 + [5, 6, 7, 8]], device=torch_device)+ # Image runs at positions 1-5 and 8-12: chunks of 4 split both of them+ two_images = torch.tensor([[1] + [63] * 5 + [5, 6] + [63] * 5 + [7, 8]], device=torch_device)+ two_prompts = torch.tensor(+ [[1] + [63] * 5 + [5, 6, 7, 8], [1, 9, 10] + [63] * 5 + [11, 12]], device=torch_device+ )+ cases = [+ (one_image, torch.randn(1, 3, 8, 8, device=torch_device), [4, 2]),+ (two_images, torch.randn(2, 3, 8, 8, device=torch_device), [4, 3]),+ (two_prompts, torch.randn(2, 3, 8, 8, device=torch_device), [4]),+ ]+ for input_ids, pixel_values, chunk_sizes in cases:+ expected = model.generate(input_ids, pixel_values=pixel_values, **generation_kwargs)+ for chunk_size in chunk_sizes:+ case = f"{pixel_values.shape[0]} images, batch {input_ids.shape[0]}, prefill_chunk_size={chunk_size}"+ out = model.generate(+ input_ids, pixel_values=pixel_values, **generation_kwargs, prefill_chunk_size=chunk_size+ )+ self.assertTrue(torch.equal(out.sequences, expected.sequences), case)+ torch.testing.assert_close(torch.stack(out.scores), torch.stack(expected.scores), msg=case)++ def test_chunked_prefill_raises_on_raw_image_inputs(self):+ """+ Raw image inputs cannot be split into chunks: a model that does not pre-encode them must raise when the prefill+ is split, and still work when the prefill fits in one chunk.+ """+ model = self._get_tiny_llava()+ model._supports_mm_encoder_outputs = lambda: False+ input_ids = torch.tensor([[1] + [63] * 5 + [5, 6, 7, 8]], device=torch_device)+ pixel_values = torch.randn(1, 3, 8, 8, device=torch_device)+ generation_kwargs = {+ "max_new_tokens": 6,+ "do_sample": False,+ "output_scores": True,+ "return_dict_in_generate": True,+ }++ with self.assertRaisesRegex(ValueError, "pixel_values"):+ model.generate(input_ids, pixel_values=pixel_values, **generation_kwargs, prefill_chunk_size=6)++ expected = model.generate(input_ids, pixel_values=pixel_values, **generation_kwargs)+ out = model.generate(+ input_ids, pixel_values=pixel_values, **generation_kwargs, prefill_chunk_size=input_ids.shape[1]+ )+ self.assertTrue(torch.equal(out.sequences, expected.sequences))+ torch.testing.assert_close(torch.stack(out.scores), torch.stack(expected.scores))++ def test_chunked_prefill_keeps_token_type_ids(self):+ """+ Gemma3 and Gemma4 build a bidirectional mask over each image run from `token_type_ids` and `mm_token_type_ids`.+ Every chunk of a chunked prefill must get them, or the image tokens of a later chunk attend causally only and+ the output differs from the unchunked prefill.+ """+ set_seed(3)+ config = Gemma3Config(+ vision_config=SiglipVisionConfig(+ hidden_size=16,+ intermediate_size=32,+ num_hidden_layers=1,+ num_attention_heads=2,+ image_size=8,+ patch_size=2,+ ),+ text_config=Gemma3TextConfig(+ vocab_size=64,+ hidden_size=16,+ intermediate_size=32,+ num_hidden_layers=2,+ num_attention_heads=2,+ num_key_value_heads=2,+ head_dim=8,+ max_position_embeddings=64,+ sliding_window=16,+ layer_types=["sliding_attention", "full_attention"],+ pad_token_id=0,+ ),+ mm_tokens_per_image=4,+ boi_token_index=61,+ eoi_token_index=62,+ image_token_index=63,+ )+ model = Gemma3ForConditionalGeneration(config).to(torch_device).eval()+ generation_kwargs = {+ "max_new_tokens": 6,+ "do_sample": False,+ "output_scores": True,+ "return_dict_in_generate": True,+ }+ image = [61] + [63] * 4 + [62]+ # Chunks of 4: the image run fills the second chunk+ later_image = torch.tensor([[1, 2, 3] + image + [5, 6, 7]], device=torch_device)+ # Chunks of 8: one image run in each chunk+ two_images = torch.tensor([[1] + image + [5, 6] + image + [7]], device=torch_device)+ cases = [+ (later_image, torch.randn(1, 3, 8, 8, device=torch_device), 4),+ (two_images, torch.randn(2, 3, 8, 8, device=torch_device), 8),+ ]+ for input_ids, pixel_values, chunk_size in cases:+ inputs = {"pixel_values": pixel_values, "token_type_ids": (input_ids == 63).long()}+ expected = model.generate(input_ids, **inputs, **generation_kwargs)+ out = model.generate(input_ids, **inputs, **generation_kwargs, prefill_chunk_size=chunk_size)+ case = f"{pixel_values.shape[0]} images, prefill_chunk_size={chunk_size}"+ self.assertTrue(torch.equal(out.sequences, expected.sequences), case)+ torch.testing.assert_close(torch.stack(out.scores), torch.stack(expected.scores), msg=case)++ config = Gemma4Config(+ vision_config=Gemma4VisionConfig(+ hidden_size=16,+ intermediate_size=32,+ num_hidden_layers=1,+ num_attention_heads=2,+ num_key_value_heads=2,+ head_dim=8,+ patch_size=2,+ pooling_kernel_size=2,+ position_embedding_size=16,+ ),+ text_config=Gemma4TextConfig(+ vocab_size=64,+ hidden_size=16,+ intermediate_size=32,+ num_hidden_layers=2,+ num_attention_heads=2,+ num_key_value_heads=2,+ head_dim=8,+ max_position_embeddings=64,+ sliding_window=16,+ layer_types=["sliding_attention", "full_attention"],+ vocab_size_per_layer_input=64,+ hidden_size_per_layer_input=8,+ use_bidirectional_attention="vision",+ pad_token_id=0,+ ),+ boi_token_id=61,+ eoi_token_id=62,+ image_token_id=63,+ video_token_id=60,+ audio_token_id=59,+ )+ model = Gemma4ForConditionalGeneration(config).to(torch_device).eval()+ # A 4x4 grid of patches, pooled 2x2 into the 4 image tokens of the second chunk+ grid = torch.stack([torch.arange(4).repeat(4), torch.arange(4).repeat_interleave(4)], dim=-1)+ inputs = {+ "pixel_values": torch.randn(1, 16, 12, device=torch_device),+ "image_position_ids": grid[None].to(torch_device),+ "mm_token_type_ids": (later_image == 63).long(),+ }+ expected = model.generate(later_image, **inputs, **generation_kwargs)+ out = model.generate(later_image, **inputs, **generation_kwargs, prefill_chunk_size=4)+ self.assertTrue(torch.equal(out.sequences, expected.sequences))+ torch.testing.assert_close(torch.stack(out.scores), torch.stack(expected.scores))++ def test_chunked_prefill_slices_per_token_types(self):+ """+ Ernie4.5-VL-MoE routes each token by its `moe_mm_token_type_ids`, so every chunk of a chunked prefill must+ get the types of its own tokens. CTRL drops `token_type_ids` in generation, so no chunk may get them back.+ """+ set_seed(3)+ # Both layers route by type, so a wrong route of the first one reaches the later chunks through the cache+ config = Ernie4_5_VLMoeConfig(+ text_config={+ "vocab_size": 64,+ "hidden_size": 32,+ "intermediate_size": 32,+ "num_hidden_layers": 2,+ "num_attention_heads": 2,+ "num_key_value_heads": 1,+ "rope_parameters": {"rope_type": "default", "rope_theta": 500_000.0, "mrope_section": [3, 3, 2]},+ "moe_intermediate_size": [32, 32],+ "mlp_layer_types": ["sparse", "sparse"],+ "pad_token_id": 0,+ },+ vision_config={+ "depth": 1,+ "hidden_size": 32,+ "intermediate_size": 32,+ "num_heads": 2,+ "spatial_merge_size": 1,+ },+ image_start_token_id=5,+ image_end_token_id=6,+ image_token_id=7,+ video_start_token_id=3,+ video_end_token_id=4,+ video_token_id=8,+ )+ model = Ernie4_5_VLMoeForConditionalGeneration(config).to(torch_device).eval()+ generation_kwargs = {+ "max_new_tokens": 6,+ "do_sample": False,+ "output_scores": True,+ "return_dict_in_generate": True,+ }+ # Chunks of 4: the 2x2 patches of the image fill the second chunk+ input_ids = torch.tensor([[1, 9, 10, 5] + [7] * 4 + [6, 11, 12, 13]], device=torch_device)+ inputs = {+ "pixel_values": torch.randn(4, 3 * 14 * 14, device=torch_device),+ "image_grid_thw": torch.tensor([[1, 2, 2]], device=torch_device),+ "mm_token_type_ids": (input_ids == 7).long(),+ "moe_mm_token_type_ids": torch.isin(input_ids, torch.tensor([5, 6, 7], device=torch_device)).long(),+ }+ expected = model.generate(input_ids, **inputs, **generation_kwargs)+ out = model.generate(input_ids, **inputs, **generation_kwargs, prefill_chunk_size=4)+ self.assertTrue(torch.equal(out.sequences, expected.sequences))+ torch.testing.assert_close(torch.stack(out.scores), torch.stack(expected.scores))++ set_seed(3)+ config = CTRLConfig(vocab_size=64, n_embd=16, dff=32, n_layer=1, n_head=2, n_positions=64, pad_token_id=0)+ model = CTRLLMHeadModel(config).to(torch_device).eval()+ input_ids = torch.tensor([[1, 9, 10, 11, 12, 13, 14, 15, 16, 17]], device=torch_device)+ token_type_ids = torch.tensor([[0, 0, 1, 1, 0, 0, 1, 1, 0, 0]], device=torch_device)+ expected = model.generate(input_ids, token_type_ids=token_type_ids, **generation_kwargs)+ out = model.generate(input_ids, token_type_ids=token_type_ids, **generation_kwargs, prefill_chunk_size=4)+ self.assertTrue(torch.equal(out.sequences, expected.sequences))+ torch.testing.assert_close(torch.stack(out.scores), torch.stack(expected.scores))+ def test_dynamic_layers_reset_drops_their_states(self): """ Regression test: `reset` used to zero the dynamic layers' states in place, inherited from the static layers.