Frox-nano / Model /model /architecture /morph_model.py
Hritik045678's picture
Initial commit: Frox Morph Nano 1 (XL) Space
bd97ee9
Raw
History Blame Contribute Delete
20.6 kB
"""
Frox AI Morph 1.1 β€” Core Language Model
Improvements over Morph 1.0:
- 64K vocab (was 32K) for better multilingual + code coverage
- 16K context window (was 8K) via YaRN RoPE
- QK-norm enabled throughout
- Sliding window (even layers) + full attn (odd layers) interleaved
- Depth-scaled residual init
- Pre-computed causal mask (cached for reuse)
- HF GenerationMixin compatible (used by PEFT, vLLM, TRL)
- save() / from_saved() / from_pretrained() / push_to_hub() helpers
"""
from __future__ import annotations
import json
import math
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.checkpoint
from config.model_config import MorphConfig, MorphTextConfig
from model.attention.gqa import MorphDecoderLayer, MorphRMSNorm
# ── Output types ──────────────────────────────────────────────────
@dataclass
class MorphModelOutput:
last_hidden_state: torch.Tensor
past_key_values: Optional[Tuple] = None
hidden_states: Optional[Tuple] = None
attentions: Optional[Tuple] = None
@dataclass
class MorphCausalLMOutput:
"""
PEFT / HuggingFace Trainer compatible output.
Dict-style access required by PEFT internals.
"""
loss: Optional[torch.Tensor] = None
logits: Optional[torch.Tensor] = None
past_key_values: Optional[Tuple] = None
hidden_states: Optional[Tuple] = None
attentions: Optional[Tuple] = None
def __getitem__(self, key: str): return getattr(self, key)
def __setitem__(self, key: str, v): setattr(self, key, v)
def get(self, key: str, default=None): return getattr(self, key, default)
def __contains__(self, key: str):
return hasattr(self, key) and getattr(self, key) is not None
def keys(self):
return [k for k in ("loss","logits","past_key_values","hidden_states","attentions")
if getattr(self, k, None) is not None]
# ── Backbone ──────────────────────────────────────────────────────
class MorphModel(nn.Module):
"""
Frox Morph 1.1 transformer backbone.
Pure decoder β€” multimodal tokens injected via projectors.
"""
def __init__(self, config: MorphTextConfig):
super().__init__()
self.config = config
self.padding_idx = config.pad_token_id
# Embedding table covers vocab + reserved special tokens
self.embed_tokens = nn.Embedding(
config.total_vocab_size,
config.hidden_size,
padding_idx=self.padding_idx,
)
# Transformer layers
self.layers = nn.ModuleList([
MorphDecoderLayer(
hidden_size=config.hidden_size,
num_heads=config.num_attention_heads,
num_kv_heads=config.num_key_value_heads,
head_dim=config.head_dim,
intermediate_size=config.intermediate_size,
max_position_embeddings=config.max_position_embeddings,
rope_theta=config.rope_theta,
rope_scaling_factor=config.rope_scaling_factor,
rms_norm_eps=config.rms_norm_eps,
layer_idx=i,
qk_norm=config.qk_norm,
use_sliding_window=config.use_sliding_window,
sliding_window_size=config.sliding_window_size,
init_std=config.init_std,
num_hidden_layers=config.num_hidden_layers,
)
for i in range(config.num_hidden_layers)
])
self.norm = MorphRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.gradient_checkpointing = False
self._init_embeddings()
def _init_embeddings(self):
"""Standard embedding init. Larger init std for larger vocab."""
std = self.config.init_std
nn.init.normal_(self.embed_tokens.weight, mean=0.0, std=std)
if self.embed_tokens.padding_idx is not None:
self.embed_tokens.weight.data[self.embed_tokens.padding_idx].zero_()
def gradient_checkpointing_enable(self, **kwargs):
self.gradient_checkpointing = True
def gradient_checkpointing_disable(self):
self.gradient_checkpointing = False
def get_input_embeddings(self) -> nn.Embedding:
return self.embed_tokens
def set_input_embeddings(self, value: nn.Embedding):
self.embed_tokens = value
def forward(
self,
input_ids: Optional[torch.LongTensor] = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[List[Tuple]] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
use_cache: bool = True,
output_attentions: bool = False,
output_hidden_states: bool = False,
) -> MorphModelOutput:
if self.gradient_checkpointing and self.training:
use_cache = False # incompatible with grad checkpointing
# Embeddings
if inputs_embeds is None:
inputs_embeds = self.embed_tokens(input_ids)
hidden_states = inputs_embeds
B, S, _ = hidden_states.shape
# Position IDs
if position_ids is None:
past_len = past_key_values[0][0].shape[2] if past_key_values else 0
position_ids = torch.arange(
past_len, past_len + S,
device=hidden_states.device,
).unsqueeze(0).expand(B, -1)
# Build causal mask
past_len = past_key_values[0][0].shape[2] if past_key_values else 0
causal_mask = self._build_causal_mask(
attention_mask, hidden_states.dtype, hidden_states.device, S, past_len
)
all_hidden_states = () if output_hidden_states else None
all_attentions = () if output_attentions else None
next_cache = () if use_cache else None
for i, layer in enumerate(self.layers):
if output_hidden_states:
all_hidden_states += (hidden_states,)
past_kv = past_key_values[i] if past_key_values is not None else None
if self.gradient_checkpointing and self.training:
def _ckpt_forward(l):
def fn(hs, mask, pos):
return l(
hidden_states=hs,
attention_mask=mask,
position_ids=pos,
past_key_value=None,
use_cache=False,
output_attentions=output_attentions,
)
return fn
layer_outputs = torch.utils.checkpoint.checkpoint(
_ckpt_forward(layer),
hidden_states, causal_mask, position_ids,
use_reentrant=False,
)
else:
layer_outputs = layer(
hidden_states=hidden_states,
attention_mask=causal_mask,
position_ids=position_ids,
past_key_value=past_kv,
use_cache=use_cache,
output_attentions=output_attentions,
)
hidden_states = layer_outputs[0]
if output_attentions:
all_attentions += (layer_outputs[1],)
if use_cache:
next_cache += (layer_outputs[-1],)
hidden_states = self.norm(hidden_states)
if output_hidden_states:
all_hidden_states += (hidden_states,)
return MorphModelOutput(
last_hidden_state=hidden_states,
past_key_values=next_cache,
hidden_states=all_hidden_states,
attentions=all_attentions,
)
def _build_causal_mask(
self,
attention_mask: Optional[torch.Tensor],
dtype: torch.dtype,
device: torch.device,
seq_len: int,
past_len: int,
) -> Optional[torch.Tensor]:
"""
4D causal mask [B_or_1, 1, S_q, S_k].
Never becomes 5D (was the bug in Morph 1.0).
"""
total_len = seq_len + past_len
min_val = torch.finfo(dtype).min
causal = torch.full(
(seq_len, total_len), fill_value=min_val,
dtype=dtype, device=device,
)
causal = torch.triu(causal, diagonal=past_len + 1)
causal = causal[None, None, :, :] # [1, 1, S_q, S_k]
if attention_mask is not None:
pad_mask = (1.0 - attention_mask[:, None, None, :].to(dtype)) * min_val
causal = causal + pad_mask # [B, 1, S_q, S_k]
return causal
# ── Causal LM ────────────────────────────────────────────────────
class MorphForCausalLM(nn.Module):
"""
Frox Morph 1.1 β€” full causal language model.
HuggingFace / PEFT / TRL compatible:
βœ“ prepare_inputs_for_generation()
βœ“ can_generate()
βœ“ get/set input/output embeddings
βœ“ gradient_checkpointing_enable/disable
βœ“ forward() accepts return_dict + **kwargs
"""
def __init__(self, config: MorphTextConfig):
super().__init__()
self.config = config
self.model = MorphModel(config)
# LM head: same dim as embedding table
self.lm_head = nn.Linear(
config.hidden_size, config.total_vocab_size, bias=False
)
# Tie embeddings (saves ~400M params at 64K vocab)
if config.tie_word_embeddings:
self.lm_head.weight = self.model.embed_tokens.weight
# ── Embedding accessors ───────────────────────────────────────
def get_input_embeddings(self) -> nn.Embedding: return self.model.embed_tokens
def set_input_embeddings(self, v): self.model.embed_tokens = v
def get_output_embeddings(self) -> nn.Linear: return self.lm_head
def set_output_embeddings(self, v): self.lm_head = v
# ── Gradient checkpointing ────────────────────────────────────
def gradient_checkpointing_enable(self, **kwargs):
self.model.gradient_checkpointing_enable()
def gradient_checkpointing_disable(self):
self.model.gradient_checkpointing_disable()
# ── HF generation compatibility ───────────────────────────────
def can_generate(self) -> bool:
return True
def prepare_inputs_for_generation(
self,
input_ids: torch.LongTensor,
past_key_values=None,
attention_mask=None,
inputs_embeds=None,
**kwargs,
) -> dict:
if past_key_values is not None:
input_ids = input_ids[:, -1:] # only the new token
model_inputs: dict = {
"input_ids": input_ids,
"past_key_values": past_key_values,
"use_cache": kwargs.get("use_cache", True),
"attention_mask": attention_mask,
}
if inputs_embeds is not None and past_key_values is None:
model_inputs.pop("input_ids")
model_inputs["inputs_embeds"] = inputs_embeds
return model_inputs
# ── Forward ───────────────────────────────────────────────────
def forward(
self,
input_ids: Optional[torch.LongTensor] = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[List[Tuple]] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
labels: Optional[torch.LongTensor] = None,
use_cache: bool = True,
output_attentions: bool = False,
output_hidden_states: bool = False,
return_dict: bool = True, # PEFT compatibility
**kwargs, # absorb PEFT extra kwargs
) -> MorphCausalLMOutput:
outputs = self.model(
input_ids=input_ids,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
inputs_embeds=inputs_embeds,
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
)
hidden_states = outputs.last_hidden_state
logits = self.lm_head(hidden_states).float() # always float32
loss = None
if labels is not None:
# Shift for next-token prediction
shift_logits = logits[..., :-1, :].contiguous()
shift_labels = labels[..., 1:].contiguous()
loss = F.cross_entropy(
shift_logits.view(-1, shift_logits.size(-1)),
shift_labels.view(-1),
ignore_index=-100,
)
return MorphCausalLMOutput(
loss=loss,
logits=logits,
past_key_values=outputs.past_key_values,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
)
# ── Generation (used in edge / demo / inference engine) ──────
@torch.no_grad()
def generate(
self,
input_ids: torch.LongTensor,
attention_mask: Optional[torch.Tensor] = None,
max_new_tokens: int = 512,
temperature: float = 0.7,
top_p: float = 0.9,
top_k: int = 50,
repetition_penalty: float = 1.1,
eos_token_id: Optional[int] = None,
pad_token_id: Optional[int] = None,
use_cache: bool = True,
do_sample: bool = True,
stream_callback=None, # NEW 1.1: optional per-token callback
) -> torch.LongTensor:
eos = eos_token_id if eos_token_id is not None else self.config.eos_token_id
pad = pad_token_id if pad_token_id is not None else self.config.pad_token_id
B = input_ids.shape[0]
generated = input_ids.clone()
past_key_values = None
finished = torch.zeros(B, dtype=torch.bool, device=input_ids.device)
for step in range(max_new_tokens):
curr_input = generated[:, -1:] if past_key_values is not None else generated
out = self.forward(
input_ids=curr_input,
attention_mask=attention_mask,
past_key_values=past_key_values,
use_cache=use_cache,
)
logits = out.logits[:, -1, :]
past_key_values = out.past_key_values
# Repetition penalty
if repetition_penalty != 1.0:
for b in range(B):
for tid in set(generated[b].tolist()):
if logits[b, tid] < 0:
logits[b, tid] *= repetition_penalty
else:
logits[b, tid] /= repetition_penalty
if temperature != 1.0:
logits = logits / temperature
if top_k > 0:
top_k_vals, _ = torch.topk(logits, min(top_k, logits.size(-1)))
logits[logits < top_k_vals[:, -1:]] = float("-inf")
if do_sample and top_p < 1.0:
sorted_logits, sorted_idx = torch.sort(logits, descending=True)
cum_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
remove = cum_probs - F.softmax(sorted_logits, dim=-1) > top_p
sorted_logits[remove] = float("-inf")
logits = torch.zeros_like(logits).scatter_(1, sorted_idx, sorted_logits)
if do_sample:
probs = F.softmax(logits, dim=-1)
next_token = torch.multinomial(probs, num_samples=1)
else:
next_token = logits.argmax(dim=-1, keepdim=True)
next_token = torch.where(
finished.unsqueeze(-1),
torch.full_like(next_token, pad),
next_token,
)
generated = torch.cat([generated, next_token], dim=-1)
if attention_mask is not None:
attention_mask = torch.cat([
attention_mask,
torch.ones(B, 1, device=attention_mask.device),
], dim=-1)
finished = finished | (next_token.squeeze(-1) == eos)
# NEW 1.1: stream callback for real-time output
if stream_callback is not None:
for b in range(B):
if not finished[b]:
stream_callback(b, next_token[b, 0].item(), step)
if finished.all():
break
return generated
# ── Utilities ─────────────────────────────────────────────────
def param_count(self) -> dict:
total = sum(p.numel() for p in self.parameters())
trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
return {
"total": total,
"trainable": trainable,
"total_billions": round(total / 1e9, 3),
"trainable_billions": round(trainable / 1e9, 3),
}
def save(self, path: str):
from dataclasses import asdict
p = Path(path)
p.mkdir(parents=True, exist_ok=True)
torch.save(self.state_dict(), p / "model.pt")
with open(p / "config.json", "w") as f:
json.dump(asdict(self.config), f, indent=2)
print(f"βœ“ Morph 1.1 LM saved to {path}")
@classmethod
def from_saved(cls, path: str, device: str = "cpu") -> "MorphForCausalLM":
from utils.common import require_checkpoint_dir
p = require_checkpoint_dir(path)
with open(p / "config.json") as f:
cfg_dict = json.load(f)
config = MorphTextConfig(**cfg_dict)
model = cls(config)
state = torch.load(p / "model.pt", map_location=device, weights_only=True)
model.load_state_dict(state, strict=False)
return model
@classmethod
def from_config(cls, config: MorphTextConfig) -> "MorphForCausalLM":
"""Create model with random weights from config."""
return cls(config)
@classmethod
def from_morph_1_checkpoint(cls, path: str) -> "MorphForCausalLM":
"""
Load a Morph 1.0 checkpoint into a Morph 1.1 model.
Handles vocab size mismatch (32K β†’ 64K) by zero-padding embeddings.
"""
p = Path(path)
with open(p / "config.json") as f:
old_cfg = json.load(f)
# Build 1.1 config with same architecture but upgraded vocab
new_cfg = MorphTextConfig(**old_cfg)
new_cfg.vocab_size = 64000
new_cfg.qk_norm = True
new_cfg.version = "1.1.0"
model = cls(new_cfg)
# Load old weights, skip mismatched embed table
old_state = torch.load(p / "model.pt", map_location="cpu", weights_only=True)
new_state = model.state_dict()
for name, param in old_state.items():
if name not in new_state:
continue
if param.shape == new_state[name].shape:
new_state[name] = param
elif "embed_tokens" in name or "lm_head" in name:
# Pad vocabulary dimension: copy old rows, leave new rows at init
old_rows = param.shape[0]
new_state[name][:old_rows] = param
print(f" Padded {name}: {param.shape} β†’ {new_state[name].shape}")
model.load_state_dict(new_state)
print(f"βœ“ Morph 1.1 loaded from Morph 1.0 checkpoint: {path}")
return model