arthu1 commited on
Commit
6139eab
·
verified ·
1 Parent(s): e98a4f1

Add ChatML model card and local runtime

Browse files
Files changed (8) hide show
  1. .gitattributes +1 -0
  2. Aurora-3.png +3 -0
  3. README.md +74 -0
  4. aurora/__init__.py +4 -0
  5. aurora/config.py +63 -0
  6. aurora/model.py +376 -0
  7. infer.py +197 -0
  8. requirements.txt +4 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ Aurora-3.png filter=lfs diff=lfs merge=lfs -text
Aurora-3.png ADDED

Git LFS Details

  • SHA256: ec2dba8a346b2ec4cd9b38c08ad55d8bd849ff77fa89e6ad569b7c4bc8486633
  • Pointer size: 132 Bytes
  • Size of remote file: 1.96 MB
README.md ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - en
4
+ library_name: aurora
5
+ pipeline_tag: text-generation
6
+ tags:
7
+ - aurora-proelia
8
+ - north-ml
9
+ - chatml
10
+ - 207m
11
+ license: other
12
+ widget:
13
+ - text: Who are you?
14
+ - text: What is Python?
15
+ - text: Explain photosynthesis in one sentence.
16
+ ---
17
+
18
+ ![Project Banner](Aurora-3.png)
19
+
20
+ # Aurora Proelia ChatML
21
+
22
+ Aurora Proelia ChatML is a 207M-parameter experimental variant of [Aurora Proelia](https://huggingface.co/North-ML1/Aurora-Proelia). It was SFT-trained on a conventional role-based ChatML surface so applications can send system, user, and assistant turns in a familiar format.
23
+
24
+ This is a separate candidate. The original `Aurora-Proelia` repository remains the native `Question:` / `Answer:` release.
25
+
26
+ ## ChatML format
27
+
28
+ Use this format for inference:
29
+
30
+ ```text
31
+ <|im_start|>system
32
+ You are Ember Proelia. Answer directly and concisely.<|im_end|>
33
+ <|im_start|>user
34
+ What is Python?<|im_end|>
35
+ <|im_start|>assistant
36
+ ```
37
+
38
+ The model is a custom Aurora checkpoint, not a Transformers-compatible architecture. Use the included native Aurora runtime, YAML config, and tokenizer.
39
+
40
+ ## What changed
41
+
42
+ The checkpoint started from the released Aurora candidate and received 2,048 effective ChatML SFT updates over the existing answer-masked ChatML corpus. The pass was intended to teach the input/output surface, not to create a new general-knowledge model.
43
+
44
+ ## Evaluation
45
+
46
+ On a matched public benchmark mini-slice, the ChatML candidate changed as follows:
47
+
48
+ | Benchmark | Released Aurora | ChatML candidate |
49
+ |---|---:|---:|
50
+ | MMLU · 57 questions | 14/57 · 24.6% | **16/57 · 28.1%** |
51
+ | ARC-Challenge · 50 questions | 13/50 · 26.0% | **15/50 · 30.0%** |
52
+ | HellaSwag · 50 questions | 19/50 · 38.0% | 19/50 · 38.0% |
53
+ | GSM8K · 50 questions | 1/50 · 2.0% | 0/50 · 0.0% |
54
+
55
+ The exact runs are in [`benchmarks.json`](./benchmarks.json), [`regression_comparison.json`](./regression_comparison.json), and [`chatml_smoke.json`](./chatml_smoke.json). These are transparent slices of public Hugging Face datasets, not official leaderboard evaluations.
56
+
57
+ The practical result is clearer than the small score changes: the ChatML candidate answers ordinary identity and Python prompts through the role-based format, while the released checkpoint often echoes the ChatML prompt. Arithmetic and uncertainty handling remain weak.
58
+
59
+ ## Limitations
60
+
61
+ This remains a small research model. It is unreliable for multi-step arithmetic, deep reasoning, current facts, specialized questions without context, and complex instruction following. Verify important answers and provide retrieval context when freshness or factual accuracy matters.
62
+
63
+ ## Local inference
64
+
65
+ ```bash
66
+ pip install -r requirements.txt
67
+ python infer.py --checkpoint model.safetensors --prompt "<|im_start|>user\nWhat is Python?<|im_end|>\n<|im_start|>assistant\n"
68
+ ```
69
+
70
+ ## Distribution
71
+
72
+ This is a public North ML research release. No open-source license is granted; licensing is reserved by the repository owner.
73
+
74
+ `text-generation` · `aurora-proelia` · `chatml` · `north-ml` · `207m`
aurora/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .config import AuroraConfig, load_model_config
2
+ from .model import AuroraForCausalLM
3
+
4
+ __all__ = ["AuroraConfig", "AuroraForCausalLM", "load_model_config"]
aurora/config.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, fields
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import yaml
8
+
9
+
10
+ @dataclass
11
+ class AuroraConfig:
12
+ model_name: str = "Ember Proelia"
13
+ vocab_size: int = 16000
14
+ hidden_size: int = 896
15
+ num_layers: int = 23
16
+ num_attention_heads: int = 14
17
+ num_key_value_heads: int = 2
18
+ intermediate_size: int = 2432
19
+ context_length: int = 2048
20
+ rope_theta: float = 500000.0
21
+ rms_norm_eps: float = 1.0e-5
22
+ qk_norm: bool = True
23
+ tie_word_embeddings: bool = True
24
+ attention_bias: bool = False
25
+ mlp_bias: bool = False
26
+ dropout: float = 0.0
27
+ num_experts: int = 1
28
+ router_aux_loss_coef: float = 0.0
29
+ router_z_loss_coef: float = 0.0
30
+ router_noise_scale: float = 0.0
31
+ moe_capacity_factor: float = 0.0
32
+ router_use_gate_weight: bool = False
33
+
34
+ @property
35
+ def head_dim(self) -> int:
36
+ return self.hidden_size // self.num_attention_heads
37
+
38
+ def validate(self) -> None:
39
+ if self.vocab_size <= 0 or self.hidden_size <= 0 or self.num_layers <= 0:
40
+ raise ValueError("vocab_size, hidden_size, and num_layers must be positive")
41
+ if self.hidden_size % self.num_attention_heads != 0:
42
+ raise ValueError("hidden_size must divide evenly by num_attention_heads")
43
+ if self.num_attention_heads % self.num_key_value_heads != 0:
44
+ raise ValueError("num_attention_heads must divide evenly by num_key_value_heads")
45
+ if self.head_dim % 2 != 0:
46
+ raise ValueError("head_dim must be even for RoPE")
47
+ if self.context_length <= 0:
48
+ raise ValueError("context_length must be positive")
49
+ if self.num_experts <= 0:
50
+ raise ValueError("num_experts must be positive")
51
+
52
+
53
+ def load_model_config(path: str | Path) -> AuroraConfig:
54
+ path = Path(path)
55
+ with path.open("r", encoding="utf-8") as handle:
56
+ raw: dict[str, Any] = yaml.safe_load(handle) or {}
57
+ allowed = {field.name for field in fields(AuroraConfig)}
58
+ unknown = sorted(set(raw) - allowed)
59
+ if unknown:
60
+ raise ValueError(f"Unknown model config keys: {unknown}")
61
+ cfg = AuroraConfig(**raw)
62
+ cfg.validate()
63
+ return cfg
aurora/model.py ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+
9
+ from aurora.config import AuroraConfig
10
+
11
+ try:
12
+ from cut_cross_entropy import linear_cross_entropy
13
+ except ImportError:
14
+ linear_cross_entropy = None
15
+
16
+
17
+ class RMSNorm(nn.Module):
18
+ def __init__(self, dim: int, eps: float) -> None:
19
+ super().__init__()
20
+ self.weight = nn.Parameter(torch.ones(dim))
21
+ self.eps = eps
22
+
23
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
24
+ scale = torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
25
+ return self.weight * x * scale
26
+
27
+
28
+ def precompute_rope_frequencies(
29
+ seq_len: int, head_dim: int, theta: float, device: torch.device, dtype: torch.dtype
30
+ ) -> tuple[torch.Tensor, torch.Tensor]:
31
+ inv_freq = 1.0 / (theta ** (torch.arange(0, head_dim, 2, device=device).float() / head_dim))
32
+ positions = torch.arange(seq_len, device=device).float()
33
+ freqs = torch.outer(positions, inv_freq)
34
+ return freqs.cos().to(dtype=dtype), freqs.sin().to(dtype=dtype)
35
+
36
+
37
+ def apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
38
+ cos = cos[None, :, None, :]
39
+ sin = sin[None, :, None, :]
40
+ x_even = x[..., 0::2]
41
+ x_odd = x[..., 1::2]
42
+ out = torch.empty_like(x)
43
+ out[..., 0::2] = x_even * cos - x_odd * sin
44
+ out[..., 1::2] = x_even * sin + x_odd * cos
45
+ return out
46
+
47
+
48
+ class CausalSelfAttention(nn.Module):
49
+ def __init__(self, cfg: AuroraConfig) -> None:
50
+ super().__init__()
51
+ self.cfg = cfg
52
+ self.num_heads = cfg.num_attention_heads
53
+ self.num_kv_heads = cfg.num_key_value_heads
54
+ self.head_dim = cfg.head_dim
55
+ self.kv_repeat = self.num_heads // self.num_kv_heads
56
+
57
+ self.q_proj = nn.Linear(cfg.hidden_size, cfg.num_attention_heads * self.head_dim, bias=cfg.attention_bias)
58
+ self.k_proj = nn.Linear(cfg.hidden_size, cfg.num_key_value_heads * self.head_dim, bias=cfg.attention_bias)
59
+ self.v_proj = nn.Linear(cfg.hidden_size, cfg.num_key_value_heads * self.head_dim, bias=cfg.attention_bias)
60
+ self.o_proj = nn.Linear(cfg.hidden_size, cfg.hidden_size, bias=cfg.attention_bias)
61
+ self.q_norm = RMSNorm(self.head_dim, cfg.rms_norm_eps) if cfg.qk_norm else nn.Identity()
62
+ self.k_norm = RMSNorm(self.head_dim, cfg.rms_norm_eps) if cfg.qk_norm else nn.Identity()
63
+ self.dropout_p = cfg.dropout
64
+
65
+ def forward(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
66
+ batch, seq_len, _ = x.shape
67
+ q = self.q_proj(x).view(batch, seq_len, self.num_heads, self.head_dim)
68
+ k = self.k_proj(x).view(batch, seq_len, self.num_kv_heads, self.head_dim)
69
+ v = self.v_proj(x).view(batch, seq_len, self.num_kv_heads, self.head_dim)
70
+
71
+ q = self.q_norm(q)
72
+ k = self.k_norm(k)
73
+ q = apply_rope(q, cos, sin).transpose(1, 2)
74
+ k = apply_rope(k, cos, sin).transpose(1, 2)
75
+ v = v.transpose(1, 2)
76
+
77
+ # Explicit K/V expansion is mathematically equivalent to GQA and works
78
+ # across CUDA, Apple MPS, and CPU PyTorch backends.
79
+ if self.kv_repeat > 1:
80
+ k = k.repeat_interleave(self.kv_repeat, dim=1)
81
+ v = v.repeat_interleave(self.kv_repeat, dim=1)
82
+ y = F.scaled_dot_product_attention(
83
+ q,
84
+ k,
85
+ v,
86
+ attn_mask=None,
87
+ dropout_p=self.dropout_p if self.training else 0.0,
88
+ is_causal=True,
89
+ )
90
+ y = y.transpose(1, 2).contiguous().view(batch, seq_len, self.cfg.hidden_size)
91
+ return self.o_proj(y)
92
+
93
+
94
+ class SwiGLU(nn.Module):
95
+ def __init__(self, cfg: AuroraConfig, intermediate_size: int | None = None) -> None:
96
+ super().__init__()
97
+ intermediate_size = intermediate_size or cfg.intermediate_size
98
+ self.gate_proj = nn.Linear(cfg.hidden_size, intermediate_size, bias=cfg.mlp_bias)
99
+ self.up_proj = nn.Linear(cfg.hidden_size, intermediate_size, bias=cfg.mlp_bias)
100
+ self.down_proj = nn.Linear(intermediate_size, cfg.hidden_size, bias=cfg.mlp_bias)
101
+
102
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
103
+ return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
104
+
105
+
106
+ class Top1MoE(nn.Module):
107
+ """Top-1 routed SwiGLU experts with Switch-style router regularization."""
108
+
109
+ def __init__(self, cfg: AuroraConfig) -> None:
110
+ super().__init__()
111
+ self.num_experts = cfg.num_experts
112
+ self.router_aux_loss_coef = cfg.router_aux_loss_coef
113
+ self.router_z_loss_coef = cfg.router_z_loss_coef
114
+ self.router_noise_scale = cfg.router_noise_scale
115
+ self.capacity_factor = cfg.moe_capacity_factor
116
+ self.use_gate_weight = cfg.router_use_gate_weight
117
+ # Keep routing in BF16/FP32 rather than quantizing its logits to FP8.
118
+ self.router = nn.Linear(cfg.hidden_size, cfg.num_experts, bias=False)
119
+ self.experts = nn.ModuleList([SwiGLU(cfg) for _ in range(cfg.num_experts)])
120
+ # Detached summaries from the latest batch, for collapse detection in
121
+ # the trainer. They are intentionally not persistent model state.
122
+ self.last_expert_fraction: torch.Tensor | None = None
123
+ self.last_preferred_expert_fraction: torch.Tensor | None = None
124
+ self.last_forced_fraction: torch.Tensor | None = None
125
+ self.last_selected_gate_probability: torch.Tensor | None = None
126
+
127
+ def _capacity_constrained_route(
128
+ self, scores: torch.Tensor, preferred_index: torch.Tensor
129
+ ) -> torch.Tensor:
130
+ """Assign exactly one expert/token while bounding every expert load.
131
+
132
+ Experts keep their highest-scoring first-choice tokens. Overflow is
133
+ deterministically retried against each token's next preference. With
134
+ five experts this small eager-only matching pass is far cheaper than
135
+ an expert MLP and prevents a collapsed router from starving experts.
136
+ """
137
+ token_count = scores.size(0)
138
+ capacity = max(
139
+ math.ceil(token_count / self.num_experts),
140
+ math.ceil(token_count * self.capacity_factor / self.num_experts),
141
+ )
142
+ rankings = torch.argsort(scores, dim=-1, descending=True)
143
+ assigned = torch.full_like(preferred_index, -1)
144
+ remaining = [capacity for _ in range(self.num_experts)]
145
+
146
+ for rank in range(self.num_experts):
147
+ for expert_index in range(self.num_experts):
148
+ slots = remaining[expert_index]
149
+ if slots <= 0:
150
+ continue
151
+ candidates = torch.nonzero(
152
+ (assigned < 0) & (rankings[:, rank] == expert_index), as_tuple=False
153
+ ).flatten()
154
+ candidate_count = candidates.numel()
155
+ if candidate_count == 0:
156
+ continue
157
+ if candidate_count > slots:
158
+ candidate_scores = scores.index_select(0, candidates)[:, expert_index]
159
+ best_positions = torch.topk(candidate_scores, k=slots, sorted=False).indices
160
+ candidates = candidates.index_select(0, best_positions)
161
+ candidate_count = slots
162
+ assigned.index_fill_(0, candidates, expert_index)
163
+ remaining[expert_index] -= candidate_count
164
+
165
+ # The combined capacity is at least the token count and every token
166
+ # ranks every expert, so this is a logic invariant rather than an
167
+ # expected fallback.
168
+ if bool(torch.any(assigned < 0)):
169
+ raise RuntimeError("capacity-constrained MoE routing left tokens unassigned")
170
+ return assigned
171
+
172
+ def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
173
+ original_shape = x.shape
174
+ flat_x = x.reshape(-1, original_shape[-1])
175
+ router_logits = self.router(flat_x).float()
176
+ router_probs = torch.softmax(router_logits, dim=-1)
177
+ # Routing indices are discrete. Keep this bookkeeping out of the
178
+ # autograd graph; language gradients still reach the selected gate
179
+ # probability when gate weighting is enabled, while router losses use
180
+ # the clean differentiable probabilities below.
181
+ with torch.no_grad():
182
+ routing_logits = router_logits
183
+ if self.training and self.router_noise_scale > 0:
184
+ # Noisy top-1 routing keeps early training exploratory. Only
185
+ # the discrete expert choice is noisy; probability weights and
186
+ # regularization remain based on clean BF16/FP32 logits.
187
+ gumbel_noise = -torch.empty_like(router_logits).exponential_().log()
188
+ routing_logits = router_logits + self.router_noise_scale * gumbel_noise
189
+ preferred_index = torch.argmax(routing_logits, dim=-1)
190
+ route_index = (
191
+ self._capacity_constrained_route(routing_logits, preferred_index)
192
+ if self.capacity_factor
193
+ else preferred_index
194
+ )
195
+ selected_router_probability = router_probs.gather(1, route_index.unsqueeze(1)).squeeze(1)
196
+ route_weight = (
197
+ selected_router_probability
198
+ if self.use_gate_weight
199
+ else torch.ones_like(selected_router_probability)
200
+ )
201
+
202
+ output = torch.zeros_like(flat_x)
203
+ for expert_index, expert in enumerate(self.experts):
204
+ token_indices = torch.nonzero(route_index == expert_index, as_tuple=False).flatten()
205
+ if token_indices.numel() == 0:
206
+ continue
207
+ expert_input = flat_x.index_select(0, token_indices)
208
+ real_token_count = expert_input.size(0)
209
+ # TorchAO's FP8 GEMMs require their M dimension to be divisible
210
+ # by 16. Sparse routing gives every expert a variable number of
211
+ # tokens, so pad only this temporary dispatch buffer and discard
212
+ # the corresponding outputs. This changes no real-token math.
213
+ fp8_padding = (-real_token_count) % 16
214
+ if fp8_padding:
215
+ expert_input = torch.cat(
216
+ (expert_input, expert_input.new_zeros((fp8_padding, expert_input.size(-1)))), dim=0
217
+ )
218
+ expert_output = expert(expert_input)[:real_token_count]
219
+ routed_output = expert_output * route_weight.index_select(0, token_indices).to(expert_output.dtype).unsqueeze(-1)
220
+ # RMSNorm can promote the residual stream to FP32, while the FP8
221
+ # expert projections return BF16 under autocast. Restore the
222
+ # residual dtype before scattering selected expert outputs.
223
+ output = output.index_copy(0, token_indices, routed_output.to(output.dtype))
224
+
225
+ expert_fraction = F.one_hot(route_index, num_classes=self.num_experts).to(router_probs.dtype).mean(dim=0)
226
+ preferred_fraction = F.one_hot(preferred_index, num_classes=self.num_experts).to(router_probs.dtype).mean(dim=0)
227
+ mean_router_prob = router_probs.mean(dim=0)
228
+ self.last_expert_fraction = expert_fraction.detach()
229
+ self.last_preferred_expert_fraction = preferred_fraction.detach()
230
+ self.last_forced_fraction = (route_index != preferred_index).float().mean().detach()
231
+ self.last_selected_gate_probability = selected_router_probability.mean().detach()
232
+ # Balance the router's *clean preference* rather than the capacity-
233
+ # constrained dispatch, which is intentionally already near-uniform.
234
+ aux_loss = self.router_aux_loss_coef * self.num_experts * torch.sum(
235
+ preferred_fraction * mean_router_prob
236
+ )
237
+ z_loss = self.router_z_loss_coef * torch.logsumexp(router_logits, dim=-1).square().mean()
238
+ return output.reshape(original_shape), aux_loss + z_loss
239
+
240
+
241
+ class DecoderBlock(nn.Module):
242
+ def __init__(self, cfg: AuroraConfig) -> None:
243
+ super().__init__()
244
+ self.input_layernorm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
245
+ self.self_attn = CausalSelfAttention(cfg)
246
+ self.post_attention_layernorm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
247
+ self.mlp: nn.Module = Top1MoE(cfg) if cfg.num_experts > 1 else SwiGLU(cfg)
248
+
249
+ def forward(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
250
+ x = x + self.self_attn(self.input_layernorm(x), cos, sin)
251
+ mlp_input = self.post_attention_layernorm(x)
252
+ if isinstance(self.mlp, Top1MoE):
253
+ mlp_output, router_loss = self.mlp(mlp_input)
254
+ else:
255
+ mlp_output = self.mlp(mlp_input)
256
+ router_loss = x.new_zeros((), dtype=torch.float32)
257
+ return x + mlp_output, router_loss
258
+
259
+
260
+ class AuroraForCausalLM(nn.Module):
261
+ def __init__(self, cfg: AuroraConfig) -> None:
262
+ super().__init__()
263
+ cfg.validate()
264
+ self.cfg = cfg
265
+ self.embed_tokens = nn.Embedding(cfg.vocab_size, cfg.hidden_size)
266
+ self.layers = nn.ModuleList([DecoderBlock(cfg) for _ in range(cfg.num_layers)])
267
+ self.norm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
268
+ self.lm_head = nn.Linear(cfg.hidden_size, cfg.vocab_size, bias=False)
269
+ self.last_router_loss: torch.Tensor | None = None
270
+ self.register_buffer("rope_cos_cached", torch.empty(0), persistent=False)
271
+ self.register_buffer("rope_sin_cached", torch.empty(0), persistent=False)
272
+ if cfg.tie_word_embeddings:
273
+ self.lm_head.weight = self.embed_tokens.weight
274
+ self.apply(self._init_weights)
275
+
276
+ def _init_weights(self, module: nn.Module) -> None:
277
+ if isinstance(module, nn.Linear):
278
+ nn.init.normal_(module.weight, mean=0.0, std=0.02)
279
+ if module.bias is not None:
280
+ nn.init.zeros_(module.bias)
281
+ elif isinstance(module, nn.Embedding):
282
+ nn.init.normal_(module.weight, mean=0.0, std=0.02)
283
+
284
+ def _rope_cache(
285
+ self, seq_len: int, device: torch.device, dtype: torch.dtype
286
+ ) -> tuple[torch.Tensor, torch.Tensor]:
287
+ cache_miss = (
288
+ self.rope_cos_cached.numel() == 0
289
+ or self.rope_cos_cached.size(0) < seq_len
290
+ or self.rope_cos_cached.device != device
291
+ or self.rope_cos_cached.dtype != dtype
292
+ )
293
+ if cache_miss:
294
+ cos, sin = precompute_rope_frequencies(
295
+ self.cfg.context_length,
296
+ self.cfg.head_dim,
297
+ self.cfg.rope_theta,
298
+ device,
299
+ dtype,
300
+ )
301
+ self.rope_cos_cached = cos
302
+ self.rope_sin_cached = sin
303
+ return self.rope_cos_cached[:seq_len], self.rope_sin_cached[:seq_len]
304
+
305
+ def _rope_dtype(self, x: torch.Tensor) -> torch.dtype:
306
+ if x.device.type == "cuda" and torch.is_autocast_enabled("cuda"):
307
+ return torch.get_autocast_dtype("cuda")
308
+ if x.device.type == "cpu" and torch.is_autocast_enabled("cpu"):
309
+ return torch.get_autocast_dtype("cpu")
310
+ return x.dtype
311
+
312
+ def forward(
313
+ self, input_ids: torch.Tensor, labels: torch.Tensor | None = None
314
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
315
+ x = self.embed_tokens(input_ids)
316
+ cos, sin = self._rope_cache(x.size(1), x.device, self._rope_dtype(x))
317
+ router_loss = torch.zeros((), device=x.device, dtype=torch.float32)
318
+ for layer in self.layers:
319
+ x, layer_router_loss = layer(x, cos, sin)
320
+ router_loss = router_loss + layer_router_loss
321
+ # Each layer produces a regularizer of the same scale. Average them
322
+ # so the configured coefficient has the same meaning regardless of
323
+ # depth (instead of becoming 16x stronger in this MoE model).
324
+ router_loss = router_loss / max(1, len(self.layers))
325
+ self.last_router_loss = router_loss.detach()
326
+ x = self.norm(x)
327
+ if labels is not None and linear_cross_entropy is not None:
328
+ # RMSNorm may promote activations to FP32, but Cut Cross Entropy's
329
+ # backward kernel requires BF16/FP16 hidden states.
330
+ loss = linear_cross_entropy(x.to(self.lm_head.weight.dtype), self.lm_head.weight, labels, shift=True)
331
+ logits = x.new_empty(0)
332
+ else:
333
+ logits = self.lm_head(x)
334
+ loss = None
335
+ if labels is not None:
336
+ loss = F.cross_entropy(
337
+ logits[:, :-1].contiguous().view(-1, logits.size(-1)),
338
+ labels[:, 1:].contiguous().view(-1),
339
+ )
340
+ # Keep evaluation perplexity comparable to dense models: router regularization
341
+ # shapes gradients only during training and is not language-model loss.
342
+ if loss is not None and self.training:
343
+ loss = loss + router_loss
344
+ return logits, loss
345
+
346
+
347
+ def count_parameters(model: nn.Module) -> int:
348
+ seen: set[int] = set()
349
+ total = 0
350
+ for param in model.parameters():
351
+ ident = id(param)
352
+ if ident not in seen:
353
+ seen.add(ident)
354
+ total += param.numel()
355
+ return total
356
+
357
+
358
+ def count_active_parameters(model: nn.Module) -> int:
359
+ """Count parameters used by one top-1 path, without double-counting ties."""
360
+ seen: set[int] = set()
361
+ total = 0
362
+ for name, param in model.named_parameters():
363
+ if ".mlp.experts." in name:
364
+ expert_index = name.split(".mlp.experts.", 1)[1].split(".", 1)[0]
365
+ if expert_index != "0":
366
+ continue
367
+ ident = id(param)
368
+ if ident not in seen:
369
+ seen.add(ident)
370
+ total += param.numel()
371
+ return total
372
+
373
+
374
+ def estimate_parameter_count(cfg: AuroraConfig) -> int:
375
+ model = AuroraForCausalLM(cfg)
376
+ return count_parameters(model)
infer.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import re
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+
10
+ import torch
11
+ from safetensors.torch import load_file
12
+ from tokenizers import Tokenizer
13
+
14
+ from aurora.config import load_model_config
15
+ from aurora.model import AuroraForCausalLM, count_parameters
16
+
17
+ ROOT = Path(__file__).resolve().parent
18
+ ROLE_RESTART = re.compile(
19
+ r"(?:^|\n)\s*(?:(?:question|answer|problem|solution)\s*:|#\s*(?:question|answer|problem|solution|what\s+is)\b)",
20
+ re.IGNORECASE,
21
+ )
22
+
23
+
24
+ def choose_device(requested: str) -> torch.device:
25
+ if requested != "auto":
26
+ device = torch.device(requested)
27
+ if device.type == "mps" and not torch.backends.mps.is_available():
28
+ raise RuntimeError("MPS was requested, but this PyTorch build cannot access Apple Silicon GPU acceleration.")
29
+ return device
30
+ if torch.backends.mps.is_available():
31
+ return torch.device("mps")
32
+ if torch.cuda.is_available():
33
+ return torch.device("cuda")
34
+ return torch.device("cpu")
35
+
36
+
37
+ def choose_dtype(device: torch.device, requested: str) -> torch.dtype:
38
+ if requested == "float32":
39
+ return torch.float32
40
+ if requested == "float16":
41
+ return torch.float16
42
+ if requested == "bfloat16":
43
+ return torch.bfloat16
44
+ if device.type == "mps":
45
+ return torch.float16
46
+ if device.type == "cuda":
47
+ return torch.bfloat16
48
+ return torch.float32
49
+
50
+
51
+ def find_checkpoint(explicit: Path | None) -> Path:
52
+ if explicit:
53
+ if not explicit.is_file():
54
+ raise FileNotFoundError(f"Checkpoint not found: {explicit}")
55
+ return explicit
56
+ candidates = sorted(ROOT.glob("*.safetensors"))
57
+ if not candidates:
58
+ raise FileNotFoundError(
59
+ "No .safetensors file found. Drag your model .safetensors file into this folder and run again."
60
+ )
61
+ if len(candidates) > 1:
62
+ names = "\n".join(f" - {p.name}" for p in candidates)
63
+ raise RuntimeError(f"More than one .safetensors file was found. Use --checkpoint to choose one:\n{names}")
64
+ return candidates[0]
65
+
66
+
67
+ def repeats_ngram(generated: list[int], candidate: int, n: int) -> bool:
68
+ if n <= 0 or len(generated) + 1 < n:
69
+ return False
70
+ trial = generated + [candidate]
71
+ target = tuple(trial[-n:])
72
+ return any(tuple(trial[i:i+n]) == target for i in range(len(trial) - n))
73
+
74
+
75
+ def load_model(checkpoint_path: Path, device: torch.device, dtype: torch.dtype):
76
+ config = load_model_config(ROOT / "model_ember_proelia_207m_16k.yaml")
77
+ tokenizer = Tokenizer.from_file(str(ROOT / "tokenizer.json"))
78
+ expected = {"<pad>": 0, "<bos>": 1, "<eos>": 2}
79
+ actual = {token: tokenizer.token_to_id(token) for token in expected}
80
+ if actual != expected:
81
+ raise RuntimeError(f"Unexpected tokenizer special IDs: {actual}")
82
+
83
+ print(f"Loading {checkpoint_path.name}…")
84
+ state_dict = load_file(str(checkpoint_path), device="cpu")
85
+ model = AuroraForCausalLM(config)
86
+ missing, unexpected = model.load_state_dict(state_dict, strict=False)
87
+ missing = [key for key in missing if not key.endswith("._extra_state")]
88
+ if missing or unexpected:
89
+ raise RuntimeError(f"Checkpoint mismatch:\nmissing={missing}\nunexpected={unexpected}")
90
+ del state_dict
91
+ model = model.to(device=device, dtype=dtype).eval()
92
+ print(f"Loaded {count_parameters(model):,} parameters on {device} as {str(dtype).replace('torch.', '')}.\n")
93
+ return model, tokenizer, config, expected
94
+
95
+
96
+ def sample_next(logits: torch.Tensor, temperature: float, top_k: int) -> int:
97
+ if temperature <= 0:
98
+ return int(torch.argmax(logits).item())
99
+ logits = logits.float() / temperature
100
+ if top_k > 0:
101
+ top_k = min(top_k, logits.numel())
102
+ values, indices = torch.topk(logits, top_k)
103
+ pick = torch.multinomial(torch.softmax(values, dim=-1), 1)
104
+ return int(indices[pick].item())
105
+ return int(torch.multinomial(torch.softmax(logits, dim=-1), 1).item())
106
+
107
+
108
+ def generate(model, tokenizer, config, special, prompt: str, max_new_tokens: int,
109
+ temperature: float, top_k: int, no_repeat_ngram_size: int) -> tuple[str, str, int, float]:
110
+ prompt_ids = tokenizer.encode(prompt, add_special_tokens=False).ids
111
+ ids = [special["<bos>"], *prompt_ids]
112
+ generated: list[int] = []
113
+ stop_reason = "max_new_tokens"
114
+ answer_mode = prompt.rstrip().casefold().endswith("answer:")
115
+ started = time.perf_counter()
116
+
117
+ with torch.inference_mode():
118
+ for _ in range(max_new_tokens):
119
+ inputs = torch.tensor([ids[-config.context_length:]], device=next(model.parameters()).device, dtype=torch.long)
120
+ logits, _ = model(inputs)
121
+ next_id = sample_next(logits[0, -1], temperature, top_k)
122
+ if next_id == special["<eos>"]:
123
+ stop_reason = "eos"
124
+ break
125
+ if repeats_ngram(generated, next_id, no_repeat_ngram_size):
126
+ stop_reason = f"repeat_{no_repeat_ngram_size}gram"
127
+ break
128
+ generated.append(next_id)
129
+ ids.append(next_id)
130
+ if answer_mode:
131
+ text = tokenizer.decode(generated, skip_special_tokens=True)
132
+ match = ROLE_RESTART.search(text)
133
+ if match:
134
+ text = text[:match.start()].rstrip()
135
+ return text, "role_restart", len(generated), time.perf_counter() - started
136
+
137
+ text = tokenizer.decode(generated, skip_special_tokens=True).strip()
138
+ return text, stop_reason, len(generated), time.perf_counter() - started
139
+
140
+
141
+ def build_prompt(user_text: str, raw_prompt: bool) -> str:
142
+ return user_text if raw_prompt else f"Question: {user_text.strip()}\nAnswer:"
143
+
144
+
145
+ def parse_args() -> argparse.Namespace:
146
+ parser = argparse.ArgumentParser(description="Run Ember Proelia SafeTensors inference on macOS, CUDA, or CPU.")
147
+ parser.add_argument("--checkpoint", type=Path)
148
+ parser.add_argument("--prompt")
149
+ parser.add_argument("--raw-prompt", action="store_true")
150
+ parser.add_argument("--max-new-tokens", type=int, default=128)
151
+ parser.add_argument("--temperature", type=float, default=0.0, help="0 = greedy decoding")
152
+ parser.add_argument("--top-k", type=int, default=40)
153
+ parser.add_argument("--no-repeat-ngram-size", type=int, default=4)
154
+ parser.add_argument("--device", choices=["auto", "mps", "cpu", "cuda"], default="auto")
155
+ parser.add_argument("--dtype", choices=["auto", "float16", "float32", "bfloat16"], default="auto")
156
+ return parser.parse_args()
157
+
158
+
159
+ def main() -> None:
160
+ args = parse_args()
161
+ checkpoint = find_checkpoint(args.checkpoint)
162
+ device = choose_device(args.device)
163
+ dtype = choose_dtype(device, args.dtype)
164
+ model, tokenizer, config, special = load_model(checkpoint, device, dtype)
165
+
166
+ def run(text: str) -> None:
167
+ prompt = build_prompt(text, args.raw_prompt)
168
+ completion, reason, count, elapsed = generate(
169
+ model, tokenizer, config, special, prompt,
170
+ args.max_new_tokens, args.temperature, args.top_k, args.no_repeat_ngram_size,
171
+ )
172
+ rate = count / elapsed if elapsed > 0 else 0.0
173
+ print(f"\nEmber: {completion}\n\n[{reason}; {count} tokens; {rate:.1f} tok/s]\n")
174
+
175
+ if args.prompt:
176
+ run(args.prompt)
177
+ return
178
+
179
+ print("Interactive Ember Proelia inference. Type /quit to exit.")
180
+ while True:
181
+ try:
182
+ text = input("\nYou: ").strip()
183
+ except (EOFError, KeyboardInterrupt):
184
+ print()
185
+ return
186
+ if text.casefold() in {"/quit", "/exit", "quit", "exit"}:
187
+ return
188
+ if text:
189
+ run(text)
190
+
191
+
192
+ if __name__ == "__main__":
193
+ try:
194
+ main()
195
+ except Exception as exc:
196
+ print(f"\nERROR: {exc}", file=sys.stderr)
197
+ raise SystemExit(1)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ torch>=2.5
2
+ tokenizers>=0.20
3
+ safetensors>=0.4
4
+ PyYAML>=6.0