Planner-Cache / src /pcm /planner /cache.py
Dermitio's picture
Upload folder using huggingface_hub
ca3d977 verified
Raw
History Blame Contribute Delete
14 kB
"""Fixed-allocation first-class planner state cache."""
from __future__ import annotations
from dataclasses import dataclass
from enum import IntEnum
import math
from typing import Iterable
import torch
from torch import Tensor
import torch.nn.functional as F
class StateOperation(IntEnum):
KEEP = 0
CREATE = 1
MODIFY = 2
MERGE = 3
INVALIDATE = 4
IGNORE = 5
class SlotType(IntEnum):
GOAL = 0
ENTITY = 1
FACT = 2
HYPOTHESIS = 3
CONSTRAINT = 4
TASK = 5
LATENT = 6
EXTERNAL = 7
class Freshness(IntEnum):
FRESH = 0
STALE = 1
UNKNOWN = 2
class Persistence(IntEnum):
PERMANENT = 0
DURABLE = 1
SESSION = 2
EXTERNAL = 3
VOLATILE = 4
class SlotSource(IntEnum):
CONVERSATION = 0
RETRIEVAL = 1
CORRECTION = 2
TOOL = 3
INFERENCE = 4
class CacheFullProtectedError(RuntimeError):
"""Raised when every physical slot is occupied by permanent state."""
@dataclass(frozen=True)
class PlannerCacheConfig:
slots: int = 128
width: int = 512
dtype: torch.dtype = torch.float16
device: str | torch.device = "cpu"
merge_similarity: float = 0.92
def __post_init__(self) -> None:
if self.slots <= 0 or self.width <= 0:
raise ValueError("planner slots and width must be positive")
if not -1.0 <= self.merge_similarity <= 1.0:
raise ValueError("merge_similarity must be between -1 and 1")
class PlannerCache:
"""Preallocated planner values and metadata mutated strictly in place."""
def __init__(self, config: PlannerCacheConfig) -> None:
self.config = config
device = torch.device(config.device)
self.values = torch.zeros((config.slots, config.width), dtype=config.dtype, device=device)
self.valid = torch.zeros(config.slots, dtype=torch.bool, device=device)
self.slot_type = torch.full((config.slots,), int(SlotType.LATENT), dtype=torch.int8, device=device)
self.confidence = torch.zeros(config.slots, dtype=torch.float32, device=device)
self.importance = torch.zeros(config.slots, dtype=torch.float32, device=device)
self.freshness = torch.full((config.slots,), int(Freshness.UNKNOWN), dtype=torch.int8, device=device)
self.persistence = torch.full((config.slots,), int(Persistence.VOLATILE), dtype=torch.int8, device=device)
self.last_updated = torch.zeros(config.slots, dtype=torch.int64, device=device)
self.source = torch.full((config.slots,), int(SlotSource.INFERENCE), dtype=torch.int8, device=device)
self.labels: list[str | None] = [None] * config.slots
self._clock = 0
@property
def device(self) -> torch.device:
return self.values.device
def allocation_signature(self) -> tuple[tuple[int, tuple[int, ...]], ...]:
"""Stable identity/shape signature for physical-allocation tests."""
tensors = (
self.values,
self.valid,
self.slot_type,
self.confidence,
self.importance,
self.freshness,
self.persistence,
self.last_updated,
self.source,
)
return tuple((tensor.data_ptr(), tuple(tensor.shape)) for tensor in tensors)
@property
def occupied(self) -> int:
return int(self.valid.sum().item())
def _tick(self) -> int:
self._clock += 1
return self._clock
def _value(self, value: Tensor) -> Tensor:
value = value.detach().to(device=self.device, dtype=self.config.dtype)
if value.shape != (self.config.width,):
raise ValueError(f"planner value must have shape ({self.config.width},)")
return value
def _require_valid(self, index: int) -> None:
if not 0 <= index < self.config.slots or not bool(self.valid[index]):
raise IndexError(f"planner slot {index} is not valid")
def _write_metadata(
self,
index: int,
*,
slot_type: SlotType,
confidence: float,
importance: float,
freshness: Freshness,
persistence: Persistence,
source: SlotSource,
label: str | None,
) -> None:
self._validate_score("confidence", confidence)
self._validate_score("importance", importance)
slot_type = SlotType(slot_type)
freshness = Freshness(freshness)
persistence = Persistence(persistence)
source = SlotSource(source)
if label is not None and not isinstance(label, str):
raise TypeError("planner label must be a string or None")
self.slot_type[index] = int(slot_type)
self.confidence[index] = confidence
self.importance[index] = importance
self.freshness[index] = int(freshness)
self.persistence[index] = int(persistence)
self.source[index] = int(source)
self.last_updated[index] = self._tick()
self.labels[index] = label
self.valid[index] = True
@staticmethod
def _validate_score(name: str, value: float) -> None:
if not isinstance(value, (int, float)) or not math.isfinite(float(value)):
raise ValueError(f"{name} must be a finite number in [0, 1]")
if not 0.0 <= float(value) <= 1.0:
raise ValueError(f"{name} must be in [0, 1]")
def _merge_candidate(
self, value: Tensor, slot_type: SlotType, merge_mask: Tensor | None = None
) -> int | None:
compatible = self.valid & (self.slot_type == int(slot_type))
if merge_mask is not None:
merge_mask = merge_mask.detach().to(device=self.device, dtype=torch.bool)
if merge_mask.shape != self.valid.shape:
raise ValueError("merge mask must match the planner slot shape")
compatible &= merge_mask
indices = compatible.nonzero(as_tuple=False).flatten()
if indices.numel() == 0:
return None
candidates = self.values.index_select(0, indices).float()
similarities = F.cosine_similarity(candidates, value.float().unsqueeze(0), dim=-1)
best = int(similarities.argmax().item())
if float(similarities[best]) < self.config.merge_similarity:
return None
return int(indices[best].item())
def _eviction_candidate(self) -> int:
candidates = self.valid & (self.persistence != int(Persistence.PERMANENT))
indices = candidates.nonzero(as_tuple=False).flatten()
if indices.numel() == 0:
raise CacheFullProtectedError("all planner slots are permanent")
age = (self._clock + 1 - self.last_updated.index_select(0, indices)).float()
stale_bonus = (self.freshness.index_select(0, indices) != int(Freshness.FRESH)).float()
persistence_cost = torch.tensor(
[4.0, 3.0, 2.0, 1.0, 0.0], device=self.device
).index_select(0, self.persistence.index_select(0, indices).long())
keep_score = (
4.0 * self.importance.index_select(0, indices)
+ self.confidence.index_select(0, indices)
+ persistence_cost
- stale_bonus
- age * 1e-6
)
return int(indices[int(keep_score.argmin().item())].item())
@staticmethod
def _admission_score(
*,
importance: float,
confidence: float,
freshness: Freshness,
persistence: Persistence,
) -> float:
persistence_cost = (4.0, 3.0, 2.0, 1.0, 0.0)[int(persistence)]
stale_cost = 0.0 if freshness == Freshness.FRESH else 1.0
return 4.0 * importance + confidence + persistence_cost - stale_cost
def _slot_admission_score(self, index: int) -> float:
age = (self._clock + 1 - int(self.last_updated[index])) * 1e-6
return self._admission_score(
importance=float(self.importance[index]),
confidence=float(self.confidence[index]),
freshness=Freshness(int(self.freshness[index])),
persistence=Persistence(int(self.persistence[index])),
) - age
def create(
self,
value: Tensor,
*,
slot_type: SlotType = SlotType.LATENT,
confidence: float = 1.0,
importance: float = 0.5,
freshness: Freshness = Freshness.FRESH,
persistence: Persistence = Persistence.SESSION,
source: SlotSource = SlotSource.CONVERSATION,
label: str | None = None,
merge_mask: Tensor | None = None,
) -> tuple[int, StateOperation]:
value = self._value(value)
self._validate_score("confidence", confidence)
self._validate_score("importance", importance)
slot_type = SlotType(slot_type)
freshness = Freshness(freshness)
persistence = Persistence(persistence)
source = SlotSource(source)
merge_index = self._merge_candidate(value, slot_type, merge_mask)
if merge_index is not None:
self.merge((merge_index,), value=value, confidence=confidence, source=source)
self.importance[merge_index] = max(
float(self.importance[merge_index]), importance
)
self.persistence[merge_index] = min(
int(self.persistence[merge_index]), int(persistence)
)
if label is not None:
self.labels[merge_index] = label
return merge_index, StateOperation.MERGE
free = (~self.valid).nonzero(as_tuple=False).flatten()
operation = StateOperation.CREATE
if free.numel():
index = int(free[0].item())
else:
index = self._eviction_candidate()
incoming_score = self._admission_score(
importance=importance,
confidence=confidence,
freshness=freshness,
persistence=persistence,
)
if incoming_score <= self._slot_admission_score(index):
return -1, StateOperation.IGNORE
self.invalidate(index)
self.values[index].copy_(value)
self._write_metadata(
index,
slot_type=slot_type,
confidence=confidence,
importance=importance,
freshness=freshness,
persistence=persistence,
source=source,
label=label,
)
return index, operation
def keep(self, index: int, *, confidence: float | None = None) -> int:
self._require_valid(index)
if confidence is not None:
self._validate_score("confidence", confidence)
self.confidence[index] = confidence
self.last_updated[index] = self._tick()
return index
def modify(
self,
index: int,
value: Tensor,
*,
confidence: float | None = None,
freshness: Freshness = Freshness.FRESH,
source: SlotSource | None = None,
) -> int:
self._require_valid(index)
freshness = Freshness(freshness)
if source is not None:
source = SlotSource(source)
if confidence is not None:
self._validate_score("confidence", confidence)
# A model inference is lower-authority than an explicit user
# correction and cannot silently overwrite it.
if (
source == SlotSource.INFERENCE
and int(self.source[index]) == int(SlotSource.CORRECTION)
):
self.last_updated[index] = self._tick()
return index
self.values[index].copy_(self._value(value))
if confidence is not None:
self.confidence[index] = confidence
self.freshness[index] = int(freshness)
if source is not None:
self.source[index] = int(source)
self.last_updated[index] = self._tick()
return index
def merge(
self,
indices: Iterable[int],
*,
value: Tensor | None = None,
confidence: float | None = None,
source: SlotSource = SlotSource.INFERENCE,
) -> int:
indices = tuple(dict.fromkeys(indices))
if not indices:
raise ValueError("merge requires at least one slot")
for index in indices:
self._require_valid(index)
target = max(indices, key=lambda index: float(self.importance[index]))
merged = self._value(value) if value is not None else self.values[list(indices)].float().mean(0).to(self.config.dtype)
self.values[target].copy_(merged)
if confidence is None:
confidence = max(float(self.confidence[index]) for index in indices)
self._validate_score("confidence", confidence)
self.confidence[target] = confidence
self.importance[target] = max(float(self.importance[index]) for index in indices)
self.freshness[target] = int(Freshness.FRESH)
self.source[target] = int(source)
self.last_updated[target] = self._tick()
for index in indices:
if index != target:
self.invalidate(index)
return target
def invalidate(self, index: int) -> int:
self._require_valid(index)
self.valid[index] = False
self.values[index].zero_()
self.labels[index] = None
self.last_updated[index] = self._tick()
return index
def apply(self, operation: StateOperation, **kwargs):
if operation == StateOperation.KEEP:
return self.keep(**kwargs)
if operation == StateOperation.CREATE:
return self.create(**kwargs)
if operation == StateOperation.MODIFY:
return self.modify(**kwargs)
if operation == StateOperation.MERGE:
return self.merge(**kwargs)
if operation == StateOperation.INVALIDATE:
return self.invalidate(**kwargs)
if operation == StateOperation.IGNORE:
return None
raise ValueError(f"unsupported planner operation: {operation}")