refactor: simplify BaseFactory and separate ModelFactory from AutoModel
- Extract _resolve_base_type and _validate_component as module-level helpers - Replace ForwardRef._evaluate private API with eval in module namespace - Remove broad except Exception in __init_subclass__, _component_base always set - Replace direct _entries mutation in strategy.py with register() call form - Remove dead TOKENIZER_CLASSES registry from AutoTokenizer - Extract ModelFactory(BaseFactory[nn.Module]) as pure factory - AutoModel now inherits only nn.Module, no factory state - Move @AutoModel.register to @ModelFactory.register in transformer.py and encoder.py
This commit is contained in:
+41
-35
@@ -13,41 +13,63 @@ from typing import (
|
|||||||
Type,
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
|
get_args,
|
||||||
|
get_origin,
|
||||||
)
|
)
|
||||||
from typing import get_args as _get_args
|
|
||||||
from typing import get_origin as _get_origin
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
def _resolve_type(
|
def _resolve_base_type(
|
||||||
arg: Union[Type, str, ForwardRef], factory_cls: type
|
arg: Union[Type, str, ForwardRef], factory_cls: type
|
||||||
) -> Optional[Type]:
|
) -> Optional[Type]:
|
||||||
"""Resolve a generic type-arg (str forward-ref, ForwardRef, or class)."""
|
"""Resolve the generic type-arg T to a concrete class.
|
||||||
if not isinstance(arg, (str, ForwardRef)):
|
|
||||||
|
- Concrete class (``BaseFactory[MyBase]``): returned directly.
|
||||||
|
- Forward reference (``BaseFactory["MyBase"]``): ``Base["X"]``
|
||||||
|
produces a ``ForwardRef("X")`` at class-creation time. We
|
||||||
|
extract the name and evaluate it in the factory module's
|
||||||
|
global namespace — the same mechanism ``typing.get_type_hints``
|
||||||
|
uses internally.
|
||||||
|
"""
|
||||||
|
if isinstance(arg, type):
|
||||||
return arg
|
return arg
|
||||||
|
|
||||||
name = arg if isinstance(arg, str) else arg.__forward_arg__
|
if isinstance(arg, str):
|
||||||
if name == factory_cls.__name__:
|
name = arg
|
||||||
return factory_cls
|
elif isinstance(arg, ForwardRef):
|
||||||
|
name = arg.__forward_arg__
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
mod = sys.modules.get(factory_cls.__module__)
|
mod = sys.modules.get(factory_cls.__module__)
|
||||||
if mod is None:
|
if mod is None:
|
||||||
return None
|
return None
|
||||||
ns = vars(mod)
|
try:
|
||||||
|
return eval(name, vars(mod)) # noqa: S307
|
||||||
|
except NameError:
|
||||||
|
return None
|
||||||
|
|
||||||
if isinstance(arg, ForwardRef):
|
|
||||||
return arg._evaluate(ns, None, recursive_guard=frozenset())
|
|
||||||
|
|
||||||
return ns.get(name)
|
def _validate_component(component_cls: Type, base: Optional[Type]) -> None:
|
||||||
|
"""Validate that *component_cls* inherits from *base*.
|
||||||
|
|
||||||
|
No-op when *base* is ``None`` (e.g. forward-ref resolution failed).
|
||||||
|
"""
|
||||||
|
if base is not None and not issubclass(component_cls, base):
|
||||||
|
raise TypeError(f"{component_cls.__name__} must inherit from {base.__name__}")
|
||||||
|
|
||||||
|
|
||||||
class BaseFactory(ABC, Generic[T]):
|
class BaseFactory(ABC, Generic[T]):
|
||||||
"""Generic factory with decorator-based component registration.
|
"""Generic factory with decorator-based registration.
|
||||||
|
|
||||||
|
Create a factory by subclassing with the desired base type::
|
||||||
|
|
||||||
class MyFactory(BaseFactory[MyBase]):
|
class MyFactory(BaseFactory[MyBase]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
Register components with the ``register`` decorator::
|
||||||
|
|
||||||
@MyFactory.register("custom")
|
@MyFactory.register("custom")
|
||||||
class CustomComponent(MyBase):
|
class CustomComponent(MyBase):
|
||||||
...
|
...
|
||||||
@@ -64,13 +86,10 @@ class BaseFactory(ABC, Generic[T]):
|
|||||||
def __init_subclass__(cls, **kwargs):
|
def __init_subclass__(cls, **kwargs):
|
||||||
super().__init_subclass__(**kwargs)
|
super().__init_subclass__(**kwargs)
|
||||||
for orig_base in getattr(cls, "__orig_bases__", ()):
|
for orig_base in getattr(cls, "__orig_bases__", ()):
|
||||||
if _get_origin(orig_base) is BaseFactory:
|
if get_origin(orig_base) is BaseFactory:
|
||||||
(arg,) = _get_args(orig_base)
|
(arg,) = get_args(orig_base)
|
||||||
cls._entries = {}
|
cls._entries = {}
|
||||||
try:
|
cls._component_base = _resolve_base_type(arg, cls)
|
||||||
cls._component_base = _resolve_type(arg, cls)
|
|
||||||
except Exception:
|
|
||||||
cls._component_base = None
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -82,7 +101,7 @@ class BaseFactory(ABC, Generic[T]):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(component_cls: Type[T]) -> Type[T]:
|
def decorator(component_cls: Type[T]) -> Type[T]:
|
||||||
cls._validate_component(component_cls)
|
_validate_component(component_cls, cls._component_base)
|
||||||
if name in cls._entries:
|
if name in cls._entries:
|
||||||
raise ValueError(f"Component '{name}' is already registered")
|
raise ValueError(f"Component '{name}' is already registered")
|
||||||
cls._entries[name] = component_cls
|
cls._entries[name] = component_cls
|
||||||
@@ -95,12 +114,11 @@ class BaseFactory(ABC, Generic[T]):
|
|||||||
"""Create a component instance by name, filtering kwargs to match
|
"""Create a component instance by name, filtering kwargs to match
|
||||||
the component's ``__init__`` signature.
|
the component's ``__init__`` signature.
|
||||||
"""
|
"""
|
||||||
entry = cls._entries.get(name)
|
component_cls = cls._entries.get(name)
|
||||||
if entry is None:
|
if component_cls is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unknown component: '{name}'. Supported types: {sorted(cls._entries)}"
|
f"Unknown component: '{name}'. Supported types: {sorted(cls._entries)}"
|
||||||
)
|
)
|
||||||
component_cls = entry
|
|
||||||
sig = inspect.signature(component_cls.__init__)
|
sig = inspect.signature(component_cls.__init__)
|
||||||
has_var_kwargs = any(
|
has_var_kwargs = any(
|
||||||
p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
|
p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
|
||||||
@@ -114,18 +132,6 @@ class BaseFactory(ABC, Generic[T]):
|
|||||||
kwargs = {k: v for k, v in kwargs.items() if k in valid}
|
kwargs = {k: v for k, v in kwargs.items() if k in valid}
|
||||||
return component_cls(*args, **kwargs)
|
return component_cls(*args, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _validate_component(cls, component_cls: Type[T]):
|
|
||||||
"""Validate the decorated class inherits from the factory's base type.
|
|
||||||
|
|
||||||
Override for custom validation beyond ``issubclass``.
|
|
||||||
"""
|
|
||||||
base = cls._component_base
|
|
||||||
if base is not None and not issubclass(component_cls, base):
|
|
||||||
raise TypeError(
|
|
||||||
f"{component_cls.__name__} must inherit from {base.__name__}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_component_class(cls, name: str) -> Type[T]:
|
def get_component_class(cls, name: str) -> Type[T]:
|
||||||
"""Get the registered component class without instantiating it."""
|
"""Get the registered component class without instantiating it."""
|
||||||
|
|||||||
@@ -40,11 +40,12 @@ def _disable_random_init(enable: bool = True):
|
|||||||
setattr(nn.init, n, fn)
|
setattr(nn.init, n, fn)
|
||||||
|
|
||||||
|
|
||||||
class AutoModel(BaseFactory["AutoModel"], nn.Module):
|
class ModelFactory(BaseFactory[nn.Module]):
|
||||||
"""
|
"""Pure factory for model dispatch, separated from nn.Module state."""
|
||||||
Autoregressive language model base class.
|
|
||||||
Provides model loading/saving, registration, and generation.
|
|
||||||
"""
|
class AutoModel(nn.Module):
|
||||||
|
"""Model base class with loading/saving and generation."""
|
||||||
|
|
||||||
def __init__(self, config: BaseModelConfig):
|
def __init__(self, config: BaseModelConfig):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -68,7 +69,7 @@ class AutoModel(BaseFactory["AutoModel"], nn.Module):
|
|||||||
config = ConfigFactory.load(raw)
|
config = ConfigFactory.load(raw)
|
||||||
model_type = config.model_type or "autoregressive_lm"
|
model_type = config.model_type or "autoregressive_lm"
|
||||||
|
|
||||||
actual_cls = AutoModel.get_component_class(model_type)
|
actual_cls = ModelFactory.get_component_class(model_type)
|
||||||
|
|
||||||
with _disable_random_init(enable=disable_random_init):
|
with _disable_random_init(enable=disable_random_init):
|
||||||
model = actual_cls(config)
|
model = actual_cls(config)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import torch.nn as nn
|
|||||||
from torch import Tensor
|
from torch import Tensor
|
||||||
|
|
||||||
from astrai.config.model_config import EncoderConfig
|
from astrai.config.model_config import EncoderConfig
|
||||||
from astrai.model.automodel import AutoModel
|
from astrai.model.automodel import AutoModel, ModelFactory
|
||||||
from astrai.model.components.decoder_block import DecoderBlock
|
from astrai.model.components.decoder_block import DecoderBlock
|
||||||
from astrai.model.components.embedding import Embedding
|
from astrai.model.components.embedding import Embedding
|
||||||
from astrai.model.components.norm import RMSNorm
|
from astrai.model.components.norm import RMSNorm
|
||||||
@@ -13,7 +13,7 @@ from astrai.model.components.rope import RotaryEmbedding
|
|||||||
from astrai.model.transformer import process_attention_mask
|
from astrai.model.transformer import process_attention_mask
|
||||||
|
|
||||||
|
|
||||||
@AutoModel.register("embedding")
|
@ModelFactory.register("embedding")
|
||||||
class EmbeddingEncoder(AutoModel):
|
class EmbeddingEncoder(AutoModel):
|
||||||
def __init__(self, config: EncoderConfig):
|
def __init__(self, config: EncoderConfig):
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from torch import Tensor
|
|||||||
|
|
||||||
from astrai.config.model_config import AutoRegressiveLMConfig
|
from astrai.config.model_config import AutoRegressiveLMConfig
|
||||||
from astrai.inference.core.cache import CacheView
|
from astrai.inference.core.cache import CacheView
|
||||||
from astrai.model.automodel import AutoModel
|
from astrai.model.automodel import AutoModel, ModelFactory
|
||||||
from astrai.model.components.decoder_block import DecoderBlock
|
from astrai.model.components.decoder_block import DecoderBlock
|
||||||
from astrai.model.components.embedding import Embedding
|
from astrai.model.components.embedding import Embedding
|
||||||
from astrai.model.components.linear import Linear
|
from astrai.model.components.linear import Linear
|
||||||
@@ -26,7 +26,7 @@ def process_attention_mask(
|
|||||||
return input_mask
|
return input_mask
|
||||||
|
|
||||||
|
|
||||||
@AutoModel.register("autoregressive_lm")
|
@ModelFactory.register("autoregressive_lm")
|
||||||
class AutoRegressiveLM(AutoModel):
|
class AutoRegressiveLM(AutoModel):
|
||||||
"""Autoregressive language model with paged KV cache."""
|
"""Autoregressive language model with paged KV cache."""
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ Messages = List[Message]
|
|||||||
class AutoTokenizer:
|
class AutoTokenizer:
|
||||||
"""Base tokenizer class with automatic loading support"""
|
"""Base tokenizer class with automatic loading support"""
|
||||||
|
|
||||||
TOKENIZER_CLASSES = {} # Registry for auto-loading
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
path: Optional[Union[str, Path]] = None,
|
path: Optional[Union[str, Path]] = None,
|
||||||
@@ -108,17 +106,6 @@ class AutoTokenizer:
|
|||||||
with open(save_path / "tokenizer_config.json", "w", encoding="utf-8") as f:
|
with open(save_path / "tokenizer_config.json", "w", encoding="utf-8") as f:
|
||||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def register_tokenizer(cls, name: str, tokenizer_class: type):
|
|
||||||
"""
|
|
||||||
Register a new tokenizer class.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Name to register the tokenizer class under
|
|
||||||
tokenizer_class: The tokenizer class to register
|
|
||||||
"""
|
|
||||||
cls.TOKENIZER_CLASSES[name] = tokenizer_class
|
|
||||||
|
|
||||||
def encode(
|
def encode(
|
||||||
self,
|
self,
|
||||||
tokens: Union[str, List[str]],
|
tokens: Union[str, List[str]],
|
||||||
|
|||||||
@@ -501,5 +501,5 @@ class GRPOStrategy(BaseStrategy):
|
|||||||
# Factory aliases: online variants use the same strategy class; the
|
# Factory aliases: online variants use the same strategy class; the
|
||||||
# ``RolloutRunner`` is injected by ``TrainContextBuilder`` to enable
|
# ``RolloutRunner`` is injected by ``TrainContextBuilder`` to enable
|
||||||
# online mode, so no separate subclass is needed.
|
# online mode, so no separate subclass is needed.
|
||||||
StrategyFactory._entries["online_grpo"] = GRPOStrategy
|
StrategyFactory.register("online_grpo")(GRPOStrategy)
|
||||||
StrategyFactory._entries["online_dpo"] = DPOStrategy
|
StrategyFactory.register("online_dpo")(DPOStrategy)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import safetensors.torch as st
|
|||||||
import torch
|
import torch
|
||||||
|
|
||||||
from astrai.config.model_config import EncoderConfig
|
from astrai.config.model_config import EncoderConfig
|
||||||
from astrai.model.automodel import AutoModel
|
from astrai.model.automodel import ModelFactory
|
||||||
from astrai.model.encoder import EmbeddingEncoder
|
from astrai.model.encoder import EmbeddingEncoder
|
||||||
from tests.helpers import TINY_CONFIG, assert_state_dicts_equal
|
from tests.helpers import TINY_CONFIG, assert_state_dicts_equal
|
||||||
|
|
||||||
@@ -69,8 +69,8 @@ def test_encoder_normalize(device):
|
|||||||
|
|
||||||
|
|
||||||
def test_encoder_register():
|
def test_encoder_register():
|
||||||
assert AutoModel.is_registered("embedding")
|
assert ModelFactory.is_registered("embedding")
|
||||||
cls = AutoModel.get_component_class("embedding")
|
cls = ModelFactory.get_component_class("embedding")
|
||||||
assert cls is EmbeddingEncoder
|
assert cls is EmbeddingEncoder
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ def _make_dpo(device, executor=None):
|
|||||||
def test_factory_registers_online_aliases():
|
def test_factory_registers_online_aliases():
|
||||||
assert StrategyFactory.is_registered("online_grpo")
|
assert StrategyFactory.is_registered("online_grpo")
|
||||||
assert StrategyFactory.is_registered("online_dpo")
|
assert StrategyFactory.is_registered("online_dpo")
|
||||||
assert StrategyFactory._entries["online_grpo"] is GRPOStrategy
|
assert StrategyFactory.get_component_class("online_grpo") is GRPOStrategy
|
||||||
assert StrategyFactory._entries["online_dpo"] is DPOStrategy
|
assert StrategyFactory.get_component_class("online_dpo") is DPOStrategy
|
||||||
|
|
||||||
|
|
||||||
def test_grpo_supports_online(device):
|
def test_grpo_supports_online(device):
|
||||||
|
|||||||
Reference in New Issue
Block a user