rlora-olmo3-7b / model.py
jhdlee's picture
Upload model.py with huggingface_hub
b2ce45e verified
Raw
History Blame Contribute Delete
17.6 kB
"""
rLoRA Model: Retrofitted Recurrence with LoRA adapters.
Splits a pretrained transformer into prelude/recurrent/coda segments,
injects LoRA into the recurrent block, and loops it N times at inference.
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import Tensor
from torch.utils.checkpoint import checkpoint as torch_checkpoint
from typing import Optional, List, Tuple, Dict, Any, Union
from transformers import (
PreTrainedModel,
GenerationMixin,
AutoModelForCausalLM,
AutoConfig,
)
from transformers.modeling_outputs import CausalLMOutputWithPast
from transformers.masking_utils import create_causal_mask, create_sliding_window_causal_mask
from .config import RLoRAConfig
from .lora_modules import ConcatAdapter
class RLoRAModel(PreTrainedModel, GenerationMixin):
"""
Retrofitted Recurrence with LoRA.
Takes a pretrained causal LM, splits its layers into prelude/recurrent/coda,
injects LoRA into the recurrent block layers, and loops them N times.
Only LoRA parameters and the inter-recurrence adapter are trained.
"""
config_class = RLoRAConfig
base_model_prefix = "rlora"
supports_gradient_checkpointing = True
@classmethod
def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs):
"""Override to avoid meta-device initialization.
The default from_pretrained creates the model on meta device then loads
weights, but our __init__ calls AutoModelForCausalLM.from_pretrained
internally, which fails on meta device. Instead, we build the model
normally (loading the base model), then overwrite with saved weights.
"""
import os
from safetensors.torch import load_file
config = kwargs.pop("config", None)
if config is None:
config, model_kwargs = cls.config_class.from_pretrained(
pretrained_model_name_or_path,
return_unused_kwargs=True,
**kwargs,
)
else:
model_kwargs = kwargs
# Build model normally (downloads and loads base model + injects LoRA)
model = cls(config)
# Load saved weights from local dir or HuggingFace Hub
if os.path.isdir(pretrained_model_name_or_path):
weight_path = os.path.join(pretrained_model_name_or_path, "model.safetensors")
else:
from huggingface_hub import hf_hub_download
weight_path = hf_hub_download(pretrained_model_name_or_path, "model.safetensors")
state_dict = load_file(weight_path)
model.load_state_dict(state_dict, strict=True)
device = model_kwargs.get("device_map", None)
if device is not None:
model = model.to(device)
return model
def __init__(self, config: RLoRAConfig):
super().__init__(config)
# 1. Load and freeze the pretrained base model
# NOTE: Use a local variable, NOT self.base_model. PreTrainedModel defines
# a `base_model` property: getattr(self, self.base_model_prefix, self).
# Since base_model_prefix="rlora" and there's no self.rlora, the property
# returns `self`, shadowing any assignment to self.base_model.
base_config = AutoConfig.from_pretrained(
config.base_model_name, trust_remote_code=True
)
base = AutoModelForCausalLM.from_pretrained(
config.base_model_name,
config=base_config,
torch_dtype="auto",
trust_remote_code=True,
attn_implementation="sdpa",
low_cpu_mem_usage=True,
)
for param in base.parameters():
param.requires_grad = False
base.eval()
# Store base config for mask creation
self.base_config = base_config
self.hidden_size = base_config.hidden_size
self.vocab_size = base_config.vocab_size
# 2. Inject LoRA into recurrent block layers and extract all components
self._inject_lora(config, base)
# Free the base model wrapper — all layers are now registered independently
# via self.prelude_layers, self.recurrent_layers, etc.
del base
# 4. Create inter-recurrence adapter (match base model dtype, e.g. bf16)
model_dtype = self.embed_tokens.weight.dtype
if config.adapter_type == "concat":
self.adapter = ConcatAdapter(self.hidden_size, init_type=config.adapter_init).to(dtype=model_dtype)
else:
self.adapter = None # "add" mode needs no learnable module
# 5. Freeze lm_head if configured
if config.freeze_lm_head:
for param in self.lm_head.parameters():
param.requires_grad = False
# 6. Gradient checkpointing (activated via gradient_checkpointing_enable())
self.gradient_checkpointing = False
def _inject_lora(self, config: RLoRAConfig, base_model: nn.Module):
"""Inject LoRA adapters into recurrent block layers and extract all components."""
from peft import LoraConfig as PeftLoraConfig, get_peft_model
peft_config = PeftLoraConfig(
task_type="CAUSAL_LM",
r=config.lora_r,
lora_alpha=config.lora_alpha,
lora_dropout=config.lora_dropout,
target_modules=config.lora_target_modules,
layers_to_transform=config.recurrent_layers,
bias="none",
)
peft_model = get_peft_model(base_model, peft_config)
# Extract all components from the PEFT-wrapped model
# (PeftModel adds a wrapper layer: peft_model.base_model.model.model)
inner = peft_model.base_model.model.model
all_layers = list(inner.layers)
self.prelude_layers = nn.ModuleList([all_layers[i] for i in config.prelude_layers])
self.recurrent_layers = nn.ModuleList([all_layers[i] for i in config.recurrent_layers])
self.coda_layers = nn.ModuleList([all_layers[i] for i in config.coda_layers])
self.embed_tokens = inner.embed_tokens
self.final_norm = inner.norm
self.rotary_emb = inner.rotary_emb
self.lm_head = peft_model.base_model.model.lm_head
def train(self, mode: bool = True):
"""Override to keep frozen segments in eval mode (no dropout/batchnorm leakage)."""
super().train(mode)
# Guard needed: train() can be called during __init__ (e.g., via
# PreTrainedModel.eval() → self.train(False)) before these attributes exist.
if hasattr(self, 'prelude_layers'):
self.prelude_layers.eval()
self.coda_layers.eval()
self.embed_tokens.eval()
self.final_norm.eval()
return self
def print_trainable_parameters(self):
"""Print trainable vs total parameter counts."""
total = sum(p.numel() for p in self.parameters())
trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
print(
f"trainable params: {trainable:,} || all params: {total:,} "
f"|| trainable%: {100 * trainable / total:.4f}"
)
def initialize_state(self, input_embeds: Tensor, scale: float = 1.0) -> Tensor:
"""Initialize recurrent state as truncated normal noise (following the paper)."""
x = torch.randn_like(input_embeds)
std = self.config.state_init_std * scale
if std > 0:
torch.nn.init.trunc_normal_(x, mean=0.0, std=std, a=-3 * std, b=3 * std)
else:
x.zero_()
return x
def _build_causal_masks(
self,
inputs_embeds: Tensor,
attention_mask: Optional[Tensor],
cache_position: Tensor,
past_key_values=None,
position_ids: Optional[Tensor] = None,
) -> Dict[str, Optional[Tensor]]:
"""Build causal mask mapping for OLMo-3's mixed sliding/full attention."""
mask_kwargs = dict(
config=self.base_config,
inputs_embeds=inputs_embeds,
attention_mask=attention_mask,
cache_position=cache_position,
past_key_values=past_key_values,
position_ids=position_ids,
)
has_sliding = any(
hasattr(layer, "self_attn") and getattr(layer.self_attn, "attention_type", "full_attention") == "sliding_attention"
for layer in list(self.prelude_layers) + list(self.recurrent_layers) + list(self.coda_layers)
)
result = {
"full_attention": create_causal_mask(**mask_kwargs),
}
if has_sliding:
result["sliding_attention"] = create_sliding_window_causal_mask(**mask_kwargs)
return result
def _get_attention_type(self, layer: nn.Module) -> str:
"""Get the attention type for a decoder layer."""
if hasattr(layer, "self_attn") and hasattr(layer.self_attn, "attention_type"):
return layer.self_attn.attention_type
return "full_attention"
def _run_layer(
self,
layer: nn.Module,
hidden_states: Tensor,
causal_mask_mapping: Dict[str, Optional[Tensor]],
position_embeddings: Tuple[Tensor, Tensor],
position_ids: Optional[Tensor] = None,
past_key_values=None,
use_cache: bool = False,
cache_position: Optional[Tensor] = None,
) -> Tensor:
"""Run a single decoder layer with the correct attention mask."""
attn_type = self._get_attention_type(layer)
mask = causal_mask_mapping.get(attn_type, causal_mask_mapping.get("full_attention"))
if self.gradient_checkpointing and self.training:
return torch_checkpoint(
layer,
hidden_states,
attention_mask=mask,
position_embeddings=position_embeddings,
position_ids=position_ids,
past_key_values=past_key_values,
use_cache=use_cache,
cache_position=cache_position,
use_reentrant=False,
)
return layer(
hidden_states,
attention_mask=mask,
position_embeddings=position_embeddings,
position_ids=position_ids,
past_key_values=past_key_values,
use_cache=use_cache,
cache_position=cache_position,
)
def _run_layers(
self,
layers: nn.ModuleList,
hidden_states: Tensor,
causal_mask_mapping: Dict[str, Optional[Tensor]],
position_embeddings: Tuple[Tensor, Tensor],
position_ids: Optional[Tensor] = None,
past_key_values=None,
use_cache: bool = False,
cache_position: Optional[Tensor] = None,
) -> Tensor:
"""Run a sequence of decoder layers."""
for layer in layers:
hidden_states = self._run_layer(
layer, hidden_states, causal_mask_mapping,
position_embeddings, position_ids,
past_key_values, use_cache, cache_position,
)
return hidden_states
def _single_recurrence_step(
self,
state: Tensor,
prelude_output: Tensor,
causal_mask_mapping: Dict[str, Optional[Tensor]],
position_embeddings: Tuple[Tensor, Tensor],
position_ids: Optional[Tensor] = None,
) -> Tensor:
"""Execute one recurrence step: inject prelude features + run recurrent block."""
# Inject prelude features
if self.config.adapter_type == "concat":
state = self.adapter(state, prelude_output)
else: # "add"
state = state + prelude_output
# Run through recurrent block layers
state = self._run_layers(
self.recurrent_layers, state, causal_mask_mapping,
position_embeddings, position_ids,
past_key_values=None, use_cache=False,
)
return state
def forward(
self,
input_ids: Tensor,
attention_mask: Optional[Tensor] = None,
position_ids: Optional[Tensor] = None,
past_key_values=None,
labels: Optional[Tensor] = None,
use_cache: bool = False,
num_recurrences: Optional[int] = None,
num_steps_no_grad: Optional[int] = None,
num_steps_with_grad: Optional[int] = None,
**kwargs,
) -> Union[Tuple, CausalLMOutputWithPast]:
"""
Forward pass with recurrent depth and truncated BPTT.
Args:
num_recurrences: Total recurrence steps (ignored if split provided)
num_steps_no_grad: Steps WITHOUT gradients (truncated BPTT)
num_steps_with_grad: Steps WITH gradients
"""
batch_size, seq_len = input_ids.shape
device = input_ids.device
# Determine number of recurrences
if num_steps_no_grad is not None and num_steps_with_grad is not None:
total_recurrences = num_steps_no_grad + num_steps_with_grad
elif num_recurrences is not None:
num_steps_no_grad = 0
num_steps_with_grad = num_recurrences
total_recurrences = num_recurrences
else:
num_steps_no_grad = 0
num_steps_with_grad = self.config.default_num_recurrences
total_recurrences = self.config.default_num_recurrences
# === Embedding ===
input_embeds = self.embed_tokens(input_ids)
# === Position handling ===
cache_position = torch.arange(seq_len, device=device)
if position_ids is None:
position_ids = cache_position.unsqueeze(0).expand(batch_size, -1)
# Compute RoPE embeddings once (reused across all recurrence steps)
position_embeddings = self.rotary_emb(input_embeds, position_ids)
# === Build causal masks ===
causal_mask_mapping = self._build_causal_masks(
input_embeds, attention_mask, cache_position,
past_key_values=None, position_ids=position_ids,
)
# === Prelude: frozen, run once ===
hidden_states = self._run_layers(
self.prelude_layers, input_embeds, causal_mask_mapping,
position_embeddings, position_ids,
)
prelude_output = hidden_states # Save for injection at each recurrence step
# === Recurrence ===
total_recurrences = num_steps_no_grad + num_steps_with_grad
if total_recurrences == 1:
# Single pass: skip state init and adapter → identical to standard LoRA
state = self._run_layers(
self.recurrent_layers, prelude_output, causal_mask_mapping,
position_embeddings, position_ids,
past_key_values=None, use_cache=False,
)
else:
# Multi-recurrence: state init + adapter loop
state = self.initialize_state(prelude_output)
# Phase 1: No-gradient iterations (truncated BPTT efficiency)
with torch.no_grad():
for _t in range(num_steps_no_grad):
state = self._single_recurrence_step(
state, prelude_output, causal_mask_mapping,
position_embeddings, position_ids,
)
# Phase 2: With-gradient iterations
for _t in range(num_steps_with_grad):
state = self._single_recurrence_step(
state, prelude_output, causal_mask_mapping,
position_embeddings, position_ids,
)
# === Coda: frozen, run once ===
hidden_states = self._run_layers(
self.coda_layers, state, causal_mask_mapping,
position_embeddings, position_ids,
)
hidden_states = self.final_norm(hidden_states)
# === Logits ===
logits = self.lm_head(hidden_states)
# === Loss ===
loss = None
if labels is not None:
# Keep logits in bf16 until the cross_entropy call to avoid
# materializing a full float32 [B, S, V] tensor (~4.7 GiB).
# Only upcast the shifted slice fed to cross_entropy.
shift_logits = logits[..., :-1, :].reshape(-1, self.vocab_size)
shift_labels = labels[..., 1:].reshape(-1)
loss = F.cross_entropy(
shift_logits.float(), shift_labels, ignore_index=-100,
)
return CausalLMOutputWithPast(
loss=loss,
logits=None if (self.training and labels is not None) else logits,
past_key_values=None,
hidden_states=None,
attentions=None,
)
def prepare_inputs_for_generation(
self,
input_ids: Tensor,
past_key_values=None,
attention_mask: Optional[Tensor] = None,
inputs_embeds: Optional[Tensor] = None,
**kwargs,
) -> Dict[str, Any]:
"""Prepare inputs for model.generate()."""
# For now, no KV cache — full recomputation at each step
position_ids = kwargs.get("position_ids", None)
if position_ids is None and attention_mask is not None:
position_ids = attention_mask.long().cumsum(-1) - 1
position_ids.masked_fill_(attention_mask == 0, 1)
return {
"input_ids": input_ids,
"attention_mask": attention_mask,
"position_ids": position_ids,
"past_key_values": None,
"use_cache": False,
"num_recurrences": kwargs.get("num_recurrences", self.config.default_num_recurrences),
}