Lobakkang commited on
Commit
fd448dd
·
verified ·
1 Parent(s): a73cdd9

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +98 -0
  2. added_tokens.json +6 -0
  3. config.json +59 -0
  4. configuration_taonet.py +72 -0
  5. embeddings.py +23 -0
  6. export_to_hf.py +84 -0
  7. generation_config.json +9 -0
  8. mla_components.py +213 -0
  9. model.safetensors +3 -0
  10. modeling_taonet.py +86 -0
  11. special_tokens_map.json +13 -0
  12. src/taoTrain/__init__.py +8 -0
  13. src/taoTrain/__pycache__/__init__.cpython-310.pyc +0 -0
  14. src/taoTrain/__pycache__/__init__.cpython-312.pyc +0 -0
  15. src/taoTrain/__pycache__/__init__.cpython-314.pyc +0 -0
  16. src/taoTrain/__pycache__/cli.cpython-310.pyc +0 -0
  17. src/taoTrain/__pycache__/cli.cpython-312.pyc +0 -0
  18. src/taoTrain/__pycache__/cli.cpython-314.pyc +0 -0
  19. src/taoTrain/__pycache__/config.cpython-310.pyc +0 -0
  20. src/taoTrain/__pycache__/config.cpython-312.pyc +0 -0
  21. src/taoTrain/__pycache__/config.cpython-314.pyc +0 -0
  22. src/taoTrain/benchmarks/__init__.py +5 -0
  23. src/taoTrain/benchmarks/__pycache__/__init__.cpython-310.pyc +0 -0
  24. src/taoTrain/benchmarks/__pycache__/__init__.cpython-312.pyc +0 -0
  25. src/taoTrain/benchmarks/__pycache__/__init__.cpython-314.pyc +0 -0
  26. src/taoTrain/benchmarks/__pycache__/runner.cpython-310.pyc +0 -0
  27. src/taoTrain/benchmarks/__pycache__/runner.cpython-312.pyc +0 -0
  28. src/taoTrain/benchmarks/__pycache__/runner.cpython-314.pyc +0 -0
  29. src/taoTrain/benchmarks/runner.py +221 -0
  30. src/taoTrain/checkpointing/__init__.py +5 -0
  31. src/taoTrain/checkpointing/__pycache__/__init__.cpython-310.pyc +0 -0
  32. src/taoTrain/checkpointing/__pycache__/__init__.cpython-312.pyc +0 -0
  33. src/taoTrain/checkpointing/__pycache__/__init__.cpython-314.pyc +0 -0
  34. src/taoTrain/checkpointing/__pycache__/checkpoint.cpython-310.pyc +0 -0
  35. src/taoTrain/checkpointing/__pycache__/checkpoint.cpython-312.pyc +0 -0
  36. src/taoTrain/checkpointing/__pycache__/checkpoint.cpython-314.pyc +0 -0
  37. src/taoTrain/checkpointing/checkpoint.py +194 -0
  38. src/taoTrain/cli.py +470 -0
  39. src/taoTrain/config.py +871 -0
  40. src/taoTrain/core/__init__.py +5 -0
  41. src/taoTrain/core/__pycache__/__init__.cpython-310.pyc +0 -0
  42. src/taoTrain/core/__pycache__/__init__.cpython-312.pyc +0 -0
  43. src/taoTrain/core/__pycache__/__init__.cpython-314.pyc +0 -0
  44. src/taoTrain/core/__pycache__/base.cpython-310.pyc +0 -0
  45. src/taoTrain/core/__pycache__/base.cpython-312.pyc +0 -0
  46. src/taoTrain/core/__pycache__/base.cpython-314.pyc +0 -0
  47. src/taoTrain/core/base.py +279 -0
  48. src/taoTrain/data/__init__.py +47 -0
  49. src/taoTrain/data/__pycache__/__init__.cpython-310.pyc +0 -0
  50. src/taoTrain/data/__pycache__/__init__.cpython-312.pyc +0 -0
README.md CHANGED
@@ -1,3 +1,101 @@
1
  ---
 
 
 
 
 
 
 
2
  license: mit
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ library_name: transformers
3
+ tags:
4
+ - text-generation
5
+ - trust-remote-code
6
+ - sentencepiece
7
+ - custom-architecture
8
+ pipeline_tag: text-generation
9
  license: mit
10
  ---
