PanoLM-380M / modeling_panolm.py
sd24's picture
panolm kda
dfe0379 verified
Raw
History Blame Contribute Delete
11.1 kB
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
"""HuggingFace-compatible PanoLM-KDA model (Kimi Delta Attention).
Standalone, KDA-locked variant of the PanoLM HF wrapper.
Layer tree (matches the torchtitan ``PanoLMStateDictAdapter`` output verbatim):
PanoLMForCausalLM
└── model
└── text PanoLMTextModel
├── tok_embeddings nn.Embedding
├── lower_bounds nn.Parameter (n_layers, hidden_size) — only when use_lower_bound
├── layers.{i} PanoLMDecoderLayer
│ ├── attn_norm RMSNorm
│ ├── attn FLAKimiDeltaAttention w/ optional BitLinear
│ ├── mlp_norm RMSNorm
│ └── mlp PanoLMMLP (BitLinear or nn.Linear)
├── norm RMSNorm
└── output (Fused)BitLinear or nn.Linear # causal LM head
"""
from __future__ import annotations
import torch
import torch.nn as nn
import torch.nn.functional as F
from fla.layers.kda import KimiDeltaAttention as FLAKimiDeltaAttention
from fla.modules import RMSNorm as FLARMSNorm
from fla.modules.fused_bitlinear import BitLinear, FusedBitLinear
from torch.nn import RMSNorm as TorchRMSNorm
from transformers import PreTrainedModel
from transformers.generation import GenerationMixin
from transformers.modeling_outputs import CausalLMOutputWithPast
from .configuration_panolm import PanoLMConfig
def _replace_linears(module: nn.Module, fuse_bitlinear: bool) -> None:
"""Recursively replace ``nn.Linear`` with (Fused)BitLinear in-place."""
for name, child in module.named_children():
if isinstance(child, nn.Linear):
cls = FusedBitLinear if fuse_bitlinear else BitLinear
setattr(
module,
name,
cls(
in_features=child.in_features,
out_features=child.out_features,
bias=child.bias is not None,
),
)
else:
_replace_linears(child, fuse_bitlinear)
class _TorchRMSNormGatedSigmoid(TorchRMSNorm):
"""Sigmoid-gated RMSNorm — matches KDA's o_norm (FusedRMSNormGated activation='sigmoid')."""
def __init__(self, normalized_shape, eps=None, elementwise_affine=True):
super().__init__(
normalized_shape, eps=eps, elementwise_affine=elementwise_affine
)
self.register_buffer("bias", None)
def forward(self, x: torch.Tensor, g: torch.Tensor) -> torch.Tensor:
return super().forward(x) * g.sigmoid()
def _make_norm(hidden_size: int, eps: float, fuse_norm: bool) -> nn.Module:
return (
FLARMSNorm(hidden_size, eps=eps)
if fuse_norm
else TorchRMSNorm(hidden_size, eps=eps)
)
def _linear_cls(use_bitlinear: bool, fuse_bitlinear: bool) -> type[nn.Linear]:
"""Pick the projection layer type used by the MLP and LM head."""
if not use_bitlinear:
return nn.Linear
return FusedBitLinear if fuse_bitlinear else BitLinear
class PanoLMMLP(nn.Module):
"""MLP that fuses gate/up projections when ``fuse_bitlinear=True``."""
def __init__(
self,
hidden_size: int,
hidden_dim: int,
use_bitlinear: bool,
fuse_bitlinear: bool,
):
super().__init__()
cls = _linear_cls(use_bitlinear, fuse_bitlinear)
if fuse_bitlinear:
self.gate_proj = cls(hidden_size, 2 * hidden_dim, bias=False)
self.up_proj = None
else:
self.gate_proj = cls(hidden_size, hidden_dim, bias=False)
self.up_proj = cls(hidden_size, hidden_dim, bias=False)
self.down_proj = cls(hidden_dim, hidden_size, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.up_proj is not None:
gate, y = self.gate_proj(x), self.up_proj(x)
else:
y = self.gate_proj(x)
gate, y = torch.tensor_split(y, 2, -1)
return self.down_proj(F.silu(gate) * y)
def _build_attn(config: PanoLMConfig, layer_idx: int) -> nn.Module:
"""Build the fla KimiDeltaAttention module.
The submodule names produced by fla (``q/k/v_proj``, ``q/k/v_conv1d``,
``f_proj``, ``g_proj``, ``b_proj``, ``o_proj``, ``o_norm``, ``A_log``,
``dt_bias``) match the keys the PanoLM adapter expects under
``model.text.layers.{i}.attn.*``.
"""
attn = FLAKimiDeltaAttention(
hidden_size=config.hidden_size,
expand_v=config.expand_v,
head_dim=config.head_dim,
num_heads=config.num_heads,
num_v_heads=config.num_v_heads,
mode=config.attn_mode,
use_short_conv=config.use_short_conv,
allow_neg_eigval=config.allow_neg_eigval,
safe_gate=config.safe_gate,
lower_bound=config.lower_bound,
conv_size=config.conv_size,
conv_bias=config.conv_bias,
layer_idx=layer_idx,
norm_eps=config.rms_norm_eps,
)
if not config.fuse_norm:
attn.o_norm = _TorchRMSNormGatedSigmoid(
attn.o_norm.hidden_size,
eps=config.rms_norm_eps,
elementwise_affine=attn.o_norm.elementwise_affine,
)
if config.use_bitlinear:
_replace_linears(attn, config.fuse_bitlinear)
return attn
class PanoLMDecoderLayer(nn.Module):
def __init__(self, config: PanoLMConfig, layer_idx: int):
super().__init__()
self.attn_norm = _make_norm(
config.hidden_size, config.rms_norm_eps, config.fuse_norm
)
self.attn = _build_attn(config, layer_idx)
self.mlp_norm = _make_norm(
config.hidden_size, config.rms_norm_eps, config.fuse_norm
)
hidden_dim = config.mlp_hidden_dim
if hidden_dim is None:
raise ValueError(
"PanoLMConfig.mlp_hidden_dim must be set (computed at upload time)."
)
self.mlp = PanoLMMLP(
config.hidden_size,
hidden_dim,
config.use_bitlinear,
config.fuse_bitlinear,
)
def forward(
self,
x: torch.Tensor,
past_key_values=None,
use_cache: bool = False,
attention_mask: torch.Tensor | None = None,
) -> torch.Tensor:
h = self.attn_norm(x)
# KDA does not consume a hierarchical lower-bound; its channel-wise
# vector decay subsumes that role.
h, _new_past, _ = self.attn(
hidden_states=h,
past_key_values=past_key_values,
use_cache=use_cache,
attention_mask=attention_mask,
)
x = x + h
return x + self.mlp(self.mlp_norm(x))
class PanoLMTextModel(nn.Module):
"""The full text stack including the LM head. Matches the ``model.text.*`` HF prefix."""
def __init__(self, config: PanoLMConfig):
super().__init__()
extended_vocab = config.vocab_size + config.num_reserved_token_slots
self.tok_embeddings = nn.Embedding(extended_vocab, config.hidden_size)
if config.use_lower_bound:
# Carried for state-dict round-trip; KDA does not consume it.
self.lower_bounds = nn.Parameter(
torch.zeros(config.num_hidden_layers, config.hidden_size)
)
self.layers = nn.ModuleList(
[PanoLMDecoderLayer(config, i) for i in range(config.num_hidden_layers)]
)
self.norm = _make_norm(
config.hidden_size, config.rms_norm_eps, config.fuse_norm
)
out_cls = _linear_cls(config.use_bitlinear, config.fuse_bitlinear)
self.output = out_cls(config.hidden_size, extended_vocab, bias=False)
self.config = config
def forward(
self,
input_ids: torch.LongTensor | None = None,
inputs_embeds: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
past_key_values=None,
use_cache: bool = False,
) -> torch.Tensor:
if inputs_embeds is None:
inputs_embeds = self.tok_embeddings(input_ids)
h = inputs_embeds
for layer in self.layers:
h = layer(
h,
past_key_values=past_key_values,
use_cache=use_cache,
attention_mask=attention_mask,
)
return self.output(self.norm(h))
class _ModelTextWrapper(nn.Module):
"""Container exposing ``.text`` so state-dict keys carry the ``model.text.*`` prefix."""
def __init__(self, config: PanoLMConfig):
super().__init__()
self.text = PanoLMTextModel(config)
def forward(self, *args, **kwargs):
return self.text(*args, **kwargs)
class PanoLMForCausalLM(PreTrainedModel, GenerationMixin):
"""PanoLM-KDA with a causal LM head, HF-compatible."""
config_class = PanoLMConfig
base_model_prefix = "model"
supports_gradient_checkpointing = False
def __init__(self, config: PanoLMConfig):
super().__init__(config)
self.model = _ModelTextWrapper(config)
self.post_init()
def get_input_embeddings(self) -> nn.Module:
return self.model.text.tok_embeddings
def set_input_embeddings(self, value: nn.Module) -> None:
self.model.text.tok_embeddings = value
def get_output_embeddings(self) -> nn.Module:
return self.model.text.output
def set_output_embeddings(self, new_embeddings: nn.Module) -> None:
self.model.text.output = new_embeddings
def forward(
self,
input_ids: torch.LongTensor | None = None,
attention_mask: torch.Tensor | None = None,
inputs_embeds: torch.Tensor | None = None,
past_key_values=None,
labels: torch.LongTensor | None = None,
use_cache: bool | None = None,
return_dict: bool | None = None,
**kwargs,
) -> CausalLMOutputWithPast:
logits = self.model(
input_ids=input_ids,
inputs_embeds=inputs_embeds,
attention_mask=attention_mask,
past_key_values=past_key_values,
use_cache=bool(use_cache),
)
loss = None
if labels is not None:
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),
)
return CausalLMOutputWithPast(
loss=loss,
logits=logits,
past_key_values=past_key_values,
)
def _init_weights(self, module: nn.Module) -> None:
# Wrapper is loaded from converted weights; init is a no-op beyond
# what each submodule does on construction.
pass