Text Generation
Transformers
Safetensors
English
ethoric
causal-lm
custom-architecture
custom_code
slm
small-language-model
Eval Results (legacy)
Instructions to use techdotus/Ethoric-10M with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use techdotus/Ethoric-10M with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="techdotus/Ethoric-10M", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("techdotus/Ethoric-10M", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use techdotus/Ethoric-10M with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "techdotus/Ethoric-10M" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "techdotus/Ethoric-10M", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/techdotus/Ethoric-10M
- SGLang
How to use techdotus/Ethoric-10M with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "techdotus/Ethoric-10M" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "techdotus/Ethoric-10M", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "techdotus/Ethoric-10M" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "techdotus/Ethoric-10M", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use techdotus/Ethoric-10M with Docker Model Runner:
docker model run hf.co/techdotus/Ethoric-10M
Download modeling_ethoric.py from techdotus/Ethoric-10M: direct link, hf CLI and curl.
- Browser
- Download file 17.4 kB
-
https://huggingface.co/techdotus/Ethoric-10M/resolve/main/modeling_ethoric.py
- Command line
-
hf download hf://techdotus/Ethoric-10M/modeling_ethoric.py
-
curl -L -o modeling_ethoric.py https://huggingface.co/techdotus/Ethoric-10M/resolve/main/modeling_ethoric.py
17.4 kB
| """Ethoric: compact decoder-only transformer with RMSNorm, RoPE, GQA, SwiGLU, | |
| depthwise Canon convolutions and Peri-LN residuals. | |
| Each block normalises on the way into a sublayer and again on the way out, | |
| which is what Peri-LN means here: | |
| h = RMSNorm_attn_in(x); h = h + CanonA(h) | |
| x = x + RMSNorm_attn_out(Attention(h)) | |
| h = RMSNorm_mlp_in(x); h = h + CanonC(h) | |
| x = x + RMSNorm_mlp_out(SwiGLU(h)) | |
| Module and parameter names follow the usual Transformers layout. Works on | |
| Transformers 4.x and 5.x; the v5 differences are marked inline. | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import warnings | |
| from typing import Optional | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| import transformers | |
| from transformers import PreTrainedModel | |
| from transformers.modeling_outputs import CausalLMOutputWithPast | |
| try: # exported as a sibling file; importable either way | |
| from .attention import RMSNorm, EthoricAttention, build_flex_block_mask | |
| from .configuration_ethoric import EthoricConfig | |
| except ImportError: # pragma: no cover - direct script use | |
| from attention import RMSNorm, EthoricAttention, build_flex_block_mask | |
| from configuration_ethoric import EthoricConfig | |
| # ----------------------------------------------------------------------------- canon | |
| class Canon(nn.Module): | |
| """Causal depthwise conv1d, kernel k, no bias, used as a residual branch. | |
| Left-padded by k-1 so the output at t depends only on positions <= t. | |
| With `doc_ids` the convolution is also masked to the current document, so | |
| taps reaching back past an EOS contribute nothing. Leaving that off looks | |
| harmless -- only k-1 positions of the next document are touched -- but | |
| attention then spreads those positions across the rest of that document, so | |
| the whole of it is affected and `doc_masking` buys you nothing. | |
| """ | |
| def __init__(self, d_model: int, kernel: int): | |
| super().__init__() | |
| self.kernel = int(kernel) | |
| self.weight = nn.Parameter(torch.empty(d_model, 1, self.kernel)) | |
| nn.init.normal_(self.weight, mean=0.0, std=0.02) | |
| def forward( | |
| self, | |
| x: torch.Tensor, | |
| state: Optional[torch.Tensor] = None, | |
| doc_ids: Optional[torch.Tensor] = None, | |
| state_docs: Optional[torch.Tensor] = None, | |
| ): | |
| """x is [B, T, d]. `state` carries the previous k-1 inputs for decoding. | |
| Returns (out, new_state, new_state_docs) so a caller stepping one token | |
| at a time can thread the convolution window through the cache. | |
| """ | |
| k = self.kernel | |
| xt = x.transpose(1, 2) # [B, d, T] | |
| xt = F.pad(xt, (k - 1, 0)) if state is None else torch.cat([state, xt], dim=2) | |
| new_state = xt[:, :, -(k - 1):] if k > 1 else xt[:, :, :0] | |
| if doc_ids is None: | |
| out = F.conv1d(xt, self.weight.to(xt.dtype), groups=xt.shape[1]) | |
| return out.transpose(1, 2), new_state, None | |
| # -1 never matches a real document id, so the left padding masks out | |
| # exactly as zero-padding would. | |
| if state_docs is None: | |
| dpad = F.pad(doc_ids, (k - 1, 0), value=-1) | |
| else: | |
| dpad = torch.cat([state_docs, doc_ids], dim=1) | |
| new_state_docs = dpad[:, -(k - 1):] if k > 1 else dpad[:, :0] | |
| T = x.shape[1] | |
| w = self.weight.to(xt.dtype) | |
| out = torch.zeros_like(xt[:, :, :T]) | |
| for i in range(k): | |
| seg = xt[:, :, i : i + T] | |
| keep = (dpad[:, i : i + T] == doc_ids).unsqueeze(1).to(seg.dtype) | |
| out = out + w[:, 0, i].view(1, -1, 1) * seg * keep | |
| return out.transpose(1, 2), new_state, new_state_docs | |
| # ----------------------------------------------------------------------------- blocks | |
| class SwiGLU(nn.Module): | |
| def __init__(self, d_model: int, hidden: int): | |
| super().__init__() | |
| self.gate_proj = nn.Linear(d_model, hidden, bias=False) | |
| self.up_proj = nn.Linear(d_model, hidden, bias=False) | |
| self.down_proj = nn.Linear(hidden, d_model, bias=False) | |
| def forward(self, x): | |
| return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x)) | |
| class EthoricBlock(nn.Module): | |
| """One Peri-LN block: norm in, Canon, sublayer, norm out, residual add. | |
| The out-norms are the part that differs from a conventional pre-norm block. | |
| They sit inside the residual branch, so the residual stream itself is never | |
| normalised and the identity path stays exact. | |
| """ | |
| def __init__(self, config: EthoricConfig, layer_idx: int): | |
| super().__init__() | |
| d, eps = config.d_model, config.norm_eps | |
| self.attn_in_norm = RMSNorm(d, eps) | |
| self.attn_out_norm = RMSNorm(d, eps) | |
| self.mlp_in_norm = RMSNorm(d, eps) | |
| self.mlp_out_norm = RMSNorm(d, eps) | |
| self.attn = EthoricAttention(config, layer_idx) | |
| self.mlp = SwiGLU(d, config.ffn_hidden) | |
| canon = config.canon or {} | |
| self.canon_enabled = bool(canon.get("enabled", False)) | |
| canon_positions = list(canon.get("positions", [])) | |
| k = int(canon.get("kernel", 4)) | |
| self.canon_doc_masking = bool(canon.get("doc_masking", True)) | |
| self.canon_a = Canon(d, k) if self.canon_enabled and "A" in canon_positions else None | |
| self.canon_c = Canon(d, k) if self.canon_enabled and "C" in canon_positions else None | |
| def _canon(self, module, name, h, doc_ids, layer_cache): | |
| docs = doc_ids if self.canon_doc_masking else None | |
| state = layer_cache.get(name) if layer_cache is not None else None | |
| state_docs = layer_cache.get(f"{name}_docs") if layer_cache is not None else None | |
| delta, new_state, new_docs = module(h, state, docs, state_docs) | |
| if layer_cache is not None: | |
| layer_cache[name] = new_state | |
| layer_cache[f"{name}_docs"] = new_docs | |
| return h + delta | |
| def forward(self, x, *, positions, doc_ids, layer_cache=None, block_mask=None): | |
| h = self.attn_in_norm(x) | |
| if self.canon_a is not None: | |
| h = self._canon(self.canon_a, "canon_a", h, doc_ids, layer_cache) | |
| a = self.attn( | |
| h, | |
| positions=positions, | |
| doc_ids=doc_ids, | |
| layer_cache=layer_cache, | |
| block_mask=block_mask, | |
| ) | |
| x = x + self.attn_out_norm(a) | |
| h = self.mlp_in_norm(x) | |
| if self.canon_c is not None: | |
| h = self._canon(self.canon_c, "canon_c", h, doc_ids, layer_cache) | |
| return x + self.mlp_out_norm(self.mlp(h)) | |
| # ----------------------------------------------------------------------------- cache | |
| class EthoricCache(list): | |
| """One dict per layer: extended keys, values, positions, Canon conv state. | |
| A plain list of dicts rather than a transformers Cache subclass, so the file | |
| stays self-contained across transformers versions. | |
| """ | |
| def empty(cls, n_layer: int) -> "EthoricCache": | |
| return cls({} for _ in range(n_layer)) | |
| def seen_tokens(self) -> int: | |
| pos = self[0].get("positions") if len(self) else None | |
| return 0 if pos is None else int(pos.shape[1]) | |
| # ----------------------------------------------------------------------------- model | |
| class EthoricPreTrainedModel(PreTrainedModel): | |
| config_class = EthoricConfig | |
| base_model_prefix = "model" | |
| supports_gradient_checkpointing = True | |
| _no_split_modules = ["EthoricBlock"] | |
| def _init_weights(self, module): | |
| std = 0.02 | |
| 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) | |
| elif isinstance(module, RMSNorm): | |
| nn.init.ones_(module.weight) | |
| class EthoricModel(EthoricPreTrainedModel): | |
| def __init__(self, config: EthoricConfig): | |
| super().__init__(config) | |
| self.embed_tokens = nn.Embedding(config.vocab_size, config.d_model) | |
| self.layers = nn.ModuleList( | |
| EthoricBlock(config, i) for i in range(config.n_layer) | |
| ) | |
| self.final_norm = RMSNorm(config.d_model, config.norm_eps) | |
| self.post_init() | |
| def forward( | |
| self, | |
| input_ids: torch.LongTensor, | |
| *, | |
| doc_ids: Optional[torch.Tensor] = None, | |
| positions: Optional[torch.Tensor] = None, | |
| past_key_values: Optional[EthoricCache] = None, | |
| use_cache: bool = False, | |
| ): | |
| B, T = input_ids.shape | |
| device = input_ids.device | |
| if positions is None: | |
| offset = past_key_values.seen_tokens if past_key_values is not None else 0 | |
| positions = torch.arange(offset, offset + T, device=device).expand(B, T) | |
| if doc_ids is None and self.config.doc_masking: | |
| doc_ids = document_ids(input_ids, eos_id=int(self.config.eos_token_id or 0)) | |
| if past_key_values is not None and past_key_values.seen_tokens: | |
| prev = past_key_values[0].get("doc_ids") | |
| if prev is not None: | |
| doc_ids = doc_ids + prev[:, -1:] | |
| if not self.config.doc_masking: | |
| doc_ids = None | |
| block_mask = None | |
| backend = getattr(self.layers[0].attn, "backend", "dense") | |
| if backend == "flex": | |
| k_pos = positions | |
| k_doc = doc_ids | |
| if past_key_values is not None and past_key_values.seen_tokens: | |
| k_pos = torch.cat([past_key_values[0]["positions"], positions], dim=1) | |
| if doc_ids is not None and past_key_values[0].get("doc_ids") is not None: | |
| k_doc = torch.cat([past_key_values[0]["doc_ids"], doc_ids], dim=1) | |
| # Two masks: windowed layers additionally restrict to the local window. | |
| block_mask = { | |
| None: build_flex_block_mask(positions, k_pos, doc_ids, k_doc, None, self.config.n_head), | |
| } | |
| for layer in self.layers: | |
| w = layer.attn.attn_window | |
| if w is not None and w not in block_mask: | |
| block_mask[w] = build_flex_block_mask( | |
| positions, k_pos, doc_ids, k_doc, w, self.config.n_head | |
| ) | |
| x = self.embed_tokens(input_ids) | |
| for i, layer in enumerate(self.layers): | |
| cache = past_key_values[i] if (use_cache and past_key_values is not None) else None | |
| bm = None | |
| if block_mask is not None: | |
| w = layer.attn.attn_window | |
| bm = block_mask[w] | |
| x = layer( | |
| x, | |
| positions=positions, | |
| doc_ids=doc_ids, | |
| layer_cache=cache, | |
| block_mask=bm, | |
| ) | |
| return self.final_norm(x) | |
| # transformers 4.x expects a list of tied names; 5.x expects a | |
| # {target: source} mapping. Support both so the export loads either way. | |
| _TRANSFORMERS_MAJOR = int(transformers.__version__.split(".")[0]) | |
| _TIED_WEIGHTS_KEYS = ( | |
| {"lm_head.weight": "model.embed_tokens.weight"} | |
| if _TRANSFORMERS_MAJOR >= 5 | |
| else ["lm_head.weight"] | |
| ) | |
| class EthoricForCausalLM(EthoricPreTrainedModel): | |
| _tied_weights_keys = _TIED_WEIGHTS_KEYS | |
| def __init__(self, config: EthoricConfig): | |
| super().__init__(config) | |
| self.model = EthoricModel(config) | |
| self.lm_head = nn.Linear(config.d_model, config.vocab_size, bias=False) | |
| self.post_init() | |
| self._apply_residual_init() | |
| def _apply_residual_init(self): | |
| """Residual-output projections get scaled down by depth so the residual | |
| stream does not blow up at init (the GPT-2 trick). | |
| Runs after post_init(), so it overwrites what _init_weights wrote. Under | |
| from_pretrained the loaded weights land afterwards and win, which is why | |
| a trained checkpoint is unaffected by anything in here. | |
| """ | |
| std = 0.02 / math.sqrt(2 * self.config.n_layer) | |
| for layer in self.model.layers: | |
| nn.init.normal_(layer.attn.o_proj.weight, mean=0.0, std=std) | |
| nn.init.normal_(layer.mlp.down_proj.weight, mean=0.0, std=std) | |
| if layer.attn.gate_proj is not None: | |
| # Zero weight, bias 2.0: the gate starts near-open at | |
| # sigmoid(2.0) ~= 0.88 and input-independent, so early training | |
| # sees plain attention and learns the gating later. | |
| nn.init.zeros_(layer.attn.gate_proj.weight) | |
| nn.init.constant_(layer.attn.gate_proj.bias, 2.0) | |
| def get_input_embeddings(self): | |
| return self.model.embed_tokens | |
| def set_input_embeddings(self, value): | |
| self.model.embed_tokens = value | |
| def get_output_embeddings(self): | |
| return self.lm_head | |
| def set_attention_backend(self, backend: str): | |
| """'flex' or 'dense'. Dense is the CPU/MPS path and the reference.""" | |
| if backend not in ("flex", "dense"): | |
| raise ValueError("backend must be 'flex' or 'dense'") | |
| for layer in self.model.layers: | |
| layer.attn.backend = backend | |
| return self | |
| def forward( | |
| self, | |
| input_ids: Optional[torch.LongTensor] = None, | |
| attention_mask=None, | |
| labels: Optional[torch.LongTensor] = None, | |
| *, | |
| inputs_embeds=None, | |
| doc_ids=None, | |
| positions=None, | |
| past_key_values: Optional[EthoricCache] = None, | |
| use_cache: bool = False, | |
| z_loss: float = 0.0, | |
| return_dict: bool = True, | |
| **kwargs, | |
| ): | |
| if inputs_embeds is not None: | |
| raise ValueError("Ethoric requires input_ids; inputs_embeds is not supported") | |
| if input_ids is None: | |
| raise ValueError("Ethoric requires input_ids") | |
| if use_cache and past_key_values is None: | |
| past_key_values = EthoricCache.empty(self.config.n_layer) | |
| hidden = self.model( | |
| input_ids, | |
| doc_ids=doc_ids, | |
| positions=positions, | |
| past_key_values=past_key_values, | |
| use_cache=use_cache, | |
| ) | |
| logits = self.lm_head(hidden) | |
| loss = None | |
| if labels is not None: | |
| loss = self.loss_with_z(logits, labels, z_loss=z_loss) | |
| if not return_dict: | |
| return (loss, logits) if loss is not None else (logits,) | |
| return CausalLMOutputWithPast( | |
| loss=loss, | |
| logits=logits, | |
| past_key_values=past_key_values if use_cache else None, | |
| ) | |
| def loss_with_z(logits, labels, z_loss: float = 0.0): | |
| """Cross-entropy (labels already shifted by the loader) plus z-loss. | |
| Positions labelled -100 are ignored by both terms. | |
| """ | |
| flat = logits.float().view(-1, logits.shape[-1]) | |
| tgt = labels.reshape(-1) | |
| loss = F.cross_entropy(flat, tgt, ignore_index=-100) | |
| if z_loss: | |
| keep = tgt != -100 | |
| if keep.any(): | |
| lse = torch.logsumexp(flat[keep], dim=-1) | |
| loss = loss + z_loss * lse.pow(2).mean() | |
| return loss | |
| def generate( | |
| self, | |
| input_ids: torch.LongTensor, | |
| max_new_tokens: int = 32, | |
| *, | |
| do_sample: bool = False, | |
| temperature: float = 1.0, | |
| top_k: Optional[int] = None, | |
| use_cache: bool = True, | |
| eos_token_id: Optional[int] = None, | |
| **kwargs, | |
| ): | |
| """Greedy / top-k sampling. Defined here rather than inherited so the | |
| exported model does not depend on a particular transformers Cache API.""" | |
| self.eval() | |
| cache = EthoricCache.empty(self.config.n_layer) if use_cache else None | |
| ids = input_ids | |
| cur = input_ids | |
| for _ in range(int(max_new_tokens)): | |
| out = self.forward( | |
| cur if use_cache else ids, | |
| past_key_values=cache, | |
| use_cache=use_cache, | |
| return_dict=True, | |
| ) | |
| logits = out.logits[:, -1, :].float() | |
| if do_sample: | |
| logits = logits / max(float(temperature), 1e-6) | |
| if top_k: | |
| kth = logits.topk(int(top_k), dim=-1).values[:, -1:] | |
| logits = logits.masked_fill(logits < kth, float("-inf")) | |
| nxt = torch.multinomial(F.softmax(logits, dim=-1), 1) | |
| else: | |
| nxt = logits.argmax(dim=-1, keepdim=True) | |
| ids = torch.cat([ids, nxt], dim=1) | |
| cur = nxt | |
| if eos_token_id is not None and bool((nxt == eos_token_id).all()): | |
| break | |
| return ids | |
| # ----------------------------------------------------------------------------- helpers | |
| def document_ids(input_ids: torch.Tensor, eos_id: int = 0) -> torch.Tensor: | |
| """Document id per position: EOS *ends* a document, so the id increments | |
| after the EOS token. The EOS itself belongs to the document it closes. | |
| Getting that boundary backwards puts each EOS in the following document, | |
| which quietly changes what every mask built from these ids allows. | |
| """ | |
| is_eos = (input_ids == eos_id).to(torch.int32) | |
| return torch.cumsum(is_eos, dim=1) - is_eos | |
| __all__ = [ | |
| "EthoricConfig", | |
| "EthoricForCausalLM", | |
| "EthoricModel", | |
| "EthoricCache", | |
| "document_ids", | |
| ] | |