11
+
12
+ # TaoNet-mini-A2
13
+
14
+ `TaoNet-mini-A2` is packaged as a Hugging Face `trust_remote_code` repository for causal text generation.
15
+
16
+ ## Files
17
+
18
+ The repo root contains:
19
+
20
+ - `configuration_taonet.py`
21
+ - `modeling_taonet.py`
22
+ - `tokenization_taonet.py`
23
+ - `taonet_model.py`
24
+ - `mla_components.py`
25
+ - `embeddings.py`
26
+
27
+ The checkpoint export helper is:
28
+
29
+ - `export_to_hf.py`
30
+
31
+ Run that script once on your machine to convert `checkpoints/sft/final_model.pt` into a standard Hugging Face weight file such as `pytorch_model.bin`.
32
+
33
+ ## Usage
34
+
35
+ ```python
36
+ import time
37
+ import torch
38
+ from transformers import AutoModelForCausalLM, AutoTokenizer
39
+
40
+ MODEL_NAME = "TaoTern/TaoNet-mini-A2"
41
+
42
+ device = "cuda" if torch.cuda.is_available() else "cpu"
43
+ dtype = torch.bfloat16 if device == "cuda" else torch.float32
44
+
45
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
46
+ model = AutoModelForCausalLM.from_pretrained(
47
+ MODEL_NAME,
48
+ trust_remote_code=True,
49
+ torch_dtype=dtype,
50
+ ).to(device)
51
+
52
+
53
+ def generate_text(prompt, max_new_tokens=64, temperature=0.7, top_p=0.85):
54
+ inputs = tokenizer(prompt, return_tensors="pt")
55
+ inputs = {key: value.to(device) for key, value in inputs.items()}
56
+
57
+ start_time = time.time()
58
+ with torch.inference_mode():
59
+ outputs = model.generate(
60
+ **inputs,
61
+ max_new_tokens=max_new_tokens,
62
+ temperature=temperature,
63
+ top_p=top_p,
64
+ repetition_penalty=1.2,
65
+ do_sample=True,
66
+ use_cache=False,
67
+ pad_token_id=tokenizer.pad_token_id,
68
+ eos_token_id=tokenizer.eos_token_id,
69
+ )
70
+ elapsed_time = time.time() - start_time
71
+
72
+ new_tokens = outputs.shape[1] - inputs["input_ids"].shape[1]
73
+ tokens_per_second = new_tokens / elapsed_time if elapsed_time > 0 else 0.0
74
+ completion = tokenizer.decode(
75
+ outputs[0][inputs["input_ids"].shape[1] :],
76
+ skip_special_tokens=True,
77
+ )
78
+ return completion, tokens_per_second
79
+
80
+
81
+ if __name__ == "__main__":
82
+ text, tps = generate_text("Fruit is now expensive so we should")
83
+ print(text)
84
+ print(f"\\nTokens per second: {tps:.2f}")
85
+ ```
86
+
87
+ ## Export
88
+
89
+ When you are ready to package the trained checkpoint into standard HF weights, run:
90
+
91
+ ```bash
92
+ python export_to_hf.py
93
+ ```
94
+
95
+ That script will:
96
+
97
+ - load `checkpoints/sft/final_model.pt`
98
+ - rebuild the TaoNet config from the checkpoint metadata
99
+ - load the weights into the HF wrapper
100
+ - write standard Hugging Face model files at repo root
101
+ - copy `tokenizer.model`, `tokenizer.vocab`, and `tokenizer.special_tokens.json` to repo root
added_tokens.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "<BOS>": 8192,
3
+ "<EOS>": 8193,
4
+ "<PAD>": 8195,
5
+ "<UNK>": 8194
6
+ }
config.json ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "TaoNetForCausalLM"
4
+ ],
5
+ "bos_token_id": 1,
6
+ "cnn_channels": [
7
+ 32,
8
+ 64,
9
+ 128
10
+ ],
11
+ "cnn_kernel_size": 3,
12
+ "d_embed_rank": 96,
13
+ "d_latent_kv": 768,
14
+ "d_rope": 128,
15
+ "dropout": 0.02,
16
+ "dtype": "float32",
17
+ "eos_token_id": 2,
18
+ "gamma_activation": "gelu",
19
+ "gamma_discretization": "bilinear",
20
+ "gamma_dt_init": 0.01,
21
+ "gamma_dt_max": 0.1,
22
+ "gamma_dt_min": 0.001,
23
+ "gamma_gate": true,
24
+ "gamma_gate_bias": 2.0,
25
+ "gamma_hidden_dim": 1536,
26
+ "gamma_input_gate": true,
27
+ "gamma_input_gate_bias": 2.0,
28
+ "gamma_kernel_mode": "auto",
29
+ "gamma_kernel_threshold": 64,
30
+ "gamma_layer_scale_init": 0.1,
31
+ "gamma_prenorm": true,
32
+ "gamma_residual_scale": 1.0,
33
+ "gamma_use_D": true,
34
+ "gamma_use_output_linear": true,
35
+ "gqa_groups": 1,
36
+ "head_dim": 128,
37
+ "hidden_dim": 1024,
38
+ "hidden_dim_ff": 3072,
39
+ "image_size": 224,
40
+ "image_token": "<image>",
41
+ "init_std": 0.02,
42
+ "intermediate_dim": 4096,
43
+ "max_seq_length": 1024,
44
+ "model_type": "taonet",
45
+ "num_heads": 8,
46
+ "num_layers": 16,
47
+ "pad_token_id": 3,
48
+ "rope_scale": 40.0,
49
+ "transformers_version": "5.1.0",
50
+ "unk_token_id": 0,
51
+ "use_factorized_embedding": true,
52
+ "vision_encoder_type": "cnn",
53
+ "vision_output_dim": 256,
54
+ "vision_prefix_tokens": 10,
55
+ "vocab_size": 8192,
56
+ "yarn_alpha": 1.0,
57
+ "yarn_enabled": false,
58
+ "yarn_original_max_seq_length": null
59
+ }
configuration_taonet.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face configuration for TaoNet."""
2
+
3
+ from transformers import PretrainedConfig
4
+
5
+
6
+ class TaoNetConfig(PretrainedConfig):
7
+ """Configuration for the TaoNet causal language model."""
8
+
9
+ model_type = "taonet"
10
+
11
+ def __init__(
12
+ self,
13
+ vocab_size=8192,
14
+ hidden_dim=768,
15
+ num_layers=12,
16
+ num_heads=12,
17
+ head_dim=None,
18
+ intermediate_dim=None,
19
+ dropout=0.1,
20
+ max_seq_length=2048,
21
+ d_latent_kv=None,
22
+ d_rope=None,
23
+ gqa_groups=1,
24
+ hidden_dim_ff=None,
25
+ use_factorized_embedding=False,
26
+ d_embed_rank=96,
27
+ rope_scale=40.0,
28
+ yarn_enabled=False,
29
+ yarn_original_max_seq_length=None,
30
+ yarn_alpha=1.0,
31
+ init_std=0.02,
32
+ pad_token_id=3,
33
+ bos_token_id=1,
34
+ eos_token_id=2,
35
+ unk_token_id=0,
36
+ **kwargs,
37
+ ):
38
+ super().__init__(
39
+ pad_token_id=pad_token_id,
40
+ bos_token_id=bos_token_id,
41
+ eos_token_id=eos_token_id,
42
+ unk_token_id=unk_token_id,
43
+ **kwargs,
44
+ )
45
+ self.vocab_size = vocab_size
46
+ self.hidden_dim = hidden_dim
47
+ self.num_layers = num_layers
48
+ self.num_heads = num_heads
49
+ self.head_dim = head_dim if head_dim is not None else hidden_dim // num_heads
50
+ self.intermediate_dim = intermediate_dim if intermediate_dim is not None else hidden_dim * 4
51
+ self.dropout = dropout
52
+ self.max_seq_length = max_seq_length
53
+ self.d_latent_kv = d_latent_kv
54
+ self.d_rope = d_rope
55
+ self.gqa_groups = gqa_groups
56
+ self.hidden_dim_ff = hidden_dim_ff
57
+ self.use_factorized_embedding = use_factorized_embedding
58
+ self.d_embed_rank = d_embed_rank
59
+ self.rope_scale = rope_scale
60
+ self.yarn_enabled = yarn_enabled
61
+ self.yarn_original_max_seq_length = yarn_original_max_seq_length
62
+ self.yarn_alpha = yarn_alpha
63
+ self.init_std = init_std
64
+
65
+ @classmethod
66
+ def from_taotrain_model_config(cls, model_config, **extra_kwargs):
67
+ """Build an HF config from a TaoTrain model-config dictionary."""
68
+ data = dict(model_config)
69
+ data.pop("architecture_type", None)
70
+ data.pop("llm_architecture_type", None)
71
+ data.update(extra_kwargs)
72
+ return cls(**data)
embeddings.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Embedding layers used by TaoNet."""
2
+
3
+ import torch.nn as nn
4
+
5
+
6
+ class FactorizedEmbedding(nn.Module):
7
+ """Low-rank factorized embedding."""
8
+
9
+ def __init__(self, vocab_size, d_model, d_embed_rank=96):
10
+ super().__init__()
11
+ self.vocab_size = vocab_size
12
+ self.d_model = d_model
13
+ self.d_embed_rank = d_embed_rank
14
+
15
+ self.embed = nn.Embedding(vocab_size, d_embed_rank)
16
+ self.proj = nn.Linear(d_embed_rank, d_model, bias=False)
17
+
18
+ nn.init.normal_(self.embed.weight, mean=0.0, std=0.02)
19
+ nn.init.normal_(self.proj.weight, mean=0.0, std=0.02)
20
+
21
+ def forward(self, input_ids):
22
+ x = self.embed(input_ids)
23
+ return self.proj(x)
export_to_hf.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Convert TaoTrain checkpoint + tokenizer assets into a standard HF package."""
2
+
3
+ import json
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+
8
+ def normalize_checkpoint(checkpoint):
9
+ if isinstance(checkpoint, dict):
10
+ if "model_state" in checkpoint:
11
+ return checkpoint["model_state"], checkpoint.get("config", {})
12
+ if "model_state_dict" in checkpoint:
13
+ return checkpoint["model_state_dict"], checkpoint.get("config", {})
14
+ return checkpoint, {}
15
+
16
+
17
+ def infer_special_token_paths(repo_dir):
18
+ subdir_metadata = repo_dir / "tokenizer" / "tokenizer.special_tokens.json"
19
+ root_metadata = repo_dir / "tokenizer.special_tokens.json"
20
+ subdir_model = repo_dir / "tokenizer" / "tokenizer.model"
21
+ subdir_vocab = repo_dir / "tokenizer" / "tokenizer.vocab"
22
+ if subdir_metadata.exists() and subdir_model.exists() and subdir_vocab.exists():
23
+ return subdir_model, subdir_metadata, subdir_vocab
24
+ if root_metadata.exists():
25
+ return repo_dir / "tokenizer.model", root_metadata, repo_dir / "tokenizer.vocab"
26
+ return repo_dir / "tokenizer" / "tokenizer.model", subdir_metadata, repo_dir / "tokenizer" / "tokenizer.vocab"
27
+
28
+
29
+ def main():
30
+ import torch
31
+
32
+ from configuration_taonet import TaoNetConfig
33
+ from modeling_taonet import TaoNetForCausalLM
34
+ from tokenization_taonet import TaoNetTokenizer
35
+
36
+ repo_dir = Path(__file__).resolve().parent
37
+ checkpoint_path = repo_dir / "checkpoints" / "sft" / "final_model.pt"
38
+
39
+ checkpoint = torch.load(checkpoint_path, map_location="cpu")
40
+ model_state, train_config = normalize_checkpoint(checkpoint)
41
+ model_config = dict(train_config.get("model", {}))
42
+
43
+ metadata_model_path, metadata_path, vocab_path = infer_special_token_paths(repo_dir)
44
+ with open(metadata_path, "r", encoding="utf-8") as handle:
45
+ metadata = json.load(handle)
46
+
47
+ special_tokens = metadata.get("special_tokens", {})
48
+ hf_config = TaoNetConfig.from_taotrain_model_config(
49
+ model_config,
50
+ vocab_size=model_config.get("vocab_size", sum(1 for _ in open(vocab_path, "r", encoding="utf-8"))),
51
+ pad_token_id=special_tokens.get("<PAD>", 3),
52
+ bos_token_id=special_tokens.get("<BOS>", 1),
53
+ eos_token_id=special_tokens.get("<EOS>", 2),
54
+ unk_token_id=special_tokens.get("<UNK>", 0),
55
+ )
56
+
57
+ model = TaoNetForCausalLM(hf_config)
58
+ missing, unexpected = model.model.load_state_dict(model_state, strict=False)
59
+ model.tie_weights()
60
+ model.save_pretrained(repo_dir, safe_serialization=False)
61
+
62
+ tokenizer = TaoNetTokenizer(
63
+ vocab_file=str(metadata_model_path),
64
+ special_tokens_file=str(metadata_path),
65
+ )
66
+ tokenizer.save_pretrained(repo_dir)
67
+
68
+ shutil.copyfile(metadata_model_path, repo_dir / "tokenizer.model")
69
+ shutil.copyfile(metadata_path, repo_dir / "tokenizer.special_tokens.json")
70
+ shutil.copyfile(vocab_path, repo_dir / "tokenizer.vocab")
71
+
72
+ if missing:
73
+ print("Missing keys while loading model state:")
74
+ for key in missing:
75
+ print(f" - {key}")
76
+ if unexpected:
77
+ print("Unexpected keys while loading model state:")
78
+ for key in unexpected:
79
+ print(f" - {key}")
80
+ print(f"Saved Hugging Face package to: {repo_dir}")
81
+
82
+
83
+ if __name__ == "__main__":
84
+ main()
generation_config.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "bos_token_id": 1,
4
+ "eos_token_id": 2,
5
+ "output_attentions": false,
6
+ "output_hidden_states": false,
7
+ "pad_token_id": 3,
8
+ "transformers_version": "5.1.0"
9
+ }
mla_components.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MLA attention blocks used by TaoNet."""
2
+
3
+ import math
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+
9
+
10
+ class RotaryEmbedding(nn.Module):
11
+ """Rotary position embedding with optional YaRN scaling."""
12
+
13
+ def __init__(
14
+ self,
15
+ dim,
16
+ rope_scale=40.0,
17
+ max_seq_length=1024,
18
+ yarn_enabled=False,
19
+ yarn_original_max_seq_length=None,
20
+ yarn_alpha=1.0,
21
+ ):
22
+ super().__init__()
23
+ if dim % 2 != 0:
24
+ raise ValueError("RotaryEmbedding requires an even dimension")
25
+ self.dim = dim
26
+ self.rope_scale = rope_scale
27
+ self.max_seq_length = max_seq_length
28
+ self.yarn_enabled = yarn_enabled
29
+ self.yarn_original_max_seq_length = (
30
+ yarn_original_max_seq_length if yarn_original_max_seq_length is not None else max_seq_length
31
+ )
32
+ self.yarn_alpha = yarn_alpha
33
+
34
+ inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2).float() / dim))
35
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
36
+
37
+ def _apply_yarn_scaling(self, freqs, seq_len):
38
+ if not self.yarn_enabled:
39
+ return freqs
40
+ original_max_seq_length = self.yarn_original_max_seq_length
41
+ if seq_len <= original_max_seq_length:
42
+ return freqs
43
+ target_scale = self.max_seq_length / original_max_seq_length
44
+ current_ratio = seq_len / original_max_seq_length
45
+ progress = min(current_ratio / target_scale, 1.0)
46
+ scale_factor = 1.0 + (target_scale - 1.0) * (progress ** (1.0 / self.yarn_alpha))
47
+ return freqs / scale_factor
48
+
49
+ def forward(self, seq_len, device):
50
+ t = torch.arange(seq_len, device=device).type_as(self.inv_freq) / self.rope_scale
51
+ freqs = torch.einsum("i,j->ij", t, self.inv_freq)
52
+ freqs = self._apply_yarn_scaling(freqs, seq_len)
53
+ return torch.cat((freqs, freqs), dim=-1)
54
+
55
+
56
+ def rotate_half(x):
57
+ x1, x2 = x.chunk(2, dim=-1)
58
+ return torch.cat((-x2, x1), dim=-1)
59
+
60
+
61
+ def apply_rotary(x, cos, sin):
62
+ if cos.dim() == 2:
63
+ cos = cos.unsqueeze(0).unsqueeze(0)
64
+ sin = sin.unsqueeze(0).unsqueeze(0)
65
+ cos = cos[..., : x.shape[-1]]
66
+ sin = sin[..., : x.shape[-1]]
67
+ x_rot = x[..., : cos.shape[-1]]
68
+ x_base = x[..., cos.shape[-1] :]
69
+ x_rot = (x_rot * cos) + (rotate_half(x_rot) * sin)
70
+ if x_base.shape[-1] > 0:
71
+ return torch.cat([x_rot, x_base], dim=-1)
72
+ return x_rot
73
+
74
+
75
+ class DeepSeekMLA(nn.Module):
76
+ """DeepSeek-style multi-head latent attention."""
77
+
78
+ def __init__(
79
+ self,
80
+ d_model,
81
+ d_latent_kv,
82
+ n_heads,
83
+ d_rope,
84
+ dropout=0.1,
85
+ gqa_groups=1,
86
+ rope_scale=40.0,
87
+ max_seq_length=1024,
88
+ yarn_enabled=False,
89
+ yarn_original_max_seq_length=None,
90
+ yarn_alpha=1.0,
91
+ ):
92
+ super().__init__()
93
+ self.d_model = d_model
94
+ self.d_latent_kv = d_latent_kv
95
+ self.n_heads = n_heads
96
+ self.d_rope = d_rope
97
+ self.gqa_groups = gqa_groups
98
+
99
+ if d_model % n_heads != 0:
100
+ raise ValueError("d_model must be divisible by n_heads")
101
+ if d_latent_kv % n_heads != 0:
102
+ raise ValueError("d_latent_kv must be divisible by n_heads")
103
+
104
+ self.d_head_full = d_model // n_heads
105
+ self.d_head_latent = d_latent_kv // n_heads
106
+
107
+ self.norm = nn.LayerNorm(d_model)
108
+ self.q_proj = nn.Linear(d_model, d_model, bias=False)
109
+ self.k_proj = nn.Linear(d_model, d_latent_kv, bias=False)
110
+ self.v_proj = nn.Linear(d_model, d_latent_kv, bias=False)
111
+ self.rotary = RotaryEmbedding(
112
+ d_rope,
113
+ rope_scale=rope_scale,
114
+ max_seq_length=max_seq_length,
115
+ yarn_enabled=yarn_enabled,
116
+ yarn_original_max_seq_length=yarn_original_max_seq_length,
117
+ yarn_alpha=yarn_alpha,
118
+ )
119
+ self.out_proj = nn.Linear(d_latent_kv, d_model, bias=False)
120
+ self.head_weights = nn.Parameter(torch.ones(n_heads))
121
+ self.attn_dropout = nn.Dropout(dropout)
122
+ self.proj_dropout = nn.Dropout(dropout)
123
+
124
+ def forward(self, x, attention_mask=None):
125
+ batch_size, seq_len, _ = x.shape
126
+ x_norm = self.norm(x)
127
+
128
+ q = self.q_proj(x_norm)
129
+ k = self.k_proj(x_norm)
130
+ v = self.v_proj(x_norm)
131
+
132
+ q = q.view(batch_size, seq_len, self.n_heads, self.d_head_full).transpose(1, 2)
133
+ k = k.view(batch_size, seq_len, self.n_heads, self.d_head_latent).transpose(1, 2)
134
+ v = v.view(batch_size, seq_len, self.n_heads, self.d_head_latent).transpose(1, 2)
135
+
136
+ if self.d_rope > 0:
137
+ rotary_emb = self.rotary(seq_len, x.device)
138
+ cos = torch.cos(rotary_emb).unsqueeze(0).unsqueeze(0)
139
+ sin = torch.sin(rotary_emb).unsqueeze(0).unsqueeze(0)
140
+ q_rope = apply_rotary(q[..., : self.d_rope], cos, sin)
141
+ q = torch.cat([q_rope, q[..., self.d_rope :]], dim=-1)
142
+ k_rope = apply_rotary(k[..., : self.d_rope], cos, sin)
143
+ k = torch.cat([k_rope, k[..., self.d_rope :]], dim=-1)
144
+
145
+ q_for_attn = q[..., : self.d_head_latent]
146
+ attn_mask_bool = None
147
+ if attention_mask is not None:
148
+ if attention_mask.dim() == 2:
149
+ attn_mask_bool = attention_mask.bool().unsqueeze(1).unsqueeze(1)
150
+ else:
151
+ attn_mask_bool = attention_mask.bool()
152
+
153
+ dropout_p = self.attn_dropout.p if self.training else 0.0
154
+ out_heads = F.scaled_dot_product_attention(
155
+ q_for_attn,
156
+ k,
157
+ v,
158
+ attn_mask=attn_mask_bool,
159
+ dropout_p=dropout_p,
160
+ scale=None,
161
+ )
162
+ out_concat = out_heads.transpose(1, 2).reshape(batch_size, seq_len, self.d_latent_kv)
163
+ out = self.out_proj(out_concat)
164
+ return self.proj_dropout(out)
165
+
166
+
167
+ class AttentionBlock(nn.Module):
168
+ """Attention block with SwiGLU feed-forward network."""
169
+
170
+ def __init__(
171
+ self,
172
+ d_model,
173
+ d_latent_kv,
174
+ n_heads,
175
+ d_rope,
176
+ d_ff,
177
+ dropout=0.1,
178
+ gqa_groups=1,
179
+ rope_scale=40.0,
180
+ max_seq_length=1024,
181
+ yarn_enabled=False,
182
+ yarn_original_max_seq_length=None,
183
+ yarn_alpha=1.0,
184
+ ):
185
+ super().__init__()
186
+ self.mla = DeepSeekMLA(
187
+ d_model,
188
+ d_latent_kv,
189
+ n_heads,
190
+ d_rope,
191
+ dropout,
192
+ gqa_groups,
193
+ rope_scale=rope_scale,
194
+ max_seq_length=max_seq_length,
195
+ yarn_enabled=yarn_enabled,
196
+ yarn_original_max_seq_length=yarn_original_max_seq_length,
197
+ yarn_alpha=yarn_alpha,
198
+ )
199
+ self.ff_norm = nn.LayerNorm(d_model)
200
+ self.ff_gate = nn.Linear(d_model, d_ff, bias=False)
201
+ self.ff_value = nn.Linear(d_model, d_ff, bias=False)
202
+ self.ff_out = nn.Linear(d_ff, d_model, bias=False)
203
+ self.dropout = nn.Dropout(dropout)
204
+
205
+ def forward(self, x, attention_mask=None):
206
+ attn_out = self.mla(x, attention_mask)
207
+ x = x + self.dropout(attn_out)
208
+ ff_norm = self.ff_norm(x)
209
+ ff_gate = self.ff_gate(ff_norm)
210
+ ff_value = self.ff_value(ff_norm)
211
+ ff_out = ff_value * F.silu(ff_gate)
212
+ ff_out = self.ff_out(ff_out)
213
+ return x + self.dropout(ff_out)
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9dcf558286bb14902875884b2a18858a2cab7d0341ced85297814b822a369c77
3
+ size 859468256
modeling_taonet.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face model wrapper for TaoNet."""
2
+
3
+ from torch import nn
4
+ from transformers import GenerationMixin, PreTrainedModel
5
+ from transformers.modeling_outputs import CausalLMOutput
6
+
7
+ from configuration_taonet import TaoNetConfig
8
+ from taonet_model import SimpleLLM, build_runtime_config
9
+
10
+
11
+ class TaoNetForCausalLM(PreTrainedModel, GenerationMixin):
12
+ """Transformers-compatible TaoNet causal LM."""
13
+
14
+ config_class = TaoNetConfig
15
+ base_model_prefix = "model"
16
+ supports_gradient_checkpointing = False
17
+
18
+ def __init__(self, config):
19
+ super().__init__(config)
20
+ runtime_config = build_runtime_config(config)
21
+ self.model = SimpleLLM(runtime_config)
22
+ self.post_init()
23
+ self.tie_weights()
24
+
25
+ def get_input_embeddings(self):
26
+ if getattr(self.model, "use_factorized_embedding", False):
27
+ return self.model.token_embedding.embed
28
+ return self.model.token_embedding
29
+
30
+ def set_input_embeddings(self, value):
31
+ if getattr(self.model, "use_factorized_embedding", False):
32
+ self.model.token_embedding.embed = value
33
+ else:
34
+ self.model.token_embedding = value
35
+
36
+ def get_output_embeddings(self):
37
+ return self.model.output_head
38
+
39
+ def set_output_embeddings(self, new_embeddings):
40
+ self.model.output_head = new_embeddings
41
+
42
+ def tie_weights(self, *args, **kwargs):
43
+ del args, kwargs
44
+ if not getattr(self.model, "use_factorized_embedding", False):
45
+ self.model.output_head.weight = self.get_input_embeddings().weight
46
+
47
+ def _init_weights(self, module):
48
+ if isinstance(module, nn.Linear):
49
+ nn.init.normal_(module.weight, mean=0.0, std=self.config.init_std)
50
+ if module.bias is not None:
51
+ nn.init.zeros_(module.bias)
52
+ elif isinstance(module, nn.Embedding):
53
+ nn.init.normal_(module.weight, mean=0.0, std=self.config.init_std)
54
+
55
+ def forward(
56
+ self,
57
+ input_ids=None,
58
+ attention_mask=None,
59
+ labels=None,
60
+ inputs_embeds=None,
61
+ return_dict=None,
62
+ **kwargs,
63
+ ):
64
+ del kwargs
65
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
66
+ outputs = self.model(
67
+ input_ids=input_ids,
68
+ attention_mask=attention_mask,
69
+ labels=None,
70
+ inputs_embeds=inputs_embeds,
71
+ )
72
+ loss = None
73
+ if labels is not None:
74
+ shift_logits = outputs["logits"][..., :-1, :].contiguous()
75
+ shift_labels = labels[..., 1:].contiguous()
76
+ loss_fct = nn.CrossEntropyLoss(ignore_index=-100)
77
+ loss = loss_fct(
78
+ shift_logits.view(-1, shift_logits.size(-1)),
79
+ shift_labels.view(-1),
80
+ )
81
+ if not return_dict:
82
+ return (loss, outputs["logits"])
83
+ return CausalLMOutput(loss=loss, logits=outputs["logits"])
84
+
85
+ def prepare_inputs_for_generation(self, input_ids, attention_mask=None, **kwargs):
86
+ return {"input_ids": input_ids, "attention_mask": attention_mask}
special_tokens_map.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "unk_token": "<UNK>",
3
+ "bos_token": "<BOS>",
4
+ "eos_token": "<EOS>",
5
+ "pad_token": "<PAD>",
6
+ "additional_special_tokens": [
7
+ "\n",
8
+ "<think>",
9
+ "<user>",
10
+ "<assistant>",
11
+ "<image>"
12
+ ]
13
+ }
src/taoTrain/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """
2
+ TaoTrain: A clean, modular PyTorch LLM training framework.
3
+
4
+ For quick usage, see: https://github.com/your-org/taoTrain
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+ __author__ = "Felix"
src/taoTrain/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (329 Bytes). View file
 
