Compare commits
7 Commits
8ab7564d02
...
b4587c5d08
| Author | SHA1 | Date |
|---|---|---|
|
|
b4587c5d08 | |
|
|
88ec63121d | |
|
|
01d2da2893 | |
|
|
25d4ea3f91 | |
|
|
39985840c7 | |
|
|
b1adc40cfb | |
|
|
7348bac6ab |
|
|
@ -161,7 +161,7 @@ See [Inference Guide](inference.md) for HTTP API documentation.
|
||||||
| `--top_k` | int | `30` | Top-k filtering |
|
| `--top_k` | int | `30` | Top-k filtering |
|
||||||
| `--top_p` | float | `0.95` | Nucleus sampling threshold |
|
| `--top_p` | float | `0.95` | Nucleus sampling threshold |
|
||||||
| `--batch_size` | int | `1` | Batch size for generation |
|
| `--batch_size` | int | `1` | Batch size for generation |
|
||||||
| `--max_tokens` | int | `2048` | Maximum tokens to generate |
|
| `--max_tokens` | int | model config `max_len` | Maximum tokens to generate |
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class BaseModelConfig(BaseConfig):
|
||||||
"""Base config with ``model_type`` dispatch and file I/O."""
|
"""Base config with ``model_type`` dispatch and file I/O."""
|
||||||
|
|
||||||
model_type: Optional[str] = None
|
model_type: Optional[str] = None
|
||||||
|
neftune_alpha: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -70,10 +71,12 @@ class EncoderConfig(BaseModelConfig):
|
||||||
rope_theta: Optional[float] = None
|
rope_theta: Optional[float] = None
|
||||||
rope_scaling: Optional[dict] = None
|
rope_scaling: Optional[dict] = None
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
ffn_type: str = "mlp"
|
||||||
pooling_type: Optional[str] = None
|
pooling_type: Optional[str] = None
|
||||||
normalize_embeddings: Optional[bool] = None
|
normalize_embeddings: Optional[bool] = None
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import inspect
|
||||||
import sys
|
import sys
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
|
||||||
Callable,
|
Callable,
|
||||||
Dict,
|
Dict,
|
||||||
ForwardRef,
|
ForwardRef,
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ class GQA(nn.Module):
|
||||||
norm_eps: float,
|
norm_eps: float,
|
||||||
use_gated_attention: bool,
|
use_gated_attention: bool,
|
||||||
layer_id: int,
|
layer_id: int,
|
||||||
|
n_layers: int = 1,
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
assert dim % n_heads == 0
|
assert dim % n_heads == 0
|
||||||
|
|
@ -55,7 +56,7 @@ class GQA(nn.Module):
|
||||||
self.q_proj = Linear(dim, n_heads * self.head_dim)
|
self.q_proj = Linear(dim, n_heads * self.head_dim)
|
||||||
self.k_proj = Linear(dim, n_kv_heads * self.head_dim)
|
self.k_proj = Linear(dim, n_kv_heads * self.head_dim)
|
||||||
self.v_proj = Linear(dim, n_kv_heads * self.head_dim)
|
self.v_proj = Linear(dim, n_kv_heads * self.head_dim)
|
||||||
self.o_proj = Linear(dim, dim)
|
self.o_proj = Linear(dim, dim, init_std=0.02 / (2 * n_layers) ** 0.5)
|
||||||
|
|
||||||
if self.use_qk_norm:
|
if self.use_qk_norm:
|
||||||
self.q_norm = RMSNorm(self.head_dim, norm_eps)
|
self.q_norm = RMSNorm(self.head_dim, norm_eps)
|
||||||
|
|
@ -121,6 +122,7 @@ class MLA(nn.Module):
|
||||||
use_qk_norm: bool,
|
use_qk_norm: bool,
|
||||||
use_gated_attention: bool,
|
use_gated_attention: bool,
|
||||||
layer_id: int,
|
layer_id: int,
|
||||||
|
n_layers: int = 1,
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.dim = dim
|
self.dim = dim
|
||||||
|
|
@ -148,7 +150,9 @@ class MLA(nn.Module):
|
||||||
n_kv_heads * (2 * self.head_dim),
|
n_kv_heads * (2 * self.head_dim),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.o_proj = Linear(dim, dim, bias=False)
|
self.o_proj = Linear(
|
||||||
|
dim, dim, bias=False, init_std=0.02 / (2 * n_layers) ** 0.5
|
||||||
|
)
|
||||||
|
|
||||||
if use_gated_attention:
|
if use_gated_attention:
|
||||||
self.gate = Linear(dim, dim, bias=False)
|
self.gate = Linear(dim, dim, bias=False)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from dataclasses import asdict
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import torch.nn as nn
|
import torch.nn as nn
|
||||||
|
|
@ -10,35 +11,14 @@ from astrai.model.components.norm import RMSNorm
|
||||||
|
|
||||||
|
|
||||||
class DecoderBlock(nn.Module):
|
class DecoderBlock(nn.Module):
|
||||||
def __init__(
|
def __init__(self, config, layer_id: int):
|
||||||
self,
|
|
||||||
dim: int,
|
|
||||||
n_heads: int,
|
|
||||||
dim_ffn: int,
|
|
||||||
n_kv_heads: int,
|
|
||||||
norm_eps: float,
|
|
||||||
use_qk_norm: bool,
|
|
||||||
use_gated_attention: bool,
|
|
||||||
layer_id: int,
|
|
||||||
attn_type: str = "gqa",
|
|
||||||
ffn_type: str = "mlp",
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.attention = AttnFactory.create(
|
cfg = asdict(config)
|
||||||
attn_type,
|
cfg["down_init_std"] = 0.02 / (2 * config.n_layers) ** 0.5
|
||||||
dim=dim,
|
self.attention = AttnFactory.create(config.attn_type, **cfg, layer_id=layer_id)
|
||||||
n_heads=n_heads,
|
self.input_norm = RMSNorm(config.dim, config.norm_eps)
|
||||||
n_kv_heads=n_kv_heads,
|
self.post_attention_norm = RMSNorm(config.dim, config.norm_eps)
|
||||||
use_qk_norm=use_qk_norm,
|
self.mlp = FFNFactory.create(config.ffn_type, **cfg)
|
||||||
norm_eps=norm_eps,
|
|
||||||
use_gated_attention=use_gated_attention,
|
|
||||||
layer_id=layer_id,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
self.input_norm = RMSNorm(dim, norm_eps)
|
|
||||||
self.post_attention_norm = RMSNorm(dim, norm_eps)
|
|
||||||
self.mlp = FFNFactory.create(ffn_type, dim, dim_ffn, **kwargs)
|
|
||||||
|
|
||||||
def forward(
|
def forward(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,13 @@ from torch import Tensor
|
||||||
|
|
||||||
|
|
||||||
class Embedding(nn.Module):
|
class Embedding(nn.Module):
|
||||||
def __init__(self, vocab_size: int, embedding_dim: int):
|
def __init__(self, vocab_size: int, embedding_dim: int, neftune_alpha: float = 0.0):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.weight = nn.Parameter(torch.empty((vocab_size, embedding_dim)))
|
self.weight = nn.Parameter(torch.empty((vocab_size, embedding_dim)))
|
||||||
self.neftune_noise_alpha = 0.0
|
self.neftune_noise_alpha = neftune_alpha
|
||||||
|
|
||||||
|
def set_neftune_alpha(self, alpha: float):
|
||||||
|
self.neftune_noise_alpha = alpha
|
||||||
|
|
||||||
def reset_parameters(self):
|
def reset_parameters(self):
|
||||||
nn.init.normal_(self.weight, mean=0.0, std=0.02)
|
nn.init.normal_(self.weight, mean=0.0, std=0.02)
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,16 @@ from torch import Tensor
|
||||||
|
|
||||||
|
|
||||||
class Linear(nn.Module):
|
class Linear(nn.Module):
|
||||||
def __init__(self, in_dim: int, out_dim: int, bias: bool = False):
|
def __init__(
|
||||||
|
self, in_dim: int, out_dim: int, bias: bool = False, init_std: float = 0.02
|
||||||
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
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
|
||||||
|
self.init_std = init_std
|
||||||
|
|
||||||
def reset_parameters(self):
|
def reset_parameters(self):
|
||||||
nn.init.kaiming_uniform_(self.weight, a=5**0.5)
|
nn.init.normal_(self.weight, mean=0.0, std=self.init_std)
|
||||||
if self.bias is not None:
|
if self.bias is not None:
|
||||||
fan_in, _ = nn.init._calculate_fan_in_and_fan_out(self.weight)
|
fan_in, _ = nn.init._calculate_fan_in_and_fan_out(self.weight)
|
||||||
bound = 1 / (fan_in**0.5)
|
bound = 1 / (fan_in**0.5)
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,11 @@ class FFNFactory(BaseFactory[nn.Module]):
|
||||||
|
|
||||||
@FFNFactory.register("mlp")
|
@FFNFactory.register("mlp")
|
||||||
class MLP(nn.Module):
|
class MLP(nn.Module):
|
||||||
def __init__(self, dim: int, dim_ffn: int):
|
def __init__(self, dim: int, dim_ffn: int, down_init_std: float = 0.02):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.up = Linear(dim, dim_ffn)
|
self.up = Linear(dim, dim_ffn)
|
||||||
self.gate = Linear(dim, dim_ffn)
|
self.gate = Linear(dim, dim_ffn)
|
||||||
self.down = Linear(dim_ffn, dim)
|
self.down = Linear(dim_ffn, dim, init_std=down_init_std)
|
||||||
|
|
||||||
def forward(self, x: Tensor) -> Tensor:
|
def forward(self, x: Tensor) -> Tensor:
|
||||||
gated = self.up(x) * F.silu(self.gate(x))
|
gated = self.up(x) * F.silu(self.gate(x))
|
||||||
|
|
@ -35,6 +35,7 @@ class DeepSeekMoE(nn.Module):
|
||||||
n_shared_experts: int = 1,
|
n_shared_experts: int = 1,
|
||||||
n_activated_experts: int = 2,
|
n_activated_experts: int = 2,
|
||||||
topk_method: str = "greedy",
|
topk_method: str = "greedy",
|
||||||
|
n_layers: int = 1,
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.dim = dim
|
self.dim = dim
|
||||||
|
|
@ -44,12 +45,20 @@ class DeepSeekMoE(nn.Module):
|
||||||
self.topk_method = topk_method
|
self.topk_method = topk_method
|
||||||
|
|
||||||
self.router = Linear(dim, n_routed_experts, bias=False)
|
self.router = Linear(dim, n_routed_experts, bias=False)
|
||||||
|
moe_scale = 1 / max(n_shared_experts, 1) + 1 / n_activated_experts
|
||||||
|
down_init_std = 0.02 / (2 * n_layers * moe_scale) ** 0.5
|
||||||
|
|
||||||
self.shared_experts = nn.ModuleList(
|
self.shared_experts = nn.ModuleList(
|
||||||
[MLP(dim, dim_ffn) for _ in range(n_shared_experts)]
|
[
|
||||||
|
MLP(dim, dim_ffn, down_init_std=down_init_std)
|
||||||
|
for _ in range(n_shared_experts)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
self.routed_experts = nn.ModuleList(
|
self.routed_experts = nn.ModuleList(
|
||||||
[MLP(dim, dim_ffn) for _ in range(n_routed_experts)]
|
[
|
||||||
|
MLP(dim, dim_ffn, down_init_std=down_init_std)
|
||||||
|
for _ in range(n_routed_experts)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def forward(self, x: Tensor) -> Tensor:
|
def forward(self, x: Tensor) -> Tensor:
|
||||||
|
|
|
||||||
|
|
@ -23,22 +23,12 @@ class EmbeddingEncoder(AutoModel):
|
||||||
self.rotary_embedding = RotaryEmbedding(
|
self.rotary_embedding = RotaryEmbedding(
|
||||||
rope_dim, config.max_len, rope_base, rope_scaling=config.rope_scaling
|
rope_dim, config.max_len, rope_base, rope_scaling=config.rope_scaling
|
||||||
)
|
)
|
||||||
self.embed_tokens = Embedding(config.vocab_size, config.dim)
|
self.embed_tokens = Embedding(
|
||||||
|
config.vocab_size, config.dim, neftune_alpha=config.neftune_alpha
|
||||||
|
)
|
||||||
|
|
||||||
self.layers = nn.ModuleList(
|
self.layers = nn.ModuleList(
|
||||||
[
|
[DecoderBlock(config, layer_id) for layer_id in range(config.n_layers)]
|
||||||
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.norm = RMSNorm(config.dim, config.norm_eps)
|
||||||
|
|
|
||||||
|
|
@ -59,31 +59,12 @@ class AutoRegressiveLM(AutoModel):
|
||||||
self.rotary_embedding = RotaryEmbedding(
|
self.rotary_embedding = RotaryEmbedding(
|
||||||
rope_dim, config.max_len, rope_base, rope_scaling=config.rope_scaling
|
rope_dim, config.max_len, rope_base, rope_scaling=config.rope_scaling
|
||||||
)
|
)
|
||||||
self.embed_tokens = Embedding(config.vocab_size, config.dim)
|
self.embed_tokens = Embedding(
|
||||||
|
config.vocab_size, config.dim, neftune_alpha=config.neftune_alpha
|
||||||
|
)
|
||||||
|
|
||||||
self.layers = nn.ModuleList(
|
self.layers = nn.ModuleList(
|
||||||
[
|
[DecoderBlock(config, layer_id) for layer_id in range(config.n_layers)]
|
||||||
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,
|
|
||||||
attn_type=config.attn_type,
|
|
||||||
ffn_type=config.ffn_type,
|
|
||||||
n_routed_experts=config.n_routed_experts,
|
|
||||||
n_shared_experts=config.n_shared_experts,
|
|
||||||
n_activated_experts=config.n_activated_experts,
|
|
||||||
topk_method=config.topk_method,
|
|
||||||
kv_lora_rank=config.kv_lora_rank,
|
|
||||||
qk_nope_head_dim=config.qk_nope_head_dim,
|
|
||||||
qk_rope_head_dim=config.qk_rope_head_dim,
|
|
||||||
)
|
|
||||||
for layer_id in range(config.n_layers)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.norm = RMSNorm(config.dim, config.norm_eps)
|
self.norm = RMSNorm(config.dim, config.norm_eps)
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,7 @@ class MetricLoggerCallback(TrainCallback):
|
||||||
metrics: List[str] = None,
|
metrics: List[str] = None,
|
||||||
):
|
):
|
||||||
self.last_log_iter = 0
|
self.last_log_iter = 0
|
||||||
|
self._last_val_loss = None
|
||||||
self.save_interval = save_interval
|
self.save_interval = save_interval
|
||||||
self.log_interval = log_interval
|
self.log_interval = log_interval
|
||||||
self.metrics = metrics or ["loss", "lr"]
|
self.metrics = metrics or ["loss", "lr"]
|
||||||
|
|
@ -258,46 +259,54 @@ class MetricLoggerCallback(TrainCallback):
|
||||||
"grad_nan_num": ctx_get_grad_nan_num,
|
"grad_nan_num": ctx_get_grad_nan_num,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_log_data(self, context: TrainContext):
|
def _metrics(self, context: TrainContext, names):
|
||||||
data = {
|
return {
|
||||||
|
m: self._metric_funcs[m](context)
|
||||||
|
for m in names
|
||||||
|
if self._metric_funcs[m](context) is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
@only_on_rank(0)
|
||||||
|
def _append(self, event_type: str, context: TrainContext, **extra):
|
||||||
|
entry = {
|
||||||
|
"type": event_type,
|
||||||
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||||
"epoch": context.epoch,
|
"epoch": context.epoch,
|
||||||
"iter": context.iteration,
|
"iter": context.iteration,
|
||||||
|
**extra,
|
||||||
}
|
}
|
||||||
for m in self.metrics:
|
self.log_cache.append(entry)
|
||||||
val = self._metric_funcs[m](context)
|
|
||||||
if val is not None:
|
|
||||||
data[m] = val
|
|
||||||
return data
|
|
||||||
|
|
||||||
@only_on_rank(0)
|
@only_on_rank(0)
|
||||||
def _add_log(self, log_data):
|
def _flush(self, epoch, iter):
|
||||||
self.log_cache.append(log_data)
|
|
||||||
|
|
||||||
@only_on_rank(0)
|
|
||||||
def _save_log(self, epoch, iter):
|
|
||||||
log_file = self.log_dir / f"epoch_{epoch}_iter_{iter}_metric.jsonl"
|
log_file = self.log_dir / f"epoch_{epoch}_iter_{iter}_metric.jsonl"
|
||||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
with open(log_file, "w") as f:
|
with open(log_file, "w") as f:
|
||||||
for log in self.log_cache:
|
for log in self.log_cache:
|
||||||
f.write(json.dumps(log) + "\n")
|
f.write(json.dumps(log) + "\n")
|
||||||
|
|
||||||
def on_batch_end(self, context):
|
def on_batch_end(self, context):
|
||||||
if context.iteration % self.log_interval == 0:
|
if context.iteration % self.log_interval == 0:
|
||||||
log_data = self._get_log_data(context)
|
step_metrics = [m for m in self.metrics if m != "val_loss"]
|
||||||
self._add_log(log_data)
|
self._append("step", context, **self._metrics(context, step_metrics))
|
||||||
|
|
||||||
if context.iteration - self.last_log_iter >= self.save_interval:
|
if context.iteration - self.last_log_iter >= self.save_interval:
|
||||||
self._save_log(context.epoch, context.iteration)
|
self._flush(context.epoch, context.iteration)
|
||||||
self.last_log_iter = context.iteration
|
self.last_log_iter = context.iteration
|
||||||
|
|
||||||
|
def on_optimizer_step(self, context):
|
||||||
|
if context.val_loss is not None and context.val_loss != self._last_val_loss:
|
||||||
|
self._append("validation", context, val_loss=context.val_loss)
|
||||||
|
self._last_val_loss = context.val_loss
|
||||||
|
|
||||||
|
def on_epoch_end(self, context):
|
||||||
|
self._append("epoch", context)
|
||||||
|
|
||||||
def on_train_end(self, context):
|
def on_train_end(self, context):
|
||||||
if context.iteration != self.last_log_iter:
|
if context.iteration != self.last_log_iter:
|
||||||
self._save_log(context.epoch, context.iteration)
|
self._flush(context.epoch, context.iteration)
|
||||||
|
|
||||||
def on_error(self, context):
|
def on_error(self, context):
|
||||||
self._save_log(context.epoch, context.iteration)
|
self._flush(context.epoch, context.iteration)
|
||||||
|
|
||||||
|
|
||||||
@CallbackFactory.register("validation")
|
@CallbackFactory.register("validation")
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@ class TrainContextBuilder:
|
||||||
|
|
||||||
model = cfg.model_fn()
|
model = cfg.model_fn()
|
||||||
model = model.to(device=device)
|
model = model.to(device=device)
|
||||||
model.embed_tokens.neftune_noise_alpha = cfg.neftune_alpha
|
|
||||||
|
|
||||||
model_config = {}
|
model_config = {}
|
||||||
if self._resume_dir:
|
if self._resume_dir:
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ class Trainer:
|
||||||
cfg.ckpt_dir,
|
cfg.ckpt_dir,
|
||||||
cfg.ckpt_interval,
|
cfg.ckpt_interval,
|
||||||
),
|
),
|
||||||
|
CallbackFactory.create("validation"),
|
||||||
CallbackFactory.create(
|
CallbackFactory.create(
|
||||||
"metric_logger",
|
"metric_logger",
|
||||||
log_dir=cfg.log_dir,
|
log_dir=cfg.log_dir,
|
||||||
|
|
@ -43,7 +44,6 @@ class Trainer:
|
||||||
),
|
),
|
||||||
CallbackFactory.create("progress_bar", cfg.n_epoch),
|
CallbackFactory.create("progress_bar", cfg.n_epoch),
|
||||||
CallbackFactory.create("gradient_clipping", cfg.max_grad_norm),
|
CallbackFactory.create("gradient_clipping", cfg.max_grad_norm),
|
||||||
CallbackFactory.create("validation"),
|
|
||||||
]
|
]
|
||||||
return callbacks
|
return callbacks
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -571,7 +571,7 @@ def main():
|
||||||
print(f" Unsupported: {summary['unsupported_constraints']}")
|
print(f" Unsupported: {summary['unsupported_constraints']}")
|
||||||
print(f"{'=' * 60}")
|
print(f"{'=' * 60}")
|
||||||
|
|
||||||
print(f"\nPer-type accuracy:")
|
print("\nPer-type accuracy:")
|
||||||
for inst_id, stats in sorted(summary["per_type_accuracy"].items()):
|
for inst_id, stats in sorted(summary["per_type_accuracy"].items()):
|
||||||
print(
|
print(
|
||||||
f" {inst_id:50s} {stats['accuracy']:.2%} "
|
f" {inst_id:50s} {stats['accuracy']:.2%} "
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ def process_file(
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Run perplexity with a Khaosz model.")
|
parser = argparse.ArgumentParser(description="Perplexity evaluation on JSONL text.")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--param_path", type=str, required=True, help="Path to the model directory."
|
"--param_path", type=str, required=True, help="Path to the model directory."
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
|
|
@ -17,7 +18,7 @@ def processor(
|
||||||
top_p: float,
|
top_p: float,
|
||||||
question_key: str,
|
question_key: str,
|
||||||
response_key: str,
|
response_key: str,
|
||||||
max_tokens: int,
|
max_tokens: Optional[int],
|
||||||
batch_size: int,
|
batch_size: int,
|
||||||
):
|
):
|
||||||
# Load model and tokenizer
|
# Load model and tokenizer
|
||||||
|
|
@ -72,7 +73,7 @@ def processor(
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Run generate with a Khaosz model.")
|
parser = argparse.ArgumentParser(description="Batch generation from JSONL file.")
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--param_path", type=str, required=True, help="Path to the model directory."
|
"--param_path", type=str, required=True, help="Path to the model directory."
|
||||||
|
|
@ -93,36 +94,42 @@ if __name__ == "__main__":
|
||||||
"--question_key",
|
"--question_key",
|
||||||
type=str,
|
type=str,
|
||||||
default="question",
|
default="question",
|
||||||
help="Key for the question in the input JSON.",
|
help="Key for the question in the input JSON (default: question).",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--response_key",
|
"--response_key",
|
||||||
type=str,
|
type=str,
|
||||||
default="response",
|
default="response",
|
||||||
help="Key for the response in the output JSON.",
|
help="Key for the response in the output JSON (default: response).",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--temperature",
|
"--temperature",
|
||||||
type=float,
|
type=float,
|
||||||
default=0.60,
|
default=0.60,
|
||||||
help="Temperature for generating responses.",
|
help="Temperature for generating responses (default: 0.60).",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--top_k", type=int, default=30, help="Top-k value for generating responses."
|
"--top_k",
|
||||||
|
type=int,
|
||||||
|
default=30,
|
||||||
|
help="Top-k value for generating responses (default: 30).",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--top_p",
|
"--top_p",
|
||||||
type=float,
|
type=float,
|
||||||
default=0.95,
|
default=0.95,
|
||||||
help="Top-p value for generating responses.",
|
help="Top-p value for generating responses (default: 0.95).",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--batch_size", type=int, default=1, help="Batch size for generating responses."
|
"--batch_size",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help="Batch size for generating responses (default: 1).",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--max_tokens",
|
"--max_tokens",
|
||||||
type=int,
|
type=int,
|
||||||
default=2048,
|
default=None,
|
||||||
help="Maximum tokens to generate (default: model config max_len).",
|
help="Maximum tokens to generate (default: model config max_len).",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,44 @@ def parse_args() -> argparse.Namespace:
|
||||||
help="NEFTune noise alpha (0=disabled, typical: 5.0).",
|
help="NEFTune noise alpha (0=disabled, typical: 5.0).",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--schedule_type",
|
||||||
|
type=str,
|
||||||
|
default="cosine",
|
||||||
|
choices=["cosine", "sgdr", "wsd"],
|
||||||
|
help="Learning rate scheduler type.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--min_rate",
|
||||||
|
type=float,
|
||||||
|
default=None,
|
||||||
|
help="Minimum LR as fraction of base LR. Uses scheduler default if not set (cosine/sgdr: 0.05, wsd: 0.0).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--cycle_length",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help="SGDR first cycle length in steps. Defaults to total_steps - warmup_steps.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--t_mult",
|
||||||
|
type=int,
|
||||||
|
default=2,
|
||||||
|
help="SGDR cycle length multiplier per restart.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--stable_steps",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help="WSD stable plateau steps. Required when --schedule_type wsd.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--decay_steps",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help="WSD decay steps. Defaults to total_steps - warmup_steps - stable_steps.",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
@ -300,6 +338,12 @@ def train(
|
||||||
master_port: str,
|
master_port: str,
|
||||||
start_method: str,
|
start_method: str,
|
||||||
neftune_alpha: float,
|
neftune_alpha: float,
|
||||||
|
schedule_type: str,
|
||||||
|
min_rate: float,
|
||||||
|
cycle_length: int,
|
||||||
|
t_mult: int,
|
||||||
|
stable_steps: int,
|
||||||
|
decay_steps: int,
|
||||||
):
|
):
|
||||||
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)
|
||||||
|
|
@ -309,6 +353,7 @@ def train(
|
||||||
# Load config
|
# Load config
|
||||||
config_path = os.path.join(param_path, "config.json")
|
config_path = os.path.join(param_path, "config.json")
|
||||||
config = AutoRegressiveLMConfig.from_file(config_path)
|
config = AutoRegressiveLMConfig.from_file(config_path)
|
||||||
|
config.neftune_alpha = neftune_alpha
|
||||||
|
|
||||||
if window_size is None:
|
if window_size is None:
|
||||||
window_size = config.max_len
|
window_size = config.max_len
|
||||||
|
|
@ -348,14 +393,30 @@ def train(
|
||||||
len(dataset), n_epoch, batch_per_device, nprocs, grad_accum_steps
|
len(dataset), n_epoch, batch_per_device, nprocs, grad_accum_steps
|
||||||
)
|
)
|
||||||
warmup_steps = int(warmup_ratio * total_steps)
|
warmup_steps = int(warmup_ratio * total_steps)
|
||||||
|
warmup_steps = min(warmup_steps, total_steps)
|
||||||
|
|
||||||
|
scheduler_kwargs = {"warmup_steps": warmup_steps}
|
||||||
|
|
||||||
|
if schedule_type == "cosine":
|
||||||
|
scheduler_kwargs["lr_decay_steps"] = total_steps - warmup_steps
|
||||||
|
elif schedule_type == "sgdr":
|
||||||
|
scheduler_kwargs["cycle_length"] = cycle_length or (total_steps - warmup_steps)
|
||||||
|
scheduler_kwargs["t_mult"] = t_mult
|
||||||
|
elif schedule_type == "wsd":
|
||||||
|
remaining = total_steps - warmup_steps
|
||||||
|
stable_steps_ = stable_steps or max(1, int(remaining * 0.8))
|
||||||
|
scheduler_kwargs["stable_steps"] = stable_steps_
|
||||||
|
scheduler_kwargs["decay_steps"] = max(
|
||||||
|
1, decay_steps or (remaining - stable_steps_)
|
||||||
|
)
|
||||||
|
|
||||||
|
if min_rate is not None:
|
||||||
|
scheduler_kwargs["min_rate"] = min_rate
|
||||||
|
|
||||||
scheduler_fn = partial(
|
scheduler_fn = partial(
|
||||||
create_scheduler,
|
create_scheduler,
|
||||||
**{
|
schedule_type=schedule_type,
|
||||||
"schedule_type": "cosine",
|
**scheduler_kwargs,
|
||||||
"warmup_steps": min(warmup_steps, total_steps),
|
|
||||||
"lr_decay_steps": total_steps - min(warmup_steps, total_steps),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
grad_ckpt_modules = [DecoderBlock] if gradient_checkpointing else []
|
grad_ckpt_modules = [DecoderBlock] if gradient_checkpointing else []
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -8,6 +10,7 @@ from astrai.config.preprocess_config import (
|
||||||
PipelineConfig,
|
PipelineConfig,
|
||||||
ProcessingConfig,
|
ProcessingConfig,
|
||||||
)
|
)
|
||||||
|
from astrai.preprocessing.builder import SectionedMaskBuilder
|
||||||
from astrai.tokenize import AutoTokenizer
|
from astrai.tokenize import AutoTokenizer
|
||||||
|
|
||||||
_SPECIAL_TOKENS_CONFIG = {
|
_SPECIAL_TOKENS_CONFIG = {
|
||||||
|
|
@ -200,3 +203,33 @@ def make_grpo_no_template_config():
|
||||||
mask_default="mask",
|
mask_default="mask",
|
||||||
preprocessing=ProcessingConfig(max_seq_len=2048),
|
preprocessing=ProcessingConfig(max_seq_len=2048),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def builder():
|
||||||
|
return SectionedMaskBuilder()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tokenizer_dir(temp_dir, test_tokenizer):
|
||||||
|
d = os.path.join(temp_dir, "tok")
|
||||||
|
os.makedirs(d, exist_ok=True)
|
||||||
|
test_tokenizer._tokenizer.save(os.path.join(d, "tokenizer.json"))
|
||||||
|
with open(os.path.join(d, "tokenizer_config.json"), "w") as f:
|
||||||
|
json.dump(
|
||||||
|
{"special_tokens": {"pad_token": "<|_pad_|>", "unk_token": "<|_unk_|>"}}, f
|
||||||
|
)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def chat_tokenizer_dir(temp_dir, chat_tokenizer):
|
||||||
|
d = os.path.join(temp_dir, "tok")
|
||||||
|
os.makedirs(d, exist_ok=True)
|
||||||
|
chat_tokenizer._tokenizer.save(os.path.join(d, "tokenizer.json"))
|
||||||
|
with open(os.path.join(d, "tokenizer_config.json"), "w") as f:
|
||||||
|
json.dump(
|
||||||
|
{"special_tokens": _SPECIAL_TOKENS_CONFIG, "chat_template": _CHAT_TEMPLATE},
|
||||||
|
f,
|
||||||
|
)
|
||||||
|
return d
|
||||||
|
|
|
||||||
|
|
@ -15,28 +15,34 @@ from astrai.dataset.storage import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _rand_seq(length, vocab=1000):
|
||||||
|
return torch.randint(0, vocab, (length,), dtype=torch.int64)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_seq_dataset(
|
||||||
|
test_dir, name="data", seq_length=200, train_type="seq", data=None, **load_kwargs
|
||||||
|
):
|
||||||
|
if data is None:
|
||||||
|
data = {"sequence": [_rand_seq(seq_length)]}
|
||||||
|
save_h5(test_dir, name, data)
|
||||||
|
return DatasetFactory.load(
|
||||||
|
train_type,
|
||||||
|
test_dir,
|
||||||
|
window_size=load_kwargs.pop("window_size", 64),
|
||||||
|
**load_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_dataset_loader_random_paths(base_test_env):
|
def test_dataset_loader_random_paths(base_test_env):
|
||||||
"""Test dataset loader with multiple random paths"""
|
"""Test dataset loader with multiple random paths"""
|
||||||
test_dir = base_test_env["test_dir"]
|
test_dir = base_test_env["test_dir"]
|
||||||
|
|
||||||
# Create multiple mmap dataset directories with random data
|
|
||||||
num_files = np.random.randint(2, 5)
|
num_files = np.random.randint(2, 5)
|
||||||
|
|
||||||
for i in range(num_files):
|
for i in range(num_files):
|
||||||
seq_length = np.random.randint(200, 400)
|
seq_length = np.random.randint(200, 400)
|
||||||
dummy_data = {
|
dummy_data = {"sequence": [_rand_seq(seq_length) for _ in range(10)]}
|
||||||
"sequence": [
|
loaded_dataset = _make_seq_dataset(
|
||||||
torch.randint(0, 1000, (seq_length,), dtype=torch.int64)
|
test_dir, f"data_{i}", seq_length, data=dummy_data
|
||||||
for _ in range(10)
|
|
||||||
],
|
|
||||||
}
|
|
||||||
save_h5(test_dir, f"data_{i}", dummy_data)
|
|
||||||
|
|
||||||
# Test loading with multiple paths
|
|
||||||
loaded_dataset = DatasetFactory.load(
|
|
||||||
train_type="seq",
|
|
||||||
load_path=test_dir,
|
|
||||||
window_size=64,
|
|
||||||
)
|
)
|
||||||
assert loaded_dataset is not None
|
assert loaded_dataset is not None
|
||||||
assert len(loaded_dataset) > 0
|
assert len(loaded_dataset) > 0
|
||||||
|
|
@ -54,23 +60,15 @@ def test_dpo_strategy_with_random_data(base_test_env):
|
||||||
"""Test DPO strategy with randomized preference data"""
|
"""Test DPO strategy with randomized preference data"""
|
||||||
test_dir = base_test_env["test_dir"]
|
test_dir = base_test_env["test_dir"]
|
||||||
|
|
||||||
# Create DPO-style data with memory mapping format
|
|
||||||
seq_length = np.random.randint(100, 200)
|
seq_length = np.random.randint(100, 200)
|
||||||
|
|
||||||
dummy_data = {
|
dummy_data = {
|
||||||
"chosen": [torch.randint(0, 1000, (seq_length,), dtype=torch.int64)],
|
"chosen": [_rand_seq(seq_length)],
|
||||||
"rejected": [torch.randint(0, 1000, (seq_length,), dtype=torch.int64)],
|
"rejected": [_rand_seq(seq_length)],
|
||||||
"chosen_mask": [torch.ones(seq_length, dtype=torch.bool)],
|
"chosen_mask": [torch.ones(seq_length, dtype=torch.bool)],
|
||||||
"rejected_mask": [torch.ones(seq_length, dtype=torch.bool)],
|
"rejected_mask": [torch.ones(seq_length, dtype=torch.bool)],
|
||||||
}
|
}
|
||||||
|
dpo_dataset = _make_seq_dataset(
|
||||||
save_h5(test_dir, "dpo_data", dummy_data)
|
test_dir, "dpo_data", seq_length, train_type="dpo", data=dummy_data
|
||||||
|
|
||||||
# Load DPO dataset
|
|
||||||
dpo_dataset = DatasetFactory.load(
|
|
||||||
train_type="dpo",
|
|
||||||
load_path=test_dir,
|
|
||||||
window_size=64,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert dpo_dataset is not None
|
assert dpo_dataset is not None
|
||||||
|
|
@ -92,22 +90,14 @@ def test_sft_dataset_with_random_data(base_test_env):
|
||||||
"""Test SFT dataset with random data"""
|
"""Test SFT dataset with random data"""
|
||||||
test_dir = base_test_env["test_dir"]
|
test_dir = base_test_env["test_dir"]
|
||||||
|
|
||||||
# Create SFT-style data with memory mapping format
|
|
||||||
seq_length = np.random.randint(100, 200)
|
seq_length = np.random.randint(100, 200)
|
||||||
|
|
||||||
dummy_data = {
|
dummy_data = {
|
||||||
"sequence": [torch.randint(0, 1000, (seq_length,), dtype=torch.int64)],
|
"sequence": [_rand_seq(seq_length)],
|
||||||
"loss_mask": [torch.ones(seq_length, dtype=torch.bool)],
|
"loss_mask": [torch.ones(seq_length, dtype=torch.bool)],
|
||||||
"position_ids": [torch.arange(seq_length, dtype=torch.int32)],
|
"position_ids": [torch.arange(seq_length, dtype=torch.int32)],
|
||||||
}
|
}
|
||||||
|
sft_dataset = _make_seq_dataset(
|
||||||
save_h5(test_dir, "sft_data", dummy_data)
|
test_dir, "sft_data", seq_length, train_type="sft", data=dummy_data
|
||||||
|
|
||||||
# Load SFT dataset
|
|
||||||
sft_dataset = DatasetFactory.load(
|
|
||||||
train_type="sft",
|
|
||||||
load_path=test_dir,
|
|
||||||
window_size=64,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert sft_dataset is not None
|
assert sft_dataset is not None
|
||||||
|
|
@ -128,25 +118,11 @@ def test_dataset_with_custom_stride(base_test_env):
|
||||||
"""Test dataset with custom stride parameter"""
|
"""Test dataset with custom stride parameter"""
|
||||||
test_dir = base_test_env["test_dir"]
|
test_dir = base_test_env["test_dir"]
|
||||||
|
|
||||||
# Create test data
|
|
||||||
seq_length = 200
|
|
||||||
dummy_data = {
|
|
||||||
"sequence": [torch.randint(0, 1000, (seq_length,), dtype=torch.int64)],
|
|
||||||
}
|
|
||||||
|
|
||||||
save_h5(test_dir, "stride_test_data", dummy_data)
|
|
||||||
|
|
||||||
# Test with custom stride
|
|
||||||
custom_stride = 32
|
custom_stride = 32
|
||||||
dataset = DatasetFactory.load(
|
dataset = _make_seq_dataset(test_dir, "stride_test_data", stride=custom_stride)
|
||||||
train_type="seq", load_path=test_dir, window_size=64, stride=custom_stride
|
|
||||||
)
|
|
||||||
|
|
||||||
assert dataset is not None
|
assert dataset is not None
|
||||||
assert len(dataset) > 0
|
assert len(dataset) > 0
|
||||||
|
|
||||||
# With stride 32 and window 64 on 200 length data, we should get more samples
|
|
||||||
# than with default stride (which equals window size)
|
|
||||||
default_stride_dataset = DatasetFactory.load(
|
default_stride_dataset = DatasetFactory.load(
|
||||||
train_type="seq",
|
train_type="seq",
|
||||||
load_path=test_dir,
|
load_path=test_dir,
|
||||||
|
|
@ -157,25 +133,11 @@ def test_dataset_with_custom_stride(base_test_env):
|
||||||
|
|
||||||
|
|
||||||
def test_dataset_count_property(base_test_env):
|
def test_dataset_count_property(base_test_env):
|
||||||
"""Test the count property returns correct raw token count"""
|
|
||||||
test_dir = base_test_env["test_dir"]
|
test_dir = base_test_env["test_dir"]
|
||||||
|
dataset = _make_seq_dataset(test_dir, "count_test_data")
|
||||||
seq_length = 200
|
assert dataset.count == 200
|
||||||
dummy_data = {
|
assert dataset.count > len(dataset)
|
||||||
"sequence": [torch.randint(0, 1000, (seq_length,), dtype=torch.int64)],
|
assert len(dataset) == (200 - 1 - 64) // 64 + 1
|
||||||
}
|
|
||||||
|
|
||||||
save_h5(test_dir, "count_test_data", dummy_data)
|
|
||||||
|
|
||||||
dataset = DatasetFactory.load(
|
|
||||||
train_type="seq",
|
|
||||||
load_path=test_dir,
|
|
||||||
window_size=64,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert dataset.count == seq_length
|
|
||||||
assert dataset.count > len(dataset) # raw tokens > windows
|
|
||||||
assert len(dataset) == (seq_length - 1 - 64) // 64 + 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_empty_dataset_count():
|
def test_empty_dataset_count():
|
||||||
|
|
@ -186,17 +148,10 @@ def test_empty_dataset_count():
|
||||||
|
|
||||||
|
|
||||||
def test_dataset_too_short_for_window(base_test_env):
|
def test_dataset_too_short_for_window(base_test_env):
|
||||||
"""Dataset shorter than window_size returns __len__ == 0"""
|
|
||||||
test_dir = base_test_env["test_dir"]
|
test_dir = base_test_env["test_dir"]
|
||||||
seq_length = 30
|
dataset = _make_seq_dataset(test_dir, "short", seq_length=30)
|
||||||
save_h5(
|
|
||||||
test_dir,
|
|
||||||
"short",
|
|
||||||
{"sequence": [torch.randint(0, 1000, (seq_length,), dtype=torch.int64)]},
|
|
||||||
)
|
|
||||||
dataset = DatasetFactory.load("seq", test_dir, window_size=64)
|
|
||||||
assert len(dataset) == 0
|
assert len(dataset) == 0
|
||||||
assert dataset.count == seq_length
|
assert dataset.count == 30
|
||||||
|
|
||||||
|
|
||||||
def test_unloaded_dataset_getitem_raises():
|
def test_unloaded_dataset_getitem_raises():
|
||||||
|
|
@ -220,12 +175,8 @@ def test_store_unloaded_len():
|
||||||
|
|
||||||
|
|
||||||
def test_store_fetch_begin_equals_end(base_test_env):
|
def test_store_fetch_begin_equals_end(base_test_env):
|
||||||
"""Store.fetch with begin == end returns empty tensor"""
|
|
||||||
test_dir = base_test_env["test_dir"]
|
test_dir = base_test_env["test_dir"]
|
||||||
dummy = {"sequence": [torch.randint(0, 1000, (100,), dtype=torch.int64)]}
|
dataset = _make_seq_dataset(test_dir, "empty_fetch", seq_length=100, window_size=32)
|
||||||
save_h5(test_dir, "empty_fetch", dummy)
|
|
||||||
|
|
||||||
dataset = DatasetFactory.load("seq", test_dir, window_size=32)
|
|
||||||
result = dataset.storage.fetch(10, 10, "sequence")
|
result = dataset.storage.fetch(10, 10, "sequence")
|
||||||
assert result.numel() == 0
|
assert result.numel() == 0
|
||||||
|
|
||||||
|
|
@ -299,12 +250,8 @@ def test_save_load_bin_roundtrip(base_test_env):
|
||||||
|
|
||||||
|
|
||||||
def test_mmap_store_load_and_fetch(base_test_env):
|
def test_mmap_store_load_and_fetch(base_test_env):
|
||||||
"""MmapStore loads bin data and fetches correctly"""
|
|
||||||
test_dir = base_test_env["test_dir"]
|
test_dir = base_test_env["test_dir"]
|
||||||
|
data = {"sequence": [_rand_seq(200)]}
|
||||||
data = {
|
|
||||||
"sequence": [torch.randint(0, 1000, (200,), dtype=torch.int64)],
|
|
||||||
}
|
|
||||||
save_bin(test_dir, data)
|
save_bin(test_dir, data)
|
||||||
|
|
||||||
store = StoreFactory.create("bin")
|
store = StoreFactory.create("bin")
|
||||||
|
|
@ -317,14 +264,9 @@ def test_mmap_store_load_and_fetch(base_test_env):
|
||||||
|
|
||||||
|
|
||||||
def test_mmap_dataset_load(base_test_env):
|
def test_mmap_dataset_load(base_test_env):
|
||||||
"""DatasetFactory.load auto-detects bin format"""
|
|
||||||
test_dir = base_test_env["test_dir"]
|
test_dir = base_test_env["test_dir"]
|
||||||
|
data = {"sequence": [_rand_seq(200)]}
|
||||||
data = {
|
|
||||||
"sequence": [torch.randint(0, 1000, (200,), dtype=torch.int64)],
|
|
||||||
}
|
|
||||||
save_bin(test_dir, data)
|
save_bin(test_dir, data)
|
||||||
|
|
||||||
dataset = DatasetFactory.load("seq", test_dir, window_size=64)
|
dataset = DatasetFactory.load("seq", test_dir, window_size=64)
|
||||||
assert len(dataset) > 0
|
assert len(dataset) > 0
|
||||||
assert dataset.count == 200
|
assert dataset.count == 200
|
||||||
|
|
@ -348,19 +290,16 @@ def test_normalize_mixed_empty_key():
|
||||||
|
|
||||||
|
|
||||||
def test_grpo_dataset_dtype(base_test_env):
|
def test_grpo_dataset_dtype(base_test_env):
|
||||||
"""GRPODataset returns correct dtypes"""
|
|
||||||
test_dir = base_test_env["test_dir"]
|
test_dir = base_test_env["test_dir"]
|
||||||
|
dummy_data = {
|
||||||
seq_len = 100
|
"prompts": [torch.randint(0, 100, (100,), dtype=torch.int32)],
|
||||||
data = {
|
"responses": [torch.randint(0, 100, (100,), dtype=torch.int32)],
|
||||||
"prompts": [torch.randint(0, 100, (seq_len,), dtype=torch.int32)],
|
"masks": [torch.ones(100, dtype=torch.int32)],
|
||||||
"responses": [torch.randint(0, 100, (seq_len,), dtype=torch.int32)],
|
"rewards": [torch.ones(100, dtype=torch.float32)],
|
||||||
"masks": [torch.ones(seq_len, dtype=torch.int32)],
|
|
||||||
"rewards": [torch.ones(seq_len, dtype=torch.float32)],
|
|
||||||
}
|
}
|
||||||
save_h5(test_dir, "grpo_dtype", data)
|
dataset = _make_seq_dataset(
|
||||||
|
test_dir, "grpo_dtype", train_type="grpo", data=dummy_data, window_size=32
|
||||||
dataset = DatasetFactory.load("grpo", test_dir, window_size=32)
|
)
|
||||||
item = dataset[0]
|
item = dataset[0]
|
||||||
|
|
||||||
assert item["prompts"].dtype == torch.long
|
assert item["prompts"].dtype == torch.long
|
||||||
|
|
@ -370,18 +309,16 @@ def test_grpo_dataset_dtype(base_test_env):
|
||||||
|
|
||||||
|
|
||||||
def test_grpo_dataset_load(base_test_env):
|
def test_grpo_dataset_load(base_test_env):
|
||||||
"""GRPODataset loads and returns correct keys"""
|
|
||||||
test_dir = base_test_env["test_dir"]
|
test_dir = base_test_env["test_dir"]
|
||||||
seq_len = 200
|
dummy_data = {
|
||||||
data = {
|
"prompts": [_rand_seq(200)],
|
||||||
"prompts": [torch.randint(0, 1000, (seq_len,), dtype=torch.int64)],
|
"responses": [_rand_seq(200)],
|
||||||
"responses": [torch.randint(0, 1000, (seq_len,), dtype=torch.int64)],
|
"masks": [torch.ones(200, dtype=torch.int64)],
|
||||||
"masks": [torch.ones(seq_len, dtype=torch.int64)],
|
"rewards": [torch.rand(200, dtype=torch.float32)],
|
||||||
"rewards": [torch.rand(seq_len, dtype=torch.float32)],
|
|
||||||
}
|
}
|
||||||
save_h5(test_dir, "grpo_test", data)
|
dataset = _make_seq_dataset(
|
||||||
|
test_dir, "grpo_test", train_type="grpo", data=dummy_data
|
||||||
dataset = DatasetFactory.load("grpo", test_dir, window_size=64)
|
)
|
||||||
assert len(dataset) > 0
|
assert len(dataset) > 0
|
||||||
item = dataset[0]
|
item = dataset[0]
|
||||||
assert "prompts" in item
|
assert "prompts" in item
|
||||||
|
|
@ -400,7 +337,6 @@ def test_detect_format_bin_dir(base_test_env):
|
||||||
|
|
||||||
|
|
||||||
def test_store_fetch_multi_key(base_test_env):
|
def test_store_fetch_multi_key(base_test_env):
|
||||||
"""Store.fetch with List[str] returns Dict[str, Tensor]"""
|
|
||||||
test_dir = base_test_env["test_dir"]
|
test_dir = base_test_env["test_dir"]
|
||||||
save_h5(
|
save_h5(
|
||||||
test_dir,
|
test_dir,
|
||||||
|
|
@ -410,7 +346,6 @@ def test_store_fetch_multi_key(base_test_env):
|
||||||
"loss_mask": [torch.ones(100, dtype=torch.int64)],
|
"loss_mask": [torch.ones(100, dtype=torch.int64)],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
store = StoreFactory.create("h5")
|
store = StoreFactory.create("h5")
|
||||||
store.load(test_dir)
|
store.load(test_dir)
|
||||||
result = store.fetch(10, 20, ["sequence", "loss_mask"])
|
result = store.fetch(10, 20, ["sequence", "loss_mask"])
|
||||||
|
|
@ -420,10 +355,8 @@ def test_store_fetch_multi_key(base_test_env):
|
||||||
|
|
||||||
|
|
||||||
def test_store_fetch_out_of_bounds(base_test_env):
|
def test_store_fetch_out_of_bounds(base_test_env):
|
||||||
"""Store.fetch raises ValueError for out-of-bounds indices"""
|
|
||||||
test_dir = base_test_env["test_dir"]
|
test_dir = base_test_env["test_dir"]
|
||||||
save_h5(test_dir, "bounds", {"sequence": [torch.randint(0, 100, (50,))]})
|
save_h5(test_dir, "bounds", {"sequence": [torch.randint(0, 100, (50,))]})
|
||||||
|
|
||||||
store = StoreFactory.create("h5")
|
store = StoreFactory.create("h5")
|
||||||
store.load(test_dir)
|
store.load(test_dir)
|
||||||
with pytest.raises(ValueError, match="out of bounds"):
|
with pytest.raises(ValueError, match="out of bounds"):
|
||||||
|
|
@ -435,10 +368,7 @@ def test_store_fetch_out_of_bounds(base_test_env):
|
||||||
|
|
||||||
|
|
||||||
def test_dataset_load_explicit_storage_type(base_test_env):
|
def test_dataset_load_explicit_storage_type(base_test_env):
|
||||||
"""DatasetFactory.load with explicit storage_type bypasses auto-detect"""
|
|
||||||
test_dir = base_test_env["test_dir"]
|
test_dir = base_test_env["test_dir"]
|
||||||
save_h5(test_dir, "explicit", {"sequence": [torch.randint(0, 100, (200,))]})
|
dataset = _make_seq_dataset(test_dir, "explicit", storage_type="h5")
|
||||||
|
|
||||||
dataset = DatasetFactory.load("seq", test_dir, window_size=64, storage_type="h5")
|
|
||||||
assert len(dataset) > 0
|
assert len(dataset) > 0
|
||||||
assert dataset.count == 200
|
assert dataset.count == 200
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
from astrai.config.preprocess_config import (
|
from astrai.config.preprocess_config import (
|
||||||
InputConfig,
|
InputConfig,
|
||||||
OutputConfig,
|
OutputConfig,
|
||||||
|
|
@ -20,9 +22,8 @@ from tests.data.conftest import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_chat_simple(chat_tokenizer):
|
def test_chat_simple(chat_tokenizer, builder):
|
||||||
config = make_chat_config()
|
config = make_chat_config()
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {
|
item = {
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "system", "content": "You are helpful."},
|
{"role": "system", "content": "You are helpful."},
|
||||||
|
|
@ -46,9 +47,8 @@ def test_chat_simple(chat_tokenizer):
|
||||||
assert trained < total
|
assert trained < total
|
||||||
|
|
||||||
|
|
||||||
def test_chat_mask_only_assistant(chat_tokenizer):
|
def test_chat_mask_only_assistant(chat_tokenizer, builder):
|
||||||
config = make_chat_config()
|
config = make_chat_config()
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {
|
item = {
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "user", "content": "What is 2+2?"},
|
{"role": "user", "content": "What is 2+2?"},
|
||||||
|
|
@ -66,14 +66,22 @@ def test_chat_mask_only_assistant(chat_tokenizer):
|
||||||
assert len(masked) > 0
|
assert len(masked) > 0
|
||||||
|
|
||||||
|
|
||||||
def test_chat_all_masked(chat_tokenizer):
|
@pytest.mark.parametrize(
|
||||||
|
"mask_rules,mask_default,expect_nonzero",
|
||||||
|
[
|
||||||
|
({"system": "mask", "user": "mask", "assistant": "mask"}, "mask", False),
|
||||||
|
({}, "train", True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_chat_uniform_masking(
|
||||||
|
mask_rules, mask_default, expect_nonzero, chat_tokenizer, builder
|
||||||
|
):
|
||||||
config = PipelineConfig(
|
config = PipelineConfig(
|
||||||
input=InputConfig(sections=_CHAT_SECTIONS),
|
input=InputConfig(sections=_CHAT_SECTIONS),
|
||||||
mask={"system": "mask", "user": "mask", "assistant": "mask"},
|
mask=mask_rules,
|
||||||
mask_default="mask",
|
mask_default=mask_default,
|
||||||
preprocessing=ProcessingConfig(max_seq_len=2048),
|
preprocessing=ProcessingConfig(max_seq_len=2048),
|
||||||
)
|
)
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {
|
item = {
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "system", "content": "You are helpful."},
|
{"role": "system", "content": "You are helpful."},
|
||||||
|
|
@ -81,35 +89,20 @@ def test_chat_all_masked(chat_tokenizer):
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
result = builder.build(item, config, chat_tokenizer)
|
result = builder.build(item, config, chat_tokenizer)
|
||||||
assert sum(result["loss_mask"]) == 0
|
masked_count = sum(result["loss_mask"])
|
||||||
|
if expect_nonzero:
|
||||||
|
assert masked_count > 0
|
||||||
|
else:
|
||||||
|
assert masked_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_chat_all_trained(chat_tokenizer):
|
def test_chat_empty_messages(chat_tokenizer, builder):
|
||||||
config = PipelineConfig(
|
|
||||||
input=InputConfig(sections=_CHAT_SECTIONS),
|
|
||||||
mask={},
|
|
||||||
mask_default="train",
|
|
||||||
preprocessing=ProcessingConfig(max_seq_len=2048),
|
|
||||||
)
|
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {
|
|
||||||
"messages": [
|
|
||||||
{"role": "system", "content": "You are helpful."},
|
|
||||||
{"role": "assistant", "content": "Hi there!"},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
result = builder.build(item, config, chat_tokenizer)
|
|
||||||
assert sum(result["loss_mask"]) == len(result["sequence"]) - 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_chat_empty_messages(chat_tokenizer):
|
|
||||||
config = make_chat_config()
|
config = make_chat_config()
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
assert builder.build({"messages": []}, config, chat_tokenizer) is None
|
assert builder.build({"messages": []}, config, chat_tokenizer) is None
|
||||||
assert builder.build({}, config, chat_tokenizer) is None
|
assert builder.build({}, config, chat_tokenizer) is None
|
||||||
|
|
||||||
|
|
||||||
def test_chat_domain_extraction(chat_tokenizer):
|
def test_chat_domain_extraction(chat_tokenizer, builder):
|
||||||
config = PipelineConfig(
|
config = PipelineConfig(
|
||||||
input=InputConfig(sections=_CHAT_SECTIONS),
|
input=InputConfig(sections=_CHAT_SECTIONS),
|
||||||
mask={"assistant": "train"},
|
mask={"assistant": "train"},
|
||||||
|
|
@ -117,7 +110,6 @@ def test_chat_domain_extraction(chat_tokenizer):
|
||||||
preprocessing=ProcessingConfig(max_seq_len=2048),
|
preprocessing=ProcessingConfig(max_seq_len=2048),
|
||||||
output=OutputConfig(domain_key="source"),
|
output=OutputConfig(domain_key="source"),
|
||||||
)
|
)
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {
|
item = {
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "user", "content": "Hi"},
|
{"role": "user", "content": "Hi"},
|
||||||
|
|
@ -129,14 +121,13 @@ def test_chat_domain_extraction(chat_tokenizer):
|
||||||
assert result["domain"] == "wiki"
|
assert result["domain"] == "wiki"
|
||||||
|
|
||||||
|
|
||||||
def test_chat_truncation(chat_tokenizer):
|
def test_chat_truncation(chat_tokenizer, builder):
|
||||||
config = PipelineConfig(
|
config = PipelineConfig(
|
||||||
input=InputConfig(sections=_CHAT_SECTIONS),
|
input=InputConfig(sections=_CHAT_SECTIONS),
|
||||||
mask={"assistant": "train"},
|
mask={"assistant": "train"},
|
||||||
mask_default="mask",
|
mask_default="mask",
|
||||||
preprocessing=ProcessingConfig(max_seq_len=10),
|
preprocessing=ProcessingConfig(max_seq_len=10),
|
||||||
)
|
)
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {
|
item = {
|
||||||
"messages": [
|
"messages": [
|
||||||
{
|
{
|
||||||
|
|
@ -151,18 +142,16 @@ def test_chat_truncation(chat_tokenizer):
|
||||||
assert len(result["loss_mask"]) == len(result["sequence"])
|
assert len(result["loss_mask"]) == len(result["sequence"])
|
||||||
|
|
||||||
|
|
||||||
def test_instruction_basic(test_tokenizer):
|
def test_instruction_basic(test_tokenizer, builder):
|
||||||
config = make_instruction_config()
|
config = make_instruction_config()
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {"prompt": "Translate to French: Hello", "response": "Bonjour"}
|
item = {"prompt": "Translate to French: Hello", "response": "Bonjour"}
|
||||||
result = builder.build(item, config, test_tokenizer)
|
result = builder.build(item, config, test_tokenizer)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert len(result["sequence"]) == len(result["loss_mask"])
|
assert len(result["sequence"]) == len(result["loss_mask"])
|
||||||
|
|
||||||
|
|
||||||
def test_instruction_prompt_masked(test_tokenizer):
|
def test_instruction_prompt_masked(test_tokenizer, builder):
|
||||||
config = make_instruction_config()
|
config = make_instruction_config()
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {"prompt": "hello", "response": "world"}
|
item = {"prompt": "hello", "response": "world"}
|
||||||
result = builder.build(item, config, test_tokenizer)
|
result = builder.build(item, config, test_tokenizer)
|
||||||
mask = result["loss_mask"]
|
mask = result["loss_mask"]
|
||||||
|
|
@ -175,7 +164,7 @@ def test_instruction_prompt_masked(test_tokenizer):
|
||||||
assert all(m == 1 for m in mask[p_len:])
|
assert all(m == 1 for m in mask[p_len:])
|
||||||
|
|
||||||
|
|
||||||
def test_instruction_train_on_prompt(test_tokenizer):
|
def test_instruction_train_on_prompt(test_tokenizer, builder):
|
||||||
config = PipelineConfig(
|
config = PipelineConfig(
|
||||||
input=InputConfig(
|
input=InputConfig(
|
||||||
sections=[
|
sections=[
|
||||||
|
|
@ -185,7 +174,6 @@ def test_instruction_train_on_prompt(test_tokenizer):
|
||||||
),
|
),
|
||||||
preprocessing=ProcessingConfig(max_seq_len=2048),
|
preprocessing=ProcessingConfig(max_seq_len=2048),
|
||||||
)
|
)
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {"prompt": "hello", "response": "world"}
|
item = {"prompt": "hello", "response": "world"}
|
||||||
result = builder.build(item, config, test_tokenizer)
|
result = builder.build(item, config, test_tokenizer)
|
||||||
mask = result["loss_mask"]
|
mask = result["loss_mask"]
|
||||||
|
|
@ -196,9 +184,8 @@ def test_instruction_train_on_prompt(test_tokenizer):
|
||||||
assert all(m == 1 for m in mask[:p_len])
|
assert all(m == 1 for m in mask[:p_len])
|
||||||
|
|
||||||
|
|
||||||
def test_text_basic(test_tokenizer):
|
def test_text_basic(test_tokenizer, builder):
|
||||||
config = make_text_config()
|
config = make_text_config()
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {"text": "Hello world. This is a test document."}
|
item = {"text": "Hello world. This is a test document."}
|
||||||
result = builder.build(item, config, test_tokenizer)
|
result = builder.build(item, config, test_tokenizer)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
@ -207,41 +194,37 @@ def test_text_basic(test_tokenizer):
|
||||||
assert "loss_mask" not in result
|
assert "loss_mask" not in result
|
||||||
|
|
||||||
|
|
||||||
def test_text_empty(test_tokenizer):
|
def test_text_empty(test_tokenizer, builder):
|
||||||
config = make_text_config()
|
config = make_text_config()
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
assert builder.build({"text": ""}, config, test_tokenizer) is None
|
assert builder.build({"text": ""}, config, test_tokenizer) is None
|
||||||
assert builder.build({"text": " "}, config, test_tokenizer) is None
|
assert builder.build({"text": " "}, config, test_tokenizer) is None
|
||||||
|
|
||||||
|
|
||||||
def test_text_too_short(test_tokenizer):
|
def test_text_too_short(test_tokenizer, builder):
|
||||||
config = PipelineConfig(
|
config = PipelineConfig(
|
||||||
input=InputConfig(sections=_TEXT_SECTIONS),
|
input=InputConfig(sections=_TEXT_SECTIONS),
|
||||||
preprocessing=ProcessingConfig(min_chars=100),
|
preprocessing=ProcessingConfig(min_chars=100),
|
||||||
)
|
)
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
assert builder.build({"text": "short"}, config, test_tokenizer) is None
|
assert builder.build({"text": "short"}, config, test_tokenizer) is None
|
||||||
|
|
||||||
|
|
||||||
def test_text_truncation(test_tokenizer):
|
def test_text_truncation(test_tokenizer, builder):
|
||||||
config = PipelineConfig(
|
config = PipelineConfig(
|
||||||
input=InputConfig(sections=_TEXT_SECTIONS),
|
input=InputConfig(sections=_TEXT_SECTIONS),
|
||||||
preprocessing=ProcessingConfig(max_seq_len=3, min_chars=1),
|
preprocessing=ProcessingConfig(max_seq_len=3, min_chars=1),
|
||||||
)
|
)
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {"text": "This is a very long text that should be truncated"}
|
item = {"text": "This is a very long text that should be truncated"}
|
||||||
result = builder.build(item, config, test_tokenizer)
|
result = builder.build(item, config, test_tokenizer)
|
||||||
assert len(result["sequence"]) <= 3
|
assert len(result["sequence"]) <= 3
|
||||||
|
|
||||||
|
|
||||||
def test_sectioned_chat(chat_tokenizer):
|
def test_sectioned_chat(chat_tokenizer, builder):
|
||||||
config = PipelineConfig(
|
config = PipelineConfig(
|
||||||
input=InputConfig(sections=_CHAT_SECTIONS),
|
input=InputConfig(sections=_CHAT_SECTIONS),
|
||||||
mask={"system": "mask", "user": "mask", "assistant": "train"},
|
mask={"system": "mask", "user": "mask", "assistant": "train"},
|
||||||
mask_default="mask",
|
mask_default="mask",
|
||||||
preprocessing=ProcessingConfig(max_seq_len=2048),
|
preprocessing=ProcessingConfig(max_seq_len=2048),
|
||||||
)
|
)
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {
|
item = {
|
||||||
"messages": [
|
"messages": [
|
||||||
{"role": "user", "content": "What is 2+2?"},
|
{"role": "user", "content": "What is 2+2?"},
|
||||||
|
|
@ -255,12 +238,11 @@ def test_sectioned_chat(chat_tokenizer):
|
||||||
assert 0 in result["loss_mask"]
|
assert 0 in result["loss_mask"]
|
||||||
|
|
||||||
|
|
||||||
def test_sectioned_instruction(test_tokenizer):
|
def test_sectioned_instruction(test_tokenizer, builder):
|
||||||
config = PipelineConfig(
|
config = PipelineConfig(
|
||||||
input=InputConfig(sections=_INSTRUCTION_SECTIONS),
|
input=InputConfig(sections=_INSTRUCTION_SECTIONS),
|
||||||
preprocessing=ProcessingConfig(max_seq_len=2048, min_chars=0),
|
preprocessing=ProcessingConfig(max_seq_len=2048, min_chars=0),
|
||||||
)
|
)
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {"prompt": "Q: Why?", "response": "A: Because."}
|
item = {"prompt": "Q: Why?", "response": "A: Because."}
|
||||||
result = builder.build(item, config, test_tokenizer)
|
result = builder.build(item, config, test_tokenizer)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
@ -269,24 +251,22 @@ def test_sectioned_instruction(test_tokenizer):
|
||||||
assert mask[-1] == 1
|
assert mask[-1] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_sectioned_text(test_tokenizer):
|
def test_sectioned_text(test_tokenizer, builder):
|
||||||
config = PipelineConfig(
|
config = PipelineConfig(
|
||||||
input=InputConfig(sections=_TEXT_SECTIONS),
|
input=InputConfig(sections=_TEXT_SECTIONS),
|
||||||
preprocessing=ProcessingConfig(max_seq_len=2048, min_chars=1),
|
preprocessing=ProcessingConfig(max_seq_len=2048, min_chars=1),
|
||||||
)
|
)
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {"text": "Hello world, this is a test."}
|
item = {"text": "Hello world, this is a test."}
|
||||||
result = builder.build(item, config, test_tokenizer)
|
result = builder.build(item, config, test_tokenizer)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert "loss_mask" not in result
|
assert "loss_mask" not in result
|
||||||
|
|
||||||
|
|
||||||
def test_sectioned_text_too_short(test_tokenizer):
|
def test_sectioned_text_too_short(test_tokenizer, builder):
|
||||||
config = PipelineConfig(
|
config = PipelineConfig(
|
||||||
input=InputConfig(sections=_TEXT_SECTIONS),
|
input=InputConfig(sections=_TEXT_SECTIONS),
|
||||||
preprocessing=ProcessingConfig(max_seq_len=2048, min_chars=100),
|
preprocessing=ProcessingConfig(max_seq_len=2048, min_chars=100),
|
||||||
)
|
)
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
assert builder.build({"text": "short"}, config, test_tokenizer) is None
|
assert builder.build({"text": "short"}, config, test_tokenizer) is None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -296,13 +276,12 @@ def test_factory_registered():
|
||||||
|
|
||||||
|
|
||||||
def test_factory_create():
|
def test_factory_create():
|
||||||
builder = MaskBuilderFactory.create("sectioned")
|
builder_obj = MaskBuilderFactory.create("sectioned")
|
||||||
assert isinstance(builder, SectionedMaskBuilder)
|
assert isinstance(builder_obj, SectionedMaskBuilder)
|
||||||
|
|
||||||
|
|
||||||
def test_dpo_chat_basic(chat_tokenizer):
|
def test_dpo_chat_basic(chat_tokenizer, builder):
|
||||||
config = make_dpo_chat_config()
|
config = make_dpo_chat_config()
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {
|
item = {
|
||||||
"chosen": [
|
"chosen": [
|
||||||
{"role": "user", "content": "What is 2+2?"},
|
{"role": "user", "content": "What is 2+2?"},
|
||||||
|
|
@ -319,16 +298,14 @@ def test_dpo_chat_basic(chat_tokenizer):
|
||||||
assert "rejected" in result
|
assert "rejected" in result
|
||||||
assert "chosen_mask" in result
|
assert "chosen_mask" in result
|
||||||
assert "rejected_mask" in result
|
assert "rejected_mask" in result
|
||||||
assert "domain" in result
|
|
||||||
assert len(result["chosen"]) == len(result["chosen_mask"])
|
assert len(result["chosen"]) == len(result["chosen_mask"])
|
||||||
assert len(result["rejected"]) == len(result["rejected_mask"])
|
assert len(result["rejected"]) == len(result["rejected_mask"])
|
||||||
assert sum(result["chosen_mask"]) > 0
|
assert sum(result["chosen_mask"]) > 0
|
||||||
assert sum(result["rejected_mask"]) > 0
|
assert sum(result["rejected_mask"]) > 0
|
||||||
|
|
||||||
|
|
||||||
def test_dpo_chosen_only_trained(chat_tokenizer):
|
def test_dpo_chosen_only_trained(chat_tokenizer, builder):
|
||||||
config = make_dpo_chat_config()
|
config = make_dpo_chat_config()
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {
|
item = {
|
||||||
"chosen": [
|
"chosen": [
|
||||||
{"role": "user", "content": "Hi"},
|
{"role": "user", "content": "Hi"},
|
||||||
|
|
@ -346,15 +323,13 @@ def test_dpo_chosen_only_trained(chat_tokenizer):
|
||||||
assert 1 in result["rejected_mask"]
|
assert 1 in result["rejected_mask"]
|
||||||
|
|
||||||
|
|
||||||
def test_dpo_missing_field_is_none(chat_tokenizer):
|
def test_dpo_missing_field_is_none(chat_tokenizer, builder):
|
||||||
config = make_dpo_chat_config()
|
config = make_dpo_chat_config()
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
assert builder.build({"chosen": [], "rejected": []}, config, chat_tokenizer) is None
|
assert builder.build({"chosen": [], "rejected": []}, config, chat_tokenizer) is None
|
||||||
|
|
||||||
|
|
||||||
def test_grpo_basic(chat_tokenizer):
|
def test_grpo_basic(chat_tokenizer, builder):
|
||||||
config = make_grpo_config()
|
config = make_grpo_config()
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {
|
item = {
|
||||||
"prompt": [{"role": "user", "content": "What is 2+2?"}],
|
"prompt": [{"role": "user", "content": "What is 2+2?"}],
|
||||||
"responses": ["4", "The answer is four", "Four", "2+2=4"],
|
"responses": ["4", "The answer is four", "Four", "2+2=4"],
|
||||||
|
|
@ -370,9 +345,8 @@ def test_grpo_basic(chat_tokenizer):
|
||||||
assert result["rewards"] == [1.0, 0.5, 0.8, 0.2]
|
assert result["rewards"] == [1.0, 0.5, 0.8, 0.2]
|
||||||
|
|
||||||
|
|
||||||
def test_grpo_response_tokens_all_trained(chat_tokenizer):
|
def test_grpo_response_tokens_all_trained(chat_tokenizer, builder):
|
||||||
config = make_grpo_config()
|
config = make_grpo_config()
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {
|
item = {
|
||||||
"prompt": [{"role": "user", "content": "Q"}],
|
"prompt": [{"role": "user", "content": "Q"}],
|
||||||
"responses": ["A", "B"],
|
"responses": ["A", "B"],
|
||||||
|
|
@ -384,9 +358,8 @@ def test_grpo_response_tokens_all_trained(chat_tokenizer):
|
||||||
assert len(masks) == len(result["responses"])
|
assert len(masks) == len(result["responses"])
|
||||||
|
|
||||||
|
|
||||||
def test_grpo_single_reward(chat_tokenizer):
|
def test_grpo_single_reward(chat_tokenizer, builder):
|
||||||
config = make_grpo_config()
|
config = make_grpo_config()
|
||||||
builder = SectionedMaskBuilder()
|
|
||||||
item = {
|
item = {
|
||||||
"prompt": [{"role": "user", "content": "Q"}],
|
"prompt": [{"role": "user", "content": "Q"}],
|
||||||
"responses": ["A"],
|
"responses": ["A"],
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,7 @@ from astrai.config.preprocess_config import (
|
||||||
from astrai.preprocessing.pipeline import Pipeline, filter_by_length
|
from astrai.preprocessing.pipeline import Pipeline, filter_by_length
|
||||||
from tests.data.conftest import (
|
from tests.data.conftest import (
|
||||||
_CHAT_SECTIONS,
|
_CHAT_SECTIONS,
|
||||||
_CHAT_TEMPLATE,
|
|
||||||
_INSTRUCTION_SECTIONS,
|
_INSTRUCTION_SECTIONS,
|
||||||
_SPECIAL_TOKENS_CONFIG,
|
|
||||||
_TEXT_SECTIONS,
|
_TEXT_SECTIONS,
|
||||||
make_dpo_chat_config,
|
make_dpo_chat_config,
|
||||||
make_grpo_no_template_config,
|
make_grpo_no_template_config,
|
||||||
|
|
@ -26,19 +24,7 @@ def test_filter_by_length():
|
||||||
assert filter_by_length("just right", min_len=5, max_len=20)
|
assert filter_by_length("just right", min_len=5, max_len=20)
|
||||||
|
|
||||||
|
|
||||||
def test_full_chat_pipeline(temp_dir, chat_tokenizer):
|
def test_full_chat_pipeline(temp_dir, chat_tokenizer_dir):
|
||||||
tokenizer_dir = os.path.join(temp_dir, "tok")
|
|
||||||
os.makedirs(tokenizer_dir, exist_ok=True)
|
|
||||||
chat_tokenizer._tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json"))
|
|
||||||
with open(os.path.join(tokenizer_dir, "tokenizer_config.json"), "w") as f:
|
|
||||||
json.dump(
|
|
||||||
{
|
|
||||||
"special_tokens": _SPECIAL_TOKENS_CONFIG,
|
|
||||||
"chat_template": _CHAT_TEMPLATE,
|
|
||||||
},
|
|
||||||
f,
|
|
||||||
)
|
|
||||||
|
|
||||||
jsonl_path = os.path.join(temp_dir, "chat.jsonl")
|
jsonl_path = os.path.join(temp_dir, "chat.jsonl")
|
||||||
with open(jsonl_path, "w", encoding="utf-8") as f:
|
with open(jsonl_path, "w", encoding="utf-8") as f:
|
||||||
f.write(
|
f.write(
|
||||||
|
|
@ -78,7 +64,7 @@ def test_full_chat_pipeline(temp_dir, chat_tokenizer):
|
||||||
config=config,
|
config=config,
|
||||||
input_paths=[jsonl_path],
|
input_paths=[jsonl_path],
|
||||||
output_dir=out_dir,
|
output_dir=out_dir,
|
||||||
tokenizer_path=tokenizer_dir,
|
tokenizer_path=chat_tokenizer_dir,
|
||||||
).run()
|
).run()
|
||||||
|
|
||||||
meta_path = os.path.join(out_dir, "__default__", "shard_0000", "meta.json")
|
meta_path = os.path.join(out_dir, "__default__", "shard_0000", "meta.json")
|
||||||
|
|
@ -91,21 +77,7 @@ def test_full_chat_pipeline(temp_dir, chat_tokenizer):
|
||||||
assert meta["loss_mask"]["dtype"] == "int32"
|
assert meta["loss_mask"]["dtype"] == "int32"
|
||||||
|
|
||||||
|
|
||||||
def test_full_text_pipeline(temp_dir, test_tokenizer):
|
def test_full_text_pipeline(temp_dir, tokenizer_dir):
|
||||||
tokenizer_dir = os.path.join(temp_dir, "tok")
|
|
||||||
os.makedirs(tokenizer_dir, exist_ok=True)
|
|
||||||
test_tokenizer._tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json"))
|
|
||||||
with open(os.path.join(tokenizer_dir, "tokenizer_config.json"), "w") as f:
|
|
||||||
json.dump(
|
|
||||||
{
|
|
||||||
"special_tokens": {
|
|
||||||
"pad_token": "<|_pad_|>",
|
|
||||||
"unk_token": "<|_unk_|>",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
f,
|
|
||||||
)
|
|
||||||
|
|
||||||
jsonl_path = os.path.join(temp_dir, "text.jsonl")
|
jsonl_path = os.path.join(temp_dir, "text.jsonl")
|
||||||
with open(jsonl_path, "w", encoding="utf-8") as f:
|
with open(jsonl_path, "w", encoding="utf-8") as f:
|
||||||
f.write(
|
f.write(
|
||||||
|
|
@ -145,24 +117,9 @@ def test_full_text_pipeline(temp_dir, test_tokenizer):
|
||||||
meta = json.load(f)
|
meta = json.load(f)
|
||||||
assert "sequence" in meta
|
assert "sequence" in meta
|
||||||
assert "loss_mask" not in meta
|
assert "loss_mask" not in meta
|
||||||
assert meta["sequence"]["dtype"] == "int32"
|
|
||||||
|
|
||||||
|
|
||||||
def test_full_instruction_pipeline(temp_dir, test_tokenizer):
|
def test_full_instruction_pipeline(temp_dir, tokenizer_dir):
|
||||||
tokenizer_dir = os.path.join(temp_dir, "tok")
|
|
||||||
os.makedirs(tokenizer_dir, exist_ok=True)
|
|
||||||
test_tokenizer._tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json"))
|
|
||||||
with open(os.path.join(tokenizer_dir, "tokenizer_config.json"), "w") as f:
|
|
||||||
json.dump(
|
|
||||||
{
|
|
||||||
"special_tokens": {
|
|
||||||
"pad_token": "<|_pad_|>",
|
|
||||||
"unk_token": "<|_unk_|>",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
f,
|
|
||||||
)
|
|
||||||
|
|
||||||
jsonl_path = os.path.join(temp_dir, "instruct.jsonl")
|
jsonl_path = os.path.join(temp_dir, "instruct.jsonl")
|
||||||
with open(jsonl_path, "w", encoding="utf-8") as f:
|
with open(jsonl_path, "w", encoding="utf-8") as f:
|
||||||
f.write(
|
f.write(
|
||||||
|
|
@ -206,25 +163,9 @@ def test_full_instruction_pipeline(temp_dir, test_tokenizer):
|
||||||
meta = json.load(f)
|
meta = json.load(f)
|
||||||
assert "sequence" in meta
|
assert "sequence" in meta
|
||||||
assert "loss_mask" in meta
|
assert "loss_mask" in meta
|
||||||
assert meta["sequence"]["dtype"] == "int32"
|
|
||||||
assert meta["loss_mask"]["dtype"] == "int32"
|
|
||||||
|
|
||||||
|
|
||||||
def test_dtype_override(temp_dir, test_tokenizer):
|
def test_dtype_override(temp_dir, tokenizer_dir):
|
||||||
tokenizer_dir = os.path.join(temp_dir, "tok")
|
|
||||||
os.makedirs(tokenizer_dir, exist_ok=True)
|
|
||||||
test_tokenizer._tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json"))
|
|
||||||
with open(os.path.join(tokenizer_dir, "tokenizer_config.json"), "w") as f:
|
|
||||||
json.dump(
|
|
||||||
{
|
|
||||||
"special_tokens": {
|
|
||||||
"pad_token": "<|_pad_|>",
|
|
||||||
"unk_token": "<|_unk_|>",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
f,
|
|
||||||
)
|
|
||||||
|
|
||||||
jsonl_path = os.path.join(temp_dir, "data.jsonl")
|
jsonl_path = os.path.join(temp_dir, "data.jsonl")
|
||||||
with open(jsonl_path, "w", encoding="utf-8") as f:
|
with open(jsonl_path, "w", encoding="utf-8") as f:
|
||||||
f.write(json.dumps({"prompt": "Q", "response": "A"}) + "\n")
|
f.write(json.dumps({"prompt": "Q", "response": "A"}) + "\n")
|
||||||
|
|
@ -252,19 +193,7 @@ def test_dtype_override(temp_dir, test_tokenizer):
|
||||||
assert meta["loss_mask"]["dtype"] == "bool"
|
assert meta["loss_mask"]["dtype"] == "bool"
|
||||||
|
|
||||||
|
|
||||||
def test_dpo_pipeline(temp_dir, chat_tokenizer):
|
def test_dpo_pipeline(temp_dir, chat_tokenizer_dir):
|
||||||
tokenizer_dir = os.path.join(temp_dir, "tok")
|
|
||||||
os.makedirs(tokenizer_dir, exist_ok=True)
|
|
||||||
chat_tokenizer._tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json"))
|
|
||||||
with open(os.path.join(tokenizer_dir, "tokenizer_config.json"), "w") as f:
|
|
||||||
json.dump(
|
|
||||||
{
|
|
||||||
"special_tokens": _SPECIAL_TOKENS_CONFIG,
|
|
||||||
"chat_template": _CHAT_TEMPLATE,
|
|
||||||
},
|
|
||||||
f,
|
|
||||||
)
|
|
||||||
|
|
||||||
jsonl_path = os.path.join(temp_dir, "dpo.jsonl")
|
jsonl_path = os.path.join(temp_dir, "dpo.jsonl")
|
||||||
with open(jsonl_path, "w", encoding="utf-8") as f:
|
with open(jsonl_path, "w", encoding="utf-8") as f:
|
||||||
f.write(
|
f.write(
|
||||||
|
|
@ -288,7 +217,7 @@ def test_dpo_pipeline(temp_dir, chat_tokenizer):
|
||||||
config=make_dpo_chat_config(),
|
config=make_dpo_chat_config(),
|
||||||
input_paths=[jsonl_path],
|
input_paths=[jsonl_path],
|
||||||
output_dir=out_dir,
|
output_dir=out_dir,
|
||||||
tokenizer_path=tokenizer_dir,
|
tokenizer_path=chat_tokenizer_dir,
|
||||||
).run()
|
).run()
|
||||||
|
|
||||||
meta_path = os.path.join(out_dir, "__default__", "shard_0000", "meta.json")
|
meta_path = os.path.join(out_dir, "__default__", "shard_0000", "meta.json")
|
||||||
|
|
@ -302,21 +231,7 @@ def test_dpo_pipeline(temp_dir, chat_tokenizer):
|
||||||
assert "sequence" not in meta
|
assert "sequence" not in meta
|
||||||
|
|
||||||
|
|
||||||
def test_grpo_pipeline(temp_dir, test_tokenizer):
|
def test_grpo_pipeline(temp_dir, tokenizer_dir):
|
||||||
tokenizer_dir = os.path.join(temp_dir, "tok")
|
|
||||||
os.makedirs(tokenizer_dir, exist_ok=True)
|
|
||||||
test_tokenizer._tokenizer.save(os.path.join(tokenizer_dir, "tokenizer.json"))
|
|
||||||
with open(os.path.join(tokenizer_dir, "tokenizer_config.json"), "w") as f:
|
|
||||||
json.dump(
|
|
||||||
{
|
|
||||||
"special_tokens": {
|
|
||||||
"pad_token": "<|_pad_|>",
|
|
||||||
"unk_token": "<|_unk_|>",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
f,
|
|
||||||
)
|
|
||||||
|
|
||||||
jsonl_path = os.path.join(temp_dir, "grpo.jsonl")
|
jsonl_path = os.path.join(temp_dir, "grpo.jsonl")
|
||||||
with open(jsonl_path, "w", encoding="utf-8") as f:
|
with open(jsonl_path, "w", encoding="utf-8") as f:
|
||||||
f.write(
|
f.write(
|
||||||
|
|
|
||||||
|
|
@ -13,65 +13,26 @@ from astrai.inference.api.tool_parser import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_scan_complete_simple():
|
@pytest.mark.parametrize(
|
||||||
end, complete = _scan_json('{"key": "value"}', 0)
|
"text,expected_complete,check_end_eq_len",
|
||||||
assert complete is True
|
[
|
||||||
assert end == len('{"key": "value"}')
|
('{"key": "value"}', True, True),
|
||||||
|
('{"outer": {"inner": 1}}', True, True),
|
||||||
|
('{"key": "value"', False, False),
|
||||||
def test_scan_complete_nested():
|
('{"outer": {"inner": 1}', False, False),
|
||||||
text = '{"outer": {"inner": 1}}'
|
('{"key": "a{b}c"} extra', True, False),
|
||||||
|
(r'{"key": "a\"b"}', True, False),
|
||||||
|
('{"a": {"b": {"c": {"d": {"e": 5}}}}}', True, True),
|
||||||
|
('{"items": [{"x": 1}, {"x": 2}]}', True, True),
|
||||||
|
('{"fn": "function() { return 1; }"}', True, False),
|
||||||
|
('{"key": "\u5317\u4eac"}', True, False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_scan_json(text, expected_complete, check_end_eq_len):
|
||||||
end, complete = _scan_json(text, 0)
|
end, complete = _scan_json(text, 0)
|
||||||
assert complete is True
|
assert complete is expected_complete
|
||||||
assert end == len(text)
|
if check_end_eq_len:
|
||||||
|
assert end == len(text)
|
||||||
|
|
||||||
def test_scan_incomplete_unclosed():
|
|
||||||
end, complete = _scan_json('{"key": "value"', 0)
|
|
||||||
assert complete is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_scan_incomplete_nested():
|
|
||||||
end, complete = _scan_json('{"outer": {"inner": 1}', 0)
|
|
||||||
assert complete is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_scan_string_braces_ignored():
|
|
||||||
text = '{"key": "a{b}c"} extra'
|
|
||||||
end, complete = _scan_json(text, 0)
|
|
||||||
assert complete is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_scan_escaped_quote_ignored():
|
|
||||||
text = r'{"key": "a\"b"}'
|
|
||||||
end, complete = _scan_json(text, 0)
|
|
||||||
assert complete is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_scan_deeply_nested():
|
|
||||||
text = '{"a": {"b": {"c": {"d": {"e": 5}}}}}'
|
|
||||||
end, complete = _scan_json(text, 0)
|
|
||||||
assert complete is True
|
|
||||||
assert end == len(text)
|
|
||||||
|
|
||||||
|
|
||||||
def test_scan_array_with_braces():
|
|
||||||
text = '{"items": [{"x": 1}, {"x": 2}]}'
|
|
||||||
end, complete = _scan_json(text, 0)
|
|
||||||
assert complete is True
|
|
||||||
assert end == len(text)
|
|
||||||
|
|
||||||
|
|
||||||
def test_scan_code_in_string():
|
|
||||||
text = '{"fn": "function() { return 1; }"}'
|
|
||||||
end, complete = _scan_json(text, 0)
|
|
||||||
assert complete is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_scan_unicode_chars():
|
|
||||||
text = '{"key": "\u5317\u4eac"}'
|
|
||||||
end, complete = _scan_json(text, 0)
|
|
||||||
assert complete is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_find_single_tool_call():
|
def test_find_single_tool_call():
|
||||||
|
|
@ -141,10 +102,7 @@ def test_find_arguments_with_array():
|
||||||
|
|
||||||
|
|
||||||
def test_find_arguments_with_nested_array_of_objects():
|
def test_find_arguments_with_nested_array_of_objects():
|
||||||
text = (
|
text = '{"name": "batch", "arguments": {"rows": [{"id": 1, "val": "a"}, {"id": 2, "val": "b"}]}}'
|
||||||
'{"name": "batch", '
|
|
||||||
'"arguments": {"rows": [{"id": 1, "val": "a"}, {"id": 2, "val": "b"}]}}'
|
|
||||||
)
|
|
||||||
results = _find_tool_calls(text)
|
results = _find_tool_calls(text)
|
||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
assert '"rows"' in results[0]["args"]
|
assert '"rows"' in results[0]["args"]
|
||||||
|
|
@ -206,38 +164,26 @@ def test_find_extracts_correct_arg_start_position():
|
||||||
assert json_str == text
|
assert json_str == text
|
||||||
|
|
||||||
|
|
||||||
def test_partial_with_name():
|
@pytest.mark.parametrize(
|
||||||
result = _find_partial_tool_call('{"name": "func", "arguments": {"city"')
|
"text,expected_name,expected_complete",
|
||||||
assert result is not None
|
[
|
||||||
assert result["name"] == "func"
|
('{"name": "func", "arguments": {"city"', "func", False),
|
||||||
assert result["complete"] is False
|
('{"name": "func", "arguments": {"city": "BJ"}}', "func", None),
|
||||||
|
("plain text", None, None),
|
||||||
|
('{"nam', None, None),
|
||||||
def test_partial_with_full_args():
|
('{"name": "deep", "arguments": {"a": {"b": {"c": ', "deep", None),
|
||||||
result = _find_partial_tool_call('{"name": "func", "arguments": {"city": "BJ"}}')
|
('{"name": "batch", "arguments": {"items": [1, 2, ', "batch", None),
|
||||||
assert result is not None
|
],
|
||||||
assert result["name"] == "func"
|
)
|
||||||
|
def test_find_partial_tool_call(text, expected_name, expected_complete):
|
||||||
|
result = _find_partial_tool_call(text)
|
||||||
def test_partial_no_match():
|
if expected_name is None:
|
||||||
assert _find_partial_tool_call("plain text") is None
|
assert result is None
|
||||||
|
else:
|
||||||
|
assert result is not None
|
||||||
def test_partial_no_name_yet():
|
assert result["name"] == expected_name
|
||||||
assert _find_partial_tool_call('{"nam') is None
|
if expected_complete is not None:
|
||||||
|
assert result["complete"] is expected_complete
|
||||||
|
|
||||||
def test_partial_deeply_nested():
|
|
||||||
result = _find_partial_tool_call('{"name": "deep", "arguments": {"a": {"b": {"c": ')
|
|
||||||
assert result is not None
|
|
||||||
assert result["name"] == "deep"
|
|
||||||
assert '"a"' in result["args"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_partial_array_incomplete():
|
|
||||||
result = _find_partial_tool_call('{"name": "batch", "arguments": {"items": [1, 2, ')
|
|
||||||
assert result is not None
|
|
||||||
assert result["name"] == "batch"
|
|
||||||
|
|
||||||
|
|
||||||
def test_feed_plain_text():
|
def test_feed_plain_text():
|
||||||
|
|
@ -269,7 +215,6 @@ def test_feed_tool_call_args_streaming():
|
||||||
parser = SimpleJsonToolParser()
|
parser = SimpleJsonToolParser()
|
||||||
d1 = parser.feed('{"name": "f", "arguments": {"x":')
|
d1 = parser.feed('{"name": "f", "arguments": {"x":')
|
||||||
d2 = parser.feed('{"name": "f", "arguments": {"x": "1"}}')
|
d2 = parser.feed('{"name": "f", "arguments": {"x": "1"}}')
|
||||||
|
|
||||||
args_deltas = [
|
args_deltas = [
|
||||||
d
|
d
|
||||||
for batch in (d1, d2)
|
for batch in (d1, d2)
|
||||||
|
|
@ -332,17 +277,6 @@ def test_feed_content_after_tool_call_is_not_emitted():
|
||||||
assert parser.has_tool_calls
|
assert parser.has_tool_calls
|
||||||
|
|
||||||
|
|
||||||
def _collect_args_deltas(parser):
|
|
||||||
args_parts = []
|
|
||||||
for d in parser.feed(parser._text_buffer):
|
|
||||||
if "tool_calls" in d:
|
|
||||||
for tc in d["tool_calls"]:
|
|
||||||
fn = tc.get("function", {})
|
|
||||||
if "arguments" in fn and fn["arguments"]:
|
|
||||||
args_parts.append(fn["arguments"])
|
|
||||||
return args_parts
|
|
||||||
|
|
||||||
|
|
||||||
def _simulate_streaming(parser, text):
|
def _simulate_streaming(parser, text):
|
||||||
all_delta_names = []
|
all_delta_names = []
|
||||||
all_args_chunks = []
|
all_args_chunks = []
|
||||||
|
|
@ -447,7 +381,6 @@ def test_streaming_args_diff_only_emits_new_bytes():
|
||||||
parser = SimpleJsonToolParser()
|
parser = SimpleJsonToolParser()
|
||||||
step1 = parser.feed('{"name": "f", "arguments": {"city": "Bei')
|
step1 = parser.feed('{"name": "f", "arguments": {"city": "Bei')
|
||||||
step2 = parser.feed('{"name": "f", "arguments": {"city": "Beijing"}}')
|
step2 = parser.feed('{"name": "f", "arguments": {"city": "Beijing"}}')
|
||||||
|
|
||||||
all_args = []
|
all_args = []
|
||||||
for step in (step1, step2):
|
for step in (step1, step2):
|
||||||
for d in step:
|
for d in step:
|
||||||
|
|
@ -500,31 +433,21 @@ def test_parse_complete_with_content():
|
||||||
|
|
||||||
def test_parse_complete_multiple_tool_calls():
|
def test_parse_complete_multiple_tool_calls():
|
||||||
parser = SimpleJsonToolParser()
|
parser = SimpleJsonToolParser()
|
||||||
body = (
|
body = '{"name": "get_weather", "arguments": {"city": "Beijing"}}{"name": "get_time", "arguments": {"tz": "Asia/Shanghai"}}'
|
||||||
'{"name": "get_weather", "arguments": {"city": "Beijing"}}'
|
|
||||||
'{"name": "get_time", "arguments": {"tz": "Asia/Shanghai"}}'
|
|
||||||
)
|
|
||||||
result = parser.parse_complete(body)
|
result = parser.parse_complete(body)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert len(result["tool_calls"]) == 2
|
assert len(result["tool_calls"]) == 2
|
||||||
assert result["tool_calls"][0]["function"]["name"] == "get_weather"
|
assert result["tool_calls"][0]["function"]["name"] == "get_weather"
|
||||||
assert result["tool_calls"][1]["function"]["name"] == "get_time"
|
assert result["tool_calls"][1]["function"]["name"] == "get_time"
|
||||||
assert "Beijing" in result["tool_calls"][0]["function"]["arguments"]
|
|
||||||
assert "Asia/Shanghai" in result["tool_calls"][1]["function"]["arguments"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_complete_complex_real_world():
|
def test_parse_complete_complex_real_world():
|
||||||
parser = SimpleJsonToolParser()
|
parser = SimpleJsonToolParser()
|
||||||
body = (
|
body = (
|
||||||
'{"name": "send_email", '
|
'{"name": "send_email", "arguments": {'
|
||||||
'"arguments": {'
|
'"to": ["a@b.com", "c@d.com"], "cc": null, '
|
||||||
'"to": ["a@b.com", "c@d.com"], '
|
'"subject": "Hello World", "body": "This is a test email.", '
|
||||||
'"cc": null, '
|
'"priority": 1, "attachments": false}}'
|
||||||
'"subject": "Hello World", '
|
|
||||||
'"body": "This is a test email.", '
|
|
||||||
'"priority": 1, '
|
|
||||||
'"attachments": false'
|
|
||||||
"}}"
|
|
||||||
)
|
)
|
||||||
result = parser.parse_complete(body)
|
result = parser.parse_complete(body)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
@ -539,11 +462,7 @@ def test_parse_complete_complex_real_world():
|
||||||
|
|
||||||
def test_parse_complete_content_with_multiple_tool_calls():
|
def test_parse_complete_content_with_multiple_tool_calls():
|
||||||
parser = SimpleJsonToolParser()
|
parser = SimpleJsonToolParser()
|
||||||
body = (
|
body = 'I will do two things. {"name": "f1", "arguments": {"a": 1}}{"name": "f2", "arguments": {"b": 2}}'
|
||||||
"I will do two things. "
|
|
||||||
'{"name": "f1", "arguments": {"a": 1}}'
|
|
||||||
'{"name": "f2", "arguments": {"b": 2}}'
|
|
||||||
)
|
|
||||||
result = parser.parse_complete(body)
|
result = parser.parse_complete(body)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result["content"] == "I will do two things."
|
assert result["content"] == "I will do two things."
|
||||||
|
|
@ -588,30 +507,29 @@ def test_feed_then_parse_complete_same_instance():
|
||||||
assert parser.has_tool_calls
|
assert parser.has_tool_calls
|
||||||
|
|
||||||
|
|
||||||
def test_pattern_matches_basic():
|
@pytest.mark.parametrize(
|
||||||
assert _TOOL_CALL_HEAD_RE.search('{"name": "f"}')
|
"text,matches",
|
||||||
|
[
|
||||||
|
('{"name": "f"}', True),
|
||||||
def test_pattern_matches_with_whitespace():
|
('{ "name" : "f"}', True),
|
||||||
assert _TOOL_CALL_HEAD_RE.search('{ "name" : "f"}')
|
('{"other": 1}', False),
|
||||||
|
('prefix {"name": "f", "args": {}}', True),
|
||||||
|
('{"name": "f"}', True), # match at start
|
||||||
def test_pattern_no_match_without_name():
|
(' {"name": "f"}', True),
|
||||||
assert _TOOL_CALL_HEAD_RE.search('{"other": 1}') is None
|
],
|
||||||
|
)
|
||||||
|
def test_pattern_regex(text, matches):
|
||||||
def test_pattern_match_mid_text():
|
result = _TOOL_CALL_HEAD_RE.search(text)
|
||||||
assert _TOOL_CALL_HEAD_RE.search('prefix {"name": "f", "args": {}}') is not None
|
if matches:
|
||||||
|
assert result is not None
|
||||||
|
else:
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
def test_pattern_name_at_start():
|
def test_pattern_name_at_start():
|
||||||
assert _TOOL_CALL_HEAD_RE.match('{"name": "f"}')
|
assert _TOOL_CALL_HEAD_RE.match('{"name": "f"}')
|
||||||
|
|
||||||
|
|
||||||
def test_pattern_leading_whitespace():
|
|
||||||
assert _TOOL_CALL_HEAD_RE.search(' {"name": "f"}') is not None
|
|
||||||
|
|
||||||
|
|
||||||
def test_factory_register_and_create():
|
def test_factory_register_and_create():
|
||||||
parser = ToolParserFactory.create("simple_json")
|
parser = ToolParserFactory.create("simple_json")
|
||||||
assert isinstance(parser, BaseToolParser)
|
assert isinstance(parser, BaseToolParser)
|
||||||
|
|
@ -661,7 +579,6 @@ def test_feed_token_ids_do_not_affect_parsing():
|
||||||
text, current_token_ids=[1, 2, 3], delta_token_ids=[3]
|
text, current_token_ids=[1, 2, 3], delta_token_ids=[3]
|
||||||
)
|
)
|
||||||
assert len(result_no) == len(result_with)
|
assert len(result_no) == len(result_with)
|
||||||
assert len(result_no) > 0
|
|
||||||
assert (
|
assert (
|
||||||
result_no[0]["tool_calls"][0]["function"]["name"]
|
result_no[0]["tool_calls"][0]["function"]["name"]
|
||||||
== result_with[0]["tool_calls"][0]["function"]["name"]
|
== result_with[0]["tool_calls"][0]["function"]["name"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import safetensors.torch as st
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
from astrai.config.model_config import EncoderConfig
|
from astrai.config.model_config import EncoderConfig
|
||||||
|
from astrai.model.automodel import AutoModel
|
||||||
from astrai.model.encoder import EmbeddingEncoder
|
from astrai.model.encoder import EmbeddingEncoder
|
||||||
|
|
||||||
TINY_CONFIG = dict(
|
TINY_CONFIG = dict(
|
||||||
|
|
@ -14,92 +21,56 @@ TINY_CONFIG = dict(
|
||||||
norm_eps=1e-5,
|
norm_eps=1e-5,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
|
||||||
def test_encoder_forward_mean():
|
|
||||||
config = EncoderConfig(**TINY_CONFIG)
|
def _make_model(**kwargs):
|
||||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
config = EncoderConfig(**{**TINY_CONFIG, **kwargs})
|
||||||
model = EmbeddingEncoder(config).to(device=device)
|
return EmbeddingEncoder(config).to(device=_device)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("pooling_type", ["mean", "cls", "last"])
|
||||||
|
def test_encoder_forward_pooling(pooling_type):
|
||||||
|
model = _make_model(pooling_type=pooling_type)
|
||||||
model.eval()
|
model.eval()
|
||||||
|
|
||||||
batch_size, seq_len = 2, 8
|
batch_size, seq_len = 2, 8
|
||||||
input_ids = torch.randint(
|
input_ids = torch.randint(
|
||||||
0, config.vocab_size, (batch_size, seq_len), device=device
|
0, TINY_CONFIG["vocab_size"], (batch_size, seq_len), device=_device
|
||||||
)
|
)
|
||||||
|
|
||||||
with torch.no_grad():
|
with torch.no_grad():
|
||||||
output = model(input_ids)
|
output = model(input_ids)
|
||||||
|
|
||||||
assert output.shape == (batch_size, config.dim)
|
assert output.shape == (batch_size, TINY_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()
|
assert not torch.isnan(output).any()
|
||||||
|
|
||||||
|
|
||||||
def test_encoder_forward_with_padding():
|
def test_encoder_forward_with_padding():
|
||||||
config = EncoderConfig(**TINY_CONFIG)
|
model = _make_model()
|
||||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
||||||
model = EmbeddingEncoder(config).to(device=device)
|
|
||||||
model.eval()
|
model.eval()
|
||||||
|
|
||||||
batch_size, seq_len = 2, 8
|
batch_size, seq_len = 2, 8
|
||||||
input_ids = torch.randint(
|
input_ids = torch.randint(
|
||||||
0, config.vocab_size, (batch_size, seq_len), device=device
|
0, TINY_CONFIG["vocab_size"], (batch_size, seq_len), device=_device
|
||||||
)
|
)
|
||||||
input_mask = torch.ones(batch_size, seq_len, dtype=torch.bool, device=device)
|
input_mask = torch.ones(batch_size, seq_len, dtype=torch.bool, device=_device)
|
||||||
input_mask[:, 4:] = False
|
input_mask[:, 4:] = False
|
||||||
|
|
||||||
with torch.no_grad():
|
with torch.no_grad():
|
||||||
output = model(input_ids, input_mask=input_mask)
|
output = model(input_ids, input_mask=input_mask)
|
||||||
|
|
||||||
assert output.shape == (batch_size, config.dim)
|
assert output.shape == (batch_size, TINY_CONFIG["dim"])
|
||||||
assert not torch.isnan(output).any()
|
assert not torch.isnan(output).any()
|
||||||
|
|
||||||
|
|
||||||
def test_encoder_normalize():
|
def test_encoder_normalize():
|
||||||
config = EncoderConfig(
|
model = _make_model(pooling_type="mean", normalize_embeddings=True)
|
||||||
**{**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()
|
model.eval()
|
||||||
|
|
||||||
batch_size, seq_len = 2, 8
|
batch_size, seq_len = 2, 8
|
||||||
input_ids = torch.randint(
|
input_ids = torch.randint(
|
||||||
0, config.vocab_size, (batch_size, seq_len), device=device
|
0, TINY_CONFIG["vocab_size"], (batch_size, seq_len), device=_device
|
||||||
)
|
)
|
||||||
|
|
||||||
with torch.no_grad():
|
with torch.no_grad():
|
||||||
|
|
@ -110,24 +81,19 @@ def test_encoder_normalize():
|
||||||
|
|
||||||
|
|
||||||
def test_encoder_register():
|
def test_encoder_register():
|
||||||
from astrai.model.automodel import AutoModel
|
|
||||||
|
|
||||||
assert AutoModel.is_registered("embedding")
|
assert AutoModel.is_registered("embedding")
|
||||||
cls = AutoModel.get_component_class("embedding")
|
cls = AutoModel.get_component_class("embedding")
|
||||||
assert cls is EmbeddingEncoder
|
assert cls is EmbeddingEncoder
|
||||||
|
|
||||||
|
|
||||||
def test_encoder_from_transformer_checkpoint():
|
def test_encoder_from_transformer_checkpoint():
|
||||||
config = EncoderConfig(**TINY_CONFIG)
|
model = _make_model()
|
||||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
||||||
model = EmbeddingEncoder(config).to(device=device)
|
|
||||||
|
|
||||||
state_dict = model.state_dict()
|
state_dict = model.state_dict()
|
||||||
state_dict["lm_head.weight"] = torch.randn(
|
state_dict["lm_head.weight"] = torch.randn(
|
||||||
config.vocab_size, config.dim, device=device
|
TINY_CONFIG["vocab_size"], TINY_CONFIG["dim"], device=_device
|
||||||
)
|
)
|
||||||
|
|
||||||
new_model = EmbeddingEncoder(config).to(device=device)
|
new_model = _make_model()
|
||||||
new_model.load_state_dict(state_dict, strict=True)
|
new_model.load_state_dict(state_dict, strict=True)
|
||||||
|
|
||||||
for key in model.state_dict():
|
for key in model.state_dict():
|
||||||
|
|
@ -135,12 +101,6 @@ def test_encoder_from_transformer_checkpoint():
|
||||||
|
|
||||||
|
|
||||||
def test_encoder_save_load():
|
def test_encoder_save_load():
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
import safetensors.torch as st
|
|
||||||
|
|
||||||
test_dir = tempfile.mkdtemp(prefix="encoder_test_")
|
test_dir = tempfile.mkdtemp(prefix="encoder_test_")
|
||||||
config_path = os.path.join(test_dir, "config.json")
|
config_path = os.path.join(test_dir, "config.json")
|
||||||
weights_path = os.path.join(test_dir, "model.safetensors")
|
weights_path = os.path.join(test_dir, "model.safetensors")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue