|
| 1 | +"""Exercise cached exports with tiny encoders, without downloading weights.""" |
| 2 | + |
| 3 | +from unittest.mock import patch |
| 4 | + |
| 5 | +import pytest |
| 6 | +import torch |
| 7 | +from transformers import PretrainedConfig |
| 8 | +from transformers.modeling_outputs import BaseModelOutput |
| 9 | + |
| 10 | +from von.backends import option_marker_backend as backend |
| 11 | + |
| 12 | +ov = pytest.importorskip("openvino") |
| 13 | + |
| 14 | + |
| 15 | +class TinyEncoder(torch.nn.Module): |
| 16 | + def __init__(self, weight, scale=1.0): |
| 17 | + super().__init__() |
| 18 | + self.weight = torch.nn.Parameter(torch.tensor([weight], dtype=torch.float32)) |
| 19 | + self.config = PretrainedConfig(scale=scale) |
| 20 | + |
| 21 | + def forward(self, input_ids, attention_mask, position_ids=None): |
| 22 | + if isinstance(attention_mask, dict): |
| 23 | + mask = attention_mask["full_attention"].sum(-1).squeeze(1) |
| 24 | + mask = mask + attention_mask["sliding_attention"].sum(-1).squeeze(1) |
| 25 | + values = input_ids.float() + position_ids.float() + mask |
| 26 | + else: |
| 27 | + values = input_ids.float() + attention_mask.float() |
| 28 | + return BaseModelOutput(last_hidden_state=(values * self.weight * self.config.scale).unsqueeze(-1)) |
| 29 | + |
| 30 | + |
| 31 | +@pytest.mark.parametrize("independent", [False, True]) |
| 32 | +def test_cached_encoder_tracks_weights_and_config(tmp_path, monkeypatch, independent): |
| 33 | + core = ov.Core() |
| 34 | + core.set_property("CPU", {"CACHE_DIR": str(tmp_path)}) |
| 35 | + monkeypatch.setattr(backend, "_ov_core_with_cache", lambda target: (core, str(tmp_path))) |
| 36 | + compile_encoder = (backend._compile_openvino_independent_encoder if independent |
| 37 | + else backend._compile_openvino_encoder) |
| 38 | + inputs = {"input_ids": torch.ones((1, 128), dtype=torch.long)} |
| 39 | + if independent: |
| 40 | + mask = torch.ones((1, 1, 128, 128), dtype=torch.bool) |
| 41 | + inputs.update(attention_mask={"full_attention": mask, "sliding_attention": mask}, |
| 42 | + position_ids=torch.arange(128).unsqueeze(0)) |
| 43 | + else: |
| 44 | + inputs["attention_mask"] = torch.ones_like(inputs["input_ids"]) |
| 45 | + |
| 46 | + def check(encoder): |
| 47 | + compiled = compile_encoder(encoder, target="CPU") |
| 48 | + with torch.no_grad(): |
| 49 | + expected_inputs = dict(inputs) |
| 50 | + if independent: |
| 51 | + expected_inputs["attention_mask"] = { |
| 52 | + name: backend._bool_to_additive(mask) |
| 53 | + for name, mask in inputs["attention_mask"].items() |
| 54 | + } |
| 55 | + expected = encoder(**expected_inputs).last_hidden_state |
| 56 | + torch.testing.assert_close(compiled(**inputs).last_hidden_state, expected) |
| 57 | + |
| 58 | + with patch.object(ov, "convert_model", wraps=ov.convert_model) as convert: |
| 59 | + check(TinyEncoder(2.0).eval()) |
| 60 | + check(TinyEncoder(2.0).eval()) |
| 61 | + assert convert.call_count == 1, "identical encoders should reuse the export" |
| 62 | + |
| 63 | + encoder = TinyEncoder(3.0).eval() |
| 64 | + check(encoder) |
| 65 | + assert convert.call_count == 2, "different checkpoint weights need a new export" |
| 66 | + |
| 67 | + with torch.no_grad(): |
| 68 | + encoder.weight.fill_(4.0) |
| 69 | + check(encoder) |
| 70 | + assert convert.call_count == 3, "replacing weights in place must invalidate the export" |
| 71 | + |
| 72 | + encoder.config.scale = 2.0 |
| 73 | + check(encoder) |
| 74 | + assert convert.call_count == 4, "configuration changes must invalidate the export" |
0 commit comments