| """ |
| NovaMind-256M: Decoder-Only Conversational Language Model |
| ========================================================== |
| Architecture: Decoder-only Transformer |
| |
| Key design choices: |
| 1. HiRoPE (Hierarchical RoPE) β adapted from "HiRoPE: Length Extrapolation |
| for Code Models Using Hierarchical Position" (ACL 2024). |
| Original paper used it for code structure (token/statement/function hierarchy). |
| We adapt it for conversation structure: local turn position vs. global dialogue position. |
| Split head dims into local-base=10K and global-base=500K streams. |
| 2. Tag-Aware Loss Curriculum β original contribution. |
| Per-token loss weighting that changes across training phases. |
| |
| Target: ~252M parameters (after weight tying) |
| Hardware target: H100 40GB (Lightning.ai student account) |
| Precision: BF16 |
| """ |
|
|
| import math |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| from torch.utils.checkpoint import checkpoint as gradient_checkpoint |
| from dataclasses import dataclass, field, asdict |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class NovaMindConfig: |
| """ |
| All hyperparameters in one place for clean checkpointing and reproducibility. |
| Changing a value here changes the entire model β no magic numbers buried |
| in the code. |
| """ |
| |
| vocab_size: int = 32_000 |
|
|
| |
| d_model: int = 1_024 |
| n_heads: int = 16 |
| n_kv_heads: int = 4 |
| n_layers: int = 24 |
| ff_dim: int = 2_304 |
|
|
| |
| max_seq_len: int = 2_048 |
|
|
| |
| rope_local_base: float = 10_000.0 |
| rope_global_base: float = 500_000.0 |
|
|
| |
| dropout: float = 0.0 |
| attn_dropout: float = 0.0 |
|
|
| |
| init_std: float = 0.02 |
|
|
| def head_dim(self) -> int: |
| assert self.d_model % self.n_heads == 0, "d_model must be divisible by n_heads" |
| return self.d_model // self.n_heads |
|
|
| def n_rep(self) -> int: |
| """How many Q heads share each KV head.""" |
| assert self.n_heads % self.n_kv_heads == 0 |
| return self.n_heads // self.n_kv_heads |
|
|
| def to_dict(self): |
| return asdict(self) |
|
|
| @classmethod |
| def from_dict(cls, d): |
| return cls(**d) |
|
|
|
|
| |
| |
| |
|
|
| class RMSNorm(nn.Module): |
| """ |
| Root Mean Square Layer Normalization (Zhang & Sennrich, 2019). |
| Faster than LayerNorm because it skips the mean-centering step. |
| Used by LLaMA, Gemma, Qwen, Mistral β now us. |
| """ |
| def __init__(self, dim: int, eps: float = 1e-6): |
| super().__init__() |
| self.eps = eps |
| self.weight = nn.Parameter(torch.ones(dim)) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| |
| |
| rms = x.pow(2).mean(-1, keepdim=True).add(self.eps).rsqrt() |
| return x * rms * self.weight |
|
|
|
|
| |
| |
| |
| |
| |
|
|
| def build_hier_rope_cache( |
| seq_len: int, |
| head_dim: int, |
| device: torch.device, |
| local_base: float = 10_000.0, |
| global_base: float = 500_000.0, |
| ) -> tuple[torch.Tensor, torch.Tensor]: |
| """ |
| HiRoPE: Hierarchical Rotary Position Embedding Cache. |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| Adapted from: "HiRoPE: Length Extrapolation for Code Models Using |
| Hierarchical Position" (ACL 2024). |
| |
| Original paper: used code structure hierarchy (token-in-statement, |
| statement-in-function) to split RoPE dimensions. |
| Our adaptation: we use CONVERSATION structure instead. |
| |
| β’ Dims 0 β¦ head_dim//2-1 β LOCAL RoPE (base=10,000) |
| Encodes fine-grained position WITHIN a conversation turn. |
| Same frequency as standard LLaMA RoPE β well-calibrated for |
| sentence-level syntax and local coreference. |
| |
| β’ Dims head_dim//2 β¦ head_dim-1 β GLOBAL RoPE (base=500,000) |
| Encodes coarse position ACROSS conversation turns. |
| A higher base = slower rotation = can distinguish positions |
| across much longer spans without frequency wrap-around. |
| This is what the original HiRoPE paper used for cross-function |
| dependencies in code; we repurpose it for cross-turn coherence. |
| |
| Cost: ZERO extra parameters. Purely a change in the frequency basis. |
| |
| Returns: |
| cos, sin β each shape [seq_len, head_dim], cached for efficiency |
| """ |
| half = head_dim // 2 |
|
|
| |
| |
| |
| local_theta = 1.0 / ( |
| local_base ** (torch.arange(0, half, 2, device=device).float() / half) |
| ) |
| |
| global_theta = 1.0 / ( |
| global_base ** (torch.arange(0, half, 2, device=device).float() / half) |
| ) |
|
|
| positions = torch.arange(seq_len, device=device).float() |
|
|
| |
| local_freqs = torch.outer(positions, local_theta) |
| global_freqs = torch.outer(positions, global_theta) |
|
|
| |
| |
| local_freqs = torch.cat([local_freqs, local_freqs], dim=-1) |
| global_freqs = torch.cat([global_freqs, global_freqs], dim=-1) |
|
|
| |
| cos = torch.cat([local_freqs.cos(), global_freqs.cos()], dim=-1) |
| sin = torch.cat([local_freqs.sin(), global_freqs.sin()], dim=-1) |
|
|
| return cos, sin |
|
|
|
|
| def _rotate_half(x: torch.Tensor) -> torch.Tensor: |
| """Standard RoPE rotation: rotate each pair of dims by 90Β°.""" |
| half = x.shape[-1] // 2 |
| return torch.cat([-x[..., half:], x[..., :half]], dim=-1) |
|
|
|
|
| def apply_hier_rope( |
| q: torch.Tensor, |
| k: torch.Tensor, |
| cos: torch.Tensor, |
| sin: torch.Tensor, |
| ) -> tuple[torch.Tensor, torch.Tensor]: |
| """ |
| Apply HierRoPE to query and key tensors. |
| Because cos/sin are already split into local//global halves, |
| this is mathematically identical to standard RoPE β the hierarchy |
| lives in the frequency basis, not the application code. |
| |
| Args: |
| q, k: [batch, n_heads, seq_len, head_dim] |
| cos, sin: [seq_len, head_dim] (from build_hier_rope_cache) |
| Returns: |
| rotated q, k: same shapes as input |
| """ |
| |
| cos = cos[None, None, :, :] |
| sin = sin[None, None, :, :] |
|
|
| q_rot = q * cos + _rotate_half(q) * sin |
| k_rot = k * cos + _rotate_half(k) * sin |
| return q_rot, k_rot |
|
|
|
|
| |
| |
| |
|
|
| class GQACausalAttention(nn.Module): |
| """ |
| Grouped Query Attention (GQA) with HiRoPE (adapted from ACL 2024 paper). |
| |
| GQA uses n_heads query heads but only n_kv_heads key/value heads. |
| Each KV head is shared by (n_heads // n_kv_heads) query heads. |
| This reduces KV cache size by 4x during inference β critical for long |
| conversations. Used by LLaMA 2/3, Mistral, Gemma. |
| |
| Attention is computed via F.scaled_dot_product_attention which uses |
| Flash Attention 2 on supported hardware (H100 β
). |
| """ |
|
|
| def __init__(self, config: NovaMindConfig): |
| super().__init__() |
| self.n_heads = config.n_heads |
| self.n_kv_heads = config.n_kv_heads |
| self.n_rep = config.n_rep() |
| self.head_dim = config.head_dim() |
| self.d_model = config.d_model |
|
|
| |
| self.q_proj = nn.Linear(self.d_model, self.n_heads * self.head_dim, bias=False) |
| self.k_proj = nn.Linear(self.d_model, self.n_kv_heads * self.head_dim, bias=False) |
| self.v_proj = nn.Linear(self.d_model, self.n_kv_heads * self.head_dim, bias=False) |
| self.o_proj = nn.Linear(self.d_model, self.d_model, bias=False) |
|
|
| self.attn_drop = config.attn_dropout |
|
|
| |
| |
| self.register_buffer( |
| "_rope_cos", |
| torch.zeros(config.max_seq_len, self.head_dim), |
| persistent=False, |
| ) |
| self.register_buffer( |
| "_rope_sin", |
| torch.zeros(config.max_seq_len, self.head_dim), |
| persistent=False, |
| ) |
| self._rope_cached_len = 0 |
| self._rope_local_base = config.rope_local_base |
| self._rope_global_base = config.rope_global_base |
|
|
| def _get_hier_rope(self, seq_len: int, device: torch.device): |
| """Lazily compute and cache HiRoPE tables.""" |
| if seq_len > self._rope_cached_len or self._rope_cos.device != device: |
| cos, sin = build_hier_rope_cache( |
| seq_len, self.head_dim, device, |
| local_base=self._rope_local_base, |
| global_base=self._rope_global_base, |
| ) |
| self._rope_cos = cos |
| self._rope_sin = sin |
| self._rope_cached_len = seq_len |
| return self._rope_cos[:seq_len], self._rope_sin[:seq_len] |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| """ |
| Args: |
| x: [B, T, D] β input hidden states |
| Returns: |
| out: [B, T, D] |
| """ |
| B, T, D = x.shape |
|
|
| |
| Q = self.q_proj(x).view(B, T, self.n_heads, self.head_dim).transpose(1, 2) |
| K = self.k_proj(x).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2) |
| V = self.v_proj(x).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2) |
| |
| |
| |
|
|
| |
| cos, sin = self._get_hier_rope(T, x.device) |
| Q, K = apply_hier_rope(Q, K, cos.to(x.dtype), sin.to(x.dtype)) |
|
|
| |
| |
| |
| K = K.unsqueeze(2).expand(B, self.n_kv_heads, self.n_rep, T, self.head_dim)\ |
| .reshape(B, self.n_heads, T, self.head_dim) |
| V = V.unsqueeze(2).expand(B, self.n_kv_heads, self.n_rep, T, self.head_dim)\ |
| .reshape(B, self.n_heads, T, self.head_dim) |
|
|
| |
| out = F.scaled_dot_product_attention( |
| Q, K, V, |
| attn_mask=None, |
| dropout_p=self.attn_drop if self.training else 0.0, |
| is_causal=True, |
| ) |
|
|
| |
| out = out.transpose(1, 2).contiguous().view(B, T, D) |
| return self.o_proj(out) |
|
|
|
|
| |
| |
| |
|
|
| class SwiGLUFFN(nn.Module): |
| """ |
| SwiGLU Feed-Forward Network (Shazeer, 2020; adopted by PaLM, LLaMA, Gemma). |
| |
| Unlike standard FFN (2 matrices: up-project + down-project), |
| SwiGLU uses 3 matrices: |
| gate(x) β passed through SiLU activation (the "gate") |
| up(x) β element-wise multiplied with the gate output |
| down(Β·) β project back to d_model |
| |
| The gating mechanism acts like learned neuron selection β it can |
| suppress irrelevant features entirely. Result: ~15% better quality |
| per parameter vs standard GELU FFN (measured in Google's PaLM paper). |
| """ |
|
|
| def __init__(self, config: NovaMindConfig): |
| super().__init__() |
| |
| self.gate = nn.Linear(config.d_model, config.ff_dim, bias=False) |
| self.up = nn.Linear(config.d_model, config.ff_dim, bias=False) |
| self.down = nn.Linear(config.ff_dim, config.d_model, bias=False) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| |
| return self.down(F.silu(self.gate(x)) * self.up(x)) |
|
|
|
|
| |
| |
| |
|
|
| class TransformerBlock(nn.Module): |
| """ |
| One transformer layer using Pre-RMSNorm architecture: |
| |
| x β RMSNorm β GQACausalAttention β +x (residual) |
| β RMSNorm β SwiGLUFFN β +x (residual) |
| |
| Pre-norm (norm BEFORE the sub-layer) is more training-stable than |
| Post-norm, especially at 24 layers deep. All modern LLMs use Pre-norm. |
| """ |
|
|
| def __init__(self, config: NovaMindConfig): |
| super().__init__() |
| self.ln_attn = RMSNorm(config.d_model) |
| self.attn = GQACausalAttention(config) |
| self.ln_ffn = RMSNorm(config.d_model) |
| self.ffn = SwiGLUFFN(config) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| |
| x = x + self.attn(self.ln_attn(x)) |
| |
| x = x + self.ffn(self.ln_ffn(x)) |
| return x |
|
|
|
|
| |
| |
| |
|
|
| def tag_aware_loss( |
| logits: torch.Tensor, |
| targets: torch.Tensor, |
| token_weights: torch.Tensor | None = None, |
| ) -> torch.Tensor: |
| """ |
| Tag-Aware Loss Curriculum (Novel Contribution #2). |
| ββββββββββββββββββββββββββββββββββββββββββββββββββ |
| Standard cross-entropy treats every predicted token equally. |
| We instead weight each token's loss contribution by its role in the |
| conversation, and by which training phase we are in: |
| |
| Token types + phase weights: |
| ββββββββββββββββββ¬βββββββββββ¬βββββββββββ¬βββββββββββββββ |
| β Token type β Phase 1 β Phase 2 β Phase 3 β |
| β β Pretrain β SFT β CoT SFT β |
| ββββββββββββββββββΌβββββββββββΌβββββββββββΌβββββββββββββββ€ |
| β <think> β 0.0 β 0.5 β 1.5 β BOOSTβ |
| β <assistant> β 1.0 β 1.0 β 1.0 β |
| β <human>/system β 0.0 β 0.0 β 0.0 β |
| ββββββββββββββββββ΄βββββββββββ΄βββββββββββ΄βββββββββββββββ |
| |
| Why this is novel: |
| - Most papers use binary masking (weight is 0 or 1) |
| - A curriculum that changes weights across phases has not been |
| published at this scale |
| - Phase 3 boosting (weight=1.5 for think tokens) forces the model to |
| "care" about the quality of its own reasoning chain |
| |
| Args: |
| logits: [B, T, V] β raw output logits |
| targets: [B, T] β target token ids (-1 = ignore this position) |
| token_weights: [B, T] β per-token float weight (None β uniform 1.0) |
| |
| Returns: |
| Scalar loss value |
| """ |
| B, T, V = logits.shape |
|
|
| |
| logits_flat = logits.view(B * T, V) |
| targets_flat = targets.view(B * T) |
|
|
| if token_weights is None: |
| |
| return F.cross_entropy(logits_flat, targets_flat, ignore_index=-1) |
|
|
| |
| per_token_loss = F.cross_entropy( |
| logits_flat, targets_flat, |
| ignore_index=-1, |
| reduction="none", |
| ) |
|
|
| weights_flat = token_weights.view(B * T) |
|
|
| |
| |
| |
| valid_mask = (targets_flat != -1).float() |
| weights_flat = weights_flat * valid_mask |
|
|
| |
| |
| weighted_loss = (per_token_loss * weights_flat).sum() |
| weight_sum = weights_flat.sum().clamp(min=1e-8) |
|
|
| return weighted_loss / weight_sum |
|
|
|
|
| |
| |
| |
|
|
| class NovaMind256M(nn.Module): |
| """ |
| NovaMind-256M: 256M parameter conversational LLM. |
| |
| Architectural features: |
| - Decoder-only transformer (autoregressive) |
| - Pre-RMSNorm for training stability |
| - GQA (16Q / 4KV) for inference efficiency |
| - SwiGLU FFN for parameter efficiency |
| - HiRoPE for hierarchical positional encoding (adapted from ACL 2024 paper) |
| - Weight-tied embedding and LM head (saves 33M params) |
| - Tag-Aware Loss Curriculum [NOVEL] |
| - BF16 training, Flash Attention 2 |
| """ |
|
|
| def __init__(self, config: NovaMindConfig): |
| super().__init__() |
| self.config = config |
| self._use_gradient_checkpointing = False |
|
|
| |
| self.token_emb = nn.Embedding(config.vocab_size, config.d_model) |
| self.emb_dropout = nn.Dropout(config.dropout) if config.dropout > 0 else nn.Identity() |
|
|
| |
| self.blocks = nn.ModuleList([ |
| TransformerBlock(config) for _ in range(config.n_layers) |
| ]) |
|
|
| |
| self.ln_f = RMSNorm(config.d_model) |
| self.lm_head = nn.Linear(config.d_model, config.vocab_size, bias=False) |
|
|
| |
| |
| |
| |
| |
| self.lm_head.weight = self.token_emb.weight |
|
|
| |
| self.apply(self._init_weights) |
|
|
| def _init_weights(self, module: nn.Module): |
| """ |
| GPT-2 style initialization with scaled residual projections. |
| |
| Output projections (o_proj, down in FFN) are scaled by 1/sqrt(2*N) |
| where N = number of layers. This prevents the residual stream from |
| growing proportionally to depth, which would destabilize training. |
| |
| Reference: GPT-2 paper, Table 2, "Modified initialization" footnote. |
| """ |
| std = self.config.init_std |
| if isinstance(module, nn.Linear): |
| nn.init.normal_(module.weight, mean=0.0, std=std) |
| if module.bias is not None: |
| nn.init.zeros_(module.bias) |
| elif isinstance(module, nn.Embedding): |
| nn.init.normal_(module.weight, mean=0.0, std=std) |
|
|
| |
| |
| residual_proj_names = {"o_proj", "down"} |
| for name, param in module.named_parameters(recurse=False): |
| |
| parent_name = type(module).__name__ |
| if any(p in parent_name.lower() or p in name.lower() |
| for p in residual_proj_names): |
| nn.init.normal_( |
| param, |
| mean=0.0, |
| std=std / math.sqrt(2 * self.config.n_layers), |
| ) |
|
|
| |
| def gradient_checkpointing_enable(self): |
| """ |
| Enable gradient checkpointing to trade compute for VRAM. |
| Discards intermediate activations during forward pass, recomputes |
| them during backward. Saves ~40% VRAM at cost of ~20% slower training. |
| CRITICAL for fitting 256M model + optimizer states on H100 40GB. |
| """ |
| self._use_gradient_checkpointing = True |
|
|
| def gradient_checkpointing_disable(self): |
| self._use_gradient_checkpointing = False |
|
|
| |
| def forward( |
| self, |
| input_ids: torch.Tensor, |
| targets: torch.Tensor | None = None, |
| token_weights: torch.Tensor | None = None, |
| ) -> tuple[torch.Tensor, torch.Tensor | None]: |
| """ |
| Args: |
| input_ids: [B, T] β token ids |
| targets: [B, T] β target ids for loss computation (-1 = ignore) |
| If None, loss is not computed (inference mode) |
| token_weights: [B, T] β per-token float weights for Tag-Aware Loss |
| If None, uses standard uniform cross-entropy |
| |
| Returns: |
| (logits, loss) |
| - logits: [B, T, V] during inference; empty tensor during training |
| (returning full logits during training on DataParallel is wasteful β |
| it forces a massive tensor transfer to GPU 0. We only need the loss.) |
| - loss: scalar tensor if targets provided, else None |
| """ |
| B, T = input_ids.shape |
| assert T <= self.config.max_seq_len, ( |
| f"Input length {T} exceeds max_seq_len {self.config.max_seq_len}" |
| ) |
|
|
| |
| x = self.emb_dropout(self.token_emb(input_ids)) |
|
|
| |
| for block in self.blocks: |
| if self._use_gradient_checkpointing and self.training: |
| |
| |
| x = gradient_checkpoint(block, x, use_reentrant=False) |
| else: |
| x = block(x) |
|
|
| |
| logits = self.lm_head(self.ln_f(x)) |
|
|
| loss = None |
| if targets is not None: |
| loss = tag_aware_loss(logits, targets, token_weights) |
| |
| if self.training: |
| logits = torch.empty(0, device=logits.device) |
|
|
| return logits, loss |
|
|
| |
| @torch.no_grad() |
| def generate( |
| self, |
| input_ids: torch.Tensor, |
| max_new_tokens: int = 256, |
| temperature: float = 0.7, |
| top_k: int = 50, |
| top_p: float = 0.9, |
| repetition_penalty: float = 1.2, |
| stop_token_ids: list[int] | None = None, |
| ) -> torch.Tensor: |
| """ |
| Autoregressive text generation with temperature, top-k, top-p sampling |
| and repetition penalty. |
| |
| Args: |
| input_ids: [1, T] β prompt tokens |
| max_new_tokens: maximum tokens to generate |
| temperature: sampling temperature (lower = more focused) |
| top_k: keep only top-k logits before sampling |
| top_p: nucleus sampling threshold |
| repetition_penalty: >1.0 penalizes repeated tokens |
| stop_token_ids: list of token ids that stop generation |
| |
| Returns: |
| token ids including the prompt: [1, T + generated] |
| """ |
| self.eval() |
| for _ in range(max_new_tokens): |
| |
| idx_cond = input_ids[:, -self.config.max_seq_len:] |
|
|
| logits, _ = self(idx_cond) |
| logits = logits[:, -1, :] |
|
|
| |
| if repetition_penalty != 1.0: |
| for token_id in set(idx_cond[0].tolist()): |
| if logits[0, token_id] < 0: |
| logits[0, token_id] *= repetition_penalty |
| else: |
| logits[0, token_id] /= repetition_penalty |
|
|
| |
| logits = logits / max(temperature, 1e-8) |
|
|
| |
| if top_k is not None and top_k > 0: |
| top_k_val = min(top_k, logits.size(-1)) |
| kth_val = torch.topk(logits, top_k_val).values[:, -1, None] |
| logits[logits < kth_val] = float("-inf") |
|
|
| |
| |
| if top_p is not None and 0.0 < top_p < 1.0: |
| sorted_logits, sorted_idx = torch.sort(logits, dim=-1, descending=True) |
| cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1) |
| |
| |
| sorted_remove = cumulative_probs - F.softmax(sorted_logits, dim=-1) > top_p |
| sorted_logits[sorted_remove] = float("-inf") |
| logits = torch.zeros_like(logits).scatter_( |
| dim=-1, index=sorted_idx, src=sorted_logits |
| ) |
|
|
| probs = F.softmax(logits, dim=-1) |
| next_id = torch.multinomial(probs, num_samples=1) |
|
|
| input_ids = torch.cat([input_ids, next_id], dim=1) |
|
|
| |
| if stop_token_ids and next_id.item() in stop_token_ids: |
| break |
|
|
| return input_ids |
|
|
| |
| def count_parameters(self, print_table: bool = True) -> int: |
| """ |
| Count and optionally print a detailed parameter breakdown. |
| Useful for verifying we hit ~252M. |
| """ |
| cfg = self.config |
|
|
| embed_params = cfg.vocab_size * cfg.d_model |
| hd = cfg.head_dim() |
|
|
| |
| |
| qkv_params = (cfg.n_heads + 2 * cfg.n_kv_heads) * hd * cfg.d_model |
| |
| o_params = cfg.d_model * cfg.d_model |
| attn_params = qkv_params + o_params |
| |
| ffn_params = 3 * cfg.d_model * cfg.ff_dim |
| norm_params = 2 * cfg.d_model |
| per_layer = attn_params + ffn_params + norm_params |
|
|
| final_norm = cfg.d_model |
| total_with_tie = embed_params + cfg.n_layers * per_layer + final_norm |
| total_trainable = sum(p.numel() for p in self.parameters() if p.requires_grad) |
|
|
| if print_table: |
| w = 46 |
| print("β" * w) |
| print(f" NovaMind-256M Parameter Breakdown") |
| print("β" * w) |
| print(f" {'Token Embedding (shared w/ lm_head)':<40} {embed_params/1e6:>6.2f}M") |
| print(f" {'Attention QKV projections (per layer)':<40} {qkv_params/1e6:>6.2f}M") |
| print(f" {'Attention O projection (per layer)':<40} {o_params/1e6:>6.2f}M") |
| print(f" {'SwiGLU FFN (per layer)':<40} {ffn_params/1e6:>6.2f}M") |
| print(f" {'RMSNorm Γ 2 (per layer)':<40} {norm_params/1e6:>6.4f}M") |
| print(f" {'Γ {n} layers total':<40} {cfg.n_layers * per_layer/1e6:>6.2f}M") |
| print(f" {'Final RMSNorm':<40} {final_norm/1e6:>6.4f}M") |
| print(f" {'LM Head (weight-tied to embedding)':<40} {'β':>4} 0.00M") |
| print("β" * w) |
| print(f" {'TOTAL (formula estimate)':<40} {total_with_tie/1e6:>6.2f}M") |
| print(f" {'TOTAL (actual PyTorch count)':<40} {total_trainable/1e6:>6.2f}M") |
| print("β" * w) |
|
|
| return total_trainable |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| import sys |
|
|
| config = NovaMindConfig() |
| print(f"\nBuilding NovaMind-256M with config:") |
| for k, v in config.to_dict().items(): |
| print(f" {k}: {v}") |
| print() |
|
|
| model = NovaMind256M(config) |
| n_params = model.count_parameters(print_table=True) |
|
|
| |
| target_min, target_max = 245e6, 265e6 |
| status = "β
" if target_min <= n_params <= target_max else "β" |
| print(f"\n{status} Parameter count: {n_params/1e6:.2f}M (target: 245β265M)\n") |
|
|
| if not (target_min <= n_params <= target_max): |
| sys.exit(1) |
|
|
| |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| model = model.to(device) |
| model.gradient_checkpointing_enable() |
|
|
| B, T = 2, 128 |
| ids = torch.randint(0, config.vocab_size, (B, T), device=device) |
| targets = ids.clone() |
| weights = torch.ones(B, T, device=device) |
| weights[:, :10] = 0.0 |
| weights[:, 20:40] = 1.5 |
|
|
| with torch.autocast(device_type=device.type, dtype=torch.bfloat16): |
| logits, loss = model(ids, targets, weights) |
|
|
| print(f"Forward pass OK β loss: {loss.item():.4f}") |
| print(f"Logits shape during training: {logits.shape} (should be empty [0])") |
|
|
| |
| prompt = torch.randint(0, config.vocab_size, (1, 10), device=device) |
| with torch.autocast(device_type=device.type, dtype=torch.bfloat16): |
| generated = model.generate(prompt, max_new_tokens=20, temperature=0.8) |
| print(f"Generation OK β output shape: {generated.shape}") |
| print("\nβ
All checks passed. model.py is ready.\n") |
|
|