Feature Extraction
Transformers
Safetensors
cue
user-simulation
dialogue
persona
custom_code
cue / decoder_model.py
AnjaliRuban's picture
Upload folder using huggingface_hub
6c05b39 verified
Raw History Blame Contribute Delete
11.2 kB
"""Persona-manual decoder with Flamingo-style gated cross-attention (inference)."""
from __future__ import annotations
from typing import Any
import torch
import torch.nn as nn
from transformers import AutoConfig, AutoModelForCausalLM, AutoTokenizer
from .decoder_flamingo import GatedXAttnDense
DEFAULT_DECODER = "Qwen/Qwen3-0.6B-Base"
class PersonaMemory(nn.Module):
"""Expand bottleneck to a fixed set of persona memory tokens."""
def __init__(self, bottleneck_dim: int, hidden_dim: int, num_tokens: int = 16) -> None:
super().__init__()
self.num_tokens = num_tokens
self.proj = nn.Sequential(
nn.Linear(bottleneck_dim, hidden_dim),
nn.GELU(),
nn.Linear(hidden_dim, num_tokens * hidden_dim),
)
def forward(self, bottleneck: torch.Tensor) -> torch.Tensor:
out = self.proj(bottleneck)
return out.view(bottleneck.size(0), self.num_tokens, -1)
class CueDecoder(nn.Module):
def __init__(
self,
*,
lm: nn.Module,
tokenizer: Any,
memory: PersonaMemory,
gated_blocks: nn.ModuleList,
slot_embeddings: nn.Embedding | None,
slot_projection: nn.Module | None,
general_command_slots: int,
user_specific_command_slots: int,
style_command_slots: int,
insert_every: int,
config: dict[str, Any],
) -> None:
super().__init__()
self.lm = lm
self.tokenizer = tokenizer
self.memory = memory
self.gated_blocks = gated_blocks
self.slot_embeddings = slot_embeddings
self.slot_projection = slot_projection
self.general_command_slots = int(general_command_slots)
self.user_specific_command_slots = int(user_specific_command_slots)
self.style_command_slots = int(style_command_slots)
self.insert_every = insert_every
self.config = config
self._persona_cache: torch.Tensor | None = None
self._register_hooks()
def _register_hooks(self) -> None:
"""Wrap selected decoder layers with gated XAttn-Dense."""
layers = self._get_layers()
self._hook_handles = []
block_idx = 0
for i, layer in enumerate(layers):
if (i + 1) % self.insert_every != 0:
continue
if block_idx >= len(self.gated_blocks):
break
gated = self.gated_blocks[block_idx]
block_idx += 1
def _make_hook(block: GatedXAttnDense):
def hook(_module, _inputs, output):
persona = self._persona_cache
if persona is None:
return output
if isinstance(output, tuple):
hidden = output[0]
new_hidden = block(hidden, persona)
return (new_hidden,) + output[1:]
return block(output, persona)
return hook
self._hook_handles.append(layer.register_forward_hook(_make_hook(gated)))
def _get_layers(self) -> nn.ModuleList:
if hasattr(self.lm, "model") and hasattr(self.lm.model, "layers"):
return self.lm.model.layers
if hasattr(self.lm, "transformer") and hasattr(self.lm.transformer, "h"):
return self.lm.transformer.h
raise ValueError("Unsupported decoder architecture: cannot find layers")
def command_slot_id(self, head: str, slot: int) -> int:
if head == "general":
if not 0 <= slot < self.general_command_slots:
raise ValueError(f"general slot {slot} out of range")
return slot
if head == "specific":
if not 0 <= slot < self.user_specific_command_slots:
raise ValueError(f"user-specific slot {slot} out of range")
return self.general_command_slots + slot
if head == "style":
if not 0 <= slot < self.style_command_slots:
raise ValueError(f"style slot {slot} out of range")
return (
self.general_command_slots + self.user_specific_command_slots + slot
)
raise ValueError(f"unknown command head {head!r}")
def set_persona(
self,
bottleneck: torch.Tensor,
slot_ids: torch.Tensor | None = None,
) -> torch.Tensor:
# Memory stays fp32; cast encoder bottleneck in for a stable projection.
dtype = next(self.memory.parameters()).dtype
persona = self.memory(bottleneck.to(dtype=dtype))
if slot_ids is not None:
if self.slot_embeddings is None or self.slot_projection is None:
raise ValueError("slot_ids provided but decoder has no slot embeddings")
slot = self.slot_projection(self.slot_embeddings(slot_ids).to(dtype=dtype))
persona = torch.cat([slot.unsqueeze(1), persona], dim=1)
self._persona_cache = persona
return self._persona_cache
def clear_persona(self) -> None:
self._persona_cache = None
@torch.no_grad()
def generate(
self,
*,
bottleneck: torch.Tensor,
sentinel_ids: torch.Tensor,
slot_ids: torch.Tensor | None = None,
max_new_tokens: int = 1024,
temperature: float = 0.0,
top_p: float = 1.0,
) -> list[str]:
self.set_persona(bottleneck, slot_ids=slot_ids)
embed = self.lm.get_input_embeddings()
batch = bottleneck.size(0)
sentinel = embed(sentinel_ids).unsqueeze(0).expand(batch, -1, -1)
attn = torch.ones(
batch, sentinel.size(1), device=bottleneck.device, dtype=torch.long
)
generated = self.lm.generate(
inputs_embeds=sentinel,
attention_mask=attn,
max_new_tokens=max_new_tokens,
do_sample=temperature > 0,
temperature=temperature if temperature > 0 else None,
top_p=top_p if temperature > 0 else None,
pad_token_id=(
self.tokenizer.pad_token_id
if self.tokenizer.pad_token_id is not None
else self.tokenizer.eos_token_id
),
eos_token_id=self.tokenizer.eos_token_id,
)
return [
self.tokenizer.decode(seq, skip_special_tokens=True).strip()
for seq in generated
]
@classmethod
def build(
cls,
*,
model_name: str = DEFAULT_DECODER,
bottleneck_dim: int = 1024,
num_persona_tokens: int = 16,
insert_every: int = 1,
num_heads: int = 8,
general_command_slots: int = 0,
user_specific_command_slots: int = 0,
style_command_slots: int = 0,
slot_embedding_dim: int = 64,
dtype: torch.dtype = torch.bfloat16,
pretrained_lm: bool = True,
) -> CueDecoder:
tokenizer = AutoTokenizer.from_pretrained(
model_name, trust_remote_code=True, clean_up_tokenization_spaces=False
)
tokenizer.padding_side = "right"
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
if pretrained_lm:
# low_cpu_mem_usage=False forces real weights on CPU (no accelerate meta init).
# With meta init the tied lm_head is an empty meta tensor and a later .to(device)
# fails with "Cannot copy out of meta tensor".
try:
lm = AutoModelForCausalLM.from_pretrained(
model_name, dtype=dtype, trust_remote_code=True, low_cpu_mem_usage=False
)
except TypeError:
lm = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=dtype,
trust_remote_code=True,
low_cpu_mem_usage=False,
)
else:
# A complete CUE checkpoint replaces every LM weight, so only the architecture
# config is needed. Skips a redundant base-weight download.
lm_config = AutoConfig.from_pretrained(model_name, trust_remote_code=True)
lm = AutoModelForCausalLM.from_config(lm_config, trust_remote_code=True).to(
dtype=dtype
)
# Ensure tied weights (lm_head <-> embed_tokens) are materialized, not left on meta.
if hasattr(lm, "tie_weights"):
lm.tie_weights()
lm.generation_config.pad_token_id = tokenizer.pad_token_id
hidden = lm.get_input_embeddings().embedding_dim
layers = (
lm.model.layers
if hasattr(lm, "model") and hasattr(lm.model, "layers")
else lm.transformer.h
)
n_blocks = max(1, len(layers) // max(1, insert_every))
# Keep conditioning modules in fp32; GatedXAttnDense casts activations internally.
gated = nn.ModuleList(
[GatedXAttnDense(hidden, num_heads=num_heads) for _ in range(n_blocks)]
)
memory = PersonaMemory(bottleneck_dim, hidden, num_persona_tokens)
num_slots = (
max(0, int(general_command_slots))
+ max(0, int(user_specific_command_slots))
+ max(0, int(style_command_slots))
)
slot_embeddings = (
nn.Embedding(num_slots, max(1, int(slot_embedding_dim))) if num_slots else None
)
slot_projection = (
nn.Sequential(
nn.Linear(max(1, int(slot_embedding_dim)), hidden),
nn.LayerNorm(hidden),
)
if num_slots
else None
)
config = {
"model_name": model_name,
"dtype": str(dtype).removeprefix("torch."),
"bottleneck_dim": bottleneck_dim,
"num_persona_tokens": num_persona_tokens,
"insert_every": insert_every,
"num_heads": num_heads,
"general_command_slots": int(general_command_slots),
"user_specific_command_slots": int(user_specific_command_slots),
"style_command_slots": int(style_command_slots),
"slot_embedding_dim": int(slot_embedding_dim),
"injection": "flamingo_gated_xattn_dense",
}
return cls(
lm=lm,
tokenizer=tokenizer,
memory=memory,
gated_blocks=gated,
slot_embeddings=slot_embeddings,
slot_projection=slot_projection,
general_command_slots=general_command_slots,
user_specific_command_slots=user_specific_command_slots,
style_command_slots=style_command_slots,
insert_every=insert_every,
config=config,
)
def gate_magnitudes(self) -> dict[str, float]:
xs = [float(torch.tanh(b.alpha_xattn).detach().cpu()) for b in self.gated_blocks]
ds = [float(torch.tanh(b.alpha_dense).detach().cpu()) for b in self.gated_blocks]
return {
"xattn_gate_mean": sum(xs) / max(1, len(xs)),
"dense_gate_mean": sum(ds) / max(1, len(ds)),
"xattn_gate_max": max(xs) if xs else 0.0,
"dense_gate_max": max(ds) if ds else 0.0,
}