src/taoTrain/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (362 Bytes). View file
 
src/taoTrain/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (360 Bytes). View file
 
src/taoTrain/__pycache__/cli.cpython-310.pyc ADDED
Binary file (12.5 kB). View file
 
src/taoTrain/__pycache__/cli.cpython-312.pyc ADDED
Binary file (22.7 kB). View file
 
src/taoTrain/__pycache__/cli.cpython-314.pyc ADDED
Binary file (25 kB). View file
 
src/taoTrain/__pycache__/config.cpython-310.pyc ADDED
Binary file (28.6 kB). View file
 
src/taoTrain/__pycache__/config.cpython-312.pyc ADDED
Binary file (37.8 kB). View file
 
src/taoTrain/__pycache__/config.cpython-314.pyc ADDED
Binary file (43.7 kB). View file
 
src/taoTrain/benchmarks/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Benchmarking suite."""
2
+
3
+ from .runner import BenchmarkRunner
4
+
5
+ __all__ = ["BenchmarkRunner"]
src/taoTrain/benchmarks/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (261 Bytes). View file
 
src/taoTrain/benchmarks/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (246 Bytes). View file
 
src/taoTrain/benchmarks/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (243 Bytes). View file
 
src/taoTrain/benchmarks/__pycache__/runner.cpython-310.pyc ADDED
Binary file (5.81 kB). View file
 
src/taoTrain/benchmarks/__pycache__/runner.cpython-312.pyc ADDED
Binary file (9.39 kB). View file
 
src/taoTrain/benchmarks/__pycache__/runner.cpython-314.pyc ADDED
Binary file (10.5 kB). View file
 
src/taoTrain/benchmarks/runner.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmarking suite for evaluating trained models."""
2
+
3
+ import time
4
+ from pathlib import Path
5
+ from typing import Optional, Dict
6
+ import torch
7
+ from torch.utils.data import DataLoader
8
+
9
+ from taoTrain.core import BaseModel
10
+ from taoTrain.config import TrainingConfig
11
+ from taoTrain.data.loaders import get_dataloader
12
+ from taoTrain.inference import Inferencer
13
+
14
+
15
+ class BenchmarkRunner:
16
+ """Run benchmarks on a trained model."""
17
+
18
+ def __init__(
19
+ self,
20
+ model: BaseModel,
21
+ device: torch.device,
22
+ dtype: torch.dtype = torch.float32,
23
+ ):
24
+ """
25
+ Initialize benchmark runner.
26
+
27
+ Args:
28
+ model: Trained model
29
+ device: Device for inference
30
+ dtype: Data type
31
+ """
32
+ self.model = model.to(device)
33
+ self.model.eval()
34
+ self.device = device
35
+ self.dtype = dtype
36
+
37
+ @staticmethod
38
+ def load_from_checkpoint(
39
+ checkpoint_path: str | Path,
40
+ device: Optional[torch.device] = None,
41
+ ) -> "BenchmarkRunner":
42
+ """Load model from checkpoint."""
43
+ if device is None:
44
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
45
+
46
+ checkpoint = torch.load(checkpoint_path, map_location=device)
47
+
48
+ # Reconstruct model config
49
+ from taoTrain.config import ModelConfig
50
+ from taoTrain.models import get_model
51
+
52
+ model_config = ModelConfig(**checkpoint.get("config", {}).get("model", {}))
53
+ model = get_model(model_config, device=device)
54
+ model.load_state_dict(checkpoint["model_state_dict"])
55
+
56
+ return BenchmarkRunner(model, device)
57
+
58
+ def benchmark_perplexity(
59
+ self,
60
+ dataset: "DataLoader",
61
+ num_batches: Optional[int] = None,
62
+ ) -> float:
63
+ """
64
+ Compute perplexity on a dataset.
65
+
66
+ Args:
67
+ dataset: DataLoader for evaluation
68
+ num_batches: Limit evaluation to N batches
69
+
70
+ Returns:
71
+ Perplexity (exp of average loss)
72
+ """
73
+ total_loss = 0.0
74
+ total_tokens = 0
75
+
76
+ with torch.no_grad():
77
+ for batch_idx, batch in enumerate(dataset):
78
+ if num_batches and batch_idx >= num_batches:
79
+ break
80
+
81
+ # Move to device
82
+ input_ids = batch["input_ids"].to(self.device)
83
+ attention_mask = batch.get("attention_mask")
84
+ if attention_mask is not None:
85
+ attention_mask = attention_mask.to(self.device)
86
+ labels = batch.get("labels")
87
+ if labels is not None:
88
+ labels = labels.to(self.device)
89
+
90
+ # Forward pass
91
+ with torch.autocast(
92
+ device_type="cuda" if self.device.type == "cuda" else "cpu",
93
+ dtype=torch.bfloat16 if self.dtype == torch.bfloat16 else torch.float32,
94
+ ):
95
+ outputs = self.model(
96
+ input_ids=input_ids,
97
+ attention_mask=attention_mask,
98
+ labels=labels,
99
+ )
100
+ loss = outputs.get("loss")
101
+
102
+ if loss is not None:
103
+ total_loss += loss.item() * input_ids.shape[0]
104
+ total_tokens += input_ids.shape[0]
105
+
106
+ avg_loss = total_loss / total_tokens if total_tokens > 0 else float('inf')
107
+ perplexity = torch.exp(torch.tensor(avg_loss)).item()
108
+
109
+ return perplexity
110
+
111
+ def benchmark_throughput(
112
+ self,
113
+ batch_size: int = 32,
114
+ seq_length: int = 1024,
115
+ num_iters: int = 10,
116
+ ) -> Dict[str, float]:
117
+ """
118
+ Benchmark forward pass throughput.
119
+
120
+ Args:
121
+ batch_size: Batch size
122
+ seq_length: Sequence length
123
+ num_iters: Number of iterations
124
+
125
+ Returns:
126
+ Dict with throughput metrics
127
+ """
128
+ # Create dummy batch
129
+ dummy_input = torch.randint(
130
+ 0, self.model.config.vocab_size,
131
+ (batch_size, seq_length)
132
+ ).to(self.device)
133
+
134
+ # Warmup
135
+ with torch.no_grad():
136
+ for _ in range(2):
137
+ _ = self.model(dummy_input)
138
+
139
+ torch.cuda.synchronize() if torch.cuda.is_available() else None
140
+
141
+ # Benchmark forward pass
142
+ start = time.time()
143
+
144
+ with torch.no_grad():
145
+ for _ in range(num_iters):
146
+ _ = self.model(dummy_input)
147
+
148
+ torch.cuda.synchronize() if torch.cuda.is_available() else None
149
+
150
+ elapsed = time.time() - start
151
+
152
+ total_tokens = batch_size * seq_length * num_iters
153
+ tokens_per_sec = total_tokens / elapsed
154
+
155
+ return {
156
+ "throughput_tokens_per_sec": tokens_per_sec,
157
+ "throughput_samples_per_sec": (batch_size * num_iters) / elapsed,
158
+ "avg_time_per_iter_ms": (elapsed / num_iters) * 1000,
159
+ }
160
+
161
+ def benchmark_memory(self) -> Dict[str, float]:
162
+ """
163
+ Benchmark peak GPU memory usage.
164
+
165
+ Returns:
166
+ Dict with memory stats
167
+ """
168
+ if not torch.cuda.is_available():
169
+ return {"peak_memory_gb": 0.0}
170
+
171
+ torch.cuda.reset_peak_memory_stats()
172
+ torch.cuda.synchronize()
173
+
174
+ # Create dummy batch
175
+ dummy_input = torch.randint(
176
+ 0, self.model.config.vocab_size,
177
+ (16, 1024)
178
+ ).to(self.device)
179
+
180
+ with torch.no_grad():
181
+ _ = self.model(dummy_input)
182
+
183
+ torch.cuda.synchronize()
184
+
185
+ peak_memory = torch.cuda.max_memory_allocated() / (1024 ** 3) # GB
186
+
187
+ return {"peak_memory_gb": peak_memory}
188
+
189
+ def run_all_benchmarks(
190
+ self,
191
+ dataset: Optional["DataLoader"] = None,
192
+ batch_size: int = 32,
193
+ seq_length: int = 1024,
194
+ ) -> Dict[str, float]:
195
+ """
196
+ Run all benchmarks.
197
+
198
+ Args:
199
+ dataset: DataLoader for perplexity benchmark
200
+ batch_size: Batch size for throughput benchmark
201
+ seq_length: Sequence length for throughput benchmark
202
+
203
+ Returns:
204
+ Dict with all benchmark results
205
+ """
206
+ results = {}
207
+
208
+ if dataset is not None:
209
+ print("Running perplexity benchmark...")
210
+ ppl = self.benchmark_perplexity(dataset, num_batches=10)
211
+ results["perplexity"] = ppl
212
+
213
+ print("Running throughput benchmark...")
214
+ throughput = self.benchmark_throughput(batch_size, seq_length)
215
+ results.update(throughput)
216
+
217
+ print("Running memory benchmark...")
218
+ memory = self.benchmark_memory()
219
+ results.update(memory)
220
+
221
+ return results
src/taoTrain/checkpointing/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Checkpoint management."""
2
+
3
+ from .checkpoint import CheckpointManager
4
+
5
+ __all__ = ["CheckpointManager"]
src/taoTrain/checkpointing/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (273 Bytes). View file
 
src/taoTrain/checkpointing/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (258 Bytes). View file
 
src/taoTrain/checkpointing/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (255 Bytes). View file
 
src/taoTrain/checkpointing/__pycache__/checkpoint.cpython-310.pyc ADDED
Binary file (5.96 kB). View file
 
src/taoTrain/checkpointing/__pycache__/checkpoint.cpython-312.pyc ADDED
Binary file (7.55 kB). View file
 
src/taoTrain/checkpointing/__pycache__/checkpoint.cpython-314.pyc ADDED
Binary file (8.72 kB). View file
 
src/taoTrain/checkpointing/checkpoint.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Checkpoint management utilities.
2
+
3
+ Canonical Checkpoint Format (new):
4
+ {
5
+ 'step': int, # Training step number
6
+ 'model_state': Dict[str, Tensor], # Model state dict
7
+ 'optimizer_state': Dict, # Optimizer state dict (optional)
8
+ 'config': Dict, # TrainingConfig as dict
9
+ 'metrics': Dict[str, float], # Training metrics
10
+ 'global_step': int, # (deprecated, kept for compat) same as step
11
+ 'current_epoch': int, # (optional) current epoch number
12
+ 'best_loss': float, # (optional) best validation loss
13
+ }
14
+
15
+ Legacy Checkpoint Format (old, from BaseTrainer):
16
+ {
17
+ 'global_step': int,
18
+ 'current_epoch': int,
19
+ 'best_loss': float,
20
+ 'model_state_dict': Dict[str, Tensor], # ← Note: uses '_dict' suffix
21
+ 'optimizer_state_dict': Dict,
22
+ 'config': Dict,
23
+ }
24
+
25
+ The load() function auto-detects and migrates legacy format to canonical format.
26
+ """
27
+
28
+ from pathlib import Path
29
+ from typing import Dict, Any, Optional
30
+ import torch
31
+ from taoTrain.config import TrainingConfig
32
+
33
+
34
+ class CheckpointManager:
35
+ """Manage model checkpoints with versioning."""
36
+
37
+ def __init__(
38
+ self,
39
+ checkpoint_dir: str | Path,
40
+ keep_last_n: int = 3,
41
+ track_best: bool = True,
42
+ ):
43
+ """
44
+ Initialize checkpoint manager.
45
+
46
+ Args:
47
+ checkpoint_dir: Directory to save checkpoints
48
+ keep_last_n: Number of recent checkpoints to keep
49
+ track_best: Whether to track best model
50
+ """
51
+ self.checkpoint_dir = Path(checkpoint_dir)
52
+ self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
53
+
54
+ self.keep_last_n = keep_last_n
55
+ self.track_best = track_best
56
+
57
+ self.best_metric = None
58
+ self.best_metric_name = None
59
+ self.saved_checkpoints = []
60
+
61
+ def save(
62
+ self,
63
+ step: int,
64
+ model_state: Dict[str, Any],
65
+ optimizer_state: Optional[Dict[str, Any]] = None,
66
+ config: Optional[TrainingConfig] = None,
67
+ metrics: Optional[Dict[str, float]] = None,
68
+ is_best: bool = False,
69
+ ) -> Path:
70
+ """
71
+ Save a checkpoint.
72
+
73
+ Args:
74
+ step: Training step
75
+ model_state: Model state dict
76
+ optimizer_state: Optimizer state dict
77
+ config: Training config
78
+ metrics: Metrics dict
79
+ is_best: Whether this is the best model so far
80
+
81
+ Returns:
82
+ Path to saved checkpoint
83
+ """
84
+ checkpoint = {
85
+ "step": step,
86
+ "model_state": model_state,
87
+ "optimizer_state": optimizer_state,
88
+ "config": config.to_dict() if config else None,
89
+ "metrics": metrics or {},
90
+ }
91
+
92
+ filename = f"checkpoint_step_{step:06d}.pt"
93
+ if is_best:
94
+ filename = "best_model.pt"
95
+
96
+ path = self.checkpoint_dir / filename
97
+ torch.save(checkpoint, path)
98
+
99
+ # Track saved checkpoints
100
+ if not is_best:
101
+ self.saved_checkpoints.append((step, path))
102
+
103
+ # Clean up old checkpoints
104
+ if len(self.saved_checkpoints) > self.keep_last_n:
105
+ _, old_path = self.saved_checkpoints.pop(0)
106
+ if old_path.exists():
107
+ old_path.unlink()
108
+
109
+ return path
110
+
111
+ def load(
112
+ self,
113
+ checkpoint_path: str | Path,
114
+ device: Optional[torch.device] = None,
115
+ ) -> Dict[str, Any]:
116
+ """
117
+ Load a checkpoint with backward-compatible format handling.
118
+
119
+ Auto-detects checkpoint format (canonical or legacy) and normalizes
120
+ to canonical format in-memory. Legacy checkpoints are migrated without
121
+ modifying the file.
122
+
123
+ Args:
124
+ checkpoint_path: Path to checkpoint
125
+ device: Device to load to
126
+
127
+ Returns:
128
+ Checkpoint dict in canonical format with 'model_state' key
129
+ """
130
+ if device is None:
131
+ device = torch.device("cpu")
132
+
133
+ checkpoint = torch.load(checkpoint_path, map_location=device)
134
+
135
+ # Auto-detect and migrate legacy format to canonical format
136
+ checkpoint = self._normalize_checkpoint_format(checkpoint)
137
+
138
+ return checkpoint
139
+
140
+ def _normalize_checkpoint_format(self, checkpoint: Dict[str, Any]) -> Dict[str, Any]:
141
+ """
142
+ Normalize checkpoint to canonical format.
143
+
144
+ Detects if checkpoint is in legacy format (from BaseTrainer with 'model_state_dict')
145
+ and migrates it to canonical format (with 'model_state').
146
+
147
+ Args:
148
+ checkpoint: Raw checkpoint dict
149
+
150
+ Returns:
151
+ Normalized checkpoint dict with canonical keys
152
+ """
153
+ # Check if this is a legacy checkpoint (has 'model_state_dict' but not 'model_state')
154
+ if "model_state_dict" in checkpoint and "model_state" not in checkpoint:
155
+ # Migrate legacy format to canonical
156
+ migrated = {
157
+ "step": checkpoint.get("global_step", 0),
158
+ "model_state": checkpoint["model_state_dict"],
159
+ "optimizer_state": checkpoint.get("optimizer_state_dict"),
160
+ "config": checkpoint.get("config"),
161
+ "metrics": {},
162
+ # Keep legacy keys for backward compatibility in code that uses them
163
+ "global_step": checkpoint.get("global_step", 0),
164
+ "current_epoch": checkpoint.get("current_epoch", 0),
165
+ "best_loss": checkpoint.get("best_loss", float('inf')),
166
+ }
167
+ print(f"\n✓ [CheckpointManager] Detected legacy checkpoint format. Auto-migrated to canonical format.")
168
+ return migrated
169
+
170
+ # Already in canonical format or unknown format
171
+ if "model_state" not in checkpoint:
172
+ # If neither format detected, ensure model_state is accessible
173
+ # (might be a raw state_dict)
174
+ print(f"\n⚠ [CheckpointManager] Checkpoint format unclear. Assuming raw state_dict format.")
175
+ checkpoint["model_state"] = checkpoint
176
+
177
+ return checkpoint
178
+
179
+ def get_latest(self) -> Optional[Path]:
180
+ """Get path to latest checkpoint."""
181
+ if not self.saved_checkpoints:
182
+ return None
183
+ return self.saved_checkpoints[-1][1]
184
+
185
+ def get_best(self) -> Optional[Path]:
186
+ """Get path to best checkpoint."""
187
+ best_path = self.checkpoint_dir / "best_model.pt"
188
+ if best_path.exists():
189
+ return best_path
190
+ return None
191
+
192
+ def list_checkpoints(self) -> list[Path]:
193
+ """List all saved checkpoints."""
194
+ return sorted(self.checkpoint_dir.glob("checkpoint_step_*.pt"))
src/taoTrain/cli.py ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Main CLI entry point."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ import click
7
+ import torch
8
+
9
+ from taoTrain.config import (
10
+ load_config,
11
+ load_tokenizer_config,
12
+ TrainingModeEnum,
13
+ TrainingConfig,
14
+ PretrainConfig,
15
+ SFTConfig,
16
+ RLConfig,
17
+ VLMConfig,
18
+ VLMSFTConfig,
19
+ )
20
+ from taoTrain.utils import set_seed, get_device
21
+ from taoTrain.core import BaseModel, create_model, create_datasets
22
+ from taoTrain.data import get_dataloader
23
+ from taoTrain.training import PretrainTrainer, SFTTrainer, RLTrainer, VLMTrainer, VLMSFTTrainer
24
+ from taoTrain.benchmarks import BenchmarkRunner
25
+ from taoTrain.tokenizers import TokenizerTrainer
26
+ from taoTrain.data.tokenizer import load_tokenizer_runtime_info
27
+
28
+
29
+ @click.group()
30
+ def main():
31
+ """TaoTrain: A clean, modular PyTorch LLM training framework."""
32
+ pass
33
+
34
+
35
+ @main.command()
36
+ @click.option(
37
+ "--config",
38
+ type=click.Path(exists=True),
39
+ required=True,
40
+ help="Path to training config file (YAML or JSON)",
41
+ )
42
+ def pretrain(config: str):
43
+ """Pretrain a language model."""
44
+ _train_command(config, TrainingModeEnum.PRETRAIN)
45
+
46
+
47
+ @main.command()
48
+ @click.option(
49
+ "--config",
50
+ type=click.Path(exists=True),
51
+ required=True,
52
+ help="Path to training config file (YAML or JSON)",
53
+ )
54
+ def sft(config: str):
55
+ """Supervised fine-tune a language model."""
56
+ _train_command(config, TrainingModeEnum.SFT)
57
+
58
+
59
+ @main.command()
60
+ @click.option(
61
+ "--config",
62
+ type=click.Path(exists=True),
63
+ required=True,
64
+ help="Path to training config file (YAML or JSON)",
65
+ )
66
+ def rl(config: str):
67
+ """Train with reinforcement learning."""
68
+ _train_command(config, TrainingModeEnum.RL)
69
+
70
+
71
+ @main.command()
72
+ @click.option(
73
+ "--config",
74
+ type=click.Path(exists=True),
75
+ required=True,
76
+ help="Path to multimodal VLM config file (YAML or JSON)",
77
+ )
78
+ def vlm(config: str):
79
+ """Train a multimodal vision-language connector."""
80
+ _train_command(config, TrainingModeEnum.VLM)
81
+
82
+
83
+ @main.command(name="vlm-sft")
84
+ @click.option(
85
+ "--config",
86
+ type=click.Path(exists=True),
87
+ required=True,
88
+ help="Path to multimodal VLM SFT config file (YAML or JSON)",
89
+ )
90
+ def vlm_sft(config: str):
91
+ """Run end-to-end multimodal supervised fine-tuning."""
92
+ _train_command(config, TrainingModeEnum.VLM_SFT)
93
+
94
+
95
+ @main.command()
96
+ @click.option(
97
+ "--config",
98
+ type=click.Path(exists=True),
99
+ required=True,
100
+ help="Path to tokenizer config file (YAML or JSON)",
101
+ )
102
+ def train_tokenizer(config: str):
103
+ """Train a SentencePiece tokenizer from a YAML/JSON config file."""
104
+ try:
105
+ click.echo("🚀 TaoTrain Tokenizer Trainer")
106
+ click.echo(f"{'=' * 50}")
107
+
108
+ # Load tokenizer config
109
+ click.echo(f"Loading config from {config}...")
110
+ tokenizer_config = load_tokenizer_config(config)
111
+
112
+ # Train tokenizer from config
113
+ result = TokenizerTrainer.train_from_config(tokenizer_config)
114
+
115
+ # Display results
116
+ click.echo(f"\n{'=' * 50}")
117
+ click.echo("✅ Tokenizer Training Complete!")
118
+ click.echo(f"\n📊 Configuration:")
119
+ click.echo(f" - Input file: {tokenizer_config.jsonl_path}")
120
+ click.echo(f" - Samples: {tokenizer_config.max_samples or 'all'}")
121
+ click.echo(f" - Output dir: {result['output_dir']}")
122
+ click.echo(f" - Vocab size: {result['vocab_size']}")
123
+ click.echo(f" - Model type: {result['model_type']}")
124
+
125
+ if tokenizer_config.special_tokens:
126
+ click.echo(f" - Special tokens: {tokenizer_config.special_tokens}")
127
+
128
+ click.echo(f"\n📁 Generated Files:")
129
+ click.echo(f" - Model: {result['model_file']}")
130
+ click.echo(f" - Vocab: {result['vocab_file']}")
131
+ if "special_tokens_file" in result:
132
+ click.echo(f" - Special token metadata: {result['special_tokens_file']}")
133
+
134
+ click.echo(f"\n📝 Next Steps:")
135
+ click.echo(f" 1. Use this tokenizer in your pretraining config:")
136
+ click.echo(f" dataset:")
137
+ click.echo(f" local: true")
138
+ click.echo(f" jsonl_path: {tokenizer_config.jsonl_path}")
139
+ click.echo(f" tokenizer_path: {result['model_file']}")
140
+ click.echo(f"")
141
+ click.echo(f" 2. Run pretraining with:")
142
+ click.echo(f" train pretrain --config your_config.yaml")
143
+
144
+ except ImportError as e:
145
+ click.echo(f"❌ Error: {e}", err=True)
146
+ sys.exit(1)
147
+ except FileNotFoundError as e:
148
+ click.echo(f"❌ File Error: {e}", err=True)
149
+ sys.exit(1)
150
+ except ValueError as e:
151
+ click.echo(f"❌ Validation Error: {e}", err=True)
152
+ sys.exit(1)
153
+ except Exception as e:
154
+ click.echo(f"❌ Unexpected Error: {e}", err=True)
155
+ click.echo(f" Please report this issue.", err=True)
156
+ sys.exit(1)
157
+
158
+
159
+ @click.command()
160
+ @click.option(
161
+ "--jsonl-path",
162
+ type=click.Path(exists=True),
163
+ required=True,
164
+ help="Path to JSONL file containing training data",
165
+ )
166
+ @click.option(
167
+ "--output-dir",
168
+ type=click.Path(),
169
+ default="tokenizers",
170
+ help="Directory to save tokenizer files",
171
+ )
172
+ @click.option(
173
+ "--vocab-size",
174
+ type=int,
175
+ default=50000,
176
+ help="Vocabulary size for the tokenizer",
177
+ )
178
+ @click.option(
179
+ "--model-type",
180
+ type=click.Choice(["unigram", "bpe", "char", "word"]),
181
+ default="unigram",
182
+ help="SentencePiece model type",
183
+ )
184
+ @click.option(
185
+ "--character-coverage",
186
+ type=float,
187
+ default=0.9995,
188
+ help="Character coverage for SentencePiece",
189
+ )
190
+ @click.option(
191
+ "--tokenizer-prefix",
192
+ type=str,
193
+ default=None,
194
+ help="Prefix for tokenizer output files (default: model_type)",
195
+ )
196
+ def train_tokenizer_command(
197
+ jsonl_path: str,
198
+ output_dir: str,
199
+ vocab_size: int,
200
+ model_type: str,
201
+ character_coverage: float,
202
+ tokenizer_prefix: Optional[str],
203
+ ):
204
+ """Train a SentencePiece tokenizer from JSONL data."""
205
+ try:
206
+ click.echo("🚀 TaoTrain Tokenizer Trainer")
207
+ click.echo(f"{'=' * 50}")
208
+
209
+ # Train tokenizer
210
+ result = TokenizerTrainer.train_sentencepiece(
211
+ jsonl_path=jsonl_path,
212
+ output_dir=output_dir,
213
+ vocab_size=vocab_size,
214
+ model_type=model_type,
215
+ character_coverage=character_coverage,
216
+ tokenizer_prefix=tokenizer_prefix,
217
+ )
218
+
219
+ # Display results
220
+ click.echo(f"\n{'=' * 50}")
221
+ click.echo("✅ Tokenizer Training Complete!")
222
+ click.echo(f"\n📊 Configuration:")
223
+ click.echo(f" - Input file: {jsonl_path}")
224
+ click.echo(f" - Output dir: {result['output_dir']}")
225
+ click.echo(f" - Vocab size: {result['vocab_size']}")
226
+ click.echo(f" - Model type: {result['model_type']}")
227
+
228
+ click.echo(f"\n📁 Generated Files:")
229
+ click.echo(f" - Model: {result['model_file']}")
230
+ click.echo(f" - Vocab: {result['vocab_file']}")
231
+ if "special_tokens_file" in result:
232
+ click.echo(f" - Special token metadata: {result['special_tokens_file']}")
233
+
234
+ click.echo(f"\n📝 Next Steps:")
235
+ click.echo(f" 1. Use this tokenizer in your pretraining config:")
236
+ click.echo(f" dataset:")
237
+ click.echo(f" local: true")
238
+ click.echo(f" jsonl_path: {jsonl_path}")
239
+ click.echo(f" tokenizer_path: {result['model_file']}")
240
+ click.echo(f"")
241
+ click.echo(f" 2. Run pretraining with:")
242
+ click.echo(f" train pretrain --config your_config.yaml")
243
+
244
+ except ImportError as e:
245
+ click.echo(f"❌ Error: {e}", err=True)
246
+ sys.exit(1)
247
+ except FileNotFoundError as e:
248
+ click.echo(f"❌ File Error: {e}", err=True)
249
+ sys.exit(1)
250
+ except ValueError as e:
251
+ click.echo(f"❌ Validation Error: {e}", err=True)
252
+ sys.exit(1)
253
+ except Exception as e:
254
+ click.echo(f"❌ Unexpected Error: {e}", err=True)
255
+ click.echo(f" Please report this issue.", err=True)
256
+ sys.exit(1)
257
+
258
+
259
+ # Keep legacy CLI command as train-tokenizer-legacy for backward compatibility
260
+ main.add_command(train_tokenizer_command, name="train-tokenizer-legacy")
261
+
262
+
263
+ def _train_command(config_path: str, mode: TrainingModeEnum):
264
+ """Internal training command."""
265
+ try:
266
+ # Load config
267
+ click.echo(f"Loading config from {config_path}...")
268
+ train_config = load_config(config_path, mode)
269
+
270
+ _synchronize_model_vocab_with_tokenizer(train_config)
271
+
272
+ # Set seed
273
+ set_seed(train_config.seed)
274
+
275
+ # Get device
276
+ device = get_device(train_config.device)
277
+ click.echo(f"Using device: {device}")
278
+
279
+ # Create model
280
+ click.echo("Creating model...")
281
+ model = create_model(train_config, device)
282
+ total_params, trainable_params = _count_params(model)
283
+ click.echo(f" - Total parameters: {total_params:,}")
284
+ click.echo(f" - Trainable parameters: {trainable_params:,}")
285
+
286
+ # Load pretrained checkpoint if provided (for SFT/RL)
287
+ if train_config.checkpoint_path:
288
+ click.echo(f"Loading pretrained checkpoint from {train_config.checkpoint_path}...")
289
+ from taoTrain.checkpointing.checkpoint import CheckpointManager
290
+ checkpoint_manager = CheckpointManager(train_config.checkpoint_dir)
291
+ checkpoint = checkpoint_manager.load(train_config.checkpoint_path, device=device)
292
+
293
+ # CheckpointManager.load() normalizes format and ensures 'model_state' key exists
294
+ if "model_state" in checkpoint:
295
+ load_result = model.load_state_dict(checkpoint["model_state"], strict=False)
296
+ click.echo(" ✓ Checkpoint loaded successfully")
297
+ if load_result.missing_keys:
298
+ click.echo(f" Missing keys: {len(load_result.missing_keys)}")
299
+ for key in load_result.missing_keys[:10]:
300
+ click.echo(f" - {key}")
301
+ if load_result.unexpected_keys:
302
+ click.echo(f" Unexpected keys: {len(load_result.unexpected_keys)}")
303
+ for key in load_result.unexpected_keys[:10]:
304
+ click.echo(f" - {key}")
305
+ else:
306
+ raise KeyError(f"Invalid checkpoint format: 'model_state' key not found. "
307
+ f"Available keys: {list(checkpoint.keys())}")
308
+
309
+
310
+
311
+ # Create datasets
312
+ click.echo("Loading datasets...")
313
+ train_dataset, val_dataset = create_datasets(train_config)
314
+ click.echo(f" - Train samples: {len(train_dataset)}")
315
+ if val_dataset:
316
+ click.echo(f" - Val samples: {len(val_dataset)}")
317
+
318
+ # Select trainer
319
+ if mode == TrainingModeEnum.PRETRAIN:
320
+ trainer_class = PretrainTrainer
321
+ elif mode == TrainingModeEnum.SFT:
322
+ trainer_class = SFTTrainer
323
+ elif mode == TrainingModeEnum.RL:
324
+ trainer_class = RLTrainer
325
+ elif mode == TrainingModeEnum.VLM:
326
+ trainer_class = VLMTrainer
327
+ elif mode == TrainingModeEnum.VLM_SFT:
328
+ trainer_class = VLMSFTTrainer
329
+ else:
330
+ raise ValueError(f"Unknown training mode: {mode}")
331
+
332
+ # Create trainer
333
+ click.echo("Setting up trainer...")
334
+ trainer = trainer_class(
335
+ model=model,
336
+ train_dataset=train_dataset,
337
+ val_dataset=val_dataset,
338
+ config=train_config,
339
+ device=device,
340
+ )
341
+
342
+ # Training loop
343
+ click.echo("\nStarting training...\n")
344
+ for epoch in range(train_config.num_epochs):
345
+ if train_config.max_steps and trainer.global_step >= train_config.max_steps:
346
+ break
347
+
348
+ epoch_metrics = trainer.train_epoch()
349
+ click.echo(f"\nEpoch {epoch + 1} complete")
350
+ click.echo(f" - Loss: {epoch_metrics.get('loss', 'N/A')}")
351
+ click.echo(f" - Learning rate: {epoch_metrics.get('lr', 'N/A')}")
352
+
353
+ # Final checkpoint
354
+ final_path = Path(train_config.checkpoint_dir) / "final_model.pt"
355
+ trainer.save_checkpoint(final_path)
356
+ click.echo(f"\nTraining complete! Final model saved to {final_path}")
357
+
358
+ # Log finish
359
+ trainer.logger.finish()
360
+
361
+ except Exception as e:
362
+ click.echo(f"Error during training: {e}", err=True)
363
+ sys.exit(1)
364
+
365
+
366
+ def _synchronize_model_vocab_with_tokenizer(train_config: TrainingConfig) -> None:
367
+ """Validate tokenizer runtime IDs and align model vocab size with tokenizer artifacts."""
368
+ dataset_config = train_config.dataset
369
+ tokenizer_path = dataset_config.tokenizer_path
370
+ if not tokenizer_path:
371
+ return
372
+
373
+ tokenizer_info = load_tokenizer_runtime_info(tokenizer_path, dataset_config.tokenizer_type)
374
+ invalid_builtin_ids = {
375
+ token_name: token_id
376
+ for token_name, token_id in tokenizer_info.items()
377
+ if token_name.endswith("_id") and token_id < 0
378
+ }
379
+ if invalid_builtin_ids:
380
+ raise ValueError(
381
+ f"Tokenizer `{tokenizer_path}` is missing required built-in IDs: {invalid_builtin_ids}. "
382
+ "Retrain the tokenizer so PAD/BOS/EOS/UNK are real tokenizer IDs before training."
383
+ )
384
+
385
+ tokenizer_vocab_size = tokenizer_info["vocab_size"]
386
+ model_vocab_size = train_config.model.vocab_size
387
+ if model_vocab_size != tokenizer_vocab_size:
388
+ click.echo(
389
+ "Tokenizer/model vocab mismatch detected. "
390
+ f"Updating model.vocab_size from {model_vocab_size} to {tokenizer_vocab_size} "
391
+ f"based on tokenizer `{tokenizer_path}`."
392
+ )
393
+ train_config.model.vocab_size = tokenizer_vocab_size
394
+
395
+
396
+ @main.command()
397
+ @click.option(
398
+ "--model",
399
+ type=click.Path(exists=True),
400
+ required=True,
401
+ help="Path to model checkpoint",
402
+ )
403
+ @click.option(
404
+ "--benchmark-type",
405
+ type=click.Choice(["all", "perplexity", "throughput", "memory"]),
406
+ default="all",
407
+ help="Type of benchmark to run",
408
+ )
409
+ @click.option(
410
+ "--batch-size",
411
+ type=int,
412
+ default=32,
413
+ help="Batch size for benchmarking",
414
+ )
415
+ @click.option(
416
+ "--seq-length",
417
+ type=int,
418
+ default=1024,
419
+ help="Sequence length for benchmarking",
420
+ )
421
+ def benchmark(model: str, benchmark_type: str, batch_size: int, seq_length: int):
422
+ """Benchmark a trained model."""
423
+ try:
424
+ click.echo(f"Loading model from {model}...")
425
+ device = get_device("cuda")
426
+ runner = BenchmarkRunner.load_from_checkpoint(model, device=device)
427
+
428
+ click.echo("Running benchmarks...\n")
429
+
430
+ if benchmark_type == "throughput" or benchmark_type == "all":
431
+ click.echo("Throughput benchmark:")
432
+ results = runner.benchmark_throughput(batch_size, seq_length)
433
+ for key, val in results.items():
434
+ click.echo(f" {key}: {val:.2f}")
435
+
436
+ if benchmark_type == "memory" or benchmark_type == "all":
437
+ click.echo("\nMemory benchmark:")
438
+ results = runner.benchmark_memory()
439
+ for key, val in results.items():
440
+ click.echo(f" {key}: {val:.2f}")
441
+
442
+ click.echo("\nBenchmarking complete!")
443
+
444
+ except Exception as e:
445
+ click.echo(f"Error during benchmarking: {e}", err=True)
446
+ sys.exit(1)
447
+
448
+
449
+ @main.command()
450
+ @click.option("--repo", type=str, default=".aim", help="AimStack repository path")
451
+ def view_logs(repo: str):
452
+ """View training logs with AimStack."""
453
+ try:
454
+ import subprocess
455
+ click.echo(f"Opening AimStack dashboard for repo: {repo}")
456
+ subprocess.run(["aim", "up", "--repo", repo])
457
+ except FileNotFoundError:
458
+ click.echo("Error: 'aim' command not found. Install with: pip install aim", err=True)
459
+ sys.exit(1)
460
+
461
+
462
+ def _count_params(model: BaseModel) -> tuple[int, int]:
463
+ """Count model parameters."""
464
+ total = sum(p.numel() for p in model.parameters())
465
+ trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
466
+ return total, trainable
467
+
468
+
469
+ if __name__ == "__main__":
470
+ main()
src/taoTrain/config.py ADDED
@@ -0,0 +1,871 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pydantic configuration schemas for TaoTrain."""
2
+
3
+ from enum import Enum
4
+ from typing import Optional, Literal
5
+ from pathlib import Path
6
+ import json
7
+ from pydantic import BaseModel as PydanticBaseModel, Field, validator
8
+ import yaml
9
+
10
+
11
+ # ============================================================================
12
+ # Enums
13
+ # ============================================================================
14
+
15
+
16
+ class DataTypeEnum(str, Enum):
17
+ """Data types for training."""
18
+ FLOAT32 = "float32"
19
+ FLOAT16 = "float16"
20
+ BFLOAT16 = "bfloat16"
21
+
22
+
23
+ class OptimizerEnum(str, Enum):
24
+ """Supported optimizers."""
25
+ ADAM = "adam"
26
+ ADAMW = "adamw"
27
+ SGD = "sgd"
28
+ HYBRID_MUON_ADAMW = "hybrid_muon_adamw"
29
+
30
+
31
+ class ModelArchitectureEnum(str, Enum):
32
+ """Built-in model architectures."""
33
+ TRANSFORMER = "transformer"
34
+ TAONET = "taonet"
35
+ GAMMA_NET = "gamma_net"
36
+ MULTIMODAL_WRAPPER = "multimodal_wrapper"
37
+
38
+
39
+ class SchedulerEnum(str, Enum):
40
+ """Supported learning rate schedulers."""
41
+ LINEAR_WARMUP = "linearWarmup"
42
+ COSINE_WARMUP = "cosineWarmup"
43
+ CONSTANT = "constant"
44
+
45
+
46
+ class RLMethodEnum(str, Enum):
47
+ """Supported RL training methods."""
48
+ PPO = "ppo"
49
+ DPO = "dpo"
50
+
51
+
52
+ class TrainingModeEnum(str, Enum):
53
+ """Training stages."""
54
+ PRETRAIN = "pretrain"
55
+ SFT = "sft"
56
+ RL = "rl"
57
+ VLM = "vlm"
58
+ VLM_SFT = "vlm_sft"
59
+
60
+
61
+ # ============================================================================
62
+ # Base Configs
63
+ # ============================================================================
64
+
65
+
66
+ class BaseConfig(PydanticBaseModel):
67
+ """Base Pydantic model with utility methods."""
68
+
69
+ class Config:
70
+ """Pydantic config."""
71
+ arbitrary_types_allowed = True
72
+
73
+ def to_dict(self) -> dict:
74
+ """Convert to dictionary."""
75
+ data = self.model_dump(mode='json') # Enums -> strings
76
+ return data
77
+
78
+ def to_json_str(self) -> str:
79
+ """Convert to JSON string."""
80
+ return json.dumps(self.to_dict(), indent=2)
81
+
82
+ def save_yaml(self, path: str | Path) -> None:
83
+ """Save config to YAML file."""
84
+ path = Path(path)
85
+ path.parent.mkdir(parents=True, exist_ok=True)
86
+ with open(path, 'w') as f:
87
+ yaml.dump(self.to_dict(), f, default_flow_style=False, sort_keys=False)
88
+
89
+ def save_json(self, path: str | Path) -> None:
90
+ """Save config to JSON file."""
91
+ path = Path(path)
92
+ path.parent.mkdir(parents=True, exist_ok=True)
93
+ with open(path, 'w') as f:
94
+ f.write(self.to_json_str())
95
+
96
+ @classmethod
97
+ def load_yaml(cls, path: str | Path) -> "BaseConfig":
98
+ """Load config from YAML file."""
99
+ with open(path) as f:
100
+ data = yaml.safe_load(f)
101
+ return cls(**data)
102
+
103
+ @classmethod
104
+ def load_json(cls, path: str | Path) -> "BaseConfig":
105
+ """Load config from JSON file."""
106
+ with open(path) as f:
107
+ data = json.load(f)
108
+ return cls(**data)
109
+
110
+
111
+ # ============================================================================
112
+ # Model Config
113
+ # ============================================================================
114
+
115
+
116
+ class ModelConfig(BaseConfig):
117
+ """Configuration for model architecture."""
118
+
119
+ architecture_type: ModelArchitectureEnum = Field(
120
+ default=ModelArchitectureEnum.TRANSFORMER,
121
+ description="Type of model architecture"
122
+ )
123
+ llm_architecture_type: Optional[ModelArchitectureEnum] = Field(
124
+ default=None,
125
+ description="Base language-model architecture when using a multimodal wrapper."
126
+ )
127
+
128
+ # Transformer-specific
129
+ vocab_size: int = Field(default=50257, description="Vocabulary size")
130
+ hidden_dim: int = Field(default=768, description="Hidden dimension")
131
+ num_layers: int = Field(default=12, description="Number of transformer blocks")
132
+ num_heads: int = Field(default=12, description="Number of attention heads")
133
+ head_dim: Optional[int] = Field(
134
+ default=None,
135
+ description="Head dimension (defaults to hidden_dim // num_heads)"
136
+ )
137
+ intermediate_dim: Optional[int] = Field(
138
+ default=None,
139
+ description="FFN intermediate dimension (defaults to 4 * hidden_dim)"
140
+ )
141
+ dropout: float = Field(default=0.1, description="Dropout rate")
142
+ max_seq_length: int = Field(default=2048, description="Maximum sequence length")
143
+
144
+ # TaoNet (DeepSeek MLA) specific
145
+ d_latent_kv: Optional[int] = Field(
146
+ default=None,
147
+ description="KV compression dimension for MLA (defaults to 3/4 * hidden_dim). Only used for taonet architecture."
148
+ )
149
+ d_rope: Optional[int] = Field(
150
+ default=None,
151
+ description="RoPE dimension per head (defaults to hidden_dim // num_heads). Only used for taonet architecture."
152
+ )
153
+ gqa_groups: int = Field(
154
+ default=1,
155
+ description="Grouped Query Attention groups (1 = standard MLA, >1 = GQA). Only used for taonet architecture."
156
+ )
157
+ hidden_dim_ff: Optional[int] = Field(
158
+ default=None,
159
+ description="Feed-forward intermediate dimension (defaults to 4 * hidden_dim)."
160
+ )
161
+ use_factorized_embedding: bool = Field(
162
+ default=False,
163
+ description="Use low-rank factorized embedding instead of standard embedding (reduces params). Only for taonet."
164
+ )
165
+ d_embed_rank: int = Field(
166
+ default=96,
167
+ description="Rank dimension for factorized embedding. Only used if use_factorized_embedding=True."
168
+ )
169
+ vision_encoder_type: str = Field(
170
+ default="cnn",
171
+ description="Vision encoder type for multimodal models."
172
+ )
173
+ image_size: int = Field(
174
+ default=224,
175
+ description="Square image size for multimodal image preprocessing."
176
+ )
177
+ vision_output_dim: int = Field(
178
+ default=256,
179
+ description="Output feature dimension from the vision encoder before projection."
180
+ )
181
+ vision_prefix_tokens: int = Field(
182
+ default=10,
183
+ description="Number of visual prefix tokens projected into the LLM sequence."
184
+ )
185
+ image_token: str = Field(
186
+ default="<image>",
187
+ description="Tokenizer special token reserved for image placeholders."
188
+ )
189
+ cnn_channels: list[int] = Field(
190
+ default_factory=lambda: [32, 64, 128],
191
+ description="Per-stage channel sizes for the in-repo CNN encoder."
192
+ )
193
+ cnn_kernel_size: int = Field(
194
+ default=3,
195
+ description="Kernel size for CNN encoder convolutions."
196
+ )
197
+
198
+ # GammaSpaceModel-specific
199
+ gamma_hidden_dim: int = Field(
200
+ default=1536,
201
+ description="GammaSpaceBlock hidden_dim (state-space hidden size). Only used for gamma_net."
202
+ )
203
+ gamma_dt_min: float = Field(
204
+ default=1e-3,
205
+ description="GammaSpaceBlock dt_min. Only used for gamma_net."
206
+ )
207
+ gamma_dt_max: float = Field(
208
+ default=1e-1,
209
+ description="GammaSpaceBlock dt_max. Only used for gamma_net."
210
+ )
211
+ gamma_dt_init: float = Field(
212
+ default=1e-2,
213
+ description="GammaSpaceBlock dt_init. Only used for gamma_net."
214
+ )
215
+ gamma_discretization: str = Field(
216
+ default="bilinear",
217
+ description="GammaSpaceBlock discretization mode: bilinear, zoh, or euler. Only used for gamma_net."
218
+ )
219
+ gamma_prenorm: bool = Field(
220
+ default=True,
221
+ description="GammaSpaceBlock prenorm flag. Only used for gamma_net."
222
+ )
223
+ gamma_residual_scale: float = Field(
224
+ default=1.0,
225
+ description="GammaSpaceBlock residual_scale. Only used for gamma_net."
226
+ )
227
+ gamma_activation: str = Field(
228
+ default="gelu",
229
+ description="GammaSpaceBlock activation. Only used for gamma_net."
230
+ )
231
+ gamma_gate: bool = Field(
232
+ default=True,
233
+ description="GammaSpaceBlock gate flag. Only used for gamma_net."
234
+ )
235
+ gamma_use_D: bool = Field(
236
+ default=True,
237
+ description="GammaSpaceBlock use_D flag. Only used for gamma_net."
238
+ )
239
+ gamma_kernel_mode: str = Field(
240
+ default="auto",
241
+ description="GammaSpaceBlock kernel_mode: auto, recurrent, or conv. Only used for gamma_net."
242
+ )
243
+ gamma_kernel_threshold: int = Field(
244
+ default=64,
245
+ description="GammaSpaceBlock kernel_threshold. Only used for gamma_net."
246
+ )
247
+ gamma_use_output_linear: bool = Field(
248
+ default=True,
249
+ description="GammaSpaceBlock use_output_linear flag. Only used for gamma_net."
250
+ )
251
+ gamma_gate_bias: float = Field(
252
+ default=2.0,
253
+ description="GammaSpaceBlock gate_bias. Only used for gamma_net."
254
+ )
255
+ gamma_input_gate: bool = Field(
256
+ default=True,
257
+ description="GammaSpaceBlock input_gate flag. Only used for gamma_net."
258
+ )
259
+ gamma_input_gate_bias: float = Field(
260
+ default=2.0,
261
+ description="GammaSpaceBlock input_gate_bias. Only used for gamma_net."
262
+ )
263
+ gamma_layer_scale_init: float = Field(
264
+ default=0.1,
265
+ description="GammaSpaceBlock layer_scale_init. Only used for gamma_net."
266
+ )
267
+
268
+ # YaRN (Yet another RoPE eXtension) for context length extension
269
+ rope_scale: float = Field(
270
+ default=40.0,
271
+ description="Base RoPE scale factor (default: 40.0). Controls position frequency base."
272
+ )
273
+ yarn_enabled: bool = Field(
274
+ default=False,
275
+ description="Enable YaRN (Yet another RoPE eXtension) for context length interpolation."
276
+ )
277
+ yarn_original_max_seq_length: Optional[int] = Field(
278
+ default=None,
279
+ description="Original trained context length that YaRN extends from. Defaults to max_seq_length when unset."
280
+ )
281
+ yarn_alpha: float = Field(
282
+ default=1.0,
283
+ description="YaRN interpolation smoothness (1.0=smooth, <1.0=aggressive, >1.0=conservative). Only used if yarn_enabled=True."
284
+ )
285
+
286
+ # Initializations
287
+ init_std: float = Field(default=0.02, description="Weight initialization standard deviation")
288
+
289
+ @validator("head_dim", always=True)
290
+ def validate_head_dim(cls, v, values):
291
+ """Validate head dimension."""
292
+ if v is None and 'hidden_dim' in values:
293
+ return values['hidden_dim'] // values.get('num_heads', 12)
294
+ return v
295
+
296
+ @validator("intermediate_dim", always=True)
297
+ def validate_intermediate_dim(cls, v, values):
298
+ """Validate intermediate dimension."""
299
+ if v is None and 'hidden_dim' in values:
300
+ return 4 * values['hidden_dim']
301
+ return v
302
+
303
+
304
+ # ============================================================================
305
+ # Dataset Config
306
+ # ============================================================================
307
+
308
+
309
+ class DatasetConfig(BaseConfig):
310
+ """Configuration for dataset loading."""
311
+
312
+ # Local vs HuggingFace dataset selection
313
+ local: bool = Field(default=False, description="Use local JSONL dataset instead of HuggingFace")
314
+
315
+ # HuggingFace dataset fields
316
+ dataset_name: Optional[str] = Field(default=None, description="HuggingFace dataset name (e.g., 'wikitext', 'openwebtext')")
317
+ split: str = Field(default="train", description="Dataset split to use")
318
+ config: Optional[str] = Field(default=None, description="Dataset config if multi-config (e.g., 'wikitext-103')")
319
+
320
+ # Local JSONL dataset fields
321
+ jsonl_path: Optional[str] = Field(default=None, description="Path to local JSONL dataset file")
322
+ text_field: str = Field(default="text", description="Name of text field in JSONL")
323
+ image_path_column: str = Field(
324
+ default="image_path",
325
+ description="Column containing a local image path for multimodal JSONL datasets."
326
+ )
327
+ image_path_aliases: list[str] = Field(
328
+ default_factory=lambda: ["image", "image_path", "image_file", "file_name"],
329
+ description="Fallback column names to try when a multimodal JSONL record does not contain image_path_column."
330
+ )
331
+ caption_prompt: str = Field(
332
+ default="Describe the image.",
333
+ description="Prompt paired with caption-style multimodal records that only provide image + text."
334
+ )
335
+
336
+ # Text column name varies by dataset
337
+ text_column: str = Field(default="text", description="Name of text column in dataset")
338
+
339
+ # Preprocessing
340
+ max_samples: Optional[int] = Field(
341
+ default=None,
342
+ description="Limit dataset to N samples (useful for debugging)"
343
+ )
344
+ cache_dir: str = Field(default=".cache/datasets", description="HuggingFace cache directory")
345
+
346
+ # For SFT/RL datasets with instruction-response format
347
+ instruction_column: Optional[str] = Field(default=None, description="Instruction column for SFT")
348
+ response_column: Optional[str] = Field(default=None, description="Response column for SFT")
349
+ prompt_column: Optional[str] = Field(default=None, description="Prompt column for RL")
350
+
351
+ # Instruction template
352
+ instruction_template: Optional[str] = Field(
353
+ default=None,
354
+ description="Template for combining instruction and response. E.g., '{instruction}\\n{response}'"
355
+ )
356
+
357
+ # Tokenizer configuration
358
+ tokenizer_type: Optional[str] = Field(
359
+ default=None,
360
+ description="Tokenizer type: 'huggingface' or 'sentencepiece'. If None, defaults based on tokenizer_path."
361
+ )
362
+ tokenizer_path: Optional[str] = Field(
363
+ default=None,
364
+ description="Path to saved tokenizer (for SentencePiece: .model file, for HuggingFace: model name or local path)"
365
+ )
366
+
367
+ # Chunked loading for large JSONL files
368
+ enable_streaming: bool = Field(
369
+ default=True,
370
+ description="Enable streaming/chunked loading for large JSONL files to reduce memory usage"
371
+ )
372
+ chunk_size_gb: float = Field(
373
+ default=5.0,
374
+ description="Approximate chunk size in GB (ignored if samples_per_chunk is set)"
375
+ )
376
+ samples_per_chunk: Optional[int] = Field(
377
+ default=1000,
378
+ description="Number of samples per chunk (takes precedence over chunk_size_gb). Default: 1000 samples"
379
+ )
380
+
381
+ # Chunk caching
382
+ enable_chunk_metadata_cache: bool = Field(
383
+ default=True,
384
+ description="Enable caching of chunk metadata (file scan results) to avoid re-scanning large JSONL files"
385
+ )
386
+ enable_chunk_data_cache: bool = Field(
387
+ default=False,
388
+ description="Enable caching of actual chunk data as separate files for faster loading (uses more disk space)"
389
+ )
390
+ chunk_cache_dir: str = Field(
391
+ default=".cache/chunks",
392
+ description="Directory to store chunk metadata and data cache files"
393
+ )
394
+
395
+ # Tokenization parallelization
396
+ tokenizer_threads: int = Field(
397
+ default=1,
398
+ description="Number of background threads for tokenization (1-32 recommended). Higher values speed up tokenization but increase memory usage."
399
+ )
400
+
401
+ @validator('jsonl_path', always=True)
402
+ def validate_dataset_source(cls, v, values):
403
+ """Validate that either local JSONL or HuggingFace dataset is specified."""
404
+ local = values.get('local', False)
405
+ dataset_name = values.get('dataset_name')
406
+
407
+ if local and not v:
408
+ raise ValueError("jsonl_path must be provided when local=True")
409
+ if not local and not dataset_name:
410
+ raise ValueError("dataset_name must be provided when local=False (HuggingFace dataset)")
411
+
412
+ return v
413
+
414
+ @validator('tokenizer_threads')
415
+ def validate_tokenizer_threads(cls, v):
416
+ """Validate tokenizer_threads is a positive integer."""
417
+ if v < 1:
418
+ raise ValueError("tokenizer_threads must be at least 1")
419
+ if v > 128:
420
+ raise ValueError("tokenizer_threads should not exceed 128 (recommended: 1-32)")
421
+ return v
422
+
423
+
424
+ # ============================================================================
425
+ # Tokenizer Config
426
+ # ============================================================================
427
+
428
+
429
+ class TokenizerConfig(BaseConfig):
430
+ """Configuration for tokenizer training."""
431
+
432
+ # Dataset source
433
+ jsonl_path: str = Field(description="Path to JSONL file containing training data")
434
+ text_field: str = Field(default="text", description="Field name in JSONL for text data")
435
+
436
+ # Training configuration
437
+ vocab_size: int = Field(default=50000, description="Vocabulary size")
438
+ model_type: str = Field(default="unigram", description="SentencePiece model type (unigram, bpe, char, word)")
439
+ character_coverage: float = Field(
440
+ default=0.9995,
441
+ description="Character coverage for SentencePiece training"
442
+ )
443
+ output_dir: str = Field(default="tokenizers", description="Directory to save trained tokenizer")
444
+ tokenizer_prefix: Optional[str] = Field(
445
+ default=None,
446
+ description="Prefix for tokenizer output files (default: model_type)"
447
+ )
448
+
449
+ # Custom special tokens registered as SentencePiece user-defined symbols.
450
+ special_tokens: Optional[list[str]] = Field(
451
+ default=None,
452
+ description=(
453
+ "Custom special tokens such as <think>, <user>, <assistant>, or <image>. "
454
+ "Built-in SentencePiece tokens are managed by the tokenizer itself and should not be listed here."
455
+ )
456
+ )
457
+
458
+ # Data sampling
459
+ max_samples: Optional[int] = Field(
460
+ default=None,
461
+ description="Limit training to first N samples from JSONL (useful for quick testing)"
462
+ )
463
+
464
+ # Tokenizer metadata
465
+ tokenizer_name: Optional[str] = Field(
466
+ default=None,
467
+ description="Optional name for the tokenizer"
468
+ )
469
+
470
+ @validator("special_tokens", pre=True)
471
+ def normalize_special_tokens(cls, value):
472
+ """Normalize special token config to a list of custom token strings."""
473
+ if value is None:
474
+ return None
475
+
476
+ if isinstance(value, dict):
477
+ return [str(token) for token in value.keys() if str(token) not in {"<UNK>", "<BOS>", "<EOS>", "<PAD>"}]
478
+
479
+ if isinstance(value, (list, tuple)):
480
+ return [str(token) for token in value]
481
+
482
+ raise ValueError("special_tokens must be a list of token strings")
483
+
484
+ @validator("special_tokens")
485
+ def validate_special_tokens(cls, value):
486
+ """Validate custom special token registry."""
487
+ if value is None:
488
+ return value
489
+
490
+ builtin_tokens = {"<UNK>", "<BOS>", "<EOS>", "<PAD>"}
491
+ seen: set[str] = set()
492
+ normalized: list[str] = []
493
+ for token in value:
494
+ if token in builtin_tokens:
495
+ continue
496
+ if token in seen:
497
+ continue
498
+ seen.add(token)
499
+ normalized.append(token)
500
+ return normalized
501
+
502
+
503
+ # ============================================================================
504
+ # Training Config
505
+ # ============================================================================
506
+
507
+
508
+ class OptimizerConfig(BaseConfig):
509
+ """Optimizer configuration."""
510
+
511
+ optimizer_type: OptimizerEnum = Field(default=OptimizerEnum.ADAMW, description="Optimizer type")
512
+ learning_rate: float = Field(default=1e-4, description="Peak learning rate (for Muon 2D weights)")
513
+ adamw_lr: Optional[float] = Field(
514
+ default=None,
515
+ description="Learning rate for AdamW (1D parameters). If None, defaults to learning_rate / 10. Used in hybrid_muon_adamw optimizer."
516
+ )
517
+ weight_decay: float = Field(default=1e-2, description="Weight decay (L2 regularization)")
518
+ betas: tuple[float, float] = Field(default=(0.9, 0.999), description="Adam betas")
519
+ eps: float = Field(default=1e-8, description="Optimizer epsilon")
520
+
521
+ @validator('adamw_lr', always=True)
522
+ def set_default_adamw_lr(cls, v, values):
523
+ """Set default adamw_lr as 1/10 of learning_rate if not specified."""
524
+ if v is None and 'learning_rate' in values:
525
+ return values['learning_rate'] / 10
526
+ return v
527
+
528
+
529
+ class SchedulerConfig(BaseConfig):
530
+ """Learning rate scheduler configuration."""
531
+
532
+ scheduler_type: SchedulerEnum = Field(default=SchedulerEnum.LINEAR_WARMUP, description="Scheduler type")
533
+ warmup_steps: int = Field(default=0, description="Number of warmup steps (takes precedence over warmup_ratio)")
534
+ warmup_ratio: float = Field(default=0.1, description="Warmup as fraction of total steps (used if warmup_steps=0)")
535
+
536
+ # Cosine scheduler specific
537
+ num_cycles: float = Field(default=0.5, description="Number of cycles for cosine schedule")
538
+ last_epoch: int = Field(default=-1, description="Last epoch for scheduler")
539
+
540
+ # TaoNet 3-phase scheduler (warmup -> steady -> cosine decay)
541
+ steady_ratio: float = Field(
542
+ default=0.0,
543
+ description="Fraction of training steps at peak LR before cosine decay (0.0 = no steady phase). Only for cosineWarmup."
544
+ )
545
+ min_lr_ratio: float = Field(
546
+ default=0.0,
547
+ description="Minimum LR as fraction of peak LR at end of training (0.0 = decay to 0). Only for cosineWarmup."
548
+ )
549
+
550
+ @validator('warmup_ratio')
551
+ def validate_warmup_ratio(cls, v):
552
+ """Validate warmup ratio is between 0 and 1."""
553
+ if not 0 <= v <= 1:
554
+ raise ValueError("warmup_ratio must be between 0 and 1")
555
+ return v
556
+
557
+ @validator('steady_ratio')
558
+ def validate_steady_ratio(cls, v):
559
+ """Validate steady ratio is between 0 and 1."""
560
+ if not 0 <= v <= 1:
561
+ raise ValueError("steady_ratio must be between 0 and 1")
562
+ return v
563
+
564
+ @validator('min_lr_ratio')
565
+ def validate_min_lr_ratio(cls, v):
566
+ """Validate min_lr_ratio is between 0 and 1."""
567
+ if not 0 <= v <= 1:
568
+ raise ValueError("min_lr_ratio must be between 0 and 1")
569
+ return v
570
+
571
+ @validator('warmup_steps')
572
+ def validate_warmup_steps(cls, v):
573
+ """Validate warmup steps is non-negative."""
574
+ if v < 0:
575
+ raise ValueError("warmup_steps must be non-negative")
576
+ return v
577
+
578
+
579
+ class TrainingConfig(BaseConfig):
580
+ """Base training configuration shared across all modes."""
581
+
582
+ # Data and model
583
+ model: ModelConfig = Field(default_factory=ModelConfig, description="Model configuration")
584
+ dataset: DatasetConfig = Field(description="Dataset configuration")
585
+
586
+ # Training hyperparameters
587
+ batch_size: int = Field(default=32, description="Batch size per device")
588
+ num_epochs: int = Field(default=3, description="Number of training epochs")
589
+ max_steps: Optional[int] = Field(
590
+ default=None,
591
+ description="Maximum steps (overrides num_epochs if set)"
592
+ )
593
+ gradient_accumulation_steps: int = Field(
594
+ default=1,
595
+ description="Gradient accumulation steps"
596
+ )
597
+ max_grad_norm: float = Field(default=1.0, description="Gradient clipping max norm")
598
+
599
+ # Optimizer
600
+ optimizer: OptimizerConfig = Field(
601
+ default_factory=OptimizerConfig,
602
+ description="Optimizer configuration"
603
+ )
604
+
605
+ # Scheduler
606
+ scheduler: SchedulerConfig = Field(
607
+ default_factory=SchedulerConfig,
608
+ description="Learning rate scheduler configuration"
609
+ )
610
+
611
+ # Data type and device
612
+ dtype: DataTypeEnum = Field(
613
+ default=DataTypeEnum.BFLOAT16,
614
+ description="Training data type"
615
+ )
616
+ device: str = Field(default="cuda", description="Device to train on (cuda, cpu)")
617
+ seed: int = Field(default=42, description="Random seed")
618
+
619
+ # Checkpointing
620
+ checkpoint_dir: str = Field(default="checkpoints", description="Directory to save checkpoints")
621
+ checkpoint_path: Optional[str] = Field(
622
+ default=None,
623
+ description="Path to load pretrained checkpoint (for SFT/RL). If provided, loads weights before training starts."
624
+ )
625
+ save_every_steps: int = Field(default=500, description="Save checkpoint every N steps")
626
+ keep_last_n_checkpoints: int = Field(default=3, description="Keep only last N checkpoints")
627
+ save_best_model: bool = Field(default=True, description="Save best model based on validation loss")
628
+
629
+ # Validation
630
+ eval_every_steps: int = Field(default=500, description="Evaluate every N steps")
631
+ eval_samples: int = Field(default=1000, description="Number of validation samples")
632
+
633
+ # Logging
634
+ log_every_steps: int = Field(default=10, description="Log metrics every N steps")
635
+ aim_repo: str = Field(default=".aim", description="AimStack repository path")
636
+
637
+ # Misc
638
+ num_workers: int = Field(default=0, description="Number of DataLoader workers")
639
+ pin_memory: bool = Field(default=True, description="Pin memory for DataLoader")
640
+ use_compile: bool = Field(default=False, description="Use torch.compile (experimental)")
641
+
642
+ # Mode
643
+ mode: TrainingModeEnum = Field(default=TrainingModeEnum.PRETRAIN, description="Training mode")
644
+
645
+
646
+ # ============================================================================
647
+ # Stage-Specific Configs
648
+ # ============================================================================
649
+
650
+
651
+ class PretrainConfig(TrainingConfig):
652
+ """Configuration for pretraining."""
653
+
654
+ mode: Literal[TrainingModeEnum.PRETRAIN] = TrainingModeEnum.PRETRAIN
655
+
656
+ # Pretraining-specific
657
+ sequence_length: int = Field(default=1024, description="Sequence length for pretraining")
658
+
659
+
660
+ class SFTConfig(TrainingConfig):
661
+ """Configuration for supervised fine-tuning."""
662
+
663
+ mode: Literal[TrainingModeEnum.SFT] = TrainingModeEnum.SFT
664
+
665
+ # SFT-specific
666
+ response_loss_only: bool = Field(
667
+ default=True,
668
+ description="Only compute loss on response/assistant tokens (not instruction/user tokens). Uses -100 label masking."
669
+ )
670
+
671
+ # Multi-turn conversation role tokens
672
+ user_token: str = Field(
673
+ default="<user>",
674
+ description="Special token representing user/instruction role in conversations"
675
+ )
676
+ assistant_token: str = Field(
677
+ default="<assistant>",
678
+ description="Special token representing assistant/response role in conversations"
679
+ )
680
+
681
+
682
+ class RLConfig(TrainingConfig):
683
+ """Configuration for reinforcement learning training."""
684
+
685
+ mode: Literal[TrainingModeEnum.RL] = TrainingModeEnum.RL
686
+
687
+ # RL-specific
688
+ rl_method: RLMethodEnum = Field(
689
+ default=RLMethodEnum.PPO,
690
+ description="RL training method (PPO or DPO)"
691
+ )
692
+
693
+ # Reward model
694
+ reward_model_path: str = Field(description="Path to trained reward model checkpoint")
695
+
696
+ # PPO-specific
697
+ ppo_epochs: int = Field(default=4, description="PPO inner epochs")
698
+ ppo_clip_ratio: float = Field(default=0.2, description="PPO clipping ratio")
699
+ entropy_coeff: float = Field(default=0.01, description="Entropy bonus coefficient")
700
+ value_loss_coeff: float = Field(default=1.0, description="Value function loss coefficient")
701
+
702
+ # DPO-specific (Direct Preference Optimization)
703
+ dpo_beta: float = Field(default=0.1, description="DPO inverse temperature (beta)")
704
+
705
+ # Prompt distribution
706
+ prompt_dataset: Optional[DatasetConfig] = Field(
707
+ default=None,
708
+ description="Separate dataset for prompts (if different from main dataset)"
709
+ )
710
+ generation_max_length: int = Field(
711
+ default=256,
712
+ description="Maximum length for generated responses during RL"
713
+ )
714
+
715
+
716
+ class VLMConfig(TrainingConfig):
717
+ """Configuration for multimodal vision-language connector training."""
718
+
719
+ mode: Literal[TrainingModeEnum.VLM] = TrainingModeEnum.VLM
720
+
721
+ response_loss_only: bool = Field(
722
+ default=True,
723
+ description="Only compute loss on assistant/response tokens for multimodal samples."
724
+ )
725
+ user_token: str = Field(
726
+ default="<user>",
727
+ description="Special token representing user/instruction role in multimodal conversations."
728
+ )
729
+ assistant_token: str = Field(
730
+ default="<assistant>",
731
+ description="Special token representing assistant/response role in multimodal conversations."
732
+ )
733
+ freeze_llm: bool = Field(
734
+ default=True,
735
+ description="Freeze the LLM except for any explicitly unfrozen trailing layers."
736
+ )
737
+ unfreeze_last_n_layers: int = Field(
738
+ default=0,
739
+ description="Number of final LLM blocks to unfreeze during VLM connector training."
740
+ )
741
+ vision_learning_rate: float = Field(
742
+ default=1e-4,
743
+ description="Learning rate for the vision encoder and multimodal projector."
744
+ )
745
+ llm_learning_rate: float = Field(
746
+ default=5e-5,
747
+ description="Learning rate for the unfrozen LLM parameter subset."
748
+ )
749
+ vision_prefix_tokens: int = Field(
750
+ default=10,
751
+ description="Number of visual prefix tokens to inject in place of a single image placeholder token."
752
+ )
753
+ image_token: str = Field(
754
+ default="<image>",
755
+ description="Tokenizer token used as the multimodal image placeholder."
756
+ )
757
+ image_size: int = Field(
758
+ default=224,
759
+ description="Square image size used for multimodal preprocessing."
760
+ )
761
+
762
+ @validator("image_token")
763
+ def validate_image_token_matches_model_config(cls, v, values):
764
+ """Validate duplicated image token settings stay aligned."""
765
+ model = values.get("model")
766
+ if model is not None and getattr(model, "image_token", None) != v:
767
+ raise ValueError(
768
+ f"image_token must match model.image_token for VLM configs. "
769
+ f"Got top-level `{v}` and model `{model.image_token}`."
770
+ )
771
+ return v
772
+
773
+ @validator("vision_prefix_tokens")
774
+ def validate_vision_prefix_tokens_match_model_config(cls, v, values):
775
+ """Validate duplicated visual prefix settings stay aligned."""
776
+ model = values.get("model")
777
+ if model is not None and getattr(model, "vision_prefix_tokens", None) != v:
778
+ raise ValueError(
779
+ "vision_prefix_tokens must match model.vision_prefix_tokens for VLM configs. "
780
+ f"Got top-level `{v}` and model `{model.vision_prefix_tokens}`."
781
+ )
782
+ return v
783
+
784
+ @validator("image_size")
785
+ def validate_image_size_matches_model_config(cls, v, values):
786
+ """Validate duplicated image size settings stay aligned."""
787
+ model = values.get("model")
788
+ if model is not None and getattr(model, "image_size", None) != v:
789
+ raise ValueError(
790
+ f"image_size must match model.image_size for VLM configs. "
791
+ f"Got top-level `{v}` and model `{model.image_size}`."
792
+ )
793
+ return v
794
+
795
+ @validator("unfreeze_last_n_layers")
796
+ def validate_unfreeze_last_n_layers(cls, v):
797
+ """Validate unfrozen layer count."""
798
+ if v < 0:
799
+ raise ValueError("unfreeze_last_n_layers must be non-negative")
800
+ return v
801
+
802
+ @validator("vision_learning_rate", "llm_learning_rate")
803
+ def validate_multimodal_learning_rates(cls, v):
804
+ """Validate multimodal learning rates."""
805
+ if v <= 0:
806
+ raise ValueError("multimodal learning rates must be greater than 0")
807
+ return v
808
+
809
+ @validator("vision_prefix_tokens")
810
+ def validate_vision_prefix_tokens(cls, v):
811
+ """Validate visual prefix length."""
812
+ if v < 1:
813
+ raise ValueError("vision_prefix_tokens must be at least 1")
814
+ return v
815
+
816
+ @validator("image_size")
817
+ def validate_image_size(cls, v):
818
+ """Validate image size."""
819
+ if v < 8:
820
+ raise ValueError("image_size must be at least 8")
821
+ return v
822
+
823
+
824
+ class VLMSFTConfig(VLMConfig):
825
+ """Configuration for end-to-end multimodal supervised fine-tuning."""
826
+
827
+ mode: Literal[TrainingModeEnum.VLM_SFT] = TrainingModeEnum.VLM_SFT
828
+ freeze_llm: bool = Field(
829
+ default=False,
830
+ description="Whether to freeze the LLM during end-to-end multimodal SFT."
831
+ )
832
+
833
+
834
+ # ============================================================================
835
+ # Factory function
836
+ # ============================================================================
837
+
838
+
839
+ def load_config(path: str | Path, mode: TrainingModeEnum | str) -> TrainingConfig:
840
+ """Load config file and return appropriate config class."""
841
+ if isinstance(mode, str):
842
+ mode = TrainingModeEnum(mode)
843
+
844
+ config_map = {
845
+ TrainingModeEnum.PRETRAIN: PretrainConfig,
846
+ TrainingModeEnum.SFT: SFTConfig,
847
+ TrainingModeEnum.RL: RLConfig,
848
+ TrainingModeEnum.VLM: VLMConfig,
849
+ TrainingModeEnum.VLM_SFT: VLMSFTConfig,
850
+ }
851
+
852
+ config_class = config_map[mode]
853
+
854
+ path = Path(path)
855
+ if path.suffix == '.yaml' or path.suffix == '.yml':
856
+ return config_class.load_yaml(path)
857
+ elif path.suffix == '.json':
858
+ return config_class.load_json(path)
859
+ else:
860
+ raise ValueError(f"Unsupported config file format: {path.suffix}")
861
+
862
+
863
+ def load_tokenizer_config(path: str | Path) -> TokenizerConfig:
864
+ """Load tokenizer config from YAML or JSON file."""
865
+ path = Path(path)
866
+ if path.suffix == '.yaml' or path.suffix == '.yml':
867
+ return TokenizerConfig.load_yaml(path)
868
+ elif path.suffix == '.json':
869
+ return TokenizerConfig.load_json(path)
870
+ else:
871
+ raise ValueError(f"Unsupported config file format: {path.suffix}")
src/taoTrain/core/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Base classes for models, trainers, and datasets."""
2
+
3
+ from .base import BaseModel, BaseTrainer, BaseDataset, create_model, create_datasets
4
+
5
+ __all__ = ["BaseModel", "BaseTrainer", "BaseDataset", "create_model", "create_datasets"]
src/taoTrain/core/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (366 Bytes). View file
 
