feat: add field and model validators to config classes

- TrainConfig: enum validators (strategy, parallel_mode, backend, start_method, compile_mode), positive/non-negative/range validators, model_validator requiring reward_model_fn for online RL strategies
- AutoRegressiveLMConfig/EncoderConfig: attn_type, ffn_type enum validators
- ProcessingConfig: packing_strategy, truncation_mode enums, positive int validators
- OutputConfig: storage_format, position_ids_mode enum validators
This commit is contained in:
2026-07-30 08:41:14 +08:00
parent 164be9708b
commit 22cf798d81
3 changed files with 179 additions and 1 deletions
+32
View File
@@ -1,10 +1,14 @@
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from pydantic import field_validator
from pydantic.dataclasses import dataclass from pydantic.dataclasses import dataclass
from astrai.config.base import BaseConfig from astrai.config.base import BaseConfig
from astrai.factory import BaseFactory from astrai.factory import BaseFactory
_ATTN_TYPES = frozenset({"gqa", "mla"})
_FFN_TYPES = frozenset({"mlp", "moe"})
class ConfigFactory(BaseFactory[BaseConfig]): class ConfigFactory(BaseFactory[BaseConfig]):
"""Factory that dispatches config classes by ``model_type``.""" """Factory that dispatches config classes by ``model_type``."""
@@ -84,6 +88,20 @@ class AutoRegressiveLMConfig(BaseModelConfig):
n_activated_experts: Optional[int] = None n_activated_experts: Optional[int] = None
topk_method: Optional[str] = None topk_method: Optional[str] = None
@field_validator("attn_type")
def _validate_attn_type(cls, v: str) -> str:
if v not in _ATTN_TYPES:
raise ValueError(
f"attn_type must be one of {sorted(_ATTN_TYPES)}, got {v!r}"
)
return v
@field_validator("ffn_type")
def _validate_ffn_type(cls, v: str) -> str:
if v not in _FFN_TYPES:
raise ValueError(f"ffn_type must be one of {sorted(_FFN_TYPES)}, got {v!r}")
return v
@dataclass @dataclass
@ConfigFactory.register("embedding") @ConfigFactory.register("embedding")
@@ -127,3 +145,17 @@ class EncoderConfig(BaseModelConfig):
ffn_type: str = "mlp" ffn_type: str = "mlp"
pooling_type: Optional[str] = None pooling_type: Optional[str] = None
normalize_embeddings: Optional[bool] = None normalize_embeddings: Optional[bool] = None
@field_validator("attn_type")
def _validate_attn_type(cls, v: str) -> str:
if v not in _ATTN_TYPES:
raise ValueError(
f"attn_type must be one of {sorted(_ATTN_TYPES)}, got {v!r}"
)
return v
@field_validator("ffn_type")
def _validate_ffn_type(cls, v: str) -> str:
if v not in _FFN_TYPES:
raise ValueError(f"ffn_type must be one of {sorted(_FFN_TYPES)}, got {v!r}")
return v
+50
View File
@@ -8,10 +8,16 @@ modes, both driven declaratively through ``input.sections`` or
from dataclasses import field from dataclasses import field
from typing import Dict, List, Optional from typing import Dict, List, Optional
from pydantic import field_validator
from pydantic.dataclasses import dataclass from pydantic.dataclasses import dataclass
from astrai.config.base import BaseConfig from astrai.config.base import BaseConfig
_PACKING_STRATEGIES = frozenset({"simple", "bfd", "bfd_split"})
_TRUNCATION_MODES = frozenset({"keep_start", "keep_end"})
_STORAGE_FORMATS = frozenset({"bin", "jsonl"})
_POSITION_IDS_MODES = frozenset({"none", "doc_reset", "continuous"})
@dataclass @dataclass
class InputConfig(BaseConfig): class InputConfig(BaseConfig):
@@ -61,6 +67,34 @@ class ProcessingConfig(BaseConfig):
max_packed_len: int = 8192 max_packed_len: int = 8192
truncation_mode: str = "keep_start" truncation_mode: str = "keep_start"
@field_validator("packing_strategy")
def _validate_packing_strategy(cls, v: str) -> str:
if v not in _PACKING_STRATEGIES:
raise ValueError(
f"packing_strategy must be one of {sorted(_PACKING_STRATEGIES)}, got {v!r}"
)
return v
@field_validator("truncation_mode")
def _validate_truncation_mode(cls, v: str) -> str:
if v not in _TRUNCATION_MODES:
raise ValueError(
f"truncation_mode must be one of {sorted(_TRUNCATION_MODES)}, got {v!r}"
)
return v
@field_validator("max_seq_len", "batch_size", "max_packed_len")
def _validate_positive_int(cls, v: int) -> int:
if v <= 0:
raise ValueError(f"must be positive, got {v}")
return v
@field_validator("min_chars")
def _validate_non_negative(cls, v: int) -> int:
if v < 0:
raise ValueError(f"min_chars must be non-negative, got {v}")
return v
@dataclass @dataclass
class OutputConfig(BaseConfig): class OutputConfig(BaseConfig):
@@ -80,6 +114,22 @@ class OutputConfig(BaseConfig):
dtype: Dict[str, str] = field(default_factory=dict) dtype: Dict[str, str] = field(default_factory=dict)
position_ids_mode: str = "doc_reset" position_ids_mode: str = "doc_reset"
@field_validator("storage_format")
def _validate_storage_format(cls, v: str) -> str:
if v not in _STORAGE_FORMATS:
raise ValueError(
f"storage_format must be one of {sorted(_STORAGE_FORMATS)}, got {v!r}"
)
return v
@field_validator("position_ids_mode")
def _validate_position_ids_mode(cls, v: str) -> str:
if v not in _POSITION_IDS_MODES:
raise ValueError(
f"position_ids_mode must be one of {sorted(_POSITION_IDS_MODES)}, got {v!r}"
)
return v
@dataclass @dataclass
class PipelineConfig(BaseConfig): class PipelineConfig(BaseConfig):
+97 -1
View File
@@ -2,7 +2,7 @@ from dataclasses import field
from typing import Any, Callable, Dict, List, Optional from typing import Any, Callable, Dict, List, Optional
import torch.nn as nn import torch.nn as nn
from pydantic import ConfigDict from pydantic import ConfigDict, field_validator, model_validator
from pydantic.dataclasses import dataclass from pydantic.dataclasses import dataclass
from torch.optim import Optimizer from torch.optim import Optimizer
from torch.optim.lr_scheduler import LRScheduler from torch.optim.lr_scheduler import LRScheduler
@@ -11,6 +11,12 @@ from torch.utils.data import Dataset
from astrai.config.base import BaseConfig from astrai.config.base import BaseConfig
from astrai.model.components.lora import LoRAConfig from astrai.model.components.lora import LoRAConfig
_TRAIN_TYPES = frozenset({"seq", "sft", "dpo", "grpo", "online_grpo", "online_dpo"})
_PARALLEL_MODES = frozenset({"none", "ddp", "fsdp"})
_BACKENDS = frozenset({"nccl", "gloo"})
_START_METHODS = frozenset({"spawn", "fork", "forkserver"})
_COMPILE_MODES = frozenset({"default", "reduce-overhead", "max-autotune"})
@dataclass(config=ConfigDict(arbitrary_types_allowed=True)) @dataclass(config=ConfigDict(arbitrary_types_allowed=True))
class TrainConfig(BaseConfig): class TrainConfig(BaseConfig):
@@ -112,3 +118,93 @@ class TrainConfig(BaseConfig):
executor_kwargs: Dict[str, Any] = field(default_factory=dict) executor_kwargs: Dict[str, Any] = field(default_factory=dict)
extra_kwargs: Dict[str, Any] = field(default_factory=dict) extra_kwargs: Dict[str, Any] = field(default_factory=dict)
@field_validator("strategy")
def _validate_strategy(cls, v: str) -> str:
if v not in _TRAIN_TYPES:
raise ValueError(
f"strategy must be one of {sorted(_TRAIN_TYPES)}, got {v!r}"
)
return v
@field_validator("parallel_mode")
def _validate_parallel_mode(cls, v: str) -> str:
if v not in _PARALLEL_MODES:
raise ValueError(
f"parallel_mode must be one of {sorted(_PARALLEL_MODES)}, got {v!r}"
)
return v
@field_validator("backend")
def _validate_backend(cls, v: str) -> str:
if v not in _BACKENDS:
raise ValueError(f"backend must be one of {sorted(_BACKENDS)}, got {v!r}")
return v
@field_validator("start_method")
def _validate_start_method(cls, v: str) -> str:
if v not in _START_METHODS:
raise ValueError(
f"start_method must be one of {sorted(_START_METHODS)}, got {v!r}"
)
return v
@field_validator("compile_mode")
def _validate_compile_mode(cls, v: Optional[str]) -> Optional[str]:
if v is not None and v not in _COMPILE_MODES:
raise ValueError(
f"compile_mode must be one of {sorted(_COMPILE_MODES)} or None, got {v!r}"
)
return v
@field_validator(
"n_epoch",
"batch_per_device",
"grad_accum_steps",
"ckpt_interval",
"val_step",
"rollout_interval",
"rollout_max_tokens",
)
def _validate_positive_int(cls, v: int) -> int:
if v <= 0:
raise ValueError(f"must be positive, got {v}")
return v
@field_validator("rollout_temperature")
def _validate_positive_float(cls, v: float) -> float:
if v <= 0:
raise ValueError(f"must be positive, got {v}")
return v
@field_validator("rollout_top_p")
def _validate_top_p(cls, v: float) -> float:
if not 0 < v <= 1:
raise ValueError(f"rollout_top_p must be in (0, 1], got {v}")
return v
@field_validator("rollout_top_k", "num_workers", "neftune_alpha")
def _validate_non_negative(cls, v):
if v < 0:
raise ValueError(f"must be non-negative, got {v}")
return v
@field_validator("max_grad_norm")
def _validate_max_grad_norm(cls, v: Optional[float]) -> Optional[float]:
if v is not None and v <= 0:
raise ValueError(f"max_grad_norm must be positive or None, got {v}")
return v
@field_validator("val_split")
def _validate_val_split(cls, v: Optional[float]) -> Optional[float]:
if v is not None and not 0 < v < 1:
raise ValueError(f"val_split must be in (0, 1) or None, got {v}")
return v
@model_validator(mode="after")
def _validate_online_strategy(self) -> "TrainConfig":
if self.strategy.startswith("online_") and self.reward_model_fn is None:
raise ValueError(
f"reward_model_fn is required for online RL strategy {self.strategy!r}"
)
return self