18 Commits
Author SHA1 Message Date
ViperEkura 785d65436c fix: 修复 to_dict list 类型丢失与 OpenAI stop 参数失效
- to_dict() 增加 list 类型序列化支持,metrics 等字段不再丢失
- OpenAIHandler 补充 get_stop_sequences/on_token,读取 request.stop 并检测停止序列
- 文档类图补充缺失字段、修正关系分类、ChatCompletionRequest 字段增加 Optional
2026-05-19 21:07:07 +08:00
ViperEkura 64be81b7b3 feat: ProgressBarCallback 支持日志行输出到 stdout
- serialization 和 metric_logger 的 timestamp 统一使用 ISO 8601 格式
- ProgressBarCallback 新增 log_interval/file 参数,默认输出到 sys.stdout
2026-05-19 19:12:38 +08:00
ViperEkura 45479b5731 feat: metric 参数通过 TrainConfig 传递
- TrainConfig 新增 log_dir/log_interval/metrics 配置字段

- metric_logger 调用改用 **kwargs 传递,BaseFactory.create 自动过滤
2026-05-19 17:50:24 +08:00
ViperEkura e0a3337c22 docs: 更新视频链接 2026-05-19 17:34:01 +08:00
ViperEkura 812238060b fix: docker-compose UID/GID 添加默认值,修复 docker.sh logs 命令 2026-05-18 14:24:00 +08:00
ViperEkura 14b0d56197 fix: 修复无法创建子进程的问题
- mp.start_processes daemon=False
2026-05-18 09:40:32 +08:00
ViperEkura 6c8533f1d2 docs: 修正文档中类名/字段名与代码不一致之处
- ModelConfig → AutoRegressiveLMConfig, Transformer → AutoRegressiveLM
- 新增缺失类: EncoderConfig, EmbeddingEncoder, ConfigFactory, StorageFactory, ValidationCallback
- TrainConfig/TrainContext/ChatCompletionRequest 补充缺失字段
- dataflow.md 中 create_storage → StorageFactory.create
- 示例 --train_type=pt → seq 与代码一致
2026-05-17 21:02:21 +08:00
ViperEkura 2c2697390d feat: 新增 GradientCheckpointingCallback
- TrainConfig.gradient_checkpointing_modules 指定模块类型
- apply 递归遍历,兼容 DDP,不硬编码模型结构
- modules=None 时静默跳过,零开销
2026-05-17 18:21:05 +08:00
ViperEkura 7621f05d3f docs: AdamW beta 默认值改为 (0.9, 0.95)
- 与 Muon 优化器的 AdamW 子优化器保持一致
- 同步更新 train.py/training.md/params.md/README
2026-05-17 17:08:31 +08:00
ViperEkura 10ebd7211f feat: 新增 Muon 优化器
- 2D 参数用 Newton-Schulz 正交化 + Nesterov 动量更新
- 1D 参数用 AdamW 更新
- 支持 lr/momentum/weight_decay/ns_steps 配置
2026-05-17 16:44:03 +08:00
ViperEkura 42a391f0fb feat: 训练中新增验证循环
- TrainConfig 添加 val_dataset/val_step 字段
- TrainContext 添加 val_dataloader/val_loss 字段
- 新增 ValidationCallback 按 step 触发验证 + 训练结束时验证
- ProgressBar/MetricLogger 支持 val_loss 展示与记录
2026-05-17 16:12:42 +08:00
ViperEkura 97c7ac0f4f refactor: Transformer更名为AutoRegressiveLM并新增EmbeddingEncoder
- AutoRegressiveLM 注册名改为 autoregressive_lm
- 新增 EmbeddingEncoder 支持 mean/cls/last pooling
- ModelConfig 增加 pooling_type / normalize_embeddings 字段
- 导入、注释、测试全部同步更新
2026-05-17 15:29:20 +08:00
ViperEkura 8f1b32f2b6 fix: 移除多余 request 参数并增强 tokenizer 健壮性
- 路由和 _get_engine 不再需要 request 参数,直接引用模块级 app
- from_pretrained 增加文件完整性校验,缺 tokenizer.json 则抛 FileNotFoundError
- 移除 from_pretrained 中未使用的 **kwargs
2026-05-17 12:52:18 +08:00
ViperEkura c241a5dcef refactor: 优化并行训练配置与启动管理
- 配置新增 start_method 支持 spawn/fork/forkserver 选择
- 启动方式 mp.spawn 改为 mp.start_processes,支持 daemon=True
- validate() 改为基于 metadata 的反射式校验,不再硬编码字段列表
- CLI 新增 --start_method 参数
2026-05-17 12:33:10 +08:00
ViperEkura 44dab27fdc feat: 数据集加载时校验必填字段
- BaseDataset.required_keys 属性声明所需存储 key
- load() 时自动校验,缺失立即抛 KeyError
- SEQ/SFT/DPO/GRPO 各自声明 required_keys
2026-05-17 11:50:38 +08:00
ViperEkura a44fd22a99 fix: 修复训练与模型参数传递问题
- state_dict_fn 传入 CheckpointCallback,修复多卡 DDP 下 key 前缀丢失
- MLA 增加 use_qk_norm 支持,消除参数静默丢失
- moe_topk_method 统一命名为 topk_method
- checkpoint 回调移至最前
2026-05-17 11:20:13 +08:00
ViperEkura 8a11a7d444 fix: 修复训练脚本两处参数传递问题
- prepare_checkpoint 增加 DDP 判断,单卡时不访问 .module
- dpo_beta 改为 beta,对齐 DPOStrategy 参数名
2026-05-17 11:04:40 +08:00
ViperEkura 1d54491809 refactor: 改用递归子模块 init 替代统一 normal_(0.006)
- Embedding.reset_parameters: normal_(std=0.02)
- Linear.reset_parameters: kaiming_uniform_ + uniform_ bias
- Transformer._init_weights 通过 apply 递归调用子模块 reset_parameters
- 移除全局 normal_(0.006) 覆盖,各模块使用更合适的分布
2026-05-17 10:44:18 +08:00
42 changed files with 1237 additions and 247 deletions
+5 -4
View File
@@ -1,7 +1,7 @@
# AstrAI Dockerfile - Multi-stage Build (Optimized) # AstrAI Dockerfile - Multi-stage Build (Optimized)
# Build stage - use base image with minimal build tools # Build stage - use base image with minimal build tools
FROM nvidia/cuda:12.6.0-base-ubuntu24.04 AS builder FROM ubuntu:24.04 AS builder
WORKDIR /app WORKDIR /app
@@ -18,7 +18,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-ins
RUN python3.12 -m venv --copies /opt/venv RUN python3.12 -m venv --copies /opt/venv
ENV PATH="/opt/venv/bin:$PATH" ENV PATH="/opt/venv/bin:$PATH"
# Copy source code and install dependencies # Copy source code and install (deps read from pyproject.toml)
COPY astrai/ ./astrai/ COPY astrai/ ./astrai/
COPY pyproject.toml . COPY pyproject.toml .
RUN pip install --no-cache-dir --upgrade pip \ RUN pip install --no-cache-dir --upgrade pip \
@@ -26,13 +26,14 @@ RUN pip install --no-cache-dir --upgrade pip \
--extra-index-url https://download.pytorch.org/whl/cu126 --extra-index-url https://download.pytorch.org/whl/cu126
# Production stage # Production stage
FROM nvidia/cuda:12.6.0-base-ubuntu24.04 AS production FROM ubuntu:24.04 AS production
WORKDIR /app WORKDIR /app
# Install Python 3.12 runtime # Install Python 3.12 runtime and healthcheck dependency
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.12 \ python3.12 \
curl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Copy virtual environment from builder # Copy virtual environment from builder
+4 -4
View File
@@ -82,7 +82,7 @@ export CUDA_VISIBLE_DEVICES=0,1,2,3
nohup python scripts/tools/train.py \ nohup python scripts/tools/train.py \
--nprocs=4 \ --nprocs=4 \
--train_type=pt \ --train_type=seq \
--data_root_path=/path/to/dataset \ --data_root_path=/path/to/dataset \
--param_path=/path/to/model \ --param_path=/path/to/model \
--batch_per_device=4 \ --batch_per_device=4 \
@@ -90,8 +90,8 @@ nohup python scripts/tools/train.py \
--warmup_ratio=0.05 \ --warmup_ratio=0.05 \
--max_lr=1e-4 \ --max_lr=1e-4 \
--max_grad_norm=1.0 \ --max_grad_norm=1.0 \
--adamw_beta1=0.95 \ --adamw_beta1=0.9 \
--adamw_beta2=0.99 \ --adamw_beta2=0.95 \
--adamw_weight_decay=0.01 \ --adamw_weight_decay=0.01 \
--window_size=2048 \ --window_size=2048 \
--ckpt_interval=10000 \ --ckpt_interval=10000 \
@@ -213,7 +213,7 @@ python scripts/demo/generate_batch.py
python scripts/demo/generate_ar.py python scripts/demo/generate_ar.py
``` ```
Watch a video walkthrough on [bilibili](https://www.bilibili.com/video/BV1z5RPYHEkd). Watch a video walkthrough on [bilibili](https://www.bilibili.com/video/BV1fuLB6yEj6).
### Documentation ### Documentation
+4 -4
View File
@@ -88,7 +88,7 @@ export CUDA_VISIBLE_DEVICES=0,1,2,3
nohup python scripts/tools/train.py \ nohup python scripts/tools/train.py \
--nprocs=4 \ --nprocs=4 \
--train_type=pt \ --train_type=seq \
--data_root_path=/path/to/dataset \ --data_root_path=/path/to/dataset \
--param_path=/path/to/model \ --param_path=/path/to/model \
--batch_per_device=4 \ --batch_per_device=4 \
@@ -96,8 +96,8 @@ nohup python scripts/tools/train.py \
--warmup_ratio=0.05 \ --warmup_ratio=0.05 \
--max_lr=1e-4 \ --max_lr=1e-4 \
--max_grad_norm=1.0 \ --max_grad_norm=1.0 \
--adamw_beta1=0.95 \ --adamw_beta1=0.9 \
--adamw_beta2=0.99 \ --adamw_beta2=0.95 \
--adamw_weight_decay=0.01 \ --adamw_weight_decay=0.01 \
--window_size=2048 \ --window_size=2048 \
--ckpt_interval=10000 \ --ckpt_interval=10000 \
@@ -219,7 +219,7 @@ python scripts/demo/generate_batch.py
python scripts/demo/generate_ar.py python scripts/demo/generate_ar.py
``` ```
观看 [bilibili](https://www.bilibili.com/video/BV1z5RPYHEkd) 上的视频演示。 观看 [bilibili](https://www.bilibili.com/video/BV1fuLB6yEj6) 上的视频演示。
### 文档 ### 文档
+156 -45
View File
@@ -16,7 +16,7 @@ classDiagram
+to_file(config_path) +to_file(config_path)
} }
class ModelConfig { class AutoRegressiveLMConfig {
+int vocab_size +int vocab_size
+int dim +int dim
+int n_layers +int n_layers
@@ -25,21 +25,41 @@ classDiagram
+bool tie_weight +bool tie_weight
+int max_len +int max_len
+float rope_theta +float rope_theta
+str attn_type
+int n_heads +int n_heads
+int n_kv_heads +int n_kv_heads
+bool use_qk_norm +bool use_qk_norm
+bool use_gated_attention +bool use_gated_attention
+str attn_type +Optional[int] kv_lora_rank
+Optional[int] qk_nope_head_dim
+Optional[int] qk_rope_head_dim
+str ffn_type +str ffn_type
+int n_routed_experts +int n_routed_experts
+int n_shared_experts +int n_shared_experts
+int n_activated_experts +int n_activated_experts
+str moe_topk_method +Optional[str] topk_method
+Optional[int] kv_lora_rank }
+Optional[int] qk_nope_head_dim
+Optional[int] qk_rope_head_dim class EncoderConfig {
+load(config_path) ModelConfig +int vocab_size
+save(config_path) +int dim
+int n_layers
+float norm_eps
+int dim_ffn
+int max_len
+float rope_theta
+int n_heads
+int n_kv_heads
+bool use_qk_norm
+bool use_gated_attention
+Optional[str] pooling_type
+Optional[bool] normalize_embeddings
}
class ConfigFactory {
+Registry _registry
+register(name) decorator
+load(raw) BaseConfig
} }
class TrainConfig { class TrainConfig {
@@ -52,10 +72,14 @@ classDiagram
+int batch_per_device +int batch_per_device
+int grad_accum_steps +int grad_accum_steps
+float max_grad_norm +float max_grad_norm
+list gradient_checkpointing_modules
+int start_epoch +int start_epoch
+int start_batch +int start_batch
+str ckpt_dir +str ckpt_dir
+int ckpt_interval +int ckpt_interval
+str log_dir
+int log_interval
+List[str] metrics
+int random_seed +int random_seed
+int num_workers +int num_workers
+Optional[int] prefetch_factor +Optional[int] prefetch_factor
@@ -66,7 +90,10 @@ classDiagram
+str master_port +str master_port
+Callable parallel_wrapper +Callable parallel_wrapper
+Callable state_dict_fn +Callable state_dict_fn
+str start_method
+str device_type +str device_type
+Optional[Dataset] val_dataset
+int val_step
+dict extra_kwargs +dict extra_kwargs
+validate() +validate()
} }
@@ -138,11 +165,17 @@ classDiagram
+int iter +int iter
} }
class StorageFactory {
+Registry _registry
+register(name) decorator
+create(storage_type) BaseStorage
}
class DatasetFactory { class DatasetFactory {
+Registry _registry +Registry _registry
+register(name) decorator +register(name) decorator
+create(train_type, window_size, stride) BaseDataset +create(train_type, window_size, stride) BaseDataset
+load(train_type, load_path, window_size, stride) BaseDataset +load(train_type, load_path, window_size, stride, storage_type, tokenizer) BaseDataset
} }
} }
@@ -160,7 +193,7 @@ classDiagram
namespace model { namespace model {
class AutoModel { class AutoModel {
+ModelConfig config +BaseModelConfig config
+Registry _registry +Registry _registry
+register(model_type) decorator +register(model_type) decorator
+get_component_class(model_type) Type +get_component_class(model_type) Type
@@ -169,8 +202,8 @@ classDiagram
+to(*args, **kwargs) Self +to(*args, **kwargs) Self
} }
class Transformer { class AutoRegressiveLM {
+ModelConfig config +AutoRegressiveLMConfig config
+RotaryEmbedding rotary_embedding +RotaryEmbedding rotary_embedding
+Embedding embed_tokens +Embedding embed_tokens
+ModuleList layers +ModuleList layers
@@ -181,6 +214,18 @@ classDiagram
+state_dict() +state_dict()
} }
class EmbeddingEncoder {
+EncoderConfig config
+RotaryEmbedding rotary_embedding
+Embedding embed_tokens
+ModuleList layers
+RMSNorm norm
+str pooling_type
+bool normalize_embeddings
+forward(input_ids, input_mask, position_ids) Tensor
+load_state_dict(state_dict)
}
class DecoderBlock { class DecoderBlock {
+nn.Module attention # GQA or MLA via AttnFactory +nn.Module attention # GQA or MLA via AttnFactory
+RMSNorm input_norm +RMSNorm input_norm
@@ -322,11 +367,15 @@ classDiagram
+Optimizer optimizer +Optimizer optimizer
+LRScheduler scheduler +LRScheduler scheduler
+Checkpoint checkpoint +Checkpoint checkpoint
+TrainConfig config
+int epoch +int epoch
+int iteration +int iteration
+float loss +float loss
+DataLoader val_dataloader
+float val_loss
+int world_size +int world_size
+int rank +int rank
+dict kwargs
} }
class TrainContextBuilder { class TrainContextBuilder {
@@ -372,6 +421,7 @@ classDiagram
+str reduction +str reduction
+int sync_interval +int sync_interval
+compute_loss(batch) Tensor +compute_loss(batch) Tensor
+sync_ref_model()
} }
class BaseScheduler { class BaseScheduler {
@@ -399,6 +449,7 @@ classDiagram
} }
class TrainCallback { class TrainCallback {
<<protocol>>
+on_train_begin(context) +on_train_begin(context)
+on_train_end(context) +on_train_end(context)
+on_epoch_begin(context) +on_epoch_begin(context)
@@ -415,17 +466,32 @@ classDiagram
+on_step_begin(context) +on_step_begin(context)
} }
class GradientCheckpointingCallback {
+tuple modules
+on_train_begin(context)
+on_train_end(context)
}
class CheckpointCallback { class CheckpointCallback {
+str save_dir +str save_dir
+int interval +int interval
+bool weight_only
+Callable state_dict_fn
+Callable save_extra_fn
+Callable load_extra_fn
+_save_checkpoint(context) +_save_checkpoint(context)
+on_train_begin(context)
+on_batch_end(context) +on_batch_end(context)
+on_train_end(context) +on_train_end(context)
+on_error(context) +on_error(context)
+save_extra(context)$
+load_extra(extra, context)$
} }
class ProgressBarCallback { class ProgressBarCallback {
+int num_epoch +int num_epoch
+int log_interval
+IO file
+on_epoch_begin(context) +on_epoch_begin(context)
+on_batch_end(context) +on_batch_end(context)
+on_epoch_end(context) +on_epoch_end(context)
@@ -434,8 +500,16 @@ classDiagram
class MetricLoggerCallback { class MetricLoggerCallback {
+str log_dir +str log_dir
+int save_interval +int save_interval
+int log_interval
+List[str] metrics
+on_batch_end(context) +on_batch_end(context)
+on_train_end(context) +on_train_end(context)
+on_error(context)
}
class ValidationCallback {
+_run_validation(context)
+on_step_end(context)
} }
class CallbackFactory { class CallbackFactory {
@@ -443,6 +517,14 @@ classDiagram
+register(name) decorator +register(name) decorator
+create(name, **kwargs) TrainCallback +create(name, **kwargs) TrainCallback
} }
class Muon {
+float lr
+float momentum
+float weight_decay
+int ns_steps
+step(closure) Optional[float]
}
} }
namespace inference { namespace inference {
@@ -616,7 +698,7 @@ classDiagram
} }
class SamplingPipeline { class SamplingPipeline {
+List strategies +List[BaseSamplingStrategy] strategies
+apply(logits, filter_value) Tensor +apply(logits, filter_value) Tensor
+sample(logits, filter_value) Tensor +sample(logits, filter_value) Tensor
} }
@@ -638,14 +720,19 @@ classDiagram
} }
class ChatCompletionRequest { class ChatCompletionRequest {
+str model
+List[ChatMessage] messages +List[ChatMessage] messages
+float temperature +Optional[float] temperature
+float top_p +Optional[float] top_p
+int top_k +Optional[int] top_k
+int max_tokens +Optional[int] max_tokens
+bool stream +Optional[bool] stream
+Optional[str] stop +Optional[Union[str, List[str]]] stop
+Optional[int] n +Optional[int] n
+Optional[float] presence_penalty
+Optional[float] frequency_penalty
+Optional[Dict[int, float]] logit_bias
+Optional[str] user
} }
class AnthropicMessage { class AnthropicMessage {
@@ -654,6 +741,7 @@ classDiagram
} }
class MessagesRequest { class MessagesRequest {
+str model
+List[AnthropicMessage] messages +List[AnthropicMessage] messages
+Optional[str] system +Optional[str] system
+float temperature +float temperature
@@ -666,8 +754,13 @@ classDiagram
class ProtocolHandler { class ProtocolHandler {
<<abstract>> <<abstract>>
+request
+engine
+build_prompt() str +build_prompt() str
+create_response_id() str +create_response_id() str
+get_stop_sequences() List[str]
+create_stop_checker() StopChecker
+on_token(ctx, token, stop_checker) Optional[str]
+format_stream_start(ctx) List[str] +format_stream_start(ctx) List[str]
+format_stream_token(ctx, token) str +format_stream_token(ctx, token) str
+format_stream_end(ctx) List[str] +format_stream_end(ctx) List[str]
@@ -687,6 +780,7 @@ classDiagram
} }
class StopChecker { class StopChecker {
+has_sequences (property) bool
+check(text) Optional[str] +check(text) Optional[str]
+trim(text, matched) str +trim(text, matched) str
} }
@@ -699,6 +793,7 @@ classDiagram
+int completion_tokens +int completion_tokens
+str accumulated +str accumulated
+Optional[str] stop_matched +Optional[str] stop_matched
+str last_yield_trimmed
} }
class app { class app {
@@ -709,11 +804,13 @@ classDiagram
namespace parallel { namespace parallel {
class Functions { class Functions {
+spawn_parallel_fn(func, world_size, backend, master_addr, master_port, device_type, **kwargs) <<module>>
+spawn_parallel_fn(func, world_size, backend, master_addr, master_port, device_type, start_method, **kwargs)
+setup_parallel(rank, world_size, backend, master_addr, master_port, device_type) +setup_parallel(rank, world_size, backend, master_addr, master_port, device_type)
+get_current_device() str +get_current_device() str
+get_world_size() int +get_world_size() int
+get_rank() int +get_rank() int
+only_on_rank(rank, sync) decorator
} }
class ParallelModel { class ParallelModel {
@@ -741,6 +838,7 @@ classDiagram
BaseScheduler <|-- CosineScheduler BaseScheduler <|-- CosineScheduler
BaseScheduler <|-- SGDRScheduler BaseScheduler <|-- SGDRScheduler
TrainCallback <|-- GradientClippingCallback TrainCallback <|-- GradientClippingCallback
TrainCallback <|-- GradientCheckpointingCallback
TrainCallback <|-- CheckpointCallback TrainCallback <|-- CheckpointCallback
TrainCallback <|-- ProgressBarCallback TrainCallback <|-- ProgressBarCallback
TrainCallback <|-- MetricLoggerCallback TrainCallback <|-- MetricLoggerCallback
@@ -753,12 +851,15 @@ classDiagram
BaseSamplingStrategy <|-- TemperatureStrategy BaseSamplingStrategy <|-- TemperatureStrategy
BaseSamplingStrategy <|-- TopKStrategy BaseSamplingStrategy <|-- TopKStrategy
BaseSamplingStrategy <|-- TopPStrategy BaseSamplingStrategy <|-- TopPStrategy
BaseSamplingStrategy <|-- SamplingPipeline
ParallelModel <|-- RowParallelLinear ParallelModel <|-- RowParallelLinear
ParallelModel <|-- ColumnParallelLinear ParallelModel <|-- ColumnParallelLinear
AutoModel <|-- Transformer AutoModel <|-- AutoRegressiveLM
AutoModel <|-- EmbeddingEncoder
BaseConfig <|-- BaseModelConfig BaseConfig <|-- BaseModelConfig
BaseConfig <|-- TrainConfig BaseConfig <|-- TrainConfig
BaseModelConfig <|-- ModelConfig BaseModelConfig <|-- AutoRegressiveLMConfig
BaseModelConfig <|-- EncoderConfig
BaseFactory <|-- AutoModel BaseFactory <|-- AutoModel
BaseFactory <|-- AttnFactory BaseFactory <|-- AttnFactory
BaseFactory <|-- FFNFactory BaseFactory <|-- FFNFactory
@@ -766,6 +867,9 @@ classDiagram
BaseFactory <|-- StrategyFactory BaseFactory <|-- StrategyFactory
BaseFactory <|-- SchedulerFactory BaseFactory <|-- SchedulerFactory
BaseFactory <|-- CallbackFactory BaseFactory <|-- CallbackFactory
BaseFactory <|-- StorageFactory
BaseFactory <|-- ConfigFactory
TrainCallback <|-- ValidationCallback
ProtocolHandler <|-- OpenAIHandler ProtocolHandler <|-- OpenAIHandler
ProtocolHandler <|-- AnthropicHandler ProtocolHandler <|-- AnthropicHandler
@@ -773,31 +877,33 @@ classDiagram
KVCache *-- PagePool KVCache *-- PagePool
KVCache *-- Storage KVCache *-- Storage
KVCache *-- TaskTable KVCache *-- TaskTable
KVCache *-- Allocator PagePool *-- Allocator
KVCache *-- PrefixCache PagePool *-- PrefixCache
InferenceEngine *-- InferenceScheduler InferenceEngine *-- InferenceScheduler
InferenceScheduler *-- KVCache InferenceScheduler *-- KVCache
InferenceScheduler *-- Executor InferenceScheduler *-- Executor
InferenceScheduler *-- TaskManager InferenceScheduler *-- TaskManager
SamplingPipeline *-- BaseSamplingStrategy AutoRegressiveLM *-- DecoderBlock
TrainContextBuilder *-- TrainContext AutoRegressiveLM *-- RotaryEmbedding
Transformer *-- DecoderBlock AutoRegressiveLM *-- Embedding
Transformer *-- RotaryEmbedding EmbeddingEncoder *-- DecoderBlock
Transformer *-- Embedding EmbeddingEncoder *-- RotaryEmbedding
EmbeddingEncoder *-- Embedding
DecoderBlock *-- RMSNorm DecoderBlock *-- RMSNorm
BaseDataset *-- BaseStorage
ChatCompletionRequest *-- ChatMessage ChatCompletionRequest *-- ChatMessage
MessagesRequest *-- AnthropicMessage MessagesRequest *-- AnthropicMessage
AutoTokenizer *-- ChatTemplate
BaseFactory *-- Registry
%% --- Aggregation (weak ownership) --- %% --- Aggregation (weak ownership) ---
AutoModel o-- ModelConfig AutoModel o-- BaseModelConfig
Trainer o-- TrainCallback Trainer o-- TrainCallback
TrainContext o-- BaseStrategy TrainContext o-- BaseStrategy
TrainContext o-- BaseScheduler TrainContext o-- BaseScheduler
TrainContext o-- Checkpoint TrainContext o-- Checkpoint
AutoTokenizer o-- ChatTemplate
KvcacheView o-- Storage KvcacheView o-- Storage
BaseFactory o-- Registry SamplingPipeline o-- BaseSamplingStrategy
BaseDataset o-- BaseStorage
%% --- Dependency (uses temporarily) --- %% --- Dependency (uses temporarily) ---
TrainConfig ..> BaseStrategy : selects TrainConfig ..> BaseStrategy : selects
@@ -811,7 +917,12 @@ classDiagram
FFNFactory ..> DeepSeekMoE : creates FFNFactory ..> DeepSeekMoE : creates
DecoderBlock ..> AttnFactory : uses DecoderBlock ..> AttnFactory : uses
DecoderBlock ..> FFNFactory : uses DecoderBlock ..> FFNFactory : uses
StorageFactory ..> H5Storage : creates
StorageFactory ..> JSONStorage : creates
ConfigFactory ..> AutoRegressiveLMConfig : creates
ConfigFactory ..> EncoderConfig : creates
Trainer ..> TrainContextBuilder : uses Trainer ..> TrainContextBuilder : uses
TrainContextBuilder ..> TrainContext : creates
Trainer ..> Functions : spawns Trainer ..> Functions : spawns
TrainContextBuilder ..> StrategyFactory : uses TrainContextBuilder ..> StrategyFactory : uses
TrainContextBuilder ..> ResumableDistributedSampler : creates TrainContextBuilder ..> ResumableDistributedSampler : creates
@@ -827,13 +938,13 @@ classDiagram
%% --- Association (general usage) --- %% --- Association (general usage) ---
Trainer --> TrainConfig Trainer --> TrainConfig
DPOStrategy --> Transformer DPOStrategy --> AutoModel
GRPOStrategy --> Transformer GRPOStrategy --> AutoModel
InferenceScheduler --> Task InferenceScheduler --> Task
InferenceScheduler --> TaskStatus InferenceScheduler --> TaskStatus
Task --> TaskStatus Task --> TaskStatus
InferenceEngine --> Transformer InferenceEngine --> AutoModel
Executor --> Transformer Executor --> AutoModel
Executor --> AutoTokenizer Executor --> AutoTokenizer
TaskManager --> AutoTokenizer TaskManager --> AutoTokenizer
MultiSegmentFetcher --> BaseSegmentFetcher MultiSegmentFetcher --> BaseSegmentFetcher
@@ -846,12 +957,12 @@ classDiagram
| Module | Components | Description | | Module | Components | Description |
|--------|------------|-------------| |--------|------------|-------------|
| **astrai.config** | BaseConfig, BaseModelConfig, ModelConfig, TrainConfig | Configuration management (to_dict/from_dict, to_file/from_file) | | **astrai.config** | BaseConfig, BaseModelConfig, AutoRegressiveLMConfig, EncoderConfig, ConfigFactory, TrainConfig | Configuration management (to_dict/from_dict, to_file/from_file) |
| **astrai.dataset** | BaseDatasetGRPODataset, BaseStorageJSONStorage, BaseSegmentFetcher, MultiSegmentFetcher, ResumableDistributedSampler, DatasetFactory | Dataset loading and management | | **astrai.dataset** | BaseDatasetGRPODataset, BaseStorageJSONStorage, StorageFactory, BaseSegmentFetcher, MultiSegmentFetcher, ResumableDistributedSampler, DatasetFactory | Dataset loading and management |
| **astrai.serialization** | Checkpoint | Model serialization | | **astrai.serialization** | Checkpoint | Model serialization |
| **astrai.model** | AutoModel, Transformer, DecoderBlock, GQA, MLA, MLP, DeepSeekMoE, AttnFactory, FFNFactory, RMSNorm, Linear, RotaryEmbedding, Embedding | Neural network model | | **astrai.model** | AutoModel, AutoRegressiveLM, EmbeddingEncoder, DecoderBlock, GQA, MLA, MLP, DeepSeekMoE, AttnFactory, FFNFactory, RMSNorm, Linear, RotaryEmbedding, Embedding | Neural network model |
| **astrai.tokenize** | AutoTokenizer, ChatTemplate | Tokenizer and chat template | | **astrai.tokenize** | AutoTokenizer, ChatTemplate | Tokenizer and chat template |
| **astrai.trainer** | Trainer, TrainContext, TrainContextBuilder, BaseStrategyGRPOStrategy, StrategyFactory, BaseSchedulerSGDRScheduler, SchedulerFactory, TrainCallbackMetricLoggerCallback, CallbackFactory | Training workflow | | **astrai.trainer** | Trainer, TrainContext, TrainContextBuilder, BaseStrategyGRPOStrategy, StrategyFactory, BaseSchedulerSGDRScheduler, SchedulerFactory, TrainCallback(Protocol)ValidationCallback, CallbackFactory, Muon | Training workflow |
| **astrai.inference** | InferenceEngine, InferenceScheduler, Executor, KVCacheKvcacheView, AllocatorStorage, Task, TaskManager, TaskStatus, GenerationRequest, BaseSamplingStrategySamplingPipeline, ProtocolHandlerAnthropicHandler, ChatMessageMessagesRequest, app | Inference service | | **astrai.inference** | InferenceEngine, InferenceScheduler, Executor, KVCacheKvcacheView, AllocatorStorage, Task, TaskManager, TaskStatus, GenerationRequest, BaseSamplingStrategySamplingPipeline, ProtocolHandlerAnthropicHandler, ChatMessageMessagesRequest, app | Inference service |
| **astrai.parallel** | spawn_parallel_fn, setup_parallel, get_rank/get_world_size/get_current_device, only_on_rank, ParallelModel, RowParallelLinear, ColumnParallelLinear | Distributed parallel | | **astrai.parallel** | spawn_parallel_fn, setup_parallel, get_rank/get_world_size/get_current_device, only_on_rank, ParallelModel, RowParallelLinear, ColumnParallelLinear | Distributed parallel |
| **astrai.factory** | Registry, BaseFactory[T] | Component registration | | **astrai.factory** | Registry, BaseFactory[T] | Component registration |
@@ -860,7 +971,7 @@ classDiagram
| Pattern | Classes | Purpose | | Pattern | Classes | Purpose |
|---------|---------|---------| |---------|---------|---------|
| **Factory** | `AttnFactory`, `FFNFactory`, `StrategyFactory`, `DatasetFactory`, `SchedulerFactory`, `CallbackFactory` | Decorator-based component creation | | **Factory** | `AttnFactory`, `FFNFactory`, `StrategyFactory`, `DatasetFactory`, `SchedulerFactory`, `CallbackFactory`, `StorageFactory`, `ConfigFactory` | Decorator-based component creation |
| **Registry** | `BaseFactory`, `Registry` | Component registration with category/priority | | **Registry** | `BaseFactory`, `Registry` | Component registration with category/priority |
| **Strategy** | `SEQStrategy`, `SFTStrategy`, `DPOStrategy`, `GRPOStrategy` | Training strategy switching | | **Strategy** | `SEQStrategy`, `SFTStrategy`, `DPOStrategy`, `GRPOStrategy` | Training strategy switching |
| **Strategy (Sampling)** | `TemperatureStrategy`, `TopKStrategy`, `TopPStrategy`, `SamplingPipeline` | Composable logit transformations | | **Strategy (Sampling)** | `TemperatureStrategy`, `TopKStrategy`, `TopPStrategy`, `SamplingPipeline` | Composable logit transformations |
@@ -871,18 +982,18 @@ classDiagram
| **Object Pool** | `Allocator`, `PagePool` | Page-based KV cache with LRU eviction | | **Object Pool** | `Allocator`, `PagePool` | Page-based KV cache with LRU eviction |
| **Storage** | `BaseStorage`, `H5Storage`, `JSONStorage` | Format-agnostic data access | | **Storage** | `BaseStorage`, `H5Storage`, `JSONStorage` | Format-agnostic data access |
| **Producer-Consumer** | `InferenceScheduler`, `Task`, queues | Continuous batching | | **Producer-Consumer** | `InferenceScheduler`, `Task`, queues | Continuous batching |
| **AutoModel Registry** | `AutoModel`, `Transformer` | Model-type dynamic loading | | **AutoModel Registry** | `AutoModel`, `AutoRegressiveLM`, `EmbeddingEncoder` | Model-type dynamic loading |
## Core Relationships ## Core Relationships
1. **Config → Training**: `TrainConfig` holds model, dataset, optimizer_fn, scheduler_fn 1. **Config → Training**: `TrainConfig` holds model, dataset, optimizer_fn, scheduler_fn
2. **Training Flow**: `Trainer``TrainContextBuilder``TrainContext`, uses `BaseStrategy` for loss 2. **Training Flow**: `Trainer``TrainContextBuilder``TrainContext`, uses `BaseStrategy` for loss
3. **Strategy Selection**: `StrategyFactory` creates strategy by `train_type` 3. **Strategy Selection**: `StrategyFactory` creates strategy by `train_type`
4. **Inference Flow**: `InferenceEngine``InferenceScheduler``Transformer`, backed by `KVCache` + `SamplingPipeline` 4. **Inference Flow**: `InferenceEngine``InferenceScheduler``AutoRegressiveLM`, backed by `KVCache` + `SamplingPipeline`
5. **Distributed**: `spawn_parallel_fn` + `setup_parallel` for multi-process DDP 5. **Distributed**: `spawn_parallel_fn` + `setup_parallel` for multi-process DDP
6. **Dataset Loading**: `DatasetFactory` creates datasets, `BaseStorage` (H5Storage/JSONStorage) loads via `BaseSegmentFetcher` + `MultiSegmentFetcher` 6. **Dataset Loading**: `DatasetFactory` creates datasets, `BaseStorage` (H5Storage/JSONStorage) loads via `BaseSegmentFetcher` + `MultiSegmentFetcher`
7. **Checkpoint**: `Checkpoint` saves/loads safetensors + metadata (rank-0 only) 7. **Checkpoint**: `Checkpoint` saves/loads safetensors + metadata (rank-0 only)
8. **Scheduler**: `SchedulerFactory` creates `CosineScheduler`/`SGDRScheduler` 8. **Scheduler**: `SchedulerFactory` creates `CosineScheduler`/`SGDRScheduler`
9. **AutoModel**: `from_pretrained()` loads `config.json` + `model.safetensors`, `_disable_random_init` replaces `nn.init.*` with no-ops 9. **AutoModel**: `from_pretrained()` loads `config.json` + `model.safetensors`, `_disable_random_init` replaces `nn.init.*` with no-ops
> Document Update Time: 2026-05-16 > Document Update Time: 2026-05-17
+4 -4
View File
@@ -15,8 +15,8 @@ Raw text is tokenized via `AutoTokenizer.encode()` and saved as HDF5 (`.h5`) or
Storage format is auto-detected by `detect_format()`; backends are dispatched via registry: Storage format is auto-detected by `detect_format()`; backends are dispatched via registry:
``` ```
create_storage("h5") → H5Storage StorageFactory.create("h5") → H5Storage
create_storage("json") → JSONStorage StorageFactory.create("json") → JSONStorage
``` ```
Both support shared memory via `.share_memory_()`. Both support shared memory via `.share_memory_()`.
@@ -34,7 +34,7 @@ Both support shared memory via `.share_memory_()`.
``` ```
DatasetFactory.load(train_type, path, window_size, stride) DatasetFactory.load(train_type, path, window_size, stride)
create_storage(detect_format(path)) StorageFactory.create(detect_format(path))
→ MultiSegmentFetcher(BaseSegmentFetcher per key) → MultiSegmentFetcher(BaseSegmentFetcher per key)
→ BaseDataset.__getitem__(idx) → BaseDataset.__getitem__(idx)
→ sliding window [begin, end) via get_index(idx) → sliding window [begin, end) via get_index(idx)
@@ -54,4 +54,4 @@ DatasetFactory.load(train_type, path, window_size, stride)
Standard PyTorch `DataLoader` with configurable `batch_size`, `num_workers`, `pin_memory`, `prefetch_factor`. Sampler produces indices; dataloader fetches tensor batches via `__getitem__`. Standard PyTorch `DataLoader` with configurable `batch_size`, `num_workers`, `pin_memory`, `prefetch_factor`. Sampler produces indices; dataloader fetches tensor batches via `__getitem__`.
> Document Update Time: 2026-05-15 > Document Update Time: 2026-05-17
+1 -1
View File
@@ -137,4 +137,4 @@ engine.generate(["A", "B"], stream=True) # -> Generator[Tuple[int, str]]
await engine.generate_async("Hello", ...) # -> AsyncGenerator[str] await engine.generate_async("Hello", ...) # -> AsyncGenerator[str]
``` ```
> Document Update Time: 2026-05-15 > Document Update Time: 2026-05-17
+6 -6
View File
@@ -25,8 +25,8 @@
| Parameter | Description | Default | | Parameter | Description | Default |
|-----------|-------------|---------| |-----------|-------------|---------|
| `--adamw_beta1` | AdamW beta1 | 0.95 | | `--adamw_beta1` | AdamW beta1 | 0.9 |
| `--adamw_beta2` | AdamW beta2 | 0.99 | | `--adamw_beta2` | AdamW beta2 | 0.95 |
| `--adamw_weight_decay` | AdamW weight decay | 0.01 | | `--adamw_weight_decay` | AdamW weight decay | 0.01 |
### Data Loading ### Data Loading
@@ -73,7 +73,7 @@ export CUDA_VISIBLE_DEVICES=0,1,2,3
nohup python scripts/tools/train.py \ nohup python scripts/tools/train.py \
--nprocs=4 \ --nprocs=4 \
--train_type=pt \ --train_type=seq \
--data_root_path=/path/to/dataset \ --data_root_path=/path/to/dataset \
--param_path=/path/to/model \ --param_path=/path/to/model \
--batch_per_device=4 \ --batch_per_device=4 \
@@ -81,8 +81,8 @@ nohup python scripts/tools/train.py \
--warmup_ratio=0.05 \ --warmup_ratio=0.05 \
--max_lr=1e-4 \ --max_lr=1e-4 \
--max_grad_norm=1.0 \ --max_grad_norm=1.0 \
--adamw_beta1=0.95 \ --adamw_beta1=0.9 \
--adamw_beta2=0.99 \ --adamw_beta2=0.95 \
--adamw_weight_decay=0.01 \ --adamw_weight_decay=0.01 \
--window_size=2048 \ --window_size=2048 \
--ckpt_interval=10000 \ --ckpt_interval=10000 \
@@ -94,4 +94,4 @@ nohup python scripts/tools/train.py \
--- ---
> Document Update Time: 2026-05-16 > Document Update Time: 2026-05-17
+18 -5
View File
@@ -91,11 +91,13 @@ on_train_end
| Hook | Fires | Default callback | | Hook | Fires | Default callback |
|------|-------|-----------------| |------|-------|-----------------|
| `on_train_begin` | Before training starts | `GradientCheckpointingCallback` |
| `on_step_begin` | Every accumulation window | `GradientClippingCallback` | | `on_step_begin` | Every accumulation window | `GradientClippingCallback` |
| `on_batch_end` | Every batch | `CheckpointCallback`, `MetricLoggerCallback`, `ProgressBarCallback` | | `on_batch_end` | Every batch | `CheckpointCallback`, `MetricLoggerCallback`, `ProgressBarCallback` |
| `on_step_end` | Every accumulation window | `ValidationCallback` |
| `on_train_end` | Training ends | `CheckpointCallback`, `MetricLoggerCallback` (final save) | | `on_train_end` | Training ends | `CheckpointCallback`, `MetricLoggerCallback` (final save) |
Default callbacks: `progress_bar` (tqdm), `checkpoint` (safetensors, rank-0), `metric_logger` (JSONL, rank-0), `gradient_clipping`. Default callbacks: `gradient_checkpointing` (activation checkpointing, optional), `progress_bar` (tqdm), `checkpoint` (safetensors, rank-0), `metric_logger` (JSONL, rank-0), `gradient_clipping`, `validation` (periodic validation on val_dataset).
## Strategies ## Strategies
@@ -154,6 +156,17 @@ Keys: `prompts`, `responses`, `masks`, `rewards`.
Created by `SchedulerFactory.create(optimizer, schedule_type, **kwargs)`. Created by `SchedulerFactory.create(optimizer, schedule_type, **kwargs)`.
## Gradient Checkpointing
Trades compute for memory by recomputing activations during backward pass. Specify module types via `gradient_checkpointing_modules`:
```python
from astrai.model.components.decoder_block import DecoderBlock
config = TrainConfig(..., gradient_checkpointing_modules=[DecoderBlock])
```
Callback wraps each `DecoderBlock.forward` with `torch.utils.checkpoint.checkpoint(use_reentrant=False)`, compatible with `torch.compile`. Uses `nn.Module.apply()` for traversal — works through DDP wrappers without manual unwrap. Empty list (default) means no-op.
## Checkpoint ## Checkpoint
``` ```
@@ -188,7 +201,7 @@ export CUDA_VISIBLE_DEVICES=0,1,2,3
nohup python scripts/tools/train.py \ nohup python scripts/tools/train.py \
--nprocs=4 \ --nprocs=4 \
--train_type=pt \ --train_type=seq \
--data_root_path=/path/to/dataset \ --data_root_path=/path/to/dataset \
--param_path=/path/to/model \ --param_path=/path/to/model \
--batch_per_device=4 \ --batch_per_device=4 \
@@ -196,8 +209,8 @@ nohup python scripts/tools/train.py \
--warmup_ratio=0.05 \ --warmup_ratio=0.05 \
--max_lr=1e-4 \ --max_lr=1e-4 \
--max_grad_norm=1.0 \ --max_grad_norm=1.0 \
--adamw_beta1=0.95 \ --adamw_beta1=0.9 \
--adamw_beta2=0.99 \ --adamw_beta2=0.95 \
--adamw_weight_decay=0.01 \ --adamw_weight_decay=0.01 \
--window_size=2048 \ --window_size=2048 \
--ckpt_interval=10000 \ --ckpt_interval=10000 \
@@ -209,4 +222,4 @@ nohup python scripts/tools/train.py \
Full parameter reference at [params.md](params.md). Full parameter reference at [params.md](params.md).
> Document Update Time: 2026-05-16 > Document Update Time: 2026-05-17
+7 -5
View File
@@ -1,8 +1,9 @@
__version__ = "1.3.5" __version__ = "1.3.6"
__author__ = "ViperEkura" __author__ = "ViperEkura"
from astrai.config import ( from astrai.config import (
ModelConfig, AutoRegressiveLMConfig,
EncoderConfig,
TrainConfig, TrainConfig,
) )
from astrai.dataset import DatasetFactory from astrai.dataset import DatasetFactory
@@ -11,13 +12,14 @@ from astrai.inference import (
GenerationRequest, GenerationRequest,
InferenceEngine, InferenceEngine,
) )
from astrai.model import AutoModel, Transformer from astrai.model import AutoModel, AutoRegressiveLM
from astrai.tokenize import AutoTokenizer from astrai.tokenize import AutoTokenizer
from astrai.trainer import CallbackFactory, SchedulerFactory, StrategyFactory, Trainer from astrai.trainer import CallbackFactory, SchedulerFactory, StrategyFactory, Trainer
__all__ = [ __all__ = [
"Transformer", "AutoRegressiveLM",
"ModelConfig", "AutoRegressiveLMConfig",
"EncoderConfig",
"TrainConfig", "TrainConfig",
"DatasetFactory", "DatasetFactory",
"AutoTokenizer", "AutoTokenizer",
+10 -2
View File
@@ -1,8 +1,16 @@
from astrai.config.model_config import ModelConfig from astrai.config.model_config import (
AutoRegressiveLMConfig,
BaseModelConfig,
ConfigFactory,
EncoderConfig,
)
from astrai.config.train_config import TrainConfig from astrai.config.train_config import TrainConfig
__all__ = [ __all__ = [
# Model configuration # Model configuration
"ModelConfig", "BaseModelConfig",
"AutoRegressiveLMConfig",
"EncoderConfig",
"ConfigFactory",
"TrainConfig", "TrainConfig",
] ]
+1 -1
View File
@@ -13,7 +13,7 @@ class BaseConfig:
d[fld.name] = v d[fld.name] = v
elif v is None: elif v is None:
d[fld.name] = None d[fld.name] = None
elif isinstance(v, dict): elif isinstance(v, (dict, list)):
try: try:
json.dumps(v) json.dumps(v)
d[fld.name] = v d[fld.name] = v
+41 -21
View File
@@ -1,18 +1,24 @@
import json import json
import warnings from dataclasses import dataclass
from dataclasses import dataclass, fields
from typing import Any, Dict, Optional, Self from typing import Any, Dict, Optional, Self
from astrai.config.base import BaseConfig from astrai.config.base import BaseConfig
from astrai.factory import BaseFactory
class ConfigFactory(BaseFactory[BaseConfig]):
"""Factory that dispatches config classes by ``model_type``."""
@classmethod
def load(cls, raw: Dict[str, Any]) -> BaseConfig:
model_type = raw.get("model_type") or "autoregressive_lm"
config_cls = cls.get_component_class(model_type)
return config_cls.from_dict(raw)
@dataclass @dataclass
class BaseModelConfig(BaseConfig): class BaseModelConfig(BaseConfig):
"""Field-aware JSON from/to file for dataclass configs. """Base config with ``model_type`` dispatch and file I/O."""
Subclass with additional fields. The base ``model_type`` field
enables ``AutoModel`` to pick the correct subclass.
"""
model_type: Optional[str] = None model_type: Optional[str] = None
@@ -20,13 +26,6 @@ class BaseModelConfig(BaseConfig):
def from_file(cls, config_path: str) -> Self: def from_file(cls, config_path: str) -> Self:
with open(config_path, "r") as f: with open(config_path, "r") as f:
raw: Dict[str, Any] = json.load(f) raw: Dict[str, Any] = json.load(f)
valid = {fld.name for fld in fields(cls)}
for key in list(raw):
if key not in valid:
warnings.warn(f"Unknown config key '{key}'")
del raw[key]
return cls.from_dict(raw) return cls.from_dict(raw)
def to_file(self, config_path: str): def to_file(self, config_path: str):
@@ -37,34 +36,55 @@ class BaseModelConfig(BaseConfig):
@dataclass @dataclass
class ModelConfig(BaseModelConfig): @ConfigFactory.register("autoregressive_lm")
class AutoRegressiveLMConfig(BaseModelConfig):
"""Configuration for autoregressive language model."""
vocab_size: Optional[int] = None vocab_size: Optional[int] = None
dim: Optional[int] = None dim: Optional[int] = None
n_layers: Optional[int] = None n_layers: Optional[int] = None
norm_eps: Optional[float] = None norm_eps: Optional[float] = None
dim_ffn: Optional[int] = None dim_ffn: Optional[int] = None
tie_weight: Optional[bool] = None tie_weight: Optional[bool] = None
# RoPE
max_len: Optional[int] = None max_len: Optional[int] = None
rope_theta: Optional[float] = None rope_theta: Optional[float] = None
# attention
attn_type: str = "gqa" attn_type: str = "gqa"
n_heads: Optional[int] = None n_heads: Optional[int] = None
n_kv_heads: Optional[int] = None n_kv_heads: Optional[int] = None
use_qk_norm: Optional[bool] = None use_qk_norm: Optional[bool] = None
use_gated_attention: Optional[bool] = None use_gated_attention: Optional[bool] = None
# MLA
kv_lora_rank: Optional[int] = None kv_lora_rank: Optional[int] = None
qk_nope_head_dim: Optional[int] = None qk_nope_head_dim: Optional[int] = None
qk_rope_head_dim: Optional[int] = None qk_rope_head_dim: Optional[int] = None
# MoE
ffn_type: str = "mlp" ffn_type: str = "mlp"
n_routed_experts: Optional[int] = None n_routed_experts: Optional[int] = None
n_shared_experts: Optional[int] = None n_shared_experts: Optional[int] = None
n_activated_experts: Optional[int] = None n_activated_experts: Optional[int] = None
moe_topk_method: Optional[str] = None topk_method: Optional[str] = None
@dataclass
@ConfigFactory.register("embedding")
class EncoderConfig(BaseModelConfig):
"""Configuration for embedding encoder model."""
vocab_size: Optional[int] = None
dim: Optional[int] = None
n_layers: Optional[int] = None
norm_eps: Optional[float] = None
dim_ffn: Optional[int] = None
max_len: Optional[int] = None
rope_theta: Optional[float] = None
n_heads: Optional[int] = None
n_kv_heads: Optional[int] = None
use_qk_norm: Optional[bool] = None
use_gated_attention: Optional[bool] = None
pooling_type: Optional[str] = None
normalize_embeddings: Optional[bool] = None
+47 -18
View File
@@ -1,5 +1,5 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field, fields
from typing import Callable, Optional from typing import Callable, List, Optional
import torch.nn as nn import torch.nn as nn
from torch.optim import Optimizer from torch.optim import Optimizer
@@ -9,17 +9,25 @@ from torch.utils.data import Dataset
from astrai.config.base import BaseConfig from astrai.config.base import BaseConfig
def required(**kw):
return {"required": True, **kw}
@dataclass @dataclass
class TrainConfig(BaseConfig): class TrainConfig(BaseConfig):
# basic setting # basic setting
model: nn.Module = field(default=None, metadata={"help": "Model for training."}) model: nn.Module = field(
strategy: str = field(default=None, metadata={"help": "Training strategy."}) default=None, metadata=required(help="Model for training.")
dataset: Dataset = field(default=None, metadata={"help": "Dataset for training."}) )
strategy: str = field(default=None, metadata=required(help="Training strategy."))
dataset: Dataset = field(
default=None, metadata=required(help="Dataset for training.")
)
optimizer_fn: Callable[[nn.Module], Optimizer] = field( optimizer_fn: Callable[[nn.Module], Optimizer] = field(
default=None, metadata={"help": "Optimizer factory for training."} default=None, metadata=required(help="Optimizer factory for training.")
) )
scheduler_fn: Callable[[Optimizer], LRScheduler] = field( scheduler_fn: Callable[[Optimizer], LRScheduler] = field(
default=None, metadata={"help": "Scheduler factory for training."} default=None, metadata=required(help="Scheduler factory for training.")
) )
n_epoch: int = field(default=1, metadata={"help": "Number of epochs for training."}) n_epoch: int = field(default=1, metadata={"help": "Number of epochs for training."})
batch_per_device: int = field( batch_per_device: int = field(
@@ -31,6 +39,10 @@ class TrainConfig(BaseConfig):
max_grad_norm: float = field( max_grad_norm: float = field(
default=1.0, metadata={"help": "Maximum gradient norm."} default=1.0, metadata={"help": "Maximum gradient norm."}
) )
gradient_checkpointing_modules: list = field(
default_factory=list,
metadata={"help": "Module types to enable activation checkpointing for."},
)
# checkpoint setting # checkpoint setting
start_epoch: int = field(default=0, metadata={"help": "Start epoch for training."}) start_epoch: int = field(default=0, metadata={"help": "Start epoch for training."})
@@ -44,6 +56,19 @@ class TrainConfig(BaseConfig):
default=5000, metadata={"help": "Number of iterations between checkpoints."} default=5000, metadata={"help": "Number of iterations between checkpoints."}
) )
# metric setting
log_dir: str = field(
default="./checkpoint/logs", metadata={"help": "Directory for metric logs."}
)
log_interval: int = field(
default=100,
metadata={"help": "Number of batch iterations between metric logs."},
)
metrics: List[str] = field(
default_factory=lambda: ["loss", "lr"],
metadata={"help": "Metrics to record during training."},
)
# dataloader setting # dataloader setting
random_seed: int = field(default=3407, metadata={"help": "Random seed."}) random_seed: int = field(default=3407, metadata={"help": "Random seed."})
num_workers: int = field( num_workers: int = field(
@@ -76,11 +101,23 @@ class TrainConfig(BaseConfig):
state_dict_fn: Optional[Callable] = field( state_dict_fn: Optional[Callable] = field(
default=None, metadata={"help": "Parallel function for state dict saving."} default=None, metadata={"help": "Parallel function for state dict saving."}
) )
start_method: str = field(
default="spawn",
metadata={"help": "Multiprocessing start method (spawn/fork/forkserver)."},
)
# others # others
device_type: str = field( device_type: str = field(
default="cuda", metadata={"help": "Device type for distributed training."} default="cuda", metadata={"help": "Device type for distributed training."}
) )
val_dataset: Optional[Dataset] = field(
default=None, metadata={"help": "Dataset for validation."}
)
val_step: int = field(
default=1000,
metadata={"help": "Number of optimizer steps between validation runs."},
)
extra_kwargs: dict = field( extra_kwargs: dict = field(
default_factory=dict, metadata={"help": "Other arguments."} default_factory=dict, metadata={"help": "Other arguments."}
) )
@@ -89,14 +126,6 @@ class TrainConfig(BaseConfig):
self.validate() self.validate()
def validate(self): def validate(self):
required_fields = [ for fld in fields(self):
"model", if fld.metadata.get("required") and getattr(self, fld.name) is None:
"strategy", raise ValueError(f"TrainConfig.{fld.name} is required but got None.")
"dataset",
"optimizer_fn",
"scheduler_fn",
]
for field_name in required_fields:
if getattr(self, field_name) is None:
raise ValueError(f"{field_name} is required.")
+41
View File
@@ -28,6 +28,26 @@ class BaseDataset(Dataset, ABC):
self.stride = stride self.stride = stride
self.storage: Optional[BaseStorage] = None self.storage: Optional[BaseStorage] = None
@property
def required_keys(self) -> List[str]:
"""Return required storage keys for this dataset type.
Subclasses should override to specify expected keys.
"""
return []
def _validate_keys(self):
if not self.required_keys:
return
actual_keys = set(self.storage.keys)
missing = [k for k in self.required_keys if k not in actual_keys]
if missing:
raise KeyError(
f"Dataset {type(self).__name__} requires keys {self.required_keys}, "
f"but storage at {self._load_path} only has {sorted(actual_keys)}. "
f"Missing: {missing}"
)
def load(self, load_path: str, storage_type: Optional[str] = None, tokenizer=None): def load(self, load_path: str, storage_type: Optional[str] = None, tokenizer=None):
"""Load dataset from the given path. """Load dataset from the given path.
@@ -39,11 +59,16 @@ class BaseDataset(Dataset, ABC):
or None for auto-detection or None for auto-detection
tokenizer: Callable str -> List[int], used to tokenize raw text tokenizer: Callable str -> List[int], used to tokenize raw text
in JSON files. Ignored for HDF5. in JSON files. Ignored for HDF5.
Raises:
KeyError: If the loaded storage is missing required keys.
""" """
if storage_type is None: if storage_type is None:
storage_type = detect_format(load_path) storage_type = detect_format(load_path)
self.storage = StorageFactory.create(storage_type) self.storage = StorageFactory.create(storage_type)
self._load_path = load_path
self.storage.load(load_path, tokenizer=tokenizer) self.storage.load(load_path, tokenizer=tokenizer)
self._validate_keys()
def load_json(self, load_path: str, tokenizer=None): def load_json(self, load_path: str, tokenizer=None):
"""Load dataset from JSON files explicitly. """Load dataset from JSON files explicitly.
@@ -186,6 +211,10 @@ class SEQDataset(BaseDataset):
def __init__(self, window_size: int, stride: int): def __init__(self, window_size: int, stride: int):
super().__init__(window_size, stride) super().__init__(window_size, stride)
@property
def required_keys(self) -> List[str]:
return ["sequence"]
def _fetch_data(self, begin_idx: int, end_idx: int) -> Tensor: def _fetch_data(self, begin_idx: int, end_idx: int) -> Tensor:
return self.storage.fetch(begin_idx, end_idx, "sequence") return self.storage.fetch(begin_idx, end_idx, "sequence")
@@ -205,6 +234,10 @@ class SFTDataset(BaseDataset):
def __init__(self, window_size: int, stride: int): def __init__(self, window_size: int, stride: int):
super().__init__(window_size, stride) super().__init__(window_size, stride)
@property
def required_keys(self) -> List[str]:
return ["sequence", "loss_mask"]
def _fetch_data(self, begin_idx: int, end_idx: int, key: str) -> Tensor: def _fetch_data(self, begin_idx: int, end_idx: int, key: str) -> Tensor:
return self.storage.fetch(begin_idx, end_idx, key) return self.storage.fetch(begin_idx, end_idx, key)
@@ -229,6 +262,10 @@ class DPODataset(BaseDataset):
def __init__(self, window_size: int, stride: int): def __init__(self, window_size: int, stride: int):
super().__init__(window_size, stride) super().__init__(window_size, stride)
@property
def required_keys(self) -> List[str]:
return ["chosen", "rejected", "chosen_mask", "rejected_mask"]
def _fetch_data(self, begin_idx: int, end_idx: int, key: str) -> Tensor: def _fetch_data(self, begin_idx: int, end_idx: int, key: str) -> Tensor:
return self.storage.fetch(begin_idx, end_idx, key) return self.storage.fetch(begin_idx, end_idx, key)
@@ -259,6 +296,10 @@ class GRPODataset(BaseDataset):
def __init__(self, window_size: int, stride: int): def __init__(self, window_size: int, stride: int):
super().__init__(window_size, stride) super().__init__(window_size, stride)
@property
def required_keys(self) -> List[str]:
return ["prompts", "responses", "masks", "rewards"]
def _fetch_data(self, begin_idx: int, end_idx: int, key: str) -> Tensor: def _fetch_data(self, begin_idx: int, end_idx: int, key: str) -> Tensor:
return self.storage.fetch(begin_idx, end_idx, key) return self.storage.fetch(begin_idx, end_idx, key)
+11
View File
@@ -226,6 +226,17 @@ class OpenAIHandler(ProtocolHandler):
def create_response_id(self) -> str: def create_response_id(self) -> str:
return f"chatcmpl-{uuid.uuid4().hex[:12]}" return f"chatcmpl-{uuid.uuid4().hex[:12]}"
def get_stop_sequences(self) -> List[str]:
stop = self.request.stop
if stop is None:
return []
return [stop] if isinstance(stop, str) else stop
def on_token(
self, ctx: StreamContext, token: str, stop_checker: StopChecker
) -> Optional[str]:
return stop_checker.check(ctx.accumulated)
def format_stream_start(self, ctx: StreamContext) -> List[str]: def format_stream_start(self, ctx: StreamContext) -> List[str]:
return [ return [
_sse_event( _sse_event(
+29 -29
View File
@@ -12,7 +12,7 @@ from typing import Any, Dict, List, Optional, Union
import torch import torch
import uvicorn import uvicorn
from fastapi import FastAPI, HTTPException, Request from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from astrai.inference.api.protocol import AnthropicHandler, OpenAIHandler from astrai.inference.api.protocol import AnthropicHandler, OpenAIHandler
@@ -67,6 +67,24 @@ class MessagesRequest(BaseModel):
stop_sequences: Optional[List[str]] = None stop_sequences: Optional[List[str]] = None
@asynccontextmanager
async def lifespan(app: FastAPI):
config = app.state.server_config
if not config.get("_test", False):
try:
app.state.engine = _create_engine(**config)
except Exception as e:
logger.error(f"Failed to load model: {e}")
raise
yield
if app.state.engine:
app.state.engine.shutdown()
logger.info("Inference engine shutdown complete")
app = FastAPI(title="AstrAI Inference Server", version="0.2.0", lifespan=lifespan)
def _create_engine( def _create_engine(
param_path: Optional[Path] = None, param_path: Optional[Path] = None,
device: str = "cuda", device: str = "cuda",
@@ -92,54 +110,36 @@ def _create_engine(
return engine return engine
@asynccontextmanager def _get_engine() -> InferenceEngine:
async def lifespan(app: FastAPI): engine = app.state.engine
config = app.state.server_config
if not config.get("_test", False):
try:
app.state.engine = _create_engine(**config)
except Exception as e:
logger.error(f"Failed to load model: {e}")
raise
yield
if app.state.engine:
app.state.engine.shutdown()
logger.info("Inference engine shutdown complete")
app = FastAPI(title="AstrAI Inference Server", version="0.2.0", lifespan=lifespan)
def _get_engine(request: Request) -> InferenceEngine:
engine = request.app.state.engine
if engine is None: if engine is None:
raise HTTPException(status_code=503, detail="Engine not initialized") raise HTTPException(status_code=503, detail="Engine not initialized")
return engine return engine
@app.get("/health") @app.get("/health")
async def health(request: Request): async def health():
return { return {
"status": "ok", "status": "ok",
"model_loaded": request.app.state.engine is not None, "model_loaded": app.state.engine is not None,
} }
@app.get("/stats") @app.get("/stats")
async def get_stats(request: Request): async def get_stats():
return _get_engine(request).get_stats() return _get_engine().get_stats()
@app.post("/v1/chat/completions") @app.post("/v1/chat/completions")
async def chat_completion(request: ChatCompletionRequest, req: Request): async def chat_completion(request: ChatCompletionRequest):
engine = _get_engine(req) engine = _get_engine()
handler = OpenAIHandler(request, engine) handler = OpenAIHandler(request, engine)
return await handler.handle() return await handler.handle()
@app.post("/v1/messages") @app.post("/v1/messages")
async def create_message(request: MessagesRequest, req: Request): async def create_message(request: MessagesRequest):
engine = _get_engine(req) engine = _get_engine()
handler = AnthropicHandler(request, engine) handler = AnthropicHandler(request, engine)
return await handler.handle() return await handler.handle()
+4 -2
View File
@@ -4,7 +4,8 @@ from astrai.model.components.decoder_block import DecoderBlock
from astrai.model.components.linear import Linear from astrai.model.components.linear import Linear
from astrai.model.components.mlp import MLP from astrai.model.components.mlp import MLP
from astrai.model.components.norm import RMSNorm from astrai.model.components.norm import RMSNorm
from astrai.model.transformer import Transformer from astrai.model.encoder import EmbeddingEncoder
from astrai.model.transformer import AutoRegressiveLM
__all__ = [ __all__ = [
# Modules # Modules
@@ -14,6 +15,7 @@ __all__ = [
"GQA", "GQA",
"DecoderBlock", "DecoderBlock",
# Models # Models
"Transformer", "AutoRegressiveLM",
"EmbeddingEncoder",
"AutoModel", "AutoModel",
] ]
+7 -4
View File
@@ -2,6 +2,7 @@
AutoModel base class for model loading and saving. AutoModel base class for model loading and saving.
""" """
import json
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path from pathlib import Path
from typing import Self, Union from typing import Self, Union
@@ -9,7 +10,7 @@ from typing import Self, Union
import safetensors.torch as st import safetensors.torch as st
import torch.nn as nn import torch.nn as nn
from astrai.config import ModelConfig from astrai.config.model_config import BaseModelConfig, ConfigFactory
from astrai.factory import BaseFactory from astrai.factory import BaseFactory
@@ -45,7 +46,7 @@ class AutoModel(BaseFactory["AutoModel"], nn.Module):
Provides model loading/saving, registration, and generation. Provides model loading/saving, registration, and generation.
""" """
def __init__(self, config: ModelConfig): def __init__(self, config: BaseModelConfig):
super().__init__() super().__init__()
self.config = config self.config = config
@@ -62,11 +63,13 @@ class AutoModel(BaseFactory["AutoModel"], nn.Module):
# Load config # Load config
config_path = model_path / "config.json" config_path = model_path / "config.json"
if config_path.exists(): if config_path.exists():
config = ModelConfig.from_file(str(config_path)) with open(config_path, "r") as f:
raw = json.load(f)
config = ConfigFactory.load(raw)
model_type = config.model_type or "autoregressive_lm"
else: else:
raise FileNotFoundError(f"Config file not found: {config_path}") raise FileNotFoundError(f"Config file not found: {config_path}")
model_type = config.model_type or "transformer"
actual_cls = AutoModel.get_component_class(model_type) actual_cls = AutoModel.get_component_class(model_type)
with _disable_random_init(enable=disable_random_init): with _disable_random_init(enable=disable_random_init):
+10
View File
@@ -120,6 +120,7 @@ class MLA(nn.Module):
qk_nope_head_dim: int, qk_nope_head_dim: int,
qk_rope_head_dim: int, qk_rope_head_dim: int,
norm_eps: float, norm_eps: float,
use_qk_norm: bool,
use_gated_attention: bool, use_gated_attention: bool,
layer_id: int, layer_id: int,
): ):
@@ -133,9 +134,14 @@ class MLA(nn.Module):
self.head_dim = qk_nope_head_dim + qk_rope_head_dim self.head_dim = qk_nope_head_dim + qk_rope_head_dim
self.layer_id = layer_id self.layer_id = layer_id
self.n_rep = n_heads // n_kv_heads self.n_rep = n_heads // n_kv_heads
self.use_qk_norm = use_qk_norm
self.use_gated_attention = use_gated_attention self.use_gated_attention = use_gated_attention
self.q_proj = Linear(dim, n_heads * self.head_dim, bias=False) self.q_proj = Linear(dim, n_heads * self.head_dim, bias=False)
if self.use_qk_norm:
self.q_norm = RMSNorm(self.head_dim, norm_eps)
self.k_norm = RMSNorm(self.head_dim, norm_eps)
self.kv_a_proj = Linear(dim, kv_lora_rank, bias=False) self.kv_a_proj = Linear(dim, kv_lora_rank, bias=False)
self.kv_norm = RMSNorm(kv_lora_rank, norm_eps) self.kv_norm = RMSNorm(kv_lora_rank, norm_eps)
@@ -182,6 +188,10 @@ class MLA(nn.Module):
q = torch.cat([q_nope, q_rope], dim=-1) q = torch.cat([q_nope, q_rope], dim=-1)
k = torch.cat([k_nope, k_rope], dim=-1) k = torch.cat([k_nope, k_rope], dim=-1)
if self.use_qk_norm:
q = self.q_norm(q)
k = self.k_norm(k)
if paged_cache is not None: if paged_cache is not None:
paged_cache.write(self.layer_id, k, v) paged_cache.write(self.layer_id, k, v)
k, v = paged_cache.gather(self.layer_id) k, v = paged_cache.gather(self.layer_id)
+3
View File
@@ -9,5 +9,8 @@ class Embedding(nn.Module):
super().__init__() super().__init__()
self.weight = nn.Parameter(torch.empty((vocab_size, embedding_dim))) self.weight = nn.Parameter(torch.empty((vocab_size, embedding_dim)))
def reset_parameters(self):
nn.init.normal_(self.weight, mean=0.0, std=0.02)
def forward(self, x: Tensor) -> Tensor: def forward(self, x: Tensor) -> Tensor:
return F.embedding(x, self.weight) return F.embedding(x, self.weight)
+7
View File
@@ -10,5 +10,12 @@ class Linear(nn.Module):
self.weight = nn.Parameter(torch.empty((out_dim, in_dim))) self.weight = nn.Parameter(torch.empty((out_dim, in_dim)))
self.bias = nn.Parameter(torch.zeros(out_dim)) if bias else None self.bias = nn.Parameter(torch.zeros(out_dim)) if bias else None
def reset_parameters(self):
nn.init.kaiming_uniform_(self.weight, a=5**0.5)
if self.bias is not None:
fan_in, _ = nn.init._calculate_fan_in_and_fan_out(self.weight)
bound = 1 / (fan_in**0.5)
nn.init.uniform_(self.bias, -bound, bound)
def forward(self, x: Tensor) -> Tensor: def forward(self, x: Tensor) -> Tensor:
return F.linear(x, self.weight, self.bias) return F.linear(x, self.weight, self.bias)
+100
View File
@@ -0,0 +1,100 @@
from typing import Any, Mapping, Optional
import torch
import torch.nn as nn
from torch import Tensor
from astrai.config.model_config import EncoderConfig
from astrai.model.automodel import AutoModel
from astrai.model.components.decoder_block import DecoderBlock
from astrai.model.components.embedding import Embedding
from astrai.model.components.norm import RMSNorm
from astrai.model.components.rope import RotaryEmbedding
from astrai.model.transformer import process_attention_mask
@AutoModel.register("embedding")
class EmbeddingEncoder(AutoModel):
def __init__(self, config: EncoderConfig):
super().__init__(config)
self.config = config
rope_dim = config.dim // config.n_heads
rope_base = config.rope_theta if config.rope_theta is not None else 10000
self.rotary_embedding = RotaryEmbedding(rope_dim, config.max_len, rope_base)
self.embed_tokens = Embedding(config.vocab_size, config.dim)
self.layers = nn.ModuleList(
[
DecoderBlock(
config.dim,
config.n_heads,
config.dim_ffn,
config.n_kv_heads,
config.norm_eps,
config.use_qk_norm,
config.use_gated_attention,
layer_id,
)
for layer_id in range(config.n_layers)
]
)
self.norm = RMSNorm(config.dim, config.norm_eps)
self.pooling_type = config.pooling_type or "mean"
self.normalize_embeddings = config.normalize_embeddings or False
self.apply(self._init_weights)
def _init_weights(self, module):
if hasattr(module, "reset_parameters"):
module.reset_parameters()
def load_state_dict(self, state_dict: Mapping[str, Any], strict=True, assign=False):
state_dict = dict(state_dict)
state_dict.pop("lm_head.weight", None)
return super().load_state_dict(state_dict, strict=strict, assign=assign)
def forward(
self,
input_ids: Tensor,
input_mask: Optional[Tensor] = None,
position_ids: Optional[Tensor] = None,
) -> Tensor:
assert input_ids.ndim == 2
B, S = input_ids.shape
x = self.embed_tokens(input_ids)
if position_ids is None:
position_ids = torch.arange(S, device=x.device).unsqueeze(0).expand(B, -1)
rotary_emb = self.rotary_embedding(x, position_ids)
attn_mask = process_attention_mask(x, position_ids, input_mask, is_causal=False)
for layer in self.layers:
x = layer(x, rotary_emb, attn_mask, paged_cache=None)
hidden_states = self.norm(x)
if self.pooling_type == "cls":
pooled = hidden_states[:, 0]
elif self.pooling_type == "last":
if input_mask is not None:
lengths = input_mask.sum(dim=1) - 1
pooled = hidden_states[torch.arange(B, device=x.device), lengths]
else:
pooled = hidden_states[:, -1]
else:
if input_mask is not None:
mask = input_mask.unsqueeze(-1).to(dtype=hidden_states.dtype)
pooled = (hidden_states * mask).sum(dim=1) / mask.sum(dim=1).clamp(
min=1.0
)
else:
pooled = hidden_states.mean(dim=1)
if self.normalize_embeddings:
pooled = torch.nn.functional.normalize(pooled, p=2, dim=-1)
return pooled
+10 -11
View File
@@ -4,7 +4,7 @@ import torch
import torch.nn as nn import torch.nn as nn
from torch import Tensor from torch import Tensor
from astrai.config.model_config import ModelConfig from astrai.config.model_config import AutoRegressiveLMConfig
from astrai.inference.core.cache import KvcacheView from astrai.inference.core.cache import KvcacheView
from astrai.model.automodel import AutoModel from astrai.model.automodel import AutoModel
from astrai.model.components.decoder_block import DecoderBlock from astrai.model.components.decoder_block import DecoderBlock
@@ -46,11 +46,11 @@ def process_attention_mask(
).masked_fill_(attend.unsqueeze(1), 0.0) ).masked_fill_(attend.unsqueeze(1), 0.0)
@AutoModel.register("transformer") @AutoModel.register("autoregressive_lm")
class Transformer(AutoModel): class AutoRegressiveLM(AutoModel):
"""Transformer language model with paged KV cache.""" """Autoregressive language model with paged KV cache."""
def __init__(self, config: ModelConfig): def __init__(self, config: AutoRegressiveLMConfig):
super().__init__(config) super().__init__(config)
self.config = config self.config = config
rope_dim = ( rope_dim = (
@@ -78,7 +78,7 @@ class Transformer(AutoModel):
n_routed_experts=config.n_routed_experts, n_routed_experts=config.n_routed_experts,
n_shared_experts=config.n_shared_experts, n_shared_experts=config.n_shared_experts,
n_activated_experts=config.n_activated_experts, n_activated_experts=config.n_activated_experts,
topk_method=config.moe_topk_method, topk_method=config.topk_method,
kv_lora_rank=config.kv_lora_rank, kv_lora_rank=config.kv_lora_rank,
qk_nope_head_dim=config.qk_nope_head_dim, qk_nope_head_dim=config.qk_nope_head_dim,
qk_rope_head_dim=config.qk_rope_head_dim, qk_rope_head_dim=config.qk_rope_head_dim,
@@ -93,12 +93,11 @@ class Transformer(AutoModel):
if self.config.tie_weight is True: if self.config.tie_weight is True:
self.lm_head.weight = self.embed_tokens.weight self.lm_head.weight = self.embed_tokens.weight
self._init_weights() self.apply(self._init_weights)
def _init_weights(self): def _init_weights(self, module):
for param in self.parameters(): if hasattr(module, "reset_parameters"):
if param.dim() > 1: module.reset_parameters()
nn.init.normal_(param, mean=0.0, std=0.006)
def load_state_dict(self, state_dict: Mapping[str, Any], strict=True, assign=False): def load_state_dict(self, state_dict: Mapping[str, Any], strict=True, assign=False):
lm_head_key = "lm_head.weight" lm_head_key = "lm_head.weight"
+7 -2
View File
@@ -123,6 +123,7 @@ def spawn_parallel_fn(
master_addr: str = "localhost", master_addr: str = "localhost",
master_port: str = "29500", master_port: str = "29500",
device_type: str = "cuda", device_type: str = "cuda",
start_method: str = "spawn",
**kwargs, **kwargs,
): ):
# clear environment variables # clear environment variables
@@ -156,6 +157,10 @@ def spawn_parallel_fn(
kwargs, kwargs,
) )
mp.spawn( mp.start_processes(
wrapper_spawn_func, nprocs=world_size, args=wrapper_spawn_func_args, join=True wrapper_spawn_func,
args=wrapper_spawn_func_args,
nprocs=world_size,
start_method=start_method,
join=True,
) )
+1 -1
View File
@@ -38,7 +38,7 @@ class Checkpoint:
meta = { meta = {
"epoch": self.epoch, "epoch": self.epoch,
"iteration": self.iteration, "iteration": self.iteration,
"timestamp": time.time(), "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
} }
meta.update(self.meta) meta.update(self.meta)
with open(save_path / "meta.json", "w") as f: with open(save_path / "meta.json", "w") as f:
+19 -2
View File
@@ -51,9 +51,26 @@ class AutoTokenizer:
self.set_chat_template(config["chat_template"]) self.set_chat_template(config["chat_template"])
@classmethod @classmethod
def from_pretrained(cls, path: Union[str, Path], **kwargs) -> "AutoTokenizer": def from_pretrained(cls, path: Union[str, Path]) -> "AutoTokenizer":
"""Load tokenizer from pretrained directory.""" """Load tokenizer from pretrained directory.
Raises:
FileNotFoundError: If tokenizer.json is missing.
RuntimeError: If tokenizer failed to initialize.
"""
path = Path(path)
tokenizer_file = path / "tokenizer.json"
if not tokenizer_file.exists():
raise FileNotFoundError(
f"Tokenizer file not found: {tokenizer_file}. "
"A valid tokenizer.json is required."
)
instance = cls(path) instance = cls(path)
if instance._tokenizer is None:
raise RuntimeError(
f"Failed to load tokenizer from {path}. "
"The tokenizer.json may be corrupted or incompatible."
)
return instance return instance
def save_pretrained(self, save_path: str): def save_pretrained(self, save_path: str):
+3
View File
@@ -1,3 +1,4 @@
from astrai.trainer.optim import Muon
from astrai.trainer.schedule import BaseScheduler, SchedulerFactory from astrai.trainer.schedule import BaseScheduler, SchedulerFactory
from astrai.trainer.strategy import BaseStrategy, StrategyFactory from astrai.trainer.strategy import BaseStrategy, StrategyFactory
from astrai.trainer.train_callback import ( from astrai.trainer.train_callback import (
@@ -9,6 +10,8 @@ from astrai.trainer.trainer import Trainer
__all__ = [ __all__ = [
# Main trainer # Main trainer
"Trainer", "Trainer",
# Optimizer
"Muon",
# Strategy factory # Strategy factory
"StrategyFactory", "StrategyFactory",
"BaseStrategy", "BaseStrategy",
+4
View File
@@ -47,6 +47,10 @@ def ctx_get_lr(ctx):
return ctx.optimizer.param_groups[-1]["lr"] return ctx.optimizer.param_groups[-1]["lr"]
def ctx_get_val_loss(ctx):
return ctx.val_loss
def ctx_get_grad_norm(ctx): def ctx_get_grad_norm(ctx):
return grad_norm(ctx.model) return grad_norm(ctx.model)
+113
View File
@@ -0,0 +1,113 @@
import torch
from torch.optim import Optimizer
def _zeropower_via_newtonschulz(G: torch.Tensor, steps: int = 5):
assert G.ndim == 2
X = G.bfloat16()
scale = max(1, G.size(0) / G.size(1)) ** 0.5
X = X / (X.norm() + 1e-7) * scale
if steps == 0:
return X.type_as(G)
a, b, c = (3.4445, -4.7750, 2.0315)
for _ in range(steps):
A = X @ X.T
B = A @ X
X = a * X + b * B + c * (A @ B)
return X.type_as(G)
class Muon(Optimizer):
def __init__(
self,
params,
lr: float = 2e-3,
momentum: float = 0.95,
weight_decay: float = 0.0,
nesterov: bool = True,
ns_steps: int = 5,
adamw_lr: float = None,
adamw_betas: tuple = (0.9, 0.95),
adamw_eps: float = 1e-8,
adamw_wd: float = 0.0,
):
defaults = dict(
lr=lr,
momentum=momentum,
weight_decay=weight_decay,
nesterov=nesterov,
ns_steps=ns_steps,
adamw_lr=adamw_lr if adamw_lr is not None else lr * 0.1,
adamw_betas=adamw_betas,
adamw_eps=adamw_eps,
adamw_wd=adamw_wd,
)
super().__init__(params, defaults)
@torch.no_grad()
def step(self, closure=None):
loss = None
if closure is not None:
with torch.enable_grad():
loss = closure()
for group in self.param_groups:
for p in group["params"]:
if p.grad is None:
continue
grad = p.grad
if grad.is_sparse:
raise RuntimeError("Muon does not support sparse gradients")
if p.ndim >= 2:
self._muon_update(p, grad, group)
else:
self._adamw_update(p, grad, group)
return loss
def _muon_update(self, p, grad, group):
lr = group["lr"]
momentum = group["momentum"]
wd = group["weight_decay"]
nesterov = group["nesterov"]
ns_steps = group["ns_steps"]
state = self.state[p]
p.mul_(1 - lr * wd)
if nesterov:
grad = grad.add(p, alpha=wd)
if "momentum_buffer" not in state:
state["momentum_buffer"] = torch.zeros_like(grad)
buf = state["momentum_buffer"]
buf.lerp_(grad, 1 - momentum)
update = _zeropower_via_newtonschulz(buf, steps=ns_steps)
scale = max(1, p.size(0) / p.size(1)) ** 0.5
p.add_(update, alpha=-lr * scale)
def _adamw_update(self, p, grad, group):
lr = group["adamw_lr"]
betas = group["adamw_betas"]
eps = group["adamw_eps"]
wd = group["adamw_wd"]
state = self.state[p]
if not state:
state["step"] = 0
state["exp_avg"] = torch.zeros_like(p)
state["exp_avg_sq"] = torch.zeros_like(p)
state["step"] += 1
exp_avg, exp_avg_sq = state["exp_avg"], state["exp_avg_sq"]
beta1, beta2 = betas
exp_avg.lerp_(grad, 1 - beta1)
exp_avg_sq.lerp_(grad.square(), 1 - beta2)
step = state["step"]
bias1 = 1 - beta1**step
bias2 = 1 - beta2**step
p.mul_(1 - lr * wd)
denom = exp_avg_sq.sqrt().div_(bias2**0.5).add_(eps)
p.addcdiv_(exp_avg / bias1, denom, value=-lr)
+97 -6
View File
@@ -1,15 +1,21 @@
import json import json
import logging
import os import os
import sys
import time import time
from pathlib import Path from pathlib import Path
from typing import Callable, List, Optional, Protocol, runtime_checkable from typing import IO, Callable, List, Optional, Protocol, runtime_checkable
import torch
import torch.distributed as dist
import torch.nn as nn import torch.nn as nn
from torch.nn.utils import clip_grad_norm_ from torch.nn.utils import clip_grad_norm_
from torch.utils.checkpoint import checkpoint as torch_checkpoint
from tqdm import tqdm from tqdm import tqdm
from astrai.factory import BaseFactory from astrai.factory import BaseFactory
from astrai.parallel import only_on_rank from astrai.parallel import only_on_rank
from astrai.parallel.setup import get_current_device
from astrai.serialization import Checkpoint from astrai.serialization import Checkpoint
from astrai.trainer.metric_util import ( from astrai.trainer.metric_util import (
ctx_get_grad_max, ctx_get_grad_max,
@@ -20,9 +26,12 @@ from astrai.trainer.metric_util import (
ctx_get_grad_std, ctx_get_grad_std,
ctx_get_loss, ctx_get_loss,
ctx_get_lr, ctx_get_lr,
ctx_get_val_loss,
) )
from astrai.trainer.train_context import TrainContext from astrai.trainer.train_context import TrainContext
logger = logging.getLogger(__name__)
@runtime_checkable @runtime_checkable
class TrainCallback(Protocol): class TrainCallback(Protocol):
@@ -83,6 +92,41 @@ class GradientClippingCallback(TrainCallback):
clip_grad_norm_(context.model.parameters(), self.max_grad_norm) clip_grad_norm_(context.model.parameters(), self.max_grad_norm)
@CallbackFactory.register("gradient_checkpointing")
class GradientCheckpointingCallback(TrainCallback):
"""
Activation checkpointing callback — trades compute for memory
by recomputing specified module activations during the backward pass.
Args:
modules: Module types to apply checkpointing to.
"""
def __init__(self, modules: Optional[List[type]] = None):
self.modules = tuple(modules) if modules else ()
def _enable(self, module: nn.Module):
if self.modules and isinstance(module, self.modules):
fn = module.forward
module._original_forward = fn
module.forward = lambda *a, **kw: torch_checkpoint(
fn, *a, use_reentrant=False, **kw
)
@staticmethod
def _disable(module: nn.Module):
if hasattr(module, "_original_forward"):
module.forward = module._original_forward
del module._original_forward
def on_train_begin(self, context: TrainContext):
context.model.apply(self._enable)
logger.info("Gradient checkpointing enabled")
def on_train_end(self, context: TrainContext):
context.model.apply(self._disable)
@CallbackFactory.register("checkpoint") @CallbackFactory.register("checkpoint")
class CheckpointCallback(TrainCallback): class CheckpointCallback(TrainCallback):
""" """
@@ -168,8 +212,12 @@ class ProgressBarCallback(TrainCallback):
Progress bar callback for trainer. Progress bar callback for trainer.
""" """
def __init__(self, num_epoch: int): def __init__(
self, num_epoch: int, log_interval: int = 100, file: IO[str] = sys.stdout
):
self.num_epoch = num_epoch self.num_epoch = num_epoch
self.log_interval = log_interval
self.file = file
self.progress_bar: tqdm = None self.progress_bar: tqdm = None
@only_on_rank(0) @only_on_rank(0)
@@ -178,16 +226,18 @@ class ProgressBarCallback(TrainCallback):
context.dataloader, context.dataloader,
desc=f"Epoch {context.epoch + 1}/{self.num_epoch}", desc=f"Epoch {context.epoch + 1}/{self.num_epoch}",
dynamic_ncols=True, dynamic_ncols=True,
file=self.file,
) )
@only_on_rank(0) @only_on_rank(0)
def on_batch_end(self, context: TrainContext): def on_batch_end(self, context: TrainContext):
self.progress_bar.set_postfix( postfix = {
{
"loss": f"{context.loss:.4f}", "loss": f"{context.loss:.4f}",
"lr": f"{context.optimizer.param_groups[-1]['lr']:.2e}", "lr": f"{context.optimizer.param_groups[-1]['lr']:.2e}",
} }
) if context.val_loss > 0:
postfix["val_loss"] = f"{context.val_loss:.4f}"
self.progress_bar.set_postfix(postfix)
self.progress_bar.update(1) self.progress_bar.update(1)
@only_on_rank(0) @only_on_rank(0)
@@ -219,6 +269,7 @@ class MetricLoggerCallback(TrainCallback):
self._metric_funcs = { self._metric_funcs = {
"loss": ctx_get_loss, "loss": ctx_get_loss,
"lr": ctx_get_lr, "lr": ctx_get_lr,
"val_loss": ctx_get_val_loss,
"grad_norm": ctx_get_grad_norm, "grad_norm": ctx_get_grad_norm,
"grad_std": ctx_get_grad_std, "grad_std": ctx_get_grad_std,
"grad_max": ctx_get_grad_max, "grad_max": ctx_get_grad_max,
@@ -229,7 +280,7 @@ class MetricLoggerCallback(TrainCallback):
def _get_log_data(self, context: TrainContext): def _get_log_data(self, context: TrainContext):
return { return {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
"epoch": context.epoch, "epoch": context.epoch,
"iter": context.iteration, "iter": context.iteration,
**{m: self._metric_funcs[m](context) for m in self.metrics}, **{m: self._metric_funcs[m](context) for m in self.metrics},
@@ -262,3 +313,43 @@ class MetricLoggerCallback(TrainCallback):
def on_error(self, context): def on_error(self, context):
self._save_log(context.epoch, context.iteration) self._save_log(context.epoch, context.iteration)
@CallbackFactory.register("validation")
class ValidationCallback(TrainCallback):
def _run_validation(self, context: TrainContext):
context.model.eval()
total_loss = 0.0
num_batches = 0
with torch.no_grad():
for batch in context.val_dataloader:
loss = context.strategy(batch)
total_loss += loss.item()
num_batches += 1
avg_loss = total_loss / max(num_batches, 1)
if context.world_size > 1 and dist.is_initialized():
loss_tensor = torch.tensor([avg_loss], device=get_current_device())
dist.all_reduce(loss_tensor, op=dist.ReduceOp.AVG)
avg_loss = loss_tensor.item()
context.val_loss = avg_loss
context.model.train()
step_count = context.iteration // context.config.grad_accum_steps
logger.info(
f"Epoch {context.epoch + 1}, Step {step_count}, Val Loss: {avg_loss:.4f}"
)
def on_step_end(self, context: TrainContext):
if context.val_dataloader is None:
return
cfg = context.config
if cfg.val_step <= 0:
return
step_count = context.iteration // cfg.grad_accum_steps
if step_count % cfg.val_step == 0:
self._run_validation(context)
+19
View File
@@ -26,6 +26,8 @@ class TrainContext:
epoch: int = field(default=0) epoch: int = field(default=0)
iteration: int = field(default=0) iteration: int = field(default=0)
loss: float = field(default=0.0) loss: float = field(default=0.0)
val_dataloader: DataLoader = field(default=None)
val_loss: float = field(default=0.0)
world_size: int = field(default=1) world_size: int = field(default=1)
rank: int = field(default=0) rank: int = field(default=0)
@@ -88,6 +90,23 @@ class TrainContextBuilder:
prefetch_factor=cfg.prefetch_factor, prefetch_factor=cfg.prefetch_factor,
) )
if cfg.val_dataset is not None:
val_sampler = ResumableDistributedSampler(
data_source=cfg.val_dataset,
start_epoch=0,
start_iter=0,
seed=cfg.random_seed,
shuffle=False,
)
context.val_dataloader = DataLoader(
cfg.val_dataset,
batch_size=cfg.batch_per_device,
sampler=val_sampler,
num_workers=cfg.num_workers,
pin_memory=cfg.pin_memory,
prefetch_factor=cfg.prefetch_factor,
)
context.strategy = StrategyFactory.create( context.strategy = StrategyFactory.create(
model=context.model, model=context.model,
train_type=self.config.strategy, train_type=self.config.strategy,
+34 -16
View File
@@ -25,12 +25,29 @@ class Trainer:
def _get_default_callbacks(self) -> List[TrainCallback]: def _get_default_callbacks(self) -> List[TrainCallback]:
cfg = self.train_config cfg = self.train_config
return [ callbacks = [
CallbackFactory.create(
"gradient_checkpointing",
modules=cfg.gradient_checkpointing_modules,
),
CallbackFactory.create(
"checkpoint",
cfg.ckpt_dir,
cfg.ckpt_interval,
state_dict_fn=cfg.state_dict_fn,
),
CallbackFactory.create(
"metric_logger",
log_dir=cfg.log_dir,
save_interval=cfg.ckpt_interval,
log_interval=cfg.log_interval,
metrics=cfg.metrics,
),
CallbackFactory.create("progress_bar", cfg.n_epoch), CallbackFactory.create("progress_bar", cfg.n_epoch),
CallbackFactory.create("checkpoint", cfg.ckpt_dir, cfg.ckpt_interval),
CallbackFactory.create("metric_logger", cfg.ckpt_dir, cfg.ckpt_interval),
CallbackFactory.create("gradient_clipping", cfg.max_grad_norm), CallbackFactory.create("gradient_clipping", cfg.max_grad_norm),
CallbackFactory.create("validation"),
] ]
return callbacks
def _call_callbacks(self, method_name: str, context: TrainContext): def _call_callbacks(self, method_name: str, context: TrainContext):
for callback in self.callbacks: for callback in self.callbacks:
@@ -38,19 +55,7 @@ class Trainer:
if method: if method:
method(context) method(context)
def train(self, checkpoint: Optional[Checkpoint] = None): def _trainer_loop(self, checkpoint: Optional[Checkpoint] = None):
cfg = self.train_config
spawn_parallel_fn(
self._train_impl,
backend=cfg.backend,
world_size=cfg.nprocs,
master_addr=cfg.master_addr,
master_port=cfg.master_port,
device_type=cfg.device_type,
checkpoint=checkpoint,
)
def _train_impl(self, checkpoint: Optional[Checkpoint] = None):
cfg = self.train_config cfg = self.train_config
context = TrainContextBuilder(cfg).with_checkpoint(checkpoint).build() context = TrainContextBuilder(cfg).with_checkpoint(checkpoint).build()
self._call_callbacks("on_train_begin", context) self._call_callbacks("on_train_begin", context)
@@ -89,3 +94,16 @@ class Trainer:
raise raise
finally: finally:
self._call_callbacks("on_train_end", context) self._call_callbacks("on_train_end", context)
def train(self, checkpoint: Optional[Checkpoint] = None):
cfg = self.train_config
spawn_parallel_fn(
self._trainer_loop,
backend=cfg.backend,
world_size=cfg.nprocs,
master_addr=cfg.master_addr,
master_port=cfg.master_port,
device_type=cfg.device_type,
start_method=cfg.start_method,
checkpoint=checkpoint,
)
+8 -6
View File
@@ -1,12 +1,13 @@
services: services:
server: server:
build: . build:
image: astrai:latest context: .
dockerfile: Dockerfile
user: "${UID:-1000}:${GID:-1000}"
ports: ports:
- "8000:8000" - "8000:8000"
volumes: volumes:
- ./params:/app/params:ro - ./params:/app/params:ro
- ./checkpoints:/app/checkpoints
command: python -m scripts.tools.server --port 8000 --device cuda command: python -m scripts.tools.server --port 8000 --device cuda
deploy: deploy:
resources: resources:
@@ -25,13 +26,14 @@ services:
server-cpu: server-cpu:
profiles: [cpu] profiles: [cpu]
build: . build:
image: astrai:latest context: .
dockerfile: Dockerfile
user: "${UID:-1000}:${GID:-1000}"
ports: ports:
- "8000:8000" - "8000:8000"
volumes: volumes:
- ./params:/app/params:ro - ./params:/app/params:ro
- ./checkpoints:/app/checkpoints
command: python -m scripts.tools.server --port 8000 --device cpu command: python -m scripts.tools.server --port 8000 --device cpu
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"] test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
+8 -1
View File
@@ -16,6 +16,7 @@ NC='\033[0m' # No Color
IMAGE_NAME="astrai" IMAGE_NAME="astrai"
IMAGE_TAG="latest" IMAGE_TAG="latest"
REGISTRY="" REGISTRY=""
CONTAINER_ID=""
# Print colored messages # Print colored messages
print_info() { print_info() {
@@ -175,6 +176,10 @@ main() {
PORT="$2" PORT="$2"
shift 2 shift 2
;; ;;
--container)
CONTAINER_ID="$2"
shift 2
;;
--gpu) --gpu)
GPU=true GPU=true
shift shift
@@ -197,6 +202,7 @@ main() {
echo " --dockerfile FILE Dockerfile path (default: Dockerfile)" echo " --dockerfile FILE Dockerfile path (default: Dockerfile)"
echo " --context PATH Build context (default: .)" echo " --context PATH Build context (default: .)"
echo " --port PORT Port for run (default: 8000)" echo " --port PORT Port for run (default: 8000)"
echo " --container ID Container ID for logs"
echo " --gpu Enable GPU support" echo " --gpu Enable GPU support"
echo " --help Show this help message" echo " --help Show this help message"
echo "" echo ""
@@ -205,6 +211,7 @@ main() {
echo " $0 build --tag v1.0.0" echo " $0 build --tag v1.0.0"
echo " $0 run --port 8080" echo " $0 run --port 8080"
echo " $0 run --gpu" echo " $0 run --gpu"
echo " $0 logs --container abc123"
echo " $0 push --registry ghcr.io/username" echo " $0 push --registry ghcr.io/username"
exit 0 exit 0
;; ;;
@@ -237,7 +244,7 @@ main() {
show_info show_info
;; ;;
logs) logs)
show_logs "$2" show_logs "$CONTAINER_ID"
;; ;;
"") "")
print_error "No command specified. Use --help for usage" print_error "No command specified. Use --help for usage"
+7 -7
View File
@@ -1,13 +1,13 @@
"""Benchmark Transformer with KVCache""" """Benchmark AutoRegressiveLM with KVCache"""
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Dict from typing import Any, Dict
import torch import torch
from astrai.config import ModelConfig from astrai.config import AutoRegressiveLMConfig
from astrai.inference import KVCache from astrai.inference import KVCache
from astrai.model.transformer import Transformer from astrai.model.transformer import AutoRegressiveLM
@dataclass @dataclass
@@ -21,7 +21,7 @@ class BenchmarkResult:
class GenerationBenchmark: class GenerationBenchmark:
def __init__( def __init__(
self, self,
config: ModelConfig, config: AutoRegressiveLMConfig,
device: str = "cuda", device: str = "cuda",
dtype: torch.dtype = torch.bfloat16, dtype: torch.dtype = torch.bfloat16,
page_size: int = 128, page_size: int = 128,
@@ -29,7 +29,7 @@ class GenerationBenchmark:
self.config = config self.config = config
self.device = device self.device = device
self.dtype = dtype self.dtype = dtype
self.model = Transformer(config).to(device=device, dtype=dtype) self.model = AutoRegressiveLM(config).to(device=device, dtype=dtype)
self.model.eval() self.model.eval()
head_dim = config.dim // config.n_heads head_dim = config.dim // config.n_heads
n_pages = (config.max_len * 4 + page_size - 1) // page_size n_pages = (config.max_len * 4 + page_size - 1) // page_size
@@ -216,7 +216,7 @@ def print_benchmark_result(result: BenchmarkResult):
if __name__ == "__main__": if __name__ == "__main__":
config = ModelConfig( config = AutoRegressiveLMConfig(
vocab_size=10000, vocab_size=10000,
dim=1536, dim=1536,
n_heads=24, n_heads=24,
@@ -230,7 +230,7 @@ if __name__ == "__main__":
benchmark = GenerationBenchmark(config) benchmark = GenerationBenchmark(config)
print("=" * 80) print("=" * 80)
print("Running Transformer Generation Benchmark (KVCache)") print("Running AutoRegressiveLM Generation Benchmark (KVCache)")
print("=" * 80) print("=" * 80)
prefill_result = benchmark.run_prefill_benchmark( prefill_result = benchmark.run_prefill_benchmark(
+22 -11
View File
@@ -8,16 +8,16 @@ import torch.nn as nn
import torch.optim as optim import torch.optim as optim
from torch.nn.parallel import DistributedDataParallel as DDP from torch.nn.parallel import DistributedDataParallel as DDP
from astrai.config import ModelConfig, TrainConfig from astrai.config import AutoRegressiveLMConfig, TrainConfig
from astrai.dataset import DatasetFactory from astrai.dataset import DatasetFactory
from astrai.model import Transformer from astrai.model import AutoRegressiveLM
from astrai.parallel import get_rank from astrai.parallel import get_rank
from astrai.trainer import SchedulerFactory, Trainer from astrai.trainer import SchedulerFactory, Trainer
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Train the Transformer model.") parser = argparse.ArgumentParser(description="Train the AutoRegressiveLM model.")
parser.add_argument( parser.add_argument(
"--train_type", "--train_type",
@@ -69,14 +69,14 @@ def parse_args() -> argparse.Namespace:
parser.add_argument( parser.add_argument(
"--adamw_beta1", "--adamw_beta1",
type=float, type=float,
default=0.95, default=0.9,
help="Beta values for AdamW optimizer.", help="Beta1 for AdamW optimizer.",
) )
parser.add_argument( parser.add_argument(
"--adamw_beta2", "--adamw_beta2",
type=float, type=float,
default=0.99, default=0.95,
help="Beta values for AdamW optimizer.", help="Beta2 for AdamW optimizer.",
) )
parser.add_argument( parser.add_argument(
"--adamw_weight_decay", "--adamw_weight_decay",
@@ -149,6 +149,13 @@ def parse_args() -> argparse.Namespace:
parser.add_argument( parser.add_argument(
"--device_type", type=str, default="cuda", help="Device type to use." "--device_type", type=str, default="cuda", help="Device type to use."
) )
parser.add_argument(
"--start_method",
type=str,
default="spawn",
choices=["spawn", "fork", "forkserver"],
help="Multiprocessing start method.",
)
args = parser.parse_args() args = parser.parse_args()
@@ -180,7 +187,9 @@ def create_scheduler(
def prepare_checkpoint(model: nn.Module) -> dict: def prepare_checkpoint(model: nn.Module) -> dict:
if isinstance(model, DDP):
return model.module.state_dict() return model.module.state_dict()
return model.state_dict()
def compute_total_steps( def compute_total_steps(
@@ -230,19 +239,20 @@ def train(
stride: int, stride: int,
nprocs: int, nprocs: int,
device_type: str, device_type: str,
start_method: str,
): ):
assert train_type in ["seq", "sft", "dpo", "grpo"] assert train_type in ["seq", "sft", "dpo", "grpo"]
assert os.path.exists(param_path) assert os.path.exists(param_path)
# Load config # Load config
config_path = os.path.join(param_path, "config.json") config_path = os.path.join(param_path, "config.json")
config = ModelConfig.from_file(config_path) config = AutoRegressiveLMConfig.from_file(config_path)
if window_size is None: if window_size is None:
window_size = config.max_len window_size = config.max_len
# Create bare Transformer (for training, no tokenizer needed) # Create bare AutoRegressiveLM (for training, no tokenizer needed)
model = Transformer(config) model = AutoRegressiveLM(config)
# Load weights if available # Load weights if available
weights_path = os.path.join(param_path, "model.safetensors") weights_path = os.path.join(param_path, "model.safetensors")
@@ -253,7 +263,7 @@ def train(
model = model.to(dtype=torch.bfloat16) model = model.to(dtype=torch.bfloat16)
strategy_kwargs = { strategy_kwargs = {
"dpo_beta": dpo_beta, "beta": dpo_beta,
"label_smoothing": label_smoothing, "label_smoothing": label_smoothing,
"clip_eps": grpo_clip_eps, "clip_eps": grpo_clip_eps,
"kl_coef": grpo_kl_coef, "kl_coef": grpo_kl_coef,
@@ -312,6 +322,7 @@ def train(
parallel_wrapper=ddp_wrap, parallel_wrapper=ddp_wrap,
state_dict_fn=prepare_checkpoint, state_dict_fn=prepare_checkpoint,
device_type=device_type, device_type=device_type,
start_method=start_method,
extra_kwargs=strategy_kwargs, extra_kwargs=strategy_kwargs,
) )
+5 -5
View File
@@ -8,8 +8,8 @@ import torch
from tokenizers import Tokenizer, models, pre_tokenizers, trainers from tokenizers import Tokenizer, models, pre_tokenizers, trainers
from torch.utils.data import Dataset from torch.utils.data import Dataset
from astrai.config.model_config import ModelConfig from astrai.config.model_config import AutoRegressiveLMConfig
from astrai.model.transformer import Transformer from astrai.model.transformer import AutoRegressiveLM
from astrai.tokenize import AutoTokenizer from astrai.tokenize import AutoTokenizer
@@ -104,8 +104,8 @@ def test_tokenizer():
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def test_model(): def test_model():
"""Session-scoped small Transformer model, created once.""" """Session-scoped small AutoRegressiveLM model, created once."""
config = ModelConfig( config = AutoRegressiveLMConfig(
vocab_size=1000, vocab_size=1000,
dim=8, dim=8,
n_heads=2, n_heads=2,
@@ -116,7 +116,7 @@ def test_model():
norm_eps=1e-5, norm_eps=1e-5,
) )
device = "cuda" if torch.cuda.is_available() else "cpu" device = "cuda" if torch.cuda.is_available() else "cpu"
model = Transformer(config).to(device=device) model = AutoRegressiveLM(config).to(device=device)
return { return {
"model": model, "model": model,
+55
View File
@@ -157,5 +157,60 @@ def test_messages_with_system(client, loaded_model):
assert data["type"] == "message" assert data["type"] == "message"
def test_chat_completions_stop_sequence(client, loaded_model):
"""POST /v1/chat/completions with stop parameter truncates at stop sequence."""
async def async_gen():
yield "Hello"
yield "X"
yield "world"
app.state.engine = loaded_model
loaded_model.generate_async.return_value = async_gen()
response = client.post(
"/v1/chat/completions",
json={
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 100,
"stream": False,
"stop": ["X"],
},
)
assert response.status_code == 200
data = response.json()
content = data["choices"][0]["message"]["content"]
assert "X" in content
assert "world" not in content
def test_chat_completions_stop_sequence_stream(client, loaded_model):
"""POST /v1/chat/completions with stop parameter truncates SSE stream."""
async def async_gen():
yield "Hello"
yield "X"
yield "world"
app.state.engine = loaded_model
loaded_model.generate_async.return_value = async_gen()
response = client.post(
"/v1/chat/completions",
json={
"messages": [{"role": "user", "content": "Hello"}],
"max_tokens": 100,
"stream": True,
"stop": ["X"],
},
headers={"Accept": "text/event-stream"},
)
assert response.status_code == 200
content = response.content.decode("utf-8")
assert "Hello" in content
assert "world" not in content
assert any(
"finish_reason" in line for line in content.split("\n") if "stop" in line
)
if __name__ == "__main__": if __name__ == "__main__":
pytest.main([__file__, "-v"]) pytest.main([__file__, "-v"])
+166
View File
@@ -0,0 +1,166 @@
import torch
from astrai.config.model_config import EncoderConfig
from astrai.model.encoder import EmbeddingEncoder
TINY_CONFIG = dict(
vocab_size=128,
dim=8,
n_heads=2,
n_kv_heads=1,
dim_ffn=16,
max_len=64,
n_layers=2,
norm_eps=1e-5,
)
def test_encoder_forward_mean():
config = EncoderConfig(**TINY_CONFIG)
device = "cuda" if torch.cuda.is_available() else "cpu"
model = EmbeddingEncoder(config).to(device=device)
model.eval()
batch_size, seq_len = 2, 8
input_ids = torch.randint(
0, config.vocab_size, (batch_size, seq_len), device=device
)
with torch.no_grad():
output = model(input_ids)
assert output.shape == (batch_size, config.dim)
assert not torch.isnan(output).any()
def test_encoder_forward_cls():
config = EncoderConfig(**{**TINY_CONFIG, "pooling_type": "cls"})
device = "cuda" if torch.cuda.is_available() else "cpu"
model = EmbeddingEncoder(config).to(device=device)
model.eval()
batch_size, seq_len = 2, 8
input_ids = torch.randint(
0, config.vocab_size, (batch_size, seq_len), device=device
)
with torch.no_grad():
output = model(input_ids)
assert output.shape == (batch_size, config.dim)
assert not torch.isnan(output).any()
def test_encoder_forward_last():
config = EncoderConfig(**{**TINY_CONFIG, "pooling_type": "last"})
device = "cuda" if torch.cuda.is_available() else "cpu"
model = EmbeddingEncoder(config).to(device=device)
model.eval()
batch_size, seq_len = 2, 8
input_ids = torch.randint(
0, config.vocab_size, (batch_size, seq_len), device=device
)
with torch.no_grad():
output = model(input_ids)
assert output.shape == (batch_size, config.dim)
assert not torch.isnan(output).any()
def test_encoder_forward_with_padding():
config = EncoderConfig(**TINY_CONFIG)
device = "cuda" if torch.cuda.is_available() else "cpu"
model = EmbeddingEncoder(config).to(device=device)
model.eval()
batch_size, seq_len = 2, 8
input_ids = torch.randint(
0, config.vocab_size, (batch_size, seq_len), device=device
)
input_mask = torch.ones(batch_size, seq_len, dtype=torch.bool, device=device)
input_mask[:, 4:] = False
with torch.no_grad():
output = model(input_ids, input_mask=input_mask)
assert output.shape == (batch_size, config.dim)
assert not torch.isnan(output).any()
def test_encoder_normalize():
config = EncoderConfig(
**{**TINY_CONFIG, "pooling_type": "mean", "normalize_embeddings": True}
)
device = "cuda" if torch.cuda.is_available() else "cpu"
model = EmbeddingEncoder(config).to(device=device)
model.eval()
batch_size, seq_len = 2, 8
input_ids = torch.randint(
0, config.vocab_size, (batch_size, seq_len), device=device
)
with torch.no_grad():
output = model(input_ids)
norms = output.norm(p=2, dim=-1)
assert torch.allclose(norms, torch.ones_like(norms), atol=1e-4)
def test_encoder_register():
from astrai.model.automodel import AutoModel
assert AutoModel.is_registered("embedding")
cls = AutoModel.get_component_class("embedding")
assert cls is EmbeddingEncoder
def test_encoder_from_transformer_checkpoint():
config = EncoderConfig(**TINY_CONFIG)
device = "cuda" if torch.cuda.is_available() else "cpu"
model = EmbeddingEncoder(config).to(device=device)
state_dict = model.state_dict()
state_dict["lm_head.weight"] = torch.randn(
config.vocab_size, config.dim, device=device
)
new_model = EmbeddingEncoder(config).to(device=device)
new_model.load_state_dict(state_dict, strict=True)
for key in model.state_dict():
assert torch.equal(new_model.state_dict()[key], model.state_dict()[key])
def test_encoder_save_load():
import json
import os
import tempfile
import safetensors.torch as st
test_dir = tempfile.mkdtemp(prefix="encoder_test_")
config_path = os.path.join(test_dir, "config.json")
weights_path = os.path.join(test_dir, "model.safetensors")
try:
config_data = {**TINY_CONFIG, "pooling_type": "mean"}
with open(config_path, "w") as f:
json.dump(config_data, f)
config = EncoderConfig.from_file(config_path)
original = EmbeddingEncoder(config)
st.save_file(original.state_dict(), weights_path)
loaded = EmbeddingEncoder(config)
loaded.load_state_dict(st.load_file(weights_path))
for key in original.state_dict():
assert torch.equal(original.state_dict()[key], loaded.state_dict()[key])
finally:
if os.path.exists(test_dir):
for f in os.listdir(test_dir):
os.remove(os.path.join(test_dir, f))
os.rmdir(test_dir)
+7 -7
View File
@@ -1,8 +1,8 @@
import pytest import pytest
import torch import torch
from astrai.config.model_config import ModelConfig from astrai.config.model_config import AutoRegressiveLMConfig
from astrai.model.transformer import Transformer from astrai.model.transformer import AutoRegressiveLM
TINY_CONFIG = dict( TINY_CONFIG = dict(
vocab_size=128, vocab_size=128,
@@ -40,7 +40,7 @@ CONFIGS = [
"n_routed_experts": 4, "n_routed_experts": 4,
"n_shared_experts": 1, "n_shared_experts": 1,
"n_activated_experts": 2, "n_activated_experts": 2,
"moe_topk_method": "greedy", "topk_method": "greedy",
}, },
id="gqa_moe", id="gqa_moe",
), ),
@@ -66,9 +66,9 @@ CONFIGS = [
@pytest.mark.parametrize("config_kwargs", CONFIGS) @pytest.mark.parametrize("config_kwargs", CONFIGS)
def test_model_forward(config_kwargs): def test_model_forward(config_kwargs):
config = ModelConfig(**config_kwargs) config = AutoRegressiveLMConfig(**config_kwargs)
device = "cuda" if torch.cuda.is_available() else "cpu" device = "cuda" if torch.cuda.is_available() else "cpu"
model = Transformer(config).to(device=device) model = AutoRegressiveLM(config).to(device=device)
model.eval() model.eval()
batch_size, seq_len = 2, 8 batch_size, seq_len = 2, 8
@@ -89,9 +89,9 @@ def test_model_forward(config_kwargs):
@pytest.mark.parametrize("config_kwargs", CONFIGS) @pytest.mark.parametrize("config_kwargs", CONFIGS)
def test_model_forward_with_padding(config_kwargs): def test_model_forward_with_padding(config_kwargs):
config = ModelConfig(**config_kwargs) config = AutoRegressiveLMConfig(**config_kwargs)
device = "cuda" if torch.cuda.is_available() else "cpu" device = "cuda" if torch.cuda.is_available() else "cpu"
model = Transformer(config).to(device=device) model = AutoRegressiveLM(config).to(device=device)
model.eval() model.eval()
batch_size, seq_len = 2, 8 batch_size, seq_len = 2, 8
+12 -12
View File
@@ -6,8 +6,8 @@ import pytest
import safetensors.torch as st import safetensors.torch as st
import torch import torch
from astrai.config.model_config import ModelConfig from astrai.config.model_config import AutoRegressiveLMConfig
from astrai.model.transformer import Transformer from astrai.model.transformer import AutoRegressiveLM
@pytest.fixture @pytest.fixture
@@ -50,8 +50,8 @@ def test_tie_weight_init(transformer_test_env):
with open(config_path, "w") as f: with open(config_path, "w") as f:
json.dump(config_data, f) json.dump(config_data, f)
config = ModelConfig.from_file(config_path) config = AutoRegressiveLMConfig.from_file(config_path)
model = Transformer(config) model = AutoRegressiveLM(config)
assert torch.equal(model.lm_head.weight, model.embed_tokens.weight) assert torch.equal(model.lm_head.weight, model.embed_tokens.weight)
assert model.lm_head.weight.data_ptr() == model.embed_tokens.weight.data_ptr() assert model.lm_head.weight.data_ptr() == model.embed_tokens.weight.data_ptr()
@@ -68,8 +68,8 @@ def test_tie_weight_init(transformer_test_env):
with open(config_path, "w") as f: with open(config_path, "w") as f:
json.dump(config_data, f) json.dump(config_data, f)
config = ModelConfig.from_file(config_path) config = AutoRegressiveLMConfig.from_file(config_path)
model = Transformer(config) model = AutoRegressiveLM(config)
assert not torch.equal(model.lm_head.weight, model.embed_tokens.weight) assert not torch.equal(model.lm_head.weight, model.embed_tokens.weight)
assert model.lm_head.weight.data_ptr() != model.embed_tokens.weight.data_ptr() assert model.lm_head.weight.data_ptr() != model.embed_tokens.weight.data_ptr()
@@ -94,13 +94,13 @@ def test_model_save_load_with_tie_weight(transformer_test_env):
with open(config_path, "w") as f: with open(config_path, "w") as f:
json.dump(config_data, f) json.dump(config_data, f)
config = ModelConfig.from_file(config_path) config = AutoRegressiveLMConfig.from_file(config_path)
original_model = Transformer(config) original_model = AutoRegressiveLM(config)
st.save_file(original_model.state_dict(), model_path) st.save_file(original_model.state_dict(), model_path)
loaded_config = ModelConfig.from_file(config_path) loaded_config = AutoRegressiveLMConfig.from_file(config_path)
model = Transformer(loaded_config) model = AutoRegressiveLM(loaded_config)
model.load_state_dict(st.load_file(model_path)) model.load_state_dict(st.load_file(model_path))
assert torch.equal(model.lm_head.weight, model.embed_tokens.weight) assert torch.equal(model.lm_head.weight, model.embed_tokens.weight)
@@ -112,8 +112,8 @@ def test_model_save_load_with_tie_weight(transformer_test_env):
with open(config_path, "w") as f: with open(config_path, "w") as f:
json.dump(config_data, f) json.dump(config_data, f)
loaded_config = ModelConfig.from_file(config_path) loaded_config = AutoRegressiveLMConfig.from_file(config_path)
model = Transformer(loaded_config) model = AutoRegressiveLM(loaded_config)
model.load_state_dict(st.load_file(model_path)) model.load_state_dict(st.load_file(model_path))
assert torch.equal(model.lm_head.weight, model.embed_tokens.weight) assert torch.equal(model.lm_head.weight, model.embed_tokens.weight)
+120 -1
View File
@@ -1,11 +1,130 @@
import torch import torch
from astrai.config.train_config import TrainConfig from astrai.config.train_config import TrainConfig
from astrai.model.components.decoder_block import DecoderBlock
from astrai.trainer.schedule import SchedulerFactory from astrai.trainer.schedule import SchedulerFactory
from astrai.trainer.train_callback import TrainCallback from astrai.trainer.train_callback import GradientCheckpointingCallback, TrainCallback
from astrai.trainer.trainer import Trainer from astrai.trainer.trainer import Trainer
def test_gradient_checkpointing_enable_disable(test_model):
"""Enable wraps forward, _disable restores it."""
model = test_model["model"]
callback = GradientCheckpointingCallback(modules=[DecoderBlock])
originals = [layer.forward for layer in model.layers]
for layer in model.layers:
callback._enable(layer)
for layer in model.layers:
assert hasattr(layer, "_original_forward")
assert layer.forward is not originals[0]
for layer in model.layers:
callback._disable(layer)
for layer in model.layers:
assert not hasattr(layer, "_original_forward")
def test_gradient_checkpointing_empty_modules_noop(test_model):
"""modules=None should leave forwards untouched."""
model = test_model["model"]
callback = GradientCheckpointingCallback()
originals = [layer.forward for layer in model.layers]
for layer in model.layers:
callback._enable(layer)
for layer, orig in zip(model.layers, originals):
assert layer.forward is orig
def test_gradient_checkpointing_forward_unchanged(test_model):
"""Forward output unchanged after patching (no_grad)."""
model = test_model["model"]
device = test_model["device"]
callback = GradientCheckpointingCallback(modules=[DecoderBlock])
input_ids = torch.randint(0, 1000, (2, 32)).to(device)
with torch.no_grad():
ref = model(input_ids)["logits"].clone()
for layer in model.layers:
callback._enable(layer)
with torch.no_grad():
out = model(input_ids)["logits"]
assert torch.equal(ref, out)
def test_gradient_checkpointing_backward(test_model):
"""backward passes gradients through checkpointed layers."""
model = test_model["model"]
device = test_model["device"]
callback = GradientCheckpointingCallback(modules=[DecoderBlock])
for layer in model.layers:
callback._enable(layer)
input_ids = torch.randint(0, 1000, (2, 32)).to(device)
target_ids = torch.randint(0, 1000, (2, 32)).to(device)
logits = model(input_ids)["logits"]
loss = torch.nn.functional.cross_entropy(
logits.flatten(0, 1).float(), target_ids.flatten()
)
loss.backward()
for name, param in model.named_parameters():
if param.requires_grad:
assert param.grad is not None, f"{name} gradient is None"
for layer in model.layers:
callback._disable(layer)
model.zero_grad()
for name, p in model.named_parameters():
assert p.grad is None or p.grad.sum().item() == 0, f"{name} grad not zeroed"
def test_gradient_checkpointing_trainer_integration(base_test_env, random_dataset):
"""Gradient checkpointing runs end-to-end via Trainer."""
def optimizer_fn(model):
return torch.optim.AdamW(model.parameters())
def scheduler_fn(optim):
return SchedulerFactory.create(
optim, "cosine", warmup_steps=10, lr_decay_steps=10, min_rate=0.05
)
train_config = TrainConfig(
model=base_test_env["model"],
strategy="seq",
dataset=random_dataset,
optimizer_fn=optimizer_fn,
scheduler_fn=scheduler_fn,
ckpt_dir=base_test_env["test_dir"],
n_epoch=1,
batch_per_device=2,
ckpt_interval=3,
grad_accum_steps=1,
max_grad_norm=1.0,
random_seed=42,
device_type=base_test_env["device"],
gradient_checkpointing_modules=[DecoderBlock],
)
trainer = Trainer(train_config)
trainer.train()
# no crash = callback correctly enabled/disabled
def test_callback_integration(base_test_env, random_dataset): def test_callback_integration(base_test_env, random_dataset):
"""Test that all callbacks are properly integrated""" """Test that all callbacks are properly integrated"""