arthu1 commited on
Commit
d3ab6f5
·
verified ·
1 Parent(s): c891e3b

Publish Vortex Alpha research preview

Browse files
Aurora-5.png ADDED
README.md ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language: en
3
+ pipeline_tag: text-generation
4
+ library_name: pytorch
5
+ tags:
6
+ - causal-lm
7
+ - small-language-model
8
+ - research
9
+ - text-generation
10
+ license: other
11
+ thumbnail: Aurora-5.png
12
+ ---
13
+
14
+ # Vortex Alpha
15
+
16
+ Vortex is the working name for a compact, experimental language model. The
17
+ final public name has not been decided. This release is intended for research,
18
+ local experimentation, and further fine-tuning—not as a finished general
19
+ assistant.
20
+
21
+ ## What is included
22
+
23
+ - `model.safetensors`: the instruction/tool-format preview selected from the
24
+ best small internal behavior pilot.
25
+ - `base_model.safetensors`: the corresponding pretrained text-completion
26
+ base.
27
+ - `config.json`: the architecture configuration.
28
+ - `tokenizer.model`: the 8,192-piece SentencePiece tokenizer used for both
29
+ checkpoints.
30
+ - `vortex_model.py` and `inference.py`: a minimal dependency-light PyTorch
31
+ loader and sampler.
32
+ - `Aurora-5.png`: the project thumbnail/banner.
33
+
34
+ Optimizer state, private logs, local paths, credentials, and training-machine
35
+ metadata are intentionally not included.
36
+
37
+ ## Architecture
38
+
39
+ Vortex is a dense decoder-only Transformer with 174,942,720 trainable
40
+ parameters:
41
+
42
+ | Component | Parameters |
43
+ | --- | ---: |
44
+ | Shared token embedding and tied output head | 8,388,608 |
45
+ | Attention Q projections | 12,582,912 |
46
+ | Attention K projections | 3,145,728 |
47
+ | Attention V projections | 3,145,728 |
48
+ | Attention output projections | 12,582,912 |
49
+ | Per-head QK RMSNorm parameters | 1,536 |
50
+ | SwiGLU feed-forward networks | 135,069,696 |
51
+ | Transformer-block RMSNorm parameters | 24,576 |
52
+ | Final RMSNorm | 1,024 |
53
+ | **Total** | **174,942,720** |
54
+
55
+ Configuration: 12 layers, hidden size 1,024, 16 query heads, 4 key/value
56
+ heads, 64-dimensional heads, SwiGLU with intermediate size 3,664, pre-layer
57
+ RMSNorm, per-head QK-Norm, RoPE with base 100,000, bias-free projections,
58
+ 8,192-token vocabulary, and a 4,096-token training/inference limit.
59
+
60
+ The published weights use the readable reference PyTorch layout. They do not
61
+ require Transformer Engine to load. The input and output embeddings are tied.
62
+
63
+ ## Quick start
64
+
65
+ The minimal reference runner uses PyTorch, SentencePiece, and
66
+ `safetensors`:
67
+
68
+ ```bash
69
+ python -m pip install torch sentencepiece safetensors
70
+ python inference.py \
71
+ --weights model.safetensors \
72
+ --chat \
73
+ --prompt "Explain why the sky appears blue in two short paragraphs."
74
+ ```
75
+
76
+ For ordinary next-token completion, use the base checkpoint:
77
+
78
+ ```bash
79
+ python inference.py \
80
+ --weights base_model.safetensors \
81
+ --prompt "The sky appears blue because"
82
+ ```
83
+
84
+ The reference runner recomputes the full prefix at each generated token and
85
+ is deliberately simple. A production runner should add a KV cache and a
86
+ fused attention implementation.
87
+
88
+ The instruction preview was tuned with this compact serialization:
89
+
90
+ ```text
91
+ [SYSTEM]
92
+ You are a helpful assistant. Follow instructions, answer clearly, and say
93
+ when information is missing.
94
+ </s>
95
+ [USER]
96
+ Your question here
97
+ </s>
98
+ [ASSISTANT]
99
+ ```
100
+
101
+ The instruction preview may emit a `CALL {json}` calculator/search request
102
+ when prompted for tool use. No tool server is included in this repository;
103
+ without a tool runner, treat such output as ordinary text. The base checkpoint
104
+ is the better starting point for continued pretraining.
105
+
106
+ ## Training summary
107
+
108
+ The base run used an approximate mixture of FineWeb, DCLM, educational/math
109
+ material, and The Stack v3 code data. The recorded base checkpoint had seen
110
+ about 8.48 billion pretraining tokens. Training used BF16 model computation,
111
+ FP8-capable NVIDIA kernels where available, a WSD-style learning-rate tail,
112
+ and token-budgeted batches designed for a 16 GB consumer GPU. The instruction
113
+ preview is a lightweight supervised derivative of that base; its optimizer
114
+ state and private training records are not part of this release.
115
+
116
+ These data-mixture descriptions are a project-level summary, not a claim that
117
+ every upstream document is suitable for every downstream use. Follow the
118
+ licenses and terms of the upstream datasets.
119
+
120
+ ## Evaluation snapshot
121
+
122
+ These are exploratory measurements, not official leaderboard submissions.
123
+ The base results used greedy decoding, no tools, and the stated sample sizes:
124
+
125
+ | Test | Result | Notes |
126
+ | --- | ---: | --- |
127
+ | MMLU cloze sample | 511/2,000 = 25.55% | Wilson 95% interval: 23.69–27.51% |
128
+ | GSM8K strict numeric sample | 0/256 = 0.00% | Wilson 95% upper bound: 1.48% |
129
+ | GSM8K fallback numeric sample | 2/256 = 0.78% | Wilson 95% interval: 0.21–2.80% |
130
+
131
+ The instruction checkpoint reached 9/12 arithmetic, 4/4 grounding, 4/4
132
+ abstention, 2/4 exact-format, and 1/2 JSON checks on a 26-prompt internal
133
+ tool-format pilot when the calculator runner was available. That pilot is too
134
+ small to support general capability claims, and the arithmetic result is not
135
+ comparable to tool-free GSM8K.
136
+
137
+ The results show why this is an alpha release: the model can produce useful
138
+ local completions and structured tool calls, but it remains weak at reliable
139
+ arithmetic, broad knowledge, long-form coherence, and hallucination control.
140
+
141
+ ## Limitations and intended use
142
+
143
+ Vortex is a small research model. It can be repetitive, overconfident, or
144
+ factually wrong; architecture and training scale do not guarantee reliable
145
+ answers. Do not use it as the sole basis for medical, legal, financial,
146
+ safety-critical, or other high-stakes decisions. It has not been evaluated for
147
+ privacy, bias, cybersecurity, or comprehensive safety.
148
+
149
+ The repository is public, but no open-source license is asserted yet. The
150
+ final name, licensing terms, and a production release decision are still open.
151
+
152
+ ## Reproducibility note
153
+
154
+ The conversion removed optimizer state and Transformer Engine-only auxiliary
155
+ state, then wrote the model tensors in BF16 safetensors format. The exported
156
+ reference tensors preserve the tied-embedding model weights and can be loaded
157
+ with the included `vortex_model.py`.
RELEASE_MANIFEST.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "Vortex Alpha",
3
+ "name_status": "working name; final public name undecided",
4
+ "repo_id": "North-ML1/vortex-alpha",
5
+ "parameter_count": 174942720,
6
+ "tensor_dtype": "bfloat16",
7
+ "architecture": {
8
+ "layers": 12,
9
+ "hidden_size": 1024,
10
+ "attention_heads": 16,
11
+ "kv_heads": 4,
12
+ "head_dim": 64,
13
+ "ffn_intermediate_size": 3664,
14
+ "vocab_size": 8192,
15
+ "context_length": 4096
16
+ },
17
+ "files": {
18
+ "model.safetensors": {
19
+ "bytes": 349898992,
20
+ "sha256": "330ae517cc9bd14d055f1bbde3b330b6d3206a8740bb4ef032a771af708a96b7"
21
+ },
22
+ "base_model.safetensors": {
23
+ "bytes": 349898984,
24
+ "sha256": "a00ad274d9e5b8608f69176937a47b4c55499bc810cb0311c4dc93cae1a890c3"
25
+ },
26
+ "tokenizer.model": {
27
+ "bytes": 123865,
28
+ "sha256": "8ebb450f2786d3153d64f874d97487ba5cb8e3450b799bbf6aeddd7d92f7ff73"
29
+ },
30
+ "Aurora-5.png": {
31
+ "bytes": 96932,
32
+ "sha256": "d444edf5ecab1a42e54719881182ba45627e268dff6b6b55fa1dff39f06d5ead"
33
+ }
34
+ }
35
+ }
base_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a00ad274d9e5b8608f69176937a47b4c55499bc810cb0311c4dc93cae1a890c3
3
+ size 349898984
config.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "vortex",
3
+ "architectures": ["VortexForCausalLM"],
4
+ "vocab_size": 8192,
5
+ "hidden_size": 1024,
6
+ "num_hidden_layers": 12,
7
+ "num_attention_heads": 16,
8
+ "num_key_value_heads": 4,
9
+ "head_dim": 64,
10
+ "intermediate_size": 3664,
11
+ "hidden_act": "silu",
12
+ "max_position_embeddings": 4096,
13
+ "rope_theta": 100000.0,
14
+ "rms_norm_eps": 1e-5,
15
+ "tie_word_embeddings": true,
16
+ "attention_bias": false,
17
+ "torch_dtype": "bfloat16"
18
+ }
generation_config.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "eos_token_id": 1,
3
+ "do_sample": true,
4
+ "temperature": 0.8,
5
+ "top_p": 0.95,
6
+ "max_new_tokens": 256
7
+ }
inference.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal reference sampler for the public Vortex Alpha release."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+
9
+ import torch
10
+ from safetensors.torch import load_file
11
+
12
+ from vortex_model import VortexConfig, VortexForCausalLM
13
+
14
+
15
+ def load_config(path: Path) -> VortexConfig:
16
+ data = json.loads(path.read_text())
17
+ return VortexConfig(
18
+ vocab_size=int(data["vocab_size"]),
19
+ max_seq_len=int(data["max_position_embeddings"]),
20
+ n_layer=int(data["num_hidden_layers"]),
21
+ n_embd=int(data["hidden_size"]),
22
+ n_head=int(data["num_attention_heads"]),
23
+ n_kv_head=int(data["num_key_value_heads"]),
24
+ head_dim=int(data["head_dim"]),
25
+ intermediate_size=int(data["intermediate_size"]),
26
+ rope_theta=float(data["rope_theta"]),
27
+ norm_eps=float(data["rms_norm_eps"]),
28
+ logits_chunk_tokens=8192,
29
+ gradient_checkpointing=False,
30
+ use_transformer_engine=False,
31
+ attn_input_format="bshd",
32
+ )
33
+
34
+
35
+ def choose_device(requested: str) -> torch.device:
36
+ if requested != "auto":
37
+ return torch.device(requested)
38
+ if torch.cuda.is_available():
39
+ return torch.device("cuda")
40
+ if getattr(torch.backends, "mps", None) is not None and torch.backends.mps.is_available():
41
+ return torch.device("mps")
42
+ return torch.device("cpu")
43
+
44
+
45
+ def sample_next(logits: torch.Tensor, temperature: float, top_p: float) -> torch.Tensor:
46
+ if temperature <= 0:
47
+ return logits.argmax(dim=-1, keepdim=True)
48
+ logits = logits / temperature
49
+ if 0 < top_p < 1:
50
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True, dim=-1)
51
+ sorted_probs = torch.softmax(sorted_logits, dim=-1)
52
+ cumulative = sorted_probs.cumsum(dim=-1)
53
+ remove = cumulative - sorted_probs > top_p
54
+ sorted_logits = sorted_logits.masked_fill(remove, float("-inf"))
55
+ logits = torch.full_like(logits, float("-inf"))
56
+ logits.scatter_(dim=-1, index=sorted_indices, src=sorted_logits)
57
+ return torch.multinomial(torch.softmax(logits, dim=-1), num_samples=1)
58
+
59
+
60
+ def main() -> None:
61
+ parser = argparse.ArgumentParser()
62
+ parser.add_argument("--weights", type=Path, default=Path("model.safetensors"))
63
+ parser.add_argument("--config", type=Path, default=Path("config.json"))
64
+ parser.add_argument("--tokenizer", type=Path, default=Path("tokenizer.model"))
65
+ parser.add_argument("--prompt", required=True)
66
+ parser.add_argument("--chat", action="store_true")
67
+ parser.add_argument(
68
+ "--system",
69
+ default="You are a helpful assistant. Answer clearly and say when information is missing.",
70
+ )
71
+ parser.add_argument("--max-new-tokens", type=int, default=128)
72
+ parser.add_argument("--temperature", type=float, default=0.0)
73
+ parser.add_argument("--top-p", type=float, default=0.95)
74
+ parser.add_argument("--device", default="auto")
75
+ args = parser.parse_args()
76
+
77
+ import sentencepiece as spm
78
+
79
+ device = choose_device(args.device)
80
+ dtype = torch.bfloat16 if device.type == "cuda" else torch.float32
81
+ config = load_config(args.config)
82
+ model = VortexForCausalLM(config).to(device=device, dtype=dtype)
83
+ state = load_file(str(args.weights), device="cpu")
84
+ model.load_state_dict(state, strict=True)
85
+ model.eval()
86
+
87
+ prompt = args.prompt
88
+ if args.chat:
89
+ prompt = f"[SYSTEM]\n{args.system}\n</s>\n[USER]\n{prompt}\n</s>\n[ASSISTANT]\n"
90
+ tokenizer = spm.SentencePieceProcessor(model_file=str(args.tokenizer))
91
+ ids = tokenizer.encode(prompt, out_type=int)
92
+ if not ids:
93
+ raise ValueError("prompt encoded to zero tokens")
94
+ if len(ids) >= config.max_seq_len:
95
+ raise ValueError("prompt reaches the configured context limit")
96
+
97
+ input_ids = torch.tensor([ids], dtype=torch.long, device=device)
98
+ generated: list[int] = []
99
+ with torch.inference_mode():
100
+ for _ in range(max(0, args.max_new_tokens)):
101
+ logits, _ = model(input_ids)
102
+ next_id = sample_next(logits[:, -1, :].float(), args.temperature, args.top_p)
103
+ token_id = int(next_id.item())
104
+ if token_id == tokenizer.eos_id():
105
+ break
106
+ generated.append(token_id)
107
+ input_ids = torch.cat((input_ids, next_id), dim=1)
108
+ if input_ids.shape[1] >= config.max_seq_len:
109
+ break
110
+ print(tokenizer.decode(generated, out_type=str), end="")
111
+
112
+
113
+ if __name__ == "__main__":
114
+ main()
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:330ae517cc9bd14d055f1bbde3b330b6d3206a8740bb4ef032a771af708a96b7
3
+ size 349898992
special_tokens_map.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "unk_token": "<unk>",
3
+ "eos_token": "</s>"
4
+ }
tokenizer.model ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8ebb450f2786d3153d64f874d97487ba5cb8e3450b799bbf6aeddd7d92f7ff73
3
+ size 123865
tokenizer_config.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "vortex",
3
+ "tokenizer_class": "SentencePieceProcessor",
4
+ "vocab_size": 8192,
5
+ "unk_token": "<unk>",
6
+ "eos_token": "</s>",
7
+ "add_bos_token": false,
8
+ "add_eos_token": false
9
+ }
vortex_model.py ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Capability-first Vortex 175M decoder.
2
+
3
+ This file intentionally uses only standard PyTorch CUDA primitives. The
4
+ model is deep-and-thin, uses GQA and SwiGLU, and ties the input/output
5
+ embedding. Those choices are much easier to train and export than a custom
6
+ SSM kernel while retaining the main sub-billion-parameter wins reported by
7
+ MobileLLM-style studies.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import math
13
+ from dataclasses import asdict, dataclass
14
+
15
+ import torch
16
+ import torch.nn.functional as F
17
+ from torch import nn
18
+ from torch.utils.checkpoint import checkpoint
19
+
20
+
21
+ @dataclass
22
+ class VortexConfig:
23
+ vocab_size: int = 8_192
24
+ max_seq_len: int = 4_096
25
+ n_layer: int = 12
26
+ n_embd: int = 1024
27
+ n_head: int = 16
28
+ n_kv_head: int = 4
29
+ head_dim: int = 64
30
+ intermediate_size: int = 3_664
31
+ rope_theta: float = 100_000.0
32
+ norm_eps: float = 1e-5
33
+ logits_chunk_tokens: int = 16_384
34
+ gradient_checkpointing: bool = False
35
+ use_transformer_engine: bool = False
36
+ attn_input_format: str = "bshd"
37
+
38
+ def to_dict(self) -> dict:
39
+ return asdict(self)
40
+
41
+
42
+ class RMSNorm(nn.Module):
43
+ def __init__(self, dim: int, eps: float) -> None:
44
+ super().__init__()
45
+ self.weight = nn.Parameter(torch.ones(dim))
46
+ self.eps = eps
47
+
48
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
49
+ return F.rms_norm(x, (x.shape[-1],), self.weight, self.eps)
50
+
51
+
52
+ def _linear(config: VortexConfig, in_features: int, out_features: int) -> nn.Module:
53
+ """Create a bias-free projection, optionally backed by Transformer Engine."""
54
+ if not config.use_transformer_engine:
55
+ return nn.Linear(in_features, out_features, bias=False)
56
+ try:
57
+ import transformer_engine.pytorch as te
58
+ except ImportError as exc: # pragma: no cover - exercised only on TE runs
59
+ raise RuntimeError(
60
+ "use_transformer_engine=True requires transformer-engine[pytorch]"
61
+ ) from exc
62
+ return te.Linear(
63
+ in_features,
64
+ out_features,
65
+ bias=False,
66
+ params_dtype=torch.bfloat16,
67
+ device="cuda",
68
+ )
69
+
70
+
71
+ class RotaryEmbedding(nn.Module):
72
+ def __init__(self, dim: int, max_seq_len: int, theta: float) -> None:
73
+ super().__init__()
74
+ inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2).float() / dim))
75
+ positions = torch.arange(max_seq_len, dtype=torch.float32)
76
+ frequencies = torch.outer(positions, inv_freq)
77
+ # NeoX-style rotate-half layout: the first and second halves share
78
+ # the same frequencies, so rotate_half() remains allocation-free in
79
+ # the hot attention path apart from its concatenation.
80
+ angles = torch.cat((frequencies, frequencies), dim=-1)
81
+ self.register_buffer("cos_cached", angles.cos()[None, None], persistent=False)
82
+ self.register_buffer("sin_cached", angles.sin()[None, None], persistent=False)
83
+
84
+ @staticmethod
85
+ def rotate_half(x: torch.Tensor) -> torch.Tensor:
86
+ half = x.shape[-1] // 2
87
+ return torch.cat((-x[..., half:], x[..., :half]), dim=-1)
88
+
89
+ def forward(self, q: torch.Tensor, k: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
90
+ seq_len = q.shape[-2]
91
+ if seq_len > self.cos_cached.shape[-2]:
92
+ raise ValueError(f"sequence length {seq_len} exceeds configured maximum")
93
+ cos = self.cos_cached[:, :, :seq_len].to(dtype=q.dtype)
94
+ sin = self.sin_cached[:, :, :seq_len].to(dtype=q.dtype)
95
+ return (
96
+ q * cos + self.rotate_half(q) * sin,
97
+ k * cos + self.rotate_half(k) * sin,
98
+ )
99
+
100
+
101
+ class GQAAttention(nn.Module):
102
+ def __init__(self, config: VortexConfig) -> None:
103
+ super().__init__()
104
+ if config.n_head % config.n_kv_head:
105
+ raise ValueError("n_head must be divisible by n_kv_head")
106
+ if config.n_head * config.head_dim != config.n_embd:
107
+ raise ValueError("n_head * head_dim must equal n_embd")
108
+ self.n_head = config.n_head
109
+ self.n_kv_head = config.n_kv_head
110
+ self.head_dim = config.head_dim
111
+ kv_dim = config.n_kv_head * config.head_dim
112
+ self.q_proj = _linear(config, config.n_embd, config.n_embd)
113
+ self.k_proj = _linear(config, config.n_embd, kv_dim)
114
+ self.v_proj = _linear(config, config.n_embd, kv_dim)
115
+ self.o_proj = _linear(config, config.n_embd, config.n_embd)
116
+ # QK-Norm keeps attention logits well-conditioned at the deliberately
117
+ # high pretraining learning rate. These are per-head, parameter-light
118
+ # norms, not full hidden-size projections.
119
+ self.q_norm = RMSNorm(config.head_dim, config.norm_eps)
120
+ self.k_norm = RMSNorm(config.head_dim, config.norm_eps)
121
+ self.rope = RotaryEmbedding(config.head_dim, config.max_seq_len, config.rope_theta)
122
+
123
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
124
+ batch, seq_len, _ = x.shape
125
+ q = self.q_proj(x).view(batch, seq_len, self.n_head, self.head_dim).transpose(1, 2)
126
+ k = self.k_proj(x).view(batch, seq_len, self.n_kv_head, self.head_dim).transpose(1, 2)
127
+ v = self.v_proj(x).view(batch, seq_len, self.n_kv_head, self.head_dim).transpose(1, 2)
128
+ q, k = self.rope(self.q_norm(q), self.k_norm(k))
129
+ # PyTorch dispatches this to the fused flash/efficient causal kernel
130
+ # when the local CUDA build supports it. enable_gqa avoids material-
131
+ # izing repeated K/V heads.
132
+ y = F.scaled_dot_product_attention(
133
+ q, k, v, is_causal=True, enable_gqa=True
134
+ )
135
+ y = y.transpose(1, 2).contiguous().view(batch, seq_len, -1)
136
+ return self.o_proj(y)
137
+
138
+
139
+ class SwiGLU(nn.Module):
140
+ def __init__(self, config: VortexConfig) -> None:
141
+ super().__init__()
142
+ self.gate_proj = _linear(config, config.n_embd, config.intermediate_size)
143
+ self.up_proj = _linear(config, config.n_embd, config.intermediate_size)
144
+ self.down_proj = _linear(config, config.intermediate_size, config.n_embd)
145
+
146
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
147
+ return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
148
+
149
+
150
+ class VortexBlock(nn.Module):
151
+ def __init__(self, config: VortexConfig, layer_number: int | None = None) -> None:
152
+ super().__init__()
153
+ self.use_transformer_engine = config.use_transformer_engine
154
+ self.attn_input_format = config.attn_input_format
155
+ if self.use_transformer_engine:
156
+ try:
157
+ import transformer_engine.pytorch as te
158
+ except ImportError as exc: # pragma: no cover - TE-only path
159
+ raise RuntimeError(
160
+ "use_transformer_engine=True requires transformer-engine[pytorch]"
161
+ ) from exc
162
+ # This fused layer has the same parameter shapes as the explicit
163
+ # reference block: fused GQA QKV, RMSNorm, QK-Norm, SwiGLU, and
164
+ # the causal attention kernel. It is used only for the TE backend;
165
+ # the reference PyTorch path remains readable and exportable.
166
+ self.te_layer = te.TransformerLayer(
167
+ hidden_size=config.n_embd,
168
+ ffn_hidden_size=config.intermediate_size,
169
+ num_attention_heads=config.n_head,
170
+ num_gqa_groups=config.n_kv_head,
171
+ layernorm_epsilon=config.norm_eps,
172
+ hidden_dropout=0.0,
173
+ attention_dropout=0.0,
174
+ kv_channels=config.head_dim,
175
+ layer_number=layer_number,
176
+ bias=False,
177
+ activation="swiglu",
178
+ normalization="RMSNorm",
179
+ qk_norm_type="RMSNorm",
180
+ qk_norm_before_rope=True,
181
+ fuse_qkv_params=True,
182
+ self_attn_mask_type="causal",
183
+ attn_input_format=config.attn_input_format,
184
+ params_dtype=torch.bfloat16,
185
+ device="cuda",
186
+ )
187
+ return
188
+ self.norm1 = RMSNorm(config.n_embd, config.norm_eps)
189
+ self.attn = GQAAttention(config)
190
+ self.norm2 = RMSNorm(config.n_embd, config.norm_eps)
191
+ self.ffn = SwiGLU(config)
192
+
193
+ def forward(
194
+ self,
195
+ x: torch.Tensor,
196
+ rotary_pos_emb: torch.Tensor | None = None,
197
+ is_first_microbatch: bool | None = None,
198
+ inference_params=None,
199
+ attention_mask: torch.Tensor | None = None,
200
+ inference_decode_bshd: bool = False,
201
+ ) -> torch.Tensor:
202
+ if self.use_transformer_engine:
203
+ if inference_params is not None:
204
+ # A packed THD prompt handles variable-length prefill
205
+ # efficiently. Subsequent one-token decode is cheaper and
206
+ # more numerically stable through the regular BSHD cache
207
+ # path; switch the TE attention format explicitly between the
208
+ # two phases.
209
+ effective_format = "bshd" if inference_decode_bshd else self.attn_input_format
210
+ self.te_layer.self_attention.qkv_format = effective_format
211
+ te_kwargs = {
212
+ "attention_mask": attention_mask,
213
+ "self_attn_mask_type": (
214
+ "padding_causal" if inference_params is not None else None
215
+ ),
216
+ "rotary_pos_emb": rotary_pos_emb,
217
+ "is_first_microbatch": is_first_microbatch,
218
+ "inference_params": inference_params,
219
+ }
220
+ if inference_params is not None and not inference_decode_bshd and self.attn_input_format == "thd":
221
+ batch_size = len(inference_params.sequences)
222
+ cu_seqlens = inference_params.cu_seqlens_q[: batch_size + 1]
223
+ sequence_lengths = cu_seqlens[1:] - cu_seqlens[:-1]
224
+ te_kwargs.update(
225
+ {
226
+ "cu_seqlens_q": cu_seqlens,
227
+ "cu_seqlens_q_padded": cu_seqlens,
228
+ "max_seqlen_q": int(sequence_lengths.max().item()),
229
+ "max_seqlen_kv": int(sequence_lengths.max().item()),
230
+ }
231
+ )
232
+ return self.te_layer(
233
+ x,
234
+ **te_kwargs,
235
+ )
236
+ x = x + self.attn(self.norm1(x))
237
+ x = x + self.ffn(self.norm2(x))
238
+ return x
239
+
240
+
241
+ class VortexForCausalLM(nn.Module):
242
+ def __init__(self, config: VortexConfig | None = None) -> None:
243
+ super().__init__()
244
+ self.config = config or VortexConfig()
245
+ self.embed_tokens = nn.Embedding(self.config.vocab_size, self.config.n_embd)
246
+ self.layers = nn.ModuleList(
247
+ VortexBlock(self.config, layer_number=index + 1)
248
+ for index in range(self.config.n_layer)
249
+ )
250
+ self.norm = RMSNorm(self.config.n_embd, self.config.norm_eps)
251
+ if self.config.use_transformer_engine:
252
+ import transformer_engine.pytorch as te
253
+
254
+ self.rotary = te.RotaryPositionEmbedding(
255
+ self.config.head_dim,
256
+ rotary_base=self.config.rope_theta,
257
+ interleaved=False,
258
+ )
259
+ self._initialize_weights()
260
+
261
+ def _initialize_weights(self) -> None:
262
+ # Scale residual outputs down with depth; this gives a forgiving high-
263
+ # LR start without adding trainable parameters.
264
+ output_std = 0.02 / math.sqrt(2.0 * self.config.n_layer)
265
+ nn.init.normal_(self.embed_tokens.weight, mean=0.0, std=0.02)
266
+ for block in self.layers:
267
+ if self.config.use_transformer_engine:
268
+ for name, parameter in block.named_parameters():
269
+ if parameter.ndim == 1:
270
+ nn.init.ones_(parameter)
271
+ else:
272
+ is_output = (
273
+ name.endswith("self_attention.proj.weight")
274
+ or name.endswith("layernorm_mlp.fc2_weight")
275
+ )
276
+ nn.init.normal_(
277
+ parameter,
278
+ mean=0.0,
279
+ std=output_std if is_output else 0.02,
280
+ )
281
+ else:
282
+ for child in block.modules():
283
+ if isinstance(child, nn.Linear):
284
+ is_output = child is block.attn.o_proj or child is block.ffn.down_proj
285
+ nn.init.normal_(
286
+ child.weight,
287
+ mean=0.0,
288
+ std=output_std if is_output else 0.02,
289
+ )
290
+
291
+ def parameter_count(self) -> int:
292
+ return sum(parameter.numel() for parameter in self.parameters())
293
+
294
+ def parameter_breakdown(self) -> dict[str, int]:
295
+ c = self.config
296
+ embedding = c.vocab_size * c.n_embd
297
+ q = c.n_layer * c.n_embd * c.n_embd
298
+ k = c.n_layer * c.n_embd * (c.n_kv_head * c.head_dim)
299
+ v = k
300
+ o = q
301
+ # One learned head-dimension scale is shared across all query heads,
302
+ # and another across all KV heads, matching the module definitions
303
+ # above (not one scale vector per physical head).
304
+ qk_norm = c.n_layer * 2 * c.head_dim
305
+ ffn = c.n_layer * 3 * c.n_embd * c.intermediate_size
306
+ block_norm = c.n_layer * 2 * c.n_embd
307
+ final_norm = c.n_embd
308
+ return {
309
+ "input_embedding_and_tied_output": embedding,
310
+ "attention_q_projection": q,
311
+ "attention_k_projection": k,
312
+ "attention_v_projection": v,
313
+ "attention_o_projection": o,
314
+ "attention_qk_norm": qk_norm,
315
+ "ffn_swiglu": ffn,
316
+ "block_rmsnorm": block_norm,
317
+ "final_rmsnorm": final_norm,
318
+ "total": self.parameter_count(),
319
+ }
320
+
321
+ def _chunked_tied_loss(self, hidden: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
322
+ chunk = self.config.logits_chunk_tokens
323
+ total = hidden.new_zeros((), dtype=torch.float32)
324
+ for start in range(0, hidden.shape[0], chunk):
325
+ end = min(hidden.shape[0], start + chunk)
326
+ logits = F.linear(hidden[start:end], self.embed_tokens.weight)
327
+ total = total + F.cross_entropy(logits, targets[start:end]).float() * (end - start)
328
+ return total / hidden.shape[0]
329
+
330
+ def forward(
331
+ self,
332
+ input_ids: torch.Tensor,
333
+ labels: torch.Tensor | None = None,
334
+ is_first_microbatch: bool | None = None,
335
+ inference_params=None,
336
+ inference_attention_mask=None,
337
+ inference_decode_bshd: bool = False,
338
+ ) -> tuple[torch.Tensor | None, torch.Tensor | None]:
339
+ x = self.embed_tokens(input_ids)
340
+ rotary_pos_emb = None
341
+ attention_mask = None
342
+ if self.config.use_transformer_engine:
343
+ if inference_params is not None:
344
+ # TE's cached-attention path applies the correct absolute
345
+ # offset from inference_params. Supplying the full table is
346
+ # necessary when the current query is only one token long but
347
+ # starts after a long cached prefix.
348
+ rotary_pos_emb = self.rotary(self.config.max_seq_len)
349
+ if self.config.attn_input_format == "thd" and not inference_decode_bshd:
350
+ attention_mask = None
351
+ elif inference_attention_mask is None:
352
+ query_padding_mask = torch.zeros(
353
+ (x.shape[0], 1, 1, x.shape[1]),
354
+ dtype=torch.bool,
355
+ device=x.device,
356
+ )
357
+ key_padding_mask = torch.ones(
358
+ (x.shape[0], 1, 1, inference_params.max_sequence_length),
359
+ dtype=torch.bool,
360
+ device=x.device,
361
+ )
362
+ for batch_index, sequence_length in enumerate(
363
+ inference_params.sequences.values()
364
+ ):
365
+ key_padding_mask[batch_index, :, :, :sequence_length] = False
366
+ # TE switches the cached self-attention implementation to
367
+ # its cross-attention backend internally; that backend
368
+ # expects the query and key padding masks as a pair.
369
+ attention_mask = (query_padding_mask, key_padding_mask)
370
+ else:
371
+ attention_mask = inference_attention_mask
372
+ else:
373
+ rotary_pos_emb = self.rotary(x.shape[1])
374
+ for block in self.layers:
375
+ if self.training and self.config.gradient_checkpointing:
376
+ x = checkpoint(
377
+ block,
378
+ x,
379
+ rotary_pos_emb,
380
+ is_first_microbatch,
381
+ use_reentrant=False,
382
+ )
383
+ else:
384
+ x = block(
385
+ x,
386
+ rotary_pos_emb,
387
+ is_first_microbatch,
388
+ inference_params,
389
+ attention_mask,
390
+ inference_decode_bshd,
391
+ )
392
+ x = self.norm(x)
393
+ if labels is None:
394
+ return F.linear(x, self.embed_tokens.weight), None
395
+ hidden = x[:, :-1].reshape(-1, x.shape[-1])
396
+ targets = labels[:, 1:].reshape(-1)
397
+ return None, self._chunked_tied_loss(hidden, targets)
398
+
399
+
400
+ if __name__ == "__main__":
401
+ config = VortexConfig()
402
+ model = VortexForCausalLM(config)
403
+ print(config.to_dict())
404
+ print(model.parameter_breakdown())