Qwen3.5-0.8B-PDelta3-CLVR-Local32 / src /tinycenn_lm /gemma3_integrated_memory.py
vtava's picture
Upload verified PDelta3-CLVR checkpoint for layers [3, 7, 11]
a38f163 verified
Raw History Blame
12.8 kB
"""Gemma 3 / FunctionGemma integrated TinyCeNN memory.
This module mirrors the SmolLM2 V3 experiment but respects Gemma 3's
Q/K RMS normalization and hybrid sliding/full-attention cache. It is intended
for replacing *full-attention* Gemma 3 text layers; the original sliding-window
layers remain untouched.
"""
import copy
from contextlib import contextmanager
import torch
import torch.nn.functional as F
from torch import nn
from transformers.cache_utils import DynamicCache
from transformers.models.gemma3.modeling_gemma3 import apply_rotary_pos_emb, repeat_kv
from .optimized_memory import OptimizedMemory
FORMAT = "functiongemma-cenn-integrated-v1"
def native_dtype(device):
"""Use BF16 only when the GPU has native BF16 arithmetic."""
if torch.device(device).type != "cuda":
return torch.float32
return torch.bfloat16 if torch.cuda.get_device_capability(device)[0] >= 8 else torch.float16
class Gemma3IntegratedCache(DynamicCache):
"""Gemma 3 hybrid cache plus bounded TinyCeNN states at replaced layers."""
def __init__(self, config, memory_layers=()):
super().__init__(config=config)
self.memory_layers = frozenset(memory_layers)
self.memory_states = {}
def get_seq_length(self, layer_idx=0):
if layer_idx in self.memory_layers:
state = self.memory_states.get(layer_idx)
return state.position if state is not None else 0
return super().get_seq_length(layer_idx)
def get_mask_sizes(self, cache_position, layer_idx):
if layer_idx in self.memory_layers:
query_length = cache_position.shape[0] if isinstance(cache_position, torch.Tensor) else int(cache_position)
return self.get_seq_length(layer_idx) + query_length, 0
return super().get_mask_sizes(cache_position, layer_idx)
@property
def nbytes(self):
tensors = [
tensor
for layer in self.layers
for tensor in (getattr(layer, "keys", None), getattr(layer, "values", None))
if isinstance(tensor, torch.Tensor)
]
return sum(x.numel() * x.element_size() for x in tensors) + sum(
state.nbytes for state in self.memory_states.values()
)
def reorder_cache(self, *args, **kwargs):
raise NotImplementedError("Use batch-one greedy decoding; beam-search cache reordering is unsupported")
def crop(self, *args, **kwargs):
raise NotImplementedError("Compressed TinyCeNN history cannot be cropped; start a fresh cache")
def batch_repeat_interleave(self, *args, **kwargs):
raise NotImplementedError("Batch expansion is unsupported for TinyCeNN memory states")
def batch_select_indices(self, *args, **kwargs):
raise NotImplementedError("Batch selection is unsupported for TinyCeNN memory states")
class Gemma3IntegratedAttention(nn.Module):
"""Full-attention Gemma 3 layer replaced by a TinyCeNN memory core.
Gemma3DecoderLayer inspects attributes on ``self_attn`` before calling it
(most importantly ``is_sliding`` to select local vs global RoPE). Mirror
the lightweight structural attributes of Gemma3Attention so replacing the
module preserves the Transformers 4.57.x decoder contract.
"""
def __init__(self, original, core, layer_idx):
super().__init__()
self.original = original
self.core = core
self.layer_idx = layer_idx
# Structural Gemma3Attention API used by Gemma3DecoderLayer and by
# attention tooling. These are plain metadata values, not duplicate
# module registrations; Q/K/V/O and RMSNorm modules remain under
# ``self.original`` only.
self.is_sliding = bool(original.is_sliding)
self.config = original.config
self.head_dim = original.head_dim
self.num_key_value_groups = original.num_key_value_groups
self.scaling = original.scaling
self.attention_dropout = original.attention_dropout
self.is_causal = original.is_causal
self.attn_logit_softcapping = original.attn_logit_softcapping
self.sliding_window = original.sliding_window
if self.is_sliding:
raise ValueError("Gemma3IntegratedAttention only supports full-attention layers")
self.register_buffer("fused_weight", None, persistent=False)
def fuse(self, enabled=True):
if not enabled:
self.fused_weight = None
return
with torch.no_grad():
weight = self.original.o_proj.weight.float().reshape(
-1, self.core.num_heads, self.core.head_dim
)
self.fused_weight = torch.einsum(
"ohd,hkd->ohk", weight, self.core.readout.float()
).reshape_as(self.original.o_proj.weight).to(self.original.o_proj.weight.dtype)
def forward(
self,
hidden_states,
position_embeddings=None,
attention_mask=None,
past_key_values=None,
past_key_value=None,
cache_position=None,
**kwargs,
):
if position_embeddings is None:
raise ValueError("Gemma 3 position_embeddings are required")
cache = past_key_values if past_key_values is not None else past_key_value
if cache is not None and not isinstance(cache, Gemma3IntegratedCache):
raise TypeError("Use Gemma3IntegratedCache with a FunctionGemma TinyCeNN model")
if cache is not None and torch.is_grad_enabled():
raise RuntimeError("Train with use_cache=False")
if self.fused_weight is not None and torch.is_grad_enabled():
raise RuntimeError("Unfuse the readout before training")
if self.is_sliding:
raise RuntimeError("TinyCeNN FunctionGemma V1 only supports replacing full-attention layers")
if attention_mask is not None:
if attention_mask.ndim != 4 or bool((attention_mask[..., -1, :] < 0).any()):
raise ValueError("Only unpadded causal batches are supported")
b, t, _ = hidden_states.shape
h = self.core.num_heads
hk = self.core.num_kv_heads
d = self.core.head_dim
q = self.original.q_proj(hidden_states).view(b, t, h, d).transpose(1, 2)
k = self.original.k_proj(hidden_states).view(b, t, hk, d).transpose(1, 2)
v = self.original.v_proj(hidden_states).view(b, t, hk, d).transpose(1, 2)
q = self.original.q_norm(q)
k = self.original.k_norm(k)
cos, sin = position_embeddings
q, k = apply_rotary_pos_emb(q, k, cos, sin)
if self.core.variant == "transformer_readout":
if cache is not None:
cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
k, v = cache.update(k, v, self.layer_idx, cache_kwargs)
output = F.scaled_dot_product_attention(
q,
repeat_kv(k, h // hk),
repeat_kv(v, h // hk),
attn_mask=attention_mask,
is_causal=attention_mask is None and t > 1,
scale=float(self.scaling),
)
if self.fused_weight is None:
output = self.core.calibrate(output.float())
elif cache is not None:
output, state = self.core(
q,
k,
v,
state=cache.memory_states.get(self.layer_idx),
return_state=True,
apply_readout=self.fused_weight is None,
)
cache.memory_states[self.layer_idx] = state
else:
output = self.core(q, k, v, apply_readout=self.fused_weight is None)
flat = output.transpose(1, 2).reshape(b, t, h * d).to(hidden_states.dtype)
if self.fused_weight is None:
return self.original.o_proj(flat), None
return F.linear(flat, self.fused_weight, self.original.o_proj.bias), None
def wrappers(model):
return [
layer.self_attn
for layer in model.model.layers
if isinstance(layer.self_attn, Gemma3IntegratedAttention)
]
def full_attention_layers(model):
return [
i for i, layer in enumerate(model.model.layers)
if not getattr(layer.self_attn, "is_sliding", False)
]
def build_student(teacher, layers, variant="cenn_partition", features=64, block_size=32, sinks=4):
model = copy.deepcopy(teacher).eval().requires_grad_(False)
config = model.config.get_text_config(decoder=True)
if config.model_type != "gemma3_text":
raise ValueError(f"Expected gemma3_text, got {config.model_type}")
for index in layers:
if not 0 <= index < len(model.model.layers):
raise ValueError(f"Invalid layer {index}")
original = model.model.layers[index].self_attn
if getattr(original, "is_sliding", False):
raise ValueError(
f"Layer {index} is sliding_attention. FunctionGemma V1 intentionally replaces only full_attention layers."
)
# FunctionGemma uses query_pre_attn_scalar == head_dim (256), so its
# native attention scale matches the OptimizedMemory softmax scale.
# Keep this guard explicit for future Gemma3 checkpoints where that
# assumption may not hold.
expected_scale = original.head_dim ** -0.5
if abs(float(original.scaling) - float(expected_scale)) > 1e-8:
raise ValueError(
f"Layer {index} uses attention scale {original.scaling}, but TinyCeNN currently expects {expected_scale}."
)
core = OptimizedMemory(
config.num_attention_heads,
config.num_key_value_heads,
original.head_dim,
features,
variant,
block_size,
sinks,
).to(original.q_proj.weight.device)
model.model.layers[index].self_attn = Gemma3IntegratedAttention(original, core, index)
return model
@contextmanager
def inference_mode(model, compute_dtype="float32"):
adapters = wrappers(model)
previous = [a.core.compute_dtype for a in adapters]
try:
for adapter in adapters:
adapter.core.compute_dtype = compute_dtype
adapter.fuse()
with torch.no_grad():
yield model
finally:
for adapter, dtype in zip(adapters, previous):
adapter.fuse(False)
adapter.core.compute_dtype = dtype
def new_cache(model):
memory_layers = [
a.layer_idx for a in wrappers(model)
if a.core.variant != "transformer_readout"
]
return Gemma3IntegratedCache(model.config, memory_layers)
def adapter_payload(model, metadata=None):
return {
"format": FORMAT,
"metadata": metadata or {},
"adapters": {
str(a.layer_idx): {
"config": a.core.config,
"state_dict": {
k: v.detach().cpu().clone() for k, v in a.core.state_dict().items()
},
}
for a in wrappers(model)
},
}
def restore_student(teacher, payload):
if payload["format"] != FORMAT:
raise ValueError(f"Not a {FORMAT} checkpoint")
model = copy.deepcopy(teacher).eval().requires_grad_(False)
for key, value in payload["adapters"].items():
index = int(key)
original = model.model.layers[index].self_attn
if getattr(original, "is_sliding", False):
raise ValueError(f"Checkpoint attempts to replace sliding-attention layer {index}")
core = OptimizedMemory(**value["config"]).to(original.q_proj.weight.device)
core.load_state_dict(value["state_dict"])
model.model.layers[index].self_attn = Gemma3IntegratedAttention(original, core, index)
return model
@torch.no_grad()
def greedy_generate(model, ids, tokens=32, stop_token_ids=()):
"""Batch-one cached greedy generation for the hybrid FunctionGemma model."""
if ids.shape[0] != 1 or ids.shape[1] < 1 or tokens < 1:
raise ValueError("Use batch size one, a nonempty prompt, and positive tokens")
stop_token_ids = set(int(x) for x in stop_token_ids)
cache = new_cache(model)
output = model(input_ids=ids, past_key_values=cache, use_cache=True).logits[:, -1]
continuation = []
token = output.argmax(-1, keepdim=True)
continuation.append(token)
for _ in range(tokens - 1):
if int(continuation[-1].item()) in stop_token_ids:
break
output = model(
input_ids=continuation[-1], past_key_values=cache, use_cache=True
).logits[:, -1]
continuation.append(output.argmax(-1, keepdim=True))
return torch.cat(continuation, dim=1), cache