src/taoTrain/core/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (351 Bytes). View file
 
src/taoTrain/core/__pycache__/__init__.cpython-314.pyc ADDED
Binary file (348 Bytes). View file
 
src/taoTrain/core/__pycache__/base.cpython-310.pyc ADDED
Binary file (8.69 kB). View file
 
src/taoTrain/core/__pycache__/base.cpython-312.pyc ADDED
Binary file (11.5 kB). View file
 
src/taoTrain/core/__pycache__/base.cpython-314.pyc ADDED
Binary file (14.5 kB). View file
 
src/taoTrain/core/base.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Base classes for models, trainers, and datasets."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+ from typing import Optional, Any, Iterator
6
+ import torch
7
+ import torch.nn as nn
8
+ from torch.utils.data import Dataset as TorchDataset
9
+ from taoTrain.config import TrainingConfig, ModelConfig, TrainingModeEnum
10
+
11
+
12
+ # ============================================================================
13
+ # Base Model
14
+ # ============================================================================
15
+
16
+
17
+ class BaseModel(nn.Module, ABC):
18
+ """Abstract base class for language models."""
19
+
20
+ def __init__(self, config: Any):
21
+ """Initialize model with config."""
22
+ super().__init__()
23
+ self.config = config
24
+
25
+ @abstractmethod
26
+ def forward(
27
+ self,
28
+ input_ids: Optional[torch.Tensor] = None,
29
+ attention_mask: Optional[torch.Tensor] = None,
30
+ labels: Optional[torch.Tensor] = None,
31
+ inputs_embeds: Optional[torch.Tensor] = None,
32
+ pixel_values: Optional[torch.Tensor] = None,
33
+ ) -> dict[str, torch.Tensor]:
34
+ """
35
+ Forward pass.
36
+
37
+ Args:
38
+ input_ids: Shape (batch_size, seq_length)
39
+ attention_mask: Shape (batch_size, seq_length), optional
40
+ labels: Shape (batch_size, seq_length), optional (for loss computation)
41
+
42
+ Returns:
43
+ Dict with keys:
44
+ - 'logits': Shape (batch_size, seq_length, vocab_size)
45
+ - 'loss': Scalar (if labels provided)
46
+ """
47
+ pass
48
+
49
+ def count_parameters(self) -> int:
50
+ """Count total trainable parameters."""
51
+ return sum(p.numel() for p in self.parameters() if p.requires_grad)
52
+
53
+ def get_num_layers(self) -> int:
54
+ """Get number of layers (for model architecture)."""
55
+ return self.config.num_layers
56
+
57
+ def get_optimizer_param_groups(self, config: TrainingConfig) -> Optional[list[dict[str, Any]]]:
58
+ """Optional custom optimizer param groups for advanced training setups."""
59
+ return None
60
+
61
+
62
+ # ============================================================================
63
+ # Base Dataset
64
+ # ============================================================================
65
+
66
+
67
+ class BaseDataset(TorchDataset, ABC):
68
+ """Abstract base class for datasets."""
69
+
70
+ def __init__(self, config: "TrainingConfig"):
71
+ """Initialize dataset."""
72
+ self.config = config
73
+ self.data = None
74
+
75
+ @abstractmethod
76
+ def __len__(self) -> int:
77
+ """Return dataset size."""
78
+ pass
79
+
80
+ @abstractmethod
81
+ def __getitem__(self, idx: int) -> dict[str, torch.Tensor]:
82
+ """
83
+ Get a single sample.
84
+
85
+ Returns:
86
+ Dict with keys:
87
+ - 'input_ids': 1D tensor of token IDs
88
+ - 'attention_mask': 1D tensor of attention mask
89
+ - 'labels': 1D tensor of labels (optional)
90
+ """
91
+ pass
92
+
93
+ def load_dataset(self) -> None:
94
+ """Load dataset from HuggingFace or other source."""
95
+ pass
96
+
97
+ def preprocess(self) -> None:
98
+ """Preprocess dataset (tokenization, etc)."""
99
+ pass
100
+
101
+
102
+ # ============================================================================
103
+ # Base Trainer
104
+ # ============================================================================
105
+
106
+
107
+ class BaseTrainer(ABC):
108
+ """Abstract base class for trainers."""
109
+
110
+ def __init__(
111
+ self,
112
+ model: BaseModel,
113
+ train_dataset: BaseDataset,
114
+ val_dataset: Optional[BaseDataset],
115
+ config: TrainingConfig,
116
+ device: torch.device,
117
+ ):
118
+ """Initialize trainer."""
119
+ self.model = model.to(device)
120
+ self.train_dataset = train_dataset
121
+ self.val_dataset = val_dataset
122
+ self.config = config
123
+ self.device = device
124
+
125
+ # Training state
126
+ self.global_step = 0
127
+ self.current_epoch = 0
128
+ self.best_loss = float('inf')
129
+
130
+ # Logging
131
+ self.logger = None
132
+
133
+ # Optimizer and scheduler (to be set up by subclass)
134
+ self.optimizer = None
135
+ self.scheduler = None
136
+
137
+ @abstractmethod
138
+ def training_step(self, batch: dict[str, torch.Tensor]) -> dict[str, float]:
139
+ """
140
+ Single training step.
141
+
142
+ Args:
143
+ batch: Training batch with input_ids, attention_mask, labels, etc.
144
+
145
+ Returns:
146
+ Dict with metrics (e.g., {'loss': 0.5, 'accuracy': 0.8})
147
+ """
148
+ pass
149
+
150
+ @abstractmethod
151
+ def validation_step(self, batch: dict[str, torch.Tensor]) -> dict[str, float]:
152
+ """
153
+ Single validation step.
154
+
155
+ Args:
156
+ batch: Validation batch
157
+
158
+ Returns:
159
+ Dict with validation metrics
160
+ """
161
+ pass
162
+
163
+ @abstractmethod
164
+ def train_epoch(self) -> dict[str, float]:
165
+ """
166
+ Train for one epoch.
167
+
168
+ Returns:
169
+ Dict with epoch-level metrics
170
+ """
171
+ pass
172
+
173
+ @abstractmethod
174
+ def validate(self) -> dict[str, float]:
175
+ """
176
+ Run validation on the entire validation set.
177
+
178
+ Returns:
179
+ Dict with validation metrics
180
+ """
181
+ pass
182
+
183
+ def save_checkpoint(self, path: str | Path) -> None:
184
+ """
185
+ Save checkpoint in canonical format.
186
+
187
+ Uses canonical checkpoint format:
188
+ {
189
+ 'step': int,
190
+ 'model_state': state_dict,
191
+ 'optimizer_state': state_dict,
192
+ 'config': dict,
193
+ 'metrics': dict,
194
+ 'global_step': int, # Legacy compat
195
+ 'current_epoch': int, # Legacy compat
196
+ 'best_loss': float, # Legacy compat
197
+ }
198
+
199
+ Args:
200
+ path: Path to save checkpoint
201
+ """
202
+ path = Path(path)
203
+ path.parent.mkdir(parents=True, exist_ok=True)
204
+
205
+ # Save in canonical format
206
+ checkpoint = {
207
+ # Canonical format keys
208
+ 'step': self.global_step,
209
+ 'model_state': self.model.state_dict(),
210
+ 'optimizer_state': self.optimizer.state_dict() if self.optimizer else None,
211
+ 'config': self.config.to_dict(),
212
+ 'metrics': {},
213
+ # Legacy format keys (for backward compatibility with code that reads them)
214
+ 'global_step': self.global_step,
215
+ 'current_epoch': self.current_epoch,
216
+ 'best_loss': self.best_loss,
217
+ }
218
+
219
+ torch.save(checkpoint, path)
220
+
221
+ def load_checkpoint(self, path: str | Path) -> None:
222
+ """
223
+ Load checkpoint (handles both canonical and legacy formats).
224
+
225
+ Args:
226
+ path: Path to checkpoint
227
+ """
228
+ path = Path(path)
229
+ checkpoint = torch.load(path, map_location=self.device)
230
+
231
+ # Try canonical keys first, fall back to legacy keys
232
+ model_state_key = 'model_state' if 'model_state' in checkpoint else 'model_state_dict'
233
+ optimizer_state_key = 'optimizer_state' if 'optimizer_state' in checkpoint else 'optimizer_state_dict'
234
+
235
+ self.model.load_state_dict(checkpoint[model_state_key])
236
+ if self.optimizer and checkpoint.get(optimizer_state_key):
237
+ self.optimizer.load_state_dict(checkpoint[optimizer_state_key])
238
+
239
+ # Try canonical 'step' first, fall back to legacy 'global_step'
240
+ self.global_step = checkpoint.get('step', checkpoint.get('global_step', 0))
241
+ self.current_epoch = checkpoint.get('current_epoch', 0)
242
+ self.best_loss = checkpoint.get('best_loss', float('inf'))
243
+
244
+ def _get_lr(self) -> float:
245
+ """Get current learning rate from optimizer."""
246
+ for param_group in self.optimizer.param_groups:
247
+ return param_group['lr']
248
+ return 0.0
249
+
250
+
251
+ # ============================================================================
252
+ # Utility functions
253
+ # ============================================================================
254
+
255
+
256
+ def create_model(config: TrainingConfig, device: torch.device) -> BaseModel:
257
+ """Create model from config (calls registry)."""
258
+ from taoTrain.models import get_model
259
+ if config.mode in {TrainingModeEnum.VLM, TrainingModeEnum.VLM_SFT}:
260
+ return get_model(config, device=device)
261
+ return get_model(config.model, device=device)
262
+
263
+
264
+ def create_datasets(
265
+ config: TrainingConfig,
266
+ ) -> tuple[BaseDataset, Optional[BaseDataset]]:
267
+ """Create train and validation datasets using factory pattern."""
268
+ # Import here to avoid circular imports
269
+ from taoTrain.data import DatasetFactory
270
+
271
+ # Create train dataset
272
+ train_dataset = DatasetFactory.create_dataset(config, split="train")
273
+
274
+ # Create validation dataset (only for HuggingFace datasets with explicit validation split)
275
+ val_dataset = None
276
+ if not config.dataset.local and hasattr(config.dataset, "validation_split"):
277
+ val_dataset = DatasetFactory.create_dataset(config, split="validation")
278
+
279
+ return train_dataset, val_dataset
src/taoTrain/data/__init__.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dataset implementations and loaders."""
2
+
3
+ # HuggingFace-based datasets
4
+ from .hf_base import BaseHFDataset
5
+ from .hf_pretrain import PretrainDataset
6
+ from .hf_sft import SFTDataset
7
+ from .hf_rl import RLDataset
8
+
9
+ # JSONL-based datasets (async-only)
10
+ from .jsonl_base import BaseJSONLDataset
11
+ from .pretrain_jsonl import PretrainJSONLDataset
12
+ from .sft_jsonl import SFTJSONLDataset
13
+ from .rl_jsonl import RLJSONLDataset
14
+ from .vlm_jsonl import VLMJSONLDataset, VLMSFTJSONLDataset
15
+
16
+ # Utilities
17
+ from .tokenizer import SentencePieceTokenizerWrapper
18
+ from .sft_utils import parse_sft_record, build_sft_sequence_tokens, apply_response_masking
19
+ from .loaders import get_dataloader
20
+ from .async_loader import AsyncBatchIterator
21
+ from .tokenization_queue import TokenizationQueue
22
+ from .factory import DatasetFactory
23
+
24
+ __all__ = [
25
+ # HuggingFace datasets
26
+ "BaseHFDataset",
27
+ "PretrainDataset",
28
+ "SFTDataset",
29
+ "RLDataset",
30
+ # JSONL datasets
31
+ "BaseJSONLDataset",
32
+ "PretrainJSONLDataset",
33
+ "SFTJSONLDataset",
34
+ "RLJSONLDataset",
35
+ "VLMJSONLDataset",
36
+ "VLMSFTJSONLDataset",
37
+ # Utilities
38
+ "SentencePieceTokenizerWrapper",
39
+ "parse_sft_record",
40
+ "build_sft_sequence_tokens",
41
+ "apply_response_masking",
42
+ # Data loading
43
+ "get_dataloader",
44
+ "AsyncBatchIterator",
45
+ "TokenizationQueue",
46
+ "DatasetFactory",
47
+ ]
src/taoTrain/data/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (1.16 kB). View file
 
src/taoTrain/data/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (1.17 kB). View file