Cache OOV morphology grounding structures
- cache morphology index per vocab identity for OOV grounding - cache decomposition results per vocab/token with bounded storage - preserve OOV semantics, audit records, final closure checks, and transient isolation - add focused tests for determinism, audit preservation, transient isolation, closure, and cache reuse
This commit is contained in:
parent
f82f4d36de
commit
cc46dca87a
2 changed files with 119 additions and 3 deletions
|
|
@ -47,6 +47,18 @@ class _GroundedUnknown:
|
||||||
condition: float
|
condition: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class _MorphologyIndex:
|
||||||
|
prefixes: tuple[str, ...]
|
||||||
|
suffixes: tuple[str, ...]
|
||||||
|
roots: dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
_MORPH_INDEX_CACHE: dict[int, _MorphologyIndex] = {}
|
||||||
|
_DECOMPOSITION_CACHE: dict[tuple[int, str], tuple[str, tuple[str, ...], tuple[str, ...]]] = {}
|
||||||
|
_DECOMPOSITION_CACHE_MAX = 4096
|
||||||
|
|
||||||
|
|
||||||
def _compact_root(root: str) -> str:
|
def _compact_root(root: str) -> str:
|
||||||
return root.replace("-", "")
|
return root.replace("-", "")
|
||||||
|
|
||||||
|
|
@ -89,6 +101,22 @@ def _root_surfaces(vocab, morphology_entries: tuple[MorphologyEntry, ...]) -> di
|
||||||
return roots
|
return roots
|
||||||
|
|
||||||
|
|
||||||
|
def _build_morphology_index(vocab, morphology_entries: tuple[MorphologyEntry, ...]) -> _MorphologyIndex:
|
||||||
|
prefixes, suffixes = _known_edges(morphology_entries)
|
||||||
|
roots = _root_surfaces(vocab, morphology_entries)
|
||||||
|
return _MorphologyIndex(prefixes=prefixes, suffixes=suffixes, roots=roots)
|
||||||
|
|
||||||
|
|
||||||
|
def _morphology_index_for(vocab, morphology_entries: tuple[MorphologyEntry, ...]) -> _MorphologyIndex:
|
||||||
|
key = id(vocab)
|
||||||
|
cached = _MORPH_INDEX_CACHE.get(key)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
index = _build_morphology_index(vocab, morphology_entries)
|
||||||
|
_MORPH_INDEX_CACHE[key] = index
|
||||||
|
return index
|
||||||
|
|
||||||
|
|
||||||
def _root_affinity(candidate: str, root: str) -> int:
|
def _root_affinity(candidate: str, root: str) -> int:
|
||||||
common_prefix = 0
|
common_prefix = 0
|
||||||
for left, right in zip(candidate, root):
|
for left, right in zip(candidate, root):
|
||||||
|
|
@ -105,8 +133,16 @@ def _best_decomposition(
|
||||||
vocab,
|
vocab,
|
||||||
morphology_entries: tuple[MorphologyEntry, ...],
|
morphology_entries: tuple[MorphologyEntry, ...],
|
||||||
) -> tuple[str, tuple[str, ...], tuple[str, ...]]:
|
) -> tuple[str, tuple[str, ...], tuple[str, ...]]:
|
||||||
prefixes, suffixes = _known_edges(morphology_entries)
|
vocab_key = id(vocab)
|
||||||
roots = _root_surfaces(vocab, morphology_entries)
|
cache_key = (vocab_key, token)
|
||||||
|
cached = _DECOMPOSITION_CACHE.get(cache_key)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
index = _morphology_index_for(vocab, morphology_entries)
|
||||||
|
prefixes = index.prefixes
|
||||||
|
suffixes = index.suffixes
|
||||||
|
roots = index.roots
|
||||||
prefix_options = ("", *prefixes)
|
prefix_options = ("", *prefixes)
|
||||||
suffix_options = ("", *suffixes)
|
suffix_options = ("", *suffixes)
|
||||||
|
|
||||||
|
|
@ -158,7 +194,11 @@ def _best_decomposition(
|
||||||
if best is None:
|
if best is None:
|
||||||
raise KeyError(f"Token '{token}' cannot be decomposed against mounted morphology.")
|
raise KeyError(f"Token '{token}' cannot be decomposed against mounted morphology.")
|
||||||
_, root_surface, applied_prefixes, applied_suffixes = best
|
_, root_surface, applied_prefixes, applied_suffixes = best
|
||||||
return root_surface, applied_prefixes, applied_suffixes
|
result = (root_surface, applied_prefixes, applied_suffixes)
|
||||||
|
if len(_DECOMPOSITION_CACHE) >= _DECOMPOSITION_CACHE_MAX:
|
||||||
|
_DECOMPOSITION_CACHE.clear()
|
||||||
|
_DECOMPOSITION_CACHE[cache_key] = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _compose_delta(root_versor: np.ndarray, prefixes: tuple[str, ...], suffixes: tuple[str, ...]) -> tuple[np.ndarray, tuple[str, ...]]:
|
def _compose_delta(root_versor: np.ndarray, prefixes: tuple[str, ...], suffixes: tuple[str, ...]) -> tuple[np.ndarray, tuple[str, ...]]:
|
||||||
|
|
|
||||||
76
tests/test_oov_grounding_cache.py
Normal file
76
tests/test_oov_grounding_cache.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
"""Tests for OOV grounding morphology cache behavior."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from algebra.versor import versor_condition
|
||||||
|
from ingest import gate
|
||||||
|
from ingest.gate import inject
|
||||||
|
from language_packs.compiler import load_mounted_packs
|
||||||
|
|
||||||
|
|
||||||
|
def test_oov_grounding_repeated_token_is_deterministic() -> None:
|
||||||
|
vocab_a = load_mounted_packs(("he_logos_micro_v1",))
|
||||||
|
vocab_b = load_mounted_packs(("he_logos_micro_v1",))
|
||||||
|
|
||||||
|
state_a = inject(["דברית"], vocab_a)
|
||||||
|
state_b = inject(["דברית"], vocab_b)
|
||||||
|
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
vocab_a.get_versor("דברית"),
|
||||||
|
vocab_b.get_versor("דברית"),
|
||||||
|
atol=1e-6,
|
||||||
|
)
|
||||||
|
np.testing.assert_allclose(state_a.F, state_b.F, atol=1e-6)
|
||||||
|
|
||||||
|
|
||||||
|
def test_oov_cache_does_not_skip_unknown_token_audit() -> None:
|
||||||
|
vocab = load_mounted_packs(("he_logos_micro_v1",))
|
||||||
|
|
||||||
|
inject(["דברית"], vocab)
|
||||||
|
|
||||||
|
log = vocab.unknown_token_log
|
||||||
|
assert len(log) >= 1
|
||||||
|
assert log[0]["token"] == "דברית"
|
||||||
|
|
||||||
|
|
||||||
|
def test_oov_cache_does_not_share_transients_between_vocab_instances() -> None:
|
||||||
|
vocab_a = load_mounted_packs(("he_logos_micro_v1",))
|
||||||
|
vocab_b = load_mounted_packs(("he_logos_micro_v1",))
|
||||||
|
|
||||||
|
inject(["דברית"], vocab_a)
|
||||||
|
|
||||||
|
assert vocab_a.is_transient("דברית")
|
||||||
|
assert not vocab_b.is_transient("דברית")
|
||||||
|
|
||||||
|
|
||||||
|
def test_oov_cache_preserves_versor_condition() -> None:
|
||||||
|
vocab = load_mounted_packs(("he_logos_micro_v1",))
|
||||||
|
|
||||||
|
state = inject(["דברית"], vocab)
|
||||||
|
|
||||||
|
assert versor_condition(state.F) < 1e-6
|
||||||
|
assert versor_condition(vocab.get_versor("דברית")) < 1e-6
|
||||||
|
|
||||||
|
|
||||||
|
def test_oov_cache_reuses_morphology_index(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
vocab = load_mounted_packs(("he_logos_micro_v1",))
|
||||||
|
|
||||||
|
gate._MORPH_INDEX_CACHE.pop(id(vocab), None)
|
||||||
|
|
||||||
|
calls = 0
|
||||||
|
original = gate._build_morphology_index
|
||||||
|
|
||||||
|
def counted(v, entries):
|
||||||
|
nonlocal calls
|
||||||
|
calls += 1
|
||||||
|
return original(v, entries)
|
||||||
|
|
||||||
|
monkeypatch.setattr(gate, "_build_morphology_index", counted)
|
||||||
|
|
||||||
|
inject(["דברית"], vocab)
|
||||||
|
vocab_b = load_mounted_packs(("he_logos_micro_v1",))
|
||||||
|
inject(["דברית"], vocab_b)
|
||||||
|
|
||||||
|
assert calls <= 2
|
||||||
Loading…
Reference in a new issue