| """ECOACO — the rurtech.ai Mixture-of-Experts causal language model. |
| |
| ECOACO (Sanskrit: wealth, prosperity, purpose) is rurtech.ai's vertical |
| Mixture-of-Experts LLM for banking. |
| |
| This is a real, from-scratch sparse-MoE transformer (not a wrapper around |
| another model). Each transformer block replaces the dense feed-forward network |
| with a set of expert FFNs and a top-k router, so only a fraction of parameters |
| activate per token — the defining property of a Mixture-of-Experts model. |
| |
| Design goals: small enough to train and run CPU-only in this environment, yet |
| architecturally faithful so the same code scales up by changing the config. |
| |
| Key pieces: |
| * RMSNorm + rotary position embeddings (RoPE) |
| * Multi-head self-attention with a causal mask |
| * Top-k token routing over N experts, with a load-balancing aux loss |
| * Weight tying between the embedding and the LM head |
| """ |
|
|
| from __future__ import annotations |
|
|
| import math |
| from dataclasses import asdict, dataclass |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
|
|
|
|
| @dataclass |
| class EcoacoConfig: |
| version: str = "1.0" |
| vocab_size: int = 4096 |
| dim: int = 256 |
| n_layers: int = 6 |
| n_heads: int = 8 |
| n_experts: int = 8 |
| n_experts_per_token: int = 2 |
| ffn_hidden: int = 512 |
| max_seq_len: int = 512 |
| rope_theta: float = 10000.0 |
| aux_loss_coef: float = 0.01 |
| tie_embeddings: bool = True |
|
|
| def to_dict(self) -> dict: |
| d = asdict(self) |
| d["model_type"] = "ecoaco" |
| d["name"] = "ECOACO" |
| d["architectures"] = ["EcoacoForCausalLM"] |
| return d |
|
|
|
|
| def _rope_cache(seq_len: int, head_dim: int, theta: float, device, dtype): |
| inv_freq = 1.0 / (theta ** (torch.arange(0, head_dim, 2, device=device).float() / head_dim)) |
| t = torch.arange(seq_len, device=device).float() |
| freqs = torch.outer(t, inv_freq) |
| emb = torch.cat((freqs, freqs), dim=-1) |
| return emb.cos().to(dtype), emb.sin().to(dtype) |
|
|
|
|
| def _rotate_half(x): |
| x1, x2 = x.chunk(2, dim=-1) |
| return torch.cat((-x2, x1), dim=-1) |
|
|
|
|
| def _apply_rope(q, k, cos, sin): |
| cos = cos[None, None, :, :] |
| sin = sin[None, None, :, :] |
| q = (q * cos) + (_rotate_half(q) * sin) |
| k = (k * cos) + (_rotate_half(k) * sin) |
| return q, k |
|
|
|
|
| class RMSNorm(nn.Module): |
| def __init__(self, dim: int, eps: float = 1e-6): |
| super().__init__() |
| self.weight = nn.Parameter(torch.ones(dim)) |
| self.eps = eps |
|
|
| def forward(self, x): |
| norm = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) |
| return norm * self.weight |
|
|
|
|
| class Attention(nn.Module): |
| def __init__(self, cfg: EcoacoConfig): |
| super().__init__() |
| self.n_heads = cfg.n_heads |
| self.head_dim = cfg.dim // cfg.n_heads |
| self.wq = nn.Linear(cfg.dim, cfg.dim, bias=False) |
| self.wk = nn.Linear(cfg.dim, cfg.dim, bias=False) |
| self.wv = nn.Linear(cfg.dim, cfg.dim, bias=False) |
| self.wo = nn.Linear(cfg.dim, cfg.dim, bias=False) |
|
|
| def forward(self, x, cos, sin): |
| b, t, _ = x.shape |
| q = self.wq(x).view(b, t, self.n_heads, self.head_dim).transpose(1, 2) |
| k = self.wk(x).view(b, t, self.n_heads, self.head_dim).transpose(1, 2) |
| v = self.wv(x).view(b, t, self.n_heads, self.head_dim).transpose(1, 2) |
| q, k = _apply_rope(q, k, cos, sin) |
| out = F.scaled_dot_product_attention(q, k, v, is_causal=True) |
| out = out.transpose(1, 2).contiguous().view(b, t, -1) |
| return self.wo(out) |
|
|
|
|
| class Expert(nn.Module): |
| """A single SwiGLU feed-forward expert.""" |
|
|
| def __init__(self, dim: int, hidden: int): |
| super().__init__() |
| self.w1 = nn.Linear(dim, hidden, bias=False) |
| self.w2 = nn.Linear(hidden, dim, bias=False) |
| self.w3 = nn.Linear(dim, hidden, bias=False) |
|
|
| def forward(self, x): |
| return self.w2(F.silu(self.w1(x)) * self.w3(x)) |
|
|
|
|
| class MoELayer(nn.Module): |
| """Top-k routed mixture of experts with a load-balancing auxiliary loss.""" |
|
|
| def __init__(self, cfg: EcoacoConfig): |
| super().__init__() |
| self.n_experts = cfg.n_experts |
| self.top_k = cfg.n_experts_per_token |
| self.gate = nn.Linear(cfg.dim, cfg.n_experts, bias=False) |
| self.experts = nn.ModuleList( |
| [Expert(cfg.dim, cfg.ffn_hidden) for _ in range(cfg.n_experts)] |
| ) |
| self.aux_loss = torch.tensor(0.0) |
|
|
| def forward(self, x): |
| b, t, d = x.shape |
| x_flat = x.reshape(-1, d) |
| logits = self.gate(x_flat) |
| probs = F.softmax(logits, dim=-1) |
|
|
| topk_probs, topk_idx = probs.topk(self.top_k, dim=-1) |
| topk_probs = topk_probs / topk_probs.sum(dim=-1, keepdim=True) |
|
|
| out = torch.zeros_like(x_flat) |
| for slot in range(self.top_k): |
| expert_ids = topk_idx[:, slot] |
| weight = topk_probs[:, slot].unsqueeze(-1) |
| for e in range(self.n_experts): |
| mask = expert_ids == e |
| if mask.any(): |
| out[mask] += weight[mask] * self.experts[e](x_flat[mask]) |
|
|
| |
| importance = probs.mean(dim=0) |
| load = torch.zeros(self.n_experts, device=x.device) |
| load.scatter_add_(0, topk_idx.reshape(-1), torch.ones_like(topk_idx.reshape(-1), dtype=load.dtype)) |
| load = load / load.sum().clamp(min=1) |
| self.aux_loss = (importance * load).sum() * self.n_experts |
|
|
| return out.view(b, t, d) |
|
|
|
|
| class Block(nn.Module): |
| def __init__(self, cfg: EcoacoConfig): |
| super().__init__() |
| self.attn_norm = RMSNorm(cfg.dim) |
| self.attn = Attention(cfg) |
| self.moe_norm = RMSNorm(cfg.dim) |
| self.moe = MoELayer(cfg) |
|
|
| def forward(self, x, cos, sin): |
| x = x + self.attn(self.attn_norm(x), cos, sin) |
| x = x + self.moe(self.moe_norm(x)) |
| return x |
|
|
|
|
| class EcoacoForCausalLM(nn.Module): |
| def __init__(self, cfg: EcoacoConfig): |
| super().__init__() |
| self.cfg = cfg |
| self.embed = nn.Embedding(cfg.vocab_size, cfg.dim) |
| self.blocks = nn.ModuleList([Block(cfg) for _ in range(cfg.n_layers)]) |
| self.norm = RMSNorm(cfg.dim) |
| self.lm_head = nn.Linear(cfg.dim, cfg.vocab_size, bias=False) |
| if cfg.tie_embeddings: |
| self.lm_head.weight = self.embed.weight |
| self.apply(self._init) |
|
|
| def _init(self, module): |
| if isinstance(module, nn.Linear): |
| nn.init.normal_(module.weight, mean=0.0, std=0.02) |
| elif isinstance(module, nn.Embedding): |
| nn.init.normal_(module.weight, mean=0.0, std=0.02) |
|
|
| def forward(self, input_ids, labels=None): |
| b, t = input_ids.shape |
| x = self.embed(input_ids) |
| cos, sin = _rope_cache(t, self.cfg.dim // self.cfg.n_heads, self.cfg.rope_theta, |
| x.device, x.dtype) |
| aux = torch.tensor(0.0, device=x.device) |
| for block in self.blocks: |
| x = block(x, cos, sin) |
| aux = aux + block.moe.aux_loss |
| x = self.norm(x) |
| logits = self.lm_head(x) |
|
|
| loss = None |
| if labels is not None: |
| shift_logits = logits[:, :-1, :].contiguous() |
| shift_labels = labels[:, 1:].contiguous() |
| ce = F.cross_entropy( |
| shift_logits.view(-1, shift_logits.size(-1)), |
| shift_labels.view(-1), |
| ignore_index=-100, |
| ) |
| loss = ce + self.cfg.aux_loss_coef * (aux / self.cfg.n_layers) |
| return {"logits": logits, "loss": loss, "aux_loss": aux} |
|
|
| @torch.no_grad() |
| def generate(self, input_ids, max_new_tokens=64, temperature=0.8, top_k=40, eos_id=None): |
| self.eval() |
| for _ in range(max_new_tokens): |
| ids = input_ids[:, -self.cfg.max_seq_len:] |
| logits = self(ids)["logits"][:, -1, :] |
| if temperature > 0: |
| logits = logits / temperature |
| if top_k: |
| v, _ = logits.topk(min(top_k, logits.size(-1))) |
| logits[logits < v[:, [-1]]] = -float("inf") |
| probs = F.softmax(logits, dim=-1) |
| nxt = torch.multinomial(probs, 1) |
| else: |
| nxt = logits.argmax(-1, keepdim=True) |
| input_ids = torch.cat([input_ids, nxt], dim=1) |
| if eos_id is not None and (nxt == eos_id).all(): |
| break |
| return input_ids |
|
|
| def num_params(self) -> tuple[int, int]: |
| total = sum(p.numel() for p in self.parameters()) |
| |
| per_expert = sum(p.numel() for p in self.blocks[0].moe.experts[0].parameters()) |
| inactive = per_expert * (self.cfg.n_experts - self.cfg.n_experts_per_token) * self.cfg.n_layers |
| return total, total - inactive |
|
|
| @classmethod |
| def from_pretrained(cls, model_id_or_path: str): |
| """Load ECOACO from a local directory or a Hugging Face repo id. |
| |
| from modeling_ecoaco import EcoacoForCausalLM |
| model, tok = EcoacoForCausalLM.from_pretrained("rurtech-ai/ECOACO") |
| |
| A local path is used as-is; anything else is fetched from the HF Hub |
| (requires `pip install huggingface_hub`). |
| """ |
| import json |
| from pathlib import Path |
|
|
| from safetensors.torch import load_model |
|
|
| path = Path(model_id_or_path) |
| if not path.exists(): |
| from huggingface_hub import snapshot_download |
|
|
| path = Path(snapshot_download(model_id_or_path)) |
|
|
| cfg_d = json.loads((path / "config.json").read_text()) |
| for k in ("model_type", "architectures", "name"): |
| cfg_d.pop(k, None) |
| model = cls(EcoacoConfig(**cfg_d)) |
| load_model(model, str(path / "model.safetensors")) |
| model.eval() |
|
|
| |
| tok = None |
| tok_path = path / "tokenizer.json" |
| if tok_path.exists(): |
| try: |
| import sys |
|
|
| sys.path.insert(0, str(path)) |
| from tokenizer import ByteBPETokenizer |
|
|
| tok = ByteBPETokenizer.load(tok_path) |
| except Exception: |
| tok = None |
| return model, tok |
|
|
| @torch.no_grad() |
| def chat(self, tokenizer, prompt: str, max_new_tokens: int = 80, temperature: float = 0.7) -> str: |
| """One-line text generation given the paired tokenizer.""" |
| ids = torch.tensor([tokenizer.encode(prompt, add_bos=True)]) |
| out = self.generate(ids, max_new_tokens=max_new_tokens, |
| temperature=temperature, eos_id=tokenizer.eos_id) |
| return tokenizer.decode(out[0].tolist()) |
|
|