| import os |
| import sys |
|
|
| import torch |
|
|
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
|
|
| from opsd_utils.teacher_batching import ( |
| _image_feature_row_count, |
| align_teacher_prompt_image_tokens, |
| as_batch_num_images_tensor, |
| expected_image_feature_count, |
| get_teacher_vision_for_sample, |
| split_tensor_dict_for_opsd, |
| stack_teacher_processor_batches, |
| student_batch_num_images_tensor, |
| truncate_image_tokens, |
| ) |
|
|
|
|
| def test_split_tensor_dict_dual_image_chunks_legacy_stacked(): |
| """8-sample batch, 2 teacher images each -> 16 image rows; split into 4 GA chunks.""" |
| batch_size = 8 |
| teacher_num_images = torch.tensor([2, 2, 2, 2, 2, 2, 2, 2]) |
| teacher_pixel_values = torch.arange(16 * 2).reshape(16, 2) |
| teacher_prompt_ids = torch.zeros(batch_size, 10) |
|
|
| chunks = split_tensor_dict_for_opsd( |
| { |
| "teacher_prompt_ids": teacher_prompt_ids, |
| "teacher_pixel_values": teacher_pixel_values, |
| "teacher_num_images": teacher_num_images, |
| }, |
| num_chunks=4, |
| ) |
| assert len(chunks) == 4 |
| assert chunks[0]["teacher_prompt_ids"].shape[0] == 2 |
| assert chunks[0]["teacher_pixel_values"].shape[0] == 4 |
| assert chunks[1]["teacher_pixel_values"].shape[0] == 4 |
| assert chunks[0]["teacher_num_images"].tolist() == [2, 2] |
| assert chunks[0]["teacher_pixel_values"][2:4].shape[0] == 2 |
|
|
|
|
| def test_split_tensor_dict_vision_list_mixed_patches(): |
| """Per-sample vision lists preserve variable patch counts across GA split.""" |
| batch_size = 4 |
| pv_list = [ |
| torch.zeros(2, 7, 3, 4, 4), |
| torch.zeros(2, 5, 3, 4, 4), |
| torch.zeros(2, 7, 3, 4, 4), |
| torch.zeros(2, 3, 3, 4, 4), |
| ] |
| chunks = split_tensor_dict_for_opsd( |
| { |
| "teacher_prompt_ids": torch.zeros(batch_size, 10), |
| "teacher_pixel_values_list": pv_list, |
| "teacher_num_images": torch.tensor([2, 2, 2, 2]), |
| }, |
| num_chunks=2, |
| ) |
| assert len(chunks) == 2 |
| assert len(chunks[0]["teacher_pixel_values_list"]) == 2 |
| assert chunks[0]["teacher_pixel_values_list"][0].shape == (2, 7, 3, 4, 4) |
| assert chunks[0]["teacher_pixel_values_list"][1].shape == (2, 5, 3, 4, 4) |
| assert chunks[1]["teacher_pixel_values_list"][1].shape == (2, 3, 3, 4, 4) |
|
|
|
|
| def test_get_teacher_vision_for_sample_from_list(): |
| inputs = { |
| "prompt_ids": torch.zeros(2, 5), |
| "teacher_pixel_values_list": [ |
| torch.zeros(2, 7, 3, 4, 4), |
| torch.zeros(2, 5, 3, 4, 4), |
| ], |
| "teacher_image_sizes_list": [ |
| torch.tensor([[800, 600], [400, 300]]), |
| torch.tensor([[640, 480], [320, 240]]), |
| ], |
| "teacher_num_images": torch.tensor([2, 2]), |
| } |
| pv, sz = get_teacher_vision_for_sample(inputs, 1, [2, 2]) |
| assert pv.shape == (2, 5, 3, 4, 4) |
| assert sz.shape == (2, 2) |
|
|
|
|
| def test_stack_teacher_processor_batches_keeps_per_sample_pixels(): |
| per_sample = [ |
| { |
| "input_ids": torch.zeros(1, 5), |
| "attention_mask": torch.ones(1, 5), |
| "pixel_values": torch.zeros(2, 7, 3, 4, 4), |
| }, |
| { |
| "input_ids": torch.zeros(1, 7), |
| "attention_mask": torch.ones(1, 7), |
| "pixel_values": torch.zeros(2, 5, 3, 4, 4), |
| }, |
| ] |
|
|
| class _Tok: |
| pad_token_id = 0 |
|
|
| class _Proc: |
| tokenizer = _Tok() |
|
|
| out = stack_teacher_processor_batches(_Proc(), per_sample) |
| assert out["input_ids"].shape == (2, 7) |
| assert len(out["pixel_values_list"]) == 2 |
| assert out["pixel_values_list"][0].shape == (2, 7, 3, 4, 4) |
| assert out["pixel_values_list"][1].shape == (2, 5, 3, 4, 4) |
| assert out["batch_num_images"] == [2, 2] |
|
|
|
|
| def test_as_batch_num_images_tensor_shape(): |
| pv = torch.zeros(2, 1, 3, 384, 384) |
| bn = as_batch_num_images_tensor(2, pv) |
| assert bn is not None |
| assert bn.shape == (1,) |
| assert bn.tolist() == [2] |
| assert bn.dtype == torch.long |
|
|
|
|
| def test_as_batch_num_images_tensor_none_cases(): |
| assert as_batch_num_images_tensor(2, None) is None |
| assert as_batch_num_images_tensor(None, torch.zeros(1)) is None |
|
|
|
|
| def test_student_batch_num_images_tensor_collator_layout(): |
| """Processor-batched student pixels: dim0 is batch size, one image per row.""" |
| pv = torch.zeros(4, 7, 3, 384, 384) |
| bn = student_batch_num_images_tensor(pv, batch_rows=4) |
| assert bn is not None |
| assert bn.tolist() == [1, 1, 1, 1] |
|
|
|
|
| def test_student_batch_num_images_tensor_stacked_images(): |
| """Per-sample vision tensor with multiple images (dim0 = num images).""" |
| pv = torch.zeros(2, 7, 3, 384, 384) |
| bn = student_batch_num_images_tensor(pv, batch_rows=1) |
| assert bn is not None |
| assert bn.tolist() == [2] |
|
|
|
|
| def test_image_feature_row_count_list_return(): |
| feats = [torch.zeros(3, 64), torch.zeros(7, 64)] |
| assert _image_feature_row_count(feats) == 10 |
|
|
|
|
| def test_image_feature_row_count_pooler_output(): |
| out = type("Out", (), {"pooler_output": torch.zeros(5, 64)})() |
| assert _image_feature_row_count(out) == 5 |
|
|
|
|
| def test_expected_image_feature_count_passes_batch_num_images(): |
| captured: dict = {} |
|
|
| class _Core: |
| config = type( |
| "C", |
| (), |
| { |
| "vision_feature_layer": -1, |
| "vision_feature_select_strategy": "full", |
| "vision_aspect_ratio": "anyres_max_9", |
| }, |
| )() |
|
|
| def get_image_features(self, pixel_values, image_sizes, **kwargs): |
| captured["batch_num_images"] = kwargs.get("batch_num_images") |
| captured["return_dict"] = kwargs.get("return_dict") |
| return [torch.zeros(6, 64), torch.zeros(4, 64)] |
|
|
| class _Model: |
| model = _Core() |
|
|
| pv = torch.zeros(2, 1, 3, 4, 4) |
| sizes = torch.tensor([[800, 600], [400, 300]]) |
| bn = as_batch_num_images_tensor(2, pv) |
| count = expected_image_feature_count(_Model(), pv, sizes, batch_num_images=bn) |
| assert count == 10 |
| assert captured["batch_num_images"].tolist() == [2] |
| assert captured.get("return_dict") is None |
|
|
|
|
| def test_align_passes_batch_num_images(): |
| captured: dict = {} |
|
|
| class _Core: |
| config = type( |
| "C", |
| (), |
| { |
| "vision_feature_layer": -1, |
| "vision_feature_select_strategy": "full", |
| "vision_aspect_ratio": "anyres_max_9", |
| }, |
| )() |
|
|
| def get_image_features(self, pixel_values, image_sizes, **kwargs): |
| captured["batch_num_images"] = kwargs.get("batch_num_images") |
| return [torch.zeros(4, 64)] |
|
|
| class _Model: |
| model = _Core() |
|
|
| class _Tok: |
| pad_token_id = 0 |
| image_token_id = 151646 |
|
|
| class _Proc: |
| tokenizer = _Tok() |
| image_token = "<image>" |
|
|
| img_id = 151646 |
| ids = torch.tensor([[img_id] * 4 + [1, 2]]) |
| mask = torch.ones(1, 6, dtype=torch.long) |
| pv = torch.zeros(2, 1, 3, 4, 4) |
| sizes = torch.tensor([[800, 600], [400, 300]]) |
| bn = as_batch_num_images_tensor(2, pv) |
| out_ids, out_mask = align_teacher_prompt_image_tokens( |
| _Model(), |
| _Proc(), |
| ids, |
| mask, |
| pv, |
| sizes, |
| batch_num_images=bn, |
| ) |
| assert captured["batch_num_images"].tolist() == [2] |
| assert out_ids.shape == ids.shape |
| assert out_mask.shape == mask.shape |
|
|
|
|
| def test_align_batched_per_row_not_global(): |
| """Two-row batch must align each row independently.""" |
| captured: dict = {"calls": []} |
|
|
| class _Core: |
| config = type( |
| "C", |
| (), |
| { |
| "vision_feature_layer": -1, |
| "vision_feature_select_strategy": "full", |
| "vision_aspect_ratio": "anyres_max_9", |
| }, |
| )() |
|
|
| def get_image_features(self, pixel_values, image_sizes, **kwargs): |
| captured["calls"].append(tuple(pixel_values.shape)) |
| return [torch.zeros(3, 64)] |
|
|
| class _Model: |
| model = _Core() |
|
|
| class _Tok: |
| pad_token_id = 0 |
| image_token_id = 151646 |
|
|
| class _Proc: |
| tokenizer = _Tok() |
| image_token = "<image>" |
|
|
| img_id = 151646 |
| proc = _Proc() |
| ids = torch.tensor( |
| [ |
| [img_id] * 5 + [1, 2, 0, 0], |
| [img_id] + [3, 4, 0, 0, 0, 0, 0, 0], |
| ] |
| ) |
| mask = torch.ones_like(ids) |
| pv = torch.zeros(2, 1, 3, 4, 4) |
| bn = student_batch_num_images_tensor(pv, batch_rows=2) |
| out_ids, _out_mask = align_teacher_prompt_image_tokens( |
| _Model(), |
| proc, |
| ids, |
| mask, |
| pv, |
| None, |
| batch_num_images=bn, |
| ) |
| assert len(captured["calls"]) == 2 |
| assert captured["calls"][0] == (1, 1, 3, 4, 4) |
| assert captured["calls"][1] == (1, 1, 3, 4, 4) |
| assert int((out_ids[0] == img_id).sum().item()) == 3 |
| assert int((out_ids[1] == img_id).sum().item()) == 1 |
|
|
|
|
| def test_truncate_image_tokens_keeps_first_n(): |
| img_id = 151646 |
| pad_id = 0 |
| ids = torch.tensor([[img_id] * 10 + [1, 2, 3]]) |
| mask = torch.ones(1, 13, dtype=torch.long) |
| out_ids, out_mask = truncate_image_tokens(ids, mask, img_id, 4, pad_id) |
| assert int((out_ids == img_id).sum()) == 4 |
| assert out_ids.shape[1] == 7 |
| assert int(out_mask.sum()) == 7 |
|
|
|
|
| def test_move_pixel_values_to_model_device_cpu_model(): |
| from opsd_utils.teacher_batching import ( |
| model_inference_device, |
| model_inference_dtype, |
| move_pixel_values_to_model_device, |
| ) |
|
|
| class _Tiny(torch.nn.Module): |
| def __init__(self): |
| super().__init__() |
| self.conv = torch.nn.Conv2d(3, 4, 1) |
|
|
| model = _Tiny().to(dtype=torch.bfloat16) |
| if torch.cuda.is_available(): |
| pixel = torch.randn(1, 3, 8, 8, device="cuda") |
| else: |
| pixel = torch.randn(1, 3, 8, 8) |
| moved = move_pixel_values_to_model_device(model, pixel) |
| assert moved.device == model_inference_device(model) |
| assert moved.dtype == model_inference_dtype(model) |
|
|
|
|
| def test_model_inference_device_from_embeddings(): |
| from opsd_utils.teacher_batching import model_inference_device |
|
|
| class _EmbModel(torch.nn.Module): |
| def __init__(self): |
| super().__init__() |
| self.embed = torch.nn.Embedding(16, 8) |
|
|
| def get_input_embeddings(self): |
| return self.embed |
|
|
| model = _EmbModel() |
| assert model_inference_device(model) == model.embed.weight.device |
|
|