onyx-7b-prototype / model_onyx7b_prototype.py
CaiaTech's picture
Upload folder using huggingface_hub
e722c65 verified
Raw
History Blame Contribute Delete
27.6 kB
#!/usr/bin/env python3
"""
Onyx 7B Dense Model, Marvin Tutt, Caia Tech
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from dataclasses import dataclass
from typing import Optional, Tuple, Dict, Any, List
import math
import warnings
# Try to import optional dependencies
try:
from flash_attn import flash_attn_func
FLASH_AVAILABLE = True
except ImportError:
FLASH_AVAILABLE = False
warnings.warn("FlashAttention not available, using PyTorch SDPA", stacklevel=2)
@dataclass
class OnyxConfig:
"""Configuration for Onyx 7B Dense Model"""
# Core architecture - 7B scale
vocab_size: int = 128256 # LLaMA 3 tokenizer
d_model: int = 4096 # Hidden dimension
n_layers: int = 32 # Number of layers
n_heads: int = 32 # Attention heads
n_kv_heads: int = 8 # GQA 4:1 ratio
d_ff: int = 11008 # FFN dimension
max_seq_len: int = 16384 # 16k context
eos_token_id: int = 2 # EOS token ID for generation
# Position encoding
rope_theta: float = 500000.0 # Extended RoPE for long context
rope_scaling: Optional[Dict] = None # Reserved for future scaling
# Architecture features
use_swiglu: bool = True # SwiGLU activation
use_rms_norm: bool = True # RMSNorm instead of LayerNorm
norm_eps: float = 1e-5
use_qk_norm: bool = False # Reserved for QK normalization
# Performance optimizations
use_flash_attn: bool = True # FlashAttention v2
use_cuda_graphs: bool = False # Reserved for future optimization
use_torch_compile: bool = True # torch.compile optimization
# Training
dropout: float = 0.0
attention_dropout: float = 0.0
gradient_checkpointing: bool = False
# Embeddings
tie_embeddings: bool = True
def __post_init__(self):
if self.n_heads % self.n_kv_heads != 0:
raise ValueError(f"n_heads ({self.n_heads}) must be divisible by n_kv_heads ({self.n_kv_heads})")
if self.d_model % self.n_heads != 0:
raise ValueError(f"d_model ({self.d_model}) must be divisible by n_heads ({self.n_heads})")
# Ensure head_dim is even for RoPE
head_dim = self.d_model // self.n_heads
if head_dim % 2 != 0:
raise ValueError(f"head_dim ({head_dim}) must be even for RoPE")
class RMSNorm(nn.Module):
"""RMSNorm normalization layer"""
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:
variance = x.pow(2).mean(-1, keepdim=True)
x = x * torch.rsqrt(variance + self.eps)
return x * self.weight
class RoPE(nn.Module):
"""Rotary Position Embeddings with correct even/odd implementation"""
def __init__(self, config: OnyxConfig):
super().__init__()
self.max_seq_len = config.max_seq_len
self.d_model = config.d_model
self.n_heads = config.n_heads
self.theta = config.rope_theta
self.head_dim = self.d_model // self.n_heads
# Precompute frequencies
self.register_buffer(
"freqs",
self._compute_freqs(),
persistent=False # Don't save in state_dict
)
def _compute_freqs(self) -> torch.Tensor:
# Frequencies for pairs of dimensions
freqs = 1.0 / (self.theta ** (torch.arange(0, self.head_dim, 2).float() / self.head_dim))
return freqs
def forward(
self,
q: torch.Tensor,
k: torch.Tensor,
seq_len: int,
offset: int = 0 # For KV cache support
) -> Tuple[torch.Tensor, torch.Tensor]:
# Create position indices
t = torch.arange(offset, offset + seq_len, device=q.device, dtype=self.freqs.dtype)
freqs = torch.outer(t, self.freqs) # (seq_len, head_dim/2)
# Create cos and sin embeddings
cos = torch.cos(freqs)
sin = torch.sin(freqs)
# Apply rotary embeddings
q_embed = self._apply_rotary(q, cos, sin)
k_embed = self._apply_rotary(k, cos, sin)
return q_embed, k_embed
def _apply_rotary(
self,
x: torch.Tensor,
cos: torch.Tensor,
sin: torch.Tensor
) -> torch.Tensor:
"""Apply rotary embeddings with correct even/odd pairing"""
# x: (B, S, H, D) where D = head_dim
d = x.size(-1)
assert d % 2 == 0, "head_dim must be even for RoPE"
# Ensure cos/sin match tensor dtype and shape
cos = cos.to(x.dtype).unsqueeze(0).unsqueeze(2) # (1, S, 1, D/2)
sin = sin.to(x.dtype).unsqueeze(0).unsqueeze(2) # (1, S, 1, D/2)
# Split into even and odd indices
x_even = x[..., ::2] # (B, S, H, D/2)
x_odd = x[..., 1::2] # (B, S, H, D/2)
# Apply rotation
x_rotated_even = x_even * cos - x_odd * sin
x_rotated_odd = x_even * sin + x_odd * cos
# Interleave back
x_out = torch.empty_like(x)
x_out[..., ::2] = x_rotated_even
x_out[..., 1::2] = x_rotated_odd
return x_out
class OptimizedAttention(nn.Module):
"""Multi-head attention with GQA and proper KV cache"""
def __init__(self, config: OnyxConfig):
super().__init__()
self.config = config
self.d_model = config.d_model
self.n_heads = config.n_heads
self.n_kv_heads = config.n_kv_heads
self.head_dim = self.d_model // self.n_heads
self.n_rep = self.n_heads // self.n_kv_heads
# Projections
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)
# RoPE
self.rope = RoPE(config)
# Dropout
self.dropout = nn.Dropout(config.attention_dropout)
def forward(
self,
x: torch.Tensor,
use_cache: bool = False,
past_kv: Optional[Tuple[torch.Tensor, torch.Tensor]] = None
) -> Tuple[torch.Tensor, Optional[Tuple[torch.Tensor, torch.Tensor]]]:
batch_size, seq_len, _ = x.shape
# Projections
q = self.q_proj(x).view(batch_size, seq_len, self.n_heads, self.head_dim)
k = self.k_proj(x).view(batch_size, seq_len, self.n_kv_heads, self.head_dim)
v = self.v_proj(x).view(batch_size, seq_len, self.n_kv_heads, self.head_dim)
# Apply RoPE
offset = 0
if past_kv is not None:
offset = past_kv[0].shape[2] # Past sequence length
q, k = self.rope(q, k, seq_len, offset)
# Keep base (B,S,n_kv,D) for cache; compute repeats on the fly
q_bshd = q # (B, S, H, D)
k_base = k # (B, S, n_kv, D)
v_base = v # (B, S, n_kv, D)
# If using cache, append on base (n_kv heads) BEFORE any repeat
if past_kv is not None and use_cache:
past_k, past_v = past_kv # both (B, n_kv, S_past, D)
# bring base to (B, n_kv, S, D) to concat on seq dim
k_cat = torch.cat([past_k, k_base.transpose(1,2)], dim=2)
v_cat = torch.cat([past_v, v_base.transpose(1,2)], dim=2)
k_base = k_cat.transpose(1,2) # back to (B, S_total, n_kv, D)
v_base = v_cat.transpose(1,2)
# Repeat for compute only (don't store repeated in cache)
if self.n_rep > 1:
k_rep = k_base.repeat_interleave(self.n_rep, dim=2) # (B, S, H, D)
v_rep = v_base.repeat_interleave(self.n_rep, dim=2)
else:
k_rep, v_rep = k_base, v_base
# FlashAttention dtype/device guard
use_fa = (
FLASH_AVAILABLE and self.config.use_flash_attn and not use_cache
and q_bshd.is_cuda and q_bshd.dtype in (torch.float16, torch.bfloat16)
)
if use_fa:
# Ensure contiguous for FA
q_bshd = q_bshd.contiguous()
k_rep = k_rep.contiguous()
v_rep = v_rep.contiguous()
# FA path on (B,S,H,D)
attn_output = flash_attn_func(
q_bshd, k_rep, v_rep,
dropout_p=self.dropout.p if self.training else 0.0,
causal=True
) # (B, S, H, D)
# align with SDPA downstream
attn_output = attn_output.transpose(1, 2) # -> (B, H, S, D)
else:
# SDPA path expects (B,H,S,D)
q = q_bshd.transpose(1, 2)
k = k_rep.transpose(1, 2)
v = v_rep.transpose(1, 2)
attn_output = F.scaled_dot_product_attention(
q, k, v,
attn_mask=None, # No mask - let SDPA handle it
dropout_p=self.dropout.p if self.training else 0.0,
is_causal=True # This is the key - SDPA handles causal masking
)
# Reshape and project
attn_output = attn_output.transpose(1, 2).contiguous()
attn_output = attn_output.view(batch_size, seq_len, self.d_model)
output = self.o_proj(attn_output)
# Cache compact n_kv heads (4x memory savings)
if use_cache:
new_kv = (k_base.transpose(1,2).contiguous(), v_base.transpose(1,2).contiguous())
else:
new_kv = None
return output, new_kv
class OptimizedFFN(nn.Module):
"""Feed-forward network with SwiGLU"""
def __init__(self, config: OnyxConfig):
super().__init__()
self.config = config
if config.use_swiglu:
# SwiGLU: FFN(x) = (SiLU(xW1) ⊙ xW3)W2
self.w1 = nn.Linear(config.d_model, config.d_ff, bias=False)
self.w2 = nn.Linear(config.d_ff, config.d_model, bias=False)
self.w3 = nn.Linear(config.d_model, config.d_ff, bias=False)
else:
# Standard FFN
self.up = nn.Linear(config.d_model, config.d_ff, bias=False)
self.down = nn.Linear(config.d_ff, config.d_model, bias=False)
self.dropout = nn.Dropout(config.dropout)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.config.use_swiglu:
# SwiGLU activation
gate = F.silu(self.w1(x))
up = self.w3(x)
x = gate * up
x = self.w2(x)
else:
# Standard GELU
x = self.up(x)
x = F.gelu(x)
x = self.down(x)
return self.dropout(x)
class TransformerBlock(nn.Module):
"""Single transformer block"""
def __init__(self, config: OnyxConfig, layer_idx: int):
super().__init__()
self.config = config
self.layer_idx = layer_idx
# Normalization
self.norm1 = RMSNorm(config.d_model, config.norm_eps)
self.norm2 = RMSNorm(config.d_model, config.norm_eps)
# Attention and FFN
self.attention = OptimizedAttention(config)
self.ffn = OptimizedFFN(config)
def forward(
self,
x: torch.Tensor,
use_cache: bool = False,
past_kv: Optional[Tuple[torch.Tensor, torch.Tensor]] = None
) -> Tuple[torch.Tensor, Optional[Tuple[torch.Tensor, torch.Tensor]]]:
# Pre-norm architecture
# Attention block
residual = x
x = self.norm1(x)
attn_out, new_kv = self.attention(x, use_cache, past_kv)
x = residual + attn_out
# FFN block
residual = x
x = self.norm2(x)
x = self.ffn(x)
x = residual + x
return x, new_kv
class Onyx7B(nn.Module):
"""Onyx 7B Dense Model - Production Ready"""
def __init__(self, config: OnyxConfig):
super().__init__()
self.config = config
# Token embeddings
self.embed_tokens = nn.Embedding(config.vocab_size, config.d_model)
# Transformer blocks
self.layers = nn.ModuleList([
TransformerBlock(config, i) for i in range(config.n_layers)
])
# Final norm
self.norm = RMSNorm(config.d_model, config.norm_eps)
# Language modeling head
if config.tie_embeddings:
self.lm_head = None # Will use embed_tokens.weight
else:
self.lm_head = nn.Linear(config.d_model, config.vocab_size, bias=False)
# Initialize weights
self.apply(self._init_weights)
def _init_weights(self, module):
"""Initialize weights with scaled normal distribution"""
if isinstance(module, nn.Linear):
torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
if module.bias is not None:
torch.nn.init.zeros_(module.bias)
elif isinstance(module, nn.Embedding):
torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
def forward(
self,
input_ids: torch.Tensor,
labels: Optional[torch.Tensor] = None,
use_cache: bool = False,
past_key_values: Optional[List[Tuple[torch.Tensor, torch.Tensor]]] = None,
return_dict: bool = True
) -> Dict[str, Any]:
"""
Forward pass with KV cache support
Args:
input_ids: Input token IDs (batch_size, seq_len)
labels: Target labels for training
use_cache: Whether to use/return KV cache
past_key_values: Past KV cache from previous forward pass
return_dict: Return dictionary or tuple
Returns:
Dictionary with 'logits', optionally 'loss' and 'past_key_values'
"""
# Optional compile guard to avoid cache path in graphs
if hasattr(torch._dynamo, 'is_compiling') and torch._dynamo.is_compiling():
# Force no-cache inside compiled graph to avoid graph breaks
use_cache = False
past_key_values = None
batch_size, seq_len = input_ids.shape
# Token embeddings
hidden_states = self.embed_tokens(input_ids)
# Process through transformer layers
new_past_key_values = [] if use_cache else None
for i, layer in enumerate(self.layers):
past_kv = past_key_values[i] if past_key_values else None
if self.config.gradient_checkpointing and self.training:
# Training: checkpoint tensor-only, no cache
def cf(h):
y, _ = layer(h, use_cache=False, past_kv=None)
return y
hidden_states = torch.utils.checkpoint.checkpoint(cf, hidden_states, use_reentrant=False)
new_kv = None
else:
hidden_states, new_kv = layer(hidden_states, use_cache, past_kv)
if use_cache:
new_past_key_values.append(new_kv)
# Final norm
hidden_states = self.norm(hidden_states)
# LM head
if self.lm_head is None:
logits = F.linear(hidden_states, self.embed_tokens.weight)
else:
logits = self.lm_head(hidden_states)
# Compute loss if labels provided
loss = None
if labels is not None:
# Shift for next-token prediction
shift_logits = logits[..., :-1, :].contiguous()
shift_labels = labels[..., 1:].contiguous()
# Compute cross-entropy loss (cast to float32 for stability)
loss = F.cross_entropy(
shift_logits.float().view(-1, shift_logits.size(-1)),
shift_labels.view(-1),
ignore_index=-100
)
if return_dict:
return {
"logits": logits,
"loss": loss,
"past_key_values": new_past_key_values,
"hidden_states": hidden_states
}
return (logits, new_past_key_values) if use_cache else logits
def generate(
self,
input_ids: torch.Tensor,
max_length: int = 100,
temperature: float = 0.7,
top_p: float = 0.9,
top_k: Optional[int] = None,
use_cache: bool = True
) -> torch.Tensor:
"""
Generate text using the model with KV cache
Args:
input_ids: Starting token IDs (batch_size, seq_len)
max_length: Maximum generation length
temperature: Sampling temperature
top_p: Nucleus sampling threshold
use_cache: Use KV cache for efficiency
Returns:
Generated token IDs
"""
self.eval()
# Initialize past_key_values
past_key_values = None
generated_tokens = input_ids
temperature = max(1e-6, float(temperature))
# Early exit if already at max length
if generated_tokens.shape[1] >= max_length:
return generated_tokens[:, :max_length]
with torch.inference_mode():
for _ in range(max_length - input_ids.shape[1]):
# Use only the last token if using cache
if past_key_values is not None:
input_tokens = generated_tokens[:, -1:]
else:
input_tokens = generated_tokens
# Forward pass
outputs = self.forward(
input_tokens,
use_cache=use_cache,
past_key_values=past_key_values
)
logits = outputs["logits"]
past_key_values = outputs.get("past_key_values", None)
# Get next token logits
next_token_logits = logits[:, -1, :] / temperature
# Apply top-k sampling if requested
if top_k is not None and top_k > 0:
top_k_val = min(top_k, next_token_logits.size(-1))
kth = torch.topk(next_token_logits, top_k_val, dim=-1).values[..., -1, None]
next_token_logits = torch.where(
next_token_logits < kth,
torch.full_like(next_token_logits, float("-inf")),
next_token_logits
)
# Apply top-p sampling if needed
if top_p < 1.0:
sorted_logits, sorted_indices = torch.sort(next_token_logits, descending=True)
probs = F.softmax(sorted_logits, dim=-1)
cumulative_probs = torch.cumsum(probs, dim=-1)
sorted_indices_to_remove = cumulative_probs > top_p
sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
sorted_indices_to_remove[..., 0] = 0
# Fix: Create fresh mask to avoid scatter bug
base_mask = torch.zeros_like(sorted_indices_to_remove, dtype=torch.bool)
indices_to_remove = base_mask.scatter(1, sorted_indices, sorted_indices_to_remove)
next_token_logits = next_token_logits.masked_fill(indices_to_remove, float('-inf'))
# Sample
probs = F.softmax(next_token_logits, dim=-1)
next_token = torch.multinomial(probs, num_samples=1)
# Append to sequence
generated_tokens = torch.cat([generated_tokens, next_token], dim=-1)
# Check for EOS
if (next_token == self.config.eos_token_id).any():
break
return generated_tokens
def get_num_params(self) -> int:
"""Get total number of parameters"""
return sum(p.numel() for p in self.parameters())
def get_param_groups(self) -> Dict[str, List[nn.Parameter]]:
"""Get parameter groups for optimizer with correct weight decay"""
decay_params = []
no_decay_params = []
for name, param in self.named_parameters():
if not param.requires_grad:
continue
# Exclude norms, biases, and embeddings from weight decay
if any(k in name for k in ["norm", "bias", "embed_tokens"]):
no_decay_params.append(param)
else:
decay_params.append(param)
return {
"decay": decay_params,
"no_decay": no_decay_params
}
def create_onyx_7b(
device: str = "cuda",
dtype: torch.dtype = torch.float16,
compile_model: bool = True
) -> Onyx7B:
"""
Create and initialize Onyx 7B model
Args:
device: Device to place model on
dtype: Data type for model weights (prefer bfloat16 on Ampere+)
compile_model: Whether to compile with torch.compile
Returns:
Initialized Onyx 7B model
"""
# Set global preferences for PyTorch 2.8
if device == "cuda":
torch.backends.cuda.matmul.allow_tf32 = True
torch.set_float32_matmul_precision("high")
# Create configuration
config = OnyxConfig(
vocab_size=128256, # LLaMA 3 tokenizer
d_model=4096,
n_layers=32,
n_heads=32,
n_kv_heads=8, # GQA 4:1
d_ff=11008,
max_seq_len=16384, # 16k context
rope_theta=500000.0, # Extended RoPE
use_swiglu=True,
use_rms_norm=True,
use_flash_attn=FLASH_AVAILABLE,
use_cuda_graphs=False, # Reserved for future
use_torch_compile=compile_model
)
# Create model
model = Onyx7B(config)
# Move to device and dtype
model = model.to(device=device, dtype=dtype)
# Compile the model (single compile at top level)
if compile_model and config.use_torch_compile:
model.forward = torch.compile(
model.forward,
mode="max-autotune",
dynamic=True
)
# Print model info
num_params = model.get_num_params()
print(f"✅ Created Onyx 7B Dense Model")
print(f" Parameters: {num_params:,} ({num_params/1e9:.2f}B)")
print(f" Architecture: Dense transformer")
print(f" Optimizations: RoPE, SDPA, KV cache (4x savings)")
print(f" Device: {device}")
print(f" Dtype: {dtype}")
return model
if __name__ == "__main__":
# Test model creation and forward pass
print("Testing Onyx 7B Production Model")
print("=" * 60)
# Set seed for reproducibility
torch.manual_seed(0)
# Device and CUDA info
device = "cuda" if torch.cuda.is_available() else "cpu"
if device == "cuda":
torch.cuda.manual_seed_all(0)
print(f"CUDA Available: Yes (GPU: {torch.cuda.get_device_name()})")
print(f"CUDA Capability: {torch.cuda.get_device_capability()}")
else:
print("CUDA Available: No (using CPU)")
# Prefer bfloat16 on modern GPUs
dtype = torch.bfloat16 if (device == "cuda" and torch.cuda.get_device_capability()[0] >= 8) else torch.float16
model = create_onyx_7b(device=device, dtype=dtype, compile_model=False) # Skip compile for testing
# Test forward pass
print("\nTesting forward pass...")
input_ids = torch.randint(0, 128256, (1, 32), device=device)
with torch.inference_mode():
outputs = model(input_ids, use_cache=True)
print(f"✅ Forward pass successful!")
print(f" Output shape: {outputs['logits'].shape}")
print(f" KV cache: {len(outputs['past_key_values'])} layers")
# Test generation with KV cache
print("\nTesting generation with KV cache...")
import time
# Without cache
start = time.time()
generated = model.generate(input_ids, max_length=64, use_cache=False)
time_no_cache = time.time() - start
# With cache
start = time.time()
generated = model.generate(input_ids, max_length=64, use_cache=True)
time_with_cache = time.time() - start
print(f"✅ Generation successful!")
print(f" Without cache: {time_no_cache:.2f}s")
print(f" With cache: {time_with_cache:.2f}s")
if time_with_cache > 0:
print(f" Speedup: {time_no_cache/time_with_cache:.1f}x")
# Test top-k sampling
print("\nTesting top-k sampling...")
generated_topk = model.generate(input_ids, max_length=40, top_k=50, temperature=0.8)
print(f"✅ Top-k=50 generation: {generated_topk.shape}")
# FlashAttention vs SDPA smoke test
print("\nFA vs SDPA smoke test...")
model.eval()
x = torch.randint(0, 128256, (2, 16), device=device)
with torch.inference_mode():
# Force SDPA
model.config.use_flash_attn = False
y_sdpa = model(x, use_cache=False)["logits"]
# Try FA if possible
model.config.use_flash_attn = True
try:
y_fa = model(x.to(dtype), use_cache=False)["logits"]
assert y_fa.shape == y_sdpa.shape
print(f"✅ FA/SDPA shapes match: {y_fa.shape}")
except Exception as e:
print(f"ℹ️ FA path skipped: {repr(e)}")
finally:
model.config.use_flash_attn = FLASH_AVAILABLE
# KV cache head-count sanity check
print("\nKV cache head-count sanity...")
with torch.inference_mode():
out = model(x[:, :8], use_cache=True)
pkv = out["past_key_values"]
k0, v0 = pkv[0]
# Expect (B, n_kv_heads, S, D)
assert k0.shape[1] == model.config.n_kv_heads, f"Wrong KV heads: {k0.shape}"
print(f"✅ Compact KV cache shape: {k0.shape} (4x memory savings)")
# Generate with cache and show growth
print("\nKV cache growth during generation...")
generated_with_cache = model.generate(x[:1, :8], max_length=24, use_cache=True)
print(f" Generated sequence length: {generated_with_cache.shape[1]}")
print(f" Final KV cache per layer: (B={k0.shape[0]}, n_kv_heads={model.config.n_kv_heads}, S=varies, D={k0.shape[-1]})")
# Long-context smoke test
print("\nLong-context smoke test...")
L = min(4096, model.config.max_seq_len) # Keep reasonable for single GPU
long_input = torch.randint(0, model.config.vocab_size, (1, L), device=device)
with torch.inference_mode():
_ = model(long_input, use_cache=False)
print(f"✅ Processed {L} tokens successfully (RoPE/SDPA memory OK)")
print("\n" + "=" * 60)
print("✅ All tests passed! Model is production-ready.")
print("\nKey features:")
print(" • FlashAttention v2 support with guards")
print(" • 4x KV cache memory savings (GQA)")
print(" • Fixed top-p sampling")
print(" • Gradient checkpointing (tensor-only)")
print(" • Float32 loss for stability")
print(" • PyTorch 2.8 optimized")