Skip to content

Commit fca687f

Browse files
authored
Key OpenVINO encoder exports by weights and configuration (#20)
1 parent a26a12a commit fca687f

2 files changed

Lines changed: 91 additions & 2 deletions

File tree

‎src/von/backends/option_marker_backend.py‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Score: Single-pass ordinal rating over all levels simultaneously.
77
"""
88

9+
import hashlib
910
import json
1011
import math
1112
import os
@@ -248,12 +249,26 @@ def _ov_core_with_cache(target: str):
248249
return core, cache_dir
249250

250251

252+
def _openvino_encoder_cache_key(encoder: torch.nn.Module) -> str:
253+
import openvino as ov
254+
255+
# A release name or checkpoint path does not identify fine-tuned/replaced weights.
256+
digest = hashlib.sha256()
257+
digest.update(f"{type(encoder).__module__}.{type(encoder).__qualname__}:{torch.__version__}:{ov.__version__}".encode())
258+
digest.update(encoder.config.to_json_string(use_diff=False).encode())
259+
for name, tensor in sorted(encoder.state_dict().items()):
260+
digest.update(f"{name}:{tensor.dtype}:{tuple(tensor.shape)}".encode())
261+
# View bytes directly: supports bfloat16 without allocating a second weights blob.
262+
digest.update(tensor.detach().cpu().contiguous().reshape(-1).view(torch.uint8).numpy())
263+
return digest.hexdigest()
264+
265+
251266
def _compile_openvino_encoder(encoder: torch.nn.Module, target: str = "GPU") -> torch.nn.Module:
252267
"""Compiles the PyTorch ModernBERT encoder (plain mode) to OpenVINO with disk caching."""
253268
import openvino as ov
254269

255270
core, cache_dir = _ov_core_with_cache(target)
256-
xml_path = os.path.join(cache_dir, f"encoder_{VON_MODEL_ID}.xml")
271+
xml_path = os.path.join(cache_dir, f"encoder_{VON_MODEL_ID}_{_openvino_encoder_cache_key(encoder)}.xml")
257272
if os.path.exists(xml_path):
258273
compiled_model = core.compile_model(xml_path, device_name=target)
259274
else:
@@ -280,7 +295,7 @@ def _compile_openvino_independent_encoder(encoder: torch.nn.Module, target: str
280295
import openvino as ov
281296

282297
core, cache_dir = _ov_core_with_cache(target)
283-
xml_path = os.path.join(cache_dir, f"encoder_indep_{VON_MODEL_ID}.xml")
298+
xml_path = os.path.join(cache_dir, f"encoder_indep_{VON_MODEL_ID}_{_openvino_encoder_cache_key(encoder)}.xml")
284299
if os.path.exists(xml_path):
285300
compiled_model = core.compile_model(xml_path, device_name=target)
286301
else:

‎tests/test_openvino_cache.py‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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

Comments
 (0)