Phillnet-Mini-Max / hf_compat.py
ayjays132's picture
Complete Phillnet Mini Text-Vision release v1.1.0
1e114b1 verified
Raw
History Blame Contribute Delete
7.37 kB
"""Optional Hugging Face compatibility with a fully functional local fallback.
The core model depends only on PyTorch. When Transformers is installed the real
``PretrainedConfig``, ``PreTrainedModel``, ``GenerationMixin`` and ``ModelOutput``
classes are used. The fallback exists so architecture tests and local research do
not silently depend on a network install.
"""
from __future__ import annotations
import json
from dataclasses import dataclass, fields
from pathlib import Path
from typing import Any, ClassVar, Iterator, Mapping
import torch
from torch import nn
try: # pragma: no cover - exercised only when Transformers is available.
from transformers import GenerationConfig, PretrainedConfig, PreTrainedModel
from transformers.generation import GenerationMixin
from transformers.modeling_outputs import CausalLMOutputWithPast
from transformers.utils import ModelOutput
TRANSFORMERS_AVAILABLE = True
class DendroGenerationConfig(GenerationConfig):
"""Keep Dendro's cache backend selector out of HF generation config.
``DendroOmniConfig.cache_implementation`` selects :class:`DendroKVCache`
storage (including Dendro-specific values such as ``"int8"``). Recent
Transformers releases use the same field name for a different set of
cache classes and reject those values while a model is being constructed.
The model config retains the setting; only its generation-config copy is
removed.
"""
@classmethod
def from_model_config(cls, model_config: Any) -> "DendroGenerationConfig":
config_dict = model_config.to_dict() if not isinstance(model_config, dict) else dict(model_config)
config_dict.pop("cache_implementation", None)
return super().from_model_config(config_dict)
except Exception: # pragma: no cover - fallback is covered instead.
TRANSFORMERS_AVAILABLE = False
class PretrainedConfig:
"""Small subset of the Hugging Face configuration contract."""
model_type: ClassVar[str] = "model"
def __init__(self, **kwargs: Any) -> None:
for key, value in kwargs.items():
setattr(self, key, value)
@property
def use_return_dict(self) -> bool:
return bool(getattr(self, "return_dict", True))
def to_dict(self) -> dict[str, Any]:
result = dict(self.__dict__)
result["model_type"] = self.model_type
return result
@classmethod
def from_dict(cls, data: Mapping[str, Any], **kwargs: Any) -> "PretrainedConfig":
merged = dict(data)
merged.update(kwargs)
merged.pop("model_type", None)
return cls(**merged)
def save_pretrained(self, save_directory: str | Path) -> None:
path = Path(save_directory)
path.mkdir(parents=True, exist_ok=True)
(path / "config.json").write_text(
json.dumps(self.to_dict(), indent=2, sort_keys=True), encoding="utf-8"
)
@classmethod
def from_pretrained(cls, path: str | Path, **kwargs: Any) -> "PretrainedConfig":
data = json.loads((Path(path) / "config.json").read_text(encoding="utf-8"))
return cls.from_dict(data, **kwargs)
class GenerationMixin:
"""Marker class used by the local model's own ``generate`` implementation."""
class DendroGenerationConfig:
"""Fallback marker matching the Transformers generation config hook."""
@dataclass
class ModelOutput(Mapping[str, Any]):
"""Dataclass mapping behavior matching the useful part of HF ModelOutput."""
def _items(self) -> list[tuple[str, Any]]:
return [(field.name, getattr(self, field.name)) for field in fields(self) if getattr(self, field.name) is not None]
def __getitem__(self, key: str | int | slice) -> Any:
items = self._items()
if isinstance(key, str):
return dict(items)[key]
return tuple(value for _name, value in items)[key]
def __iter__(self) -> Iterator[str]:
return (name for name, _value in self._items())
def __len__(self) -> int:
return len(self._items())
def keys(self): # type: ignore[override]
return dict(self._items()).keys()
def values(self): # type: ignore[override]
return dict(self._items()).values()
def items(self): # type: ignore[override]
return dict(self._items()).items()
def to_tuple(self) -> tuple[Any, ...]:
return tuple(value for _name, value in self._items())
@dataclass
class CausalLMOutputWithPast(ModelOutput):
loss: torch.Tensor | None = None
logits: torch.Tensor | None = None
past_key_values: Any = None
hidden_states: tuple[torch.Tensor, ...] | None = None
attentions: tuple[torch.Tensor, ...] | None = None
class PreTrainedModel(nn.Module):
"""PyTorch-only persistence compatible with ``save_pretrained`` conventions."""
config_class = PretrainedConfig
base_model_prefix = "model"
main_input_name = "input_ids"
def __init__(self, config: PretrainedConfig, *args: Any, **kwargs: Any) -> None:
del args, kwargs
super().__init__()
self.config = config
def post_init(self) -> None:
return None
def save_pretrained(
self,
save_directory: str | Path,
*,
safe_serialization: bool = True,
**_: Any,
) -> None:
path = Path(save_directory)
path.mkdir(parents=True, exist_ok=True)
self.config.save_pretrained(path)
state = self.state_dict()
if safe_serialization:
try:
from safetensors.torch import save_file
save_file(state, str(path / "model.safetensors"))
return
except Exception:
pass
torch.save(state, path / "pytorch_model.bin")
@classmethod
def from_pretrained(
cls,
pretrained_model_name_or_path: str | Path,
*model_args: Any,
config: PretrainedConfig | None = None,
map_location: str | torch.device = "cpu",
**kwargs: Any,
) -> "PreTrainedModel":
path = Path(pretrained_model_name_or_path)
if config is None:
config = cls.config_class.from_pretrained(path)
model = cls(config, *model_args, **kwargs)
safe_path = path / "model.safetensors"
torch_path = path / "pytorch_model.bin"
if safe_path.exists():
from safetensors.torch import load_file
state = load_file(str(safe_path), device=str(map_location))
elif torch_path.exists():
state = torch.load(torch_path, map_location=map_location, weights_only=True)
else:
raise FileNotFoundError(f"No model.safetensors or pytorch_model.bin found in {path}")
model.load_state_dict(state)
return model
def is_transformers_available() -> bool:
return TRANSFORMERS_AVAILABLE