Metis-9B / metis_local_memory.py
astarkkk's picture
Add files using upload-large-folder tool
e903a9a verified
Raw
History Blame Contribute Delete
10.3 kB
import torch
import torch.nn as nn
import torch.nn.functional as F
from abc import ABC
def _qk_kernel(x: torch.Tensor, kernel_type: str = "elu_plus_one") -> torch.Tensor:
if kernel_type == "elu_plus_one":
return F.elu(x) + 1.0
if kernel_type == "relu_square":
return F.relu(x).square()
if kernel_type == "softplus":
return F.softplus(x)
raise ValueError(f"Unsupported qk kernel type: {kernel_type}")
def create_metis_local_memory(config):
return eval(config.memory_configs['metis_local_memory_type'])(config)
class MetisLocalMemoryBase(nn.Module, ABC):
def __init__(self, config) -> None:
super().__init__()
self.config = config
# Qwen 3.5 has text config, but Qwen 3 does not.
self.text_cfg = getattr(config.backbone_configs, 'text_config', config.backbone_configs)
def initialize(self) -> None:
raise NotImplementedError
def reset(self) -> None:
raise NotImplementedError
def read(self, query_for_memory):
raise NotImplementedError
def write(self, new_info) -> None:
raise NotImplementedError
@property
def state(self):
raise NotImplementedError
class DeltaNetMetisLocalMemory(MetisLocalMemoryBase):
"""Linear (DeltaNet-style) memory matrix of shape (b, D, D).
Read: output = Q_flat @ M, where Q_flat = (b, s, D)
Write: M = new_state (forget + additive update computed by HyperMemory)
"""
def __init__(self, config) -> None:
super().__init__(config)
num_q_heads = self.text_cfg.num_attention_heads
# If num_key_value_heads is not set, use num_attention_heads (MHA).
num_kv_heads = getattr(self.text_cfg, "num_key_value_heads", num_q_heads)
head_dim = getattr(self.text_cfg, "head_dim", self.text_cfg.hidden_size // num_q_heads)
self.q_dim = num_q_heads * head_dim
self.kv_dim = self._compute_kv_dim(num_q_heads, num_kv_heads, head_dim)
self.num_kv_groups = self.q_dim // self.kv_dim
self._state: torch.Tensor | None = None
@staticmethod
def _compute_kv_dim(num_q_heads: int, num_kv_heads: int, head_dim: int) -> int:
"""GQA layout: kv_dim = num_kv_heads * head_dim."""
return num_kv_heads * head_dim
def initialize(self) -> None:
self._state = None
def reset(self) -> None:
self._state = None
def _ensure_ready(
self,
batch_size: int,
device: torch.device,
dtype: torch.dtype,
) -> None:
if self._state is None or self._state.shape[0] != batch_size:
self._state = torch.zeros(
batch_size, self.kv_dim, self.kv_dim, device=device, dtype=dtype,
)
def read(self, query_for_memory: torch.Tensor) -> torch.Tensor:
"""Linear memory read: output = Q_flat @ M.
Args:
query_for_memory: (b, h, s, d)
Returns:
(b, s, D) — memory readout, ready to be fused with attention output.
"""
bsz, _h, seq_len, _d = query_for_memory.shape
self._ensure_ready(bsz, query_for_memory.device, query_for_memory.dtype)
# (b, h, s, d) → (b, s, q_dim)
q_flat = query_for_memory.transpose(1, 2).reshape(bsz, seq_len, -1)
if self.num_kv_groups > 1:
# GQA mode.
q_2d = q_flat.view(bsz, seq_len * self.num_kv_groups, self.kv_dim)
out_2d = torch.matmul(q_2d, self._state)
return out_2d.view(bsz, seq_len, self.q_dim).contiguous()
else:
# For MHA mode
# (b, s, q_dim) @ (b, q_dim, q_dim) → (b, s, q_dim)
return torch.matmul(q_flat, self._state).contiguous()
def write(self, new_state: torch.Tensor) -> None:
self._state = new_state # no detach here, so gradients flow through W_k / W_v
@property
def state(self) -> torch.Tensor | None:
return self._state
@property
def is_initialized(self) -> bool:
return self._state is not None
def norm(self) -> float:
return self._state.norm().item() if self._state is not None else 0.0
class MHADeltaNetMetisLocalMemory(DeltaNetMetisLocalMemory):
"""Legacy MHA-style memory: kv_dim = num_q_heads * head_dim (no GQA grouping).
Memory matrix is (b, q_dim, q_dim) — for Qwen3.5-4B that's 4096×4096.
Read collapses to a single MHA matmul: (b, s, q_dim) @ (b, q_dim, q_dim).
Use this for loading checkpoints trained before the GQA refactor
(e.g. experiments/4.17-* and 4.18-*).
"""
@staticmethod
def _compute_kv_dim(num_q_heads: int, num_kv_heads: int, head_dim: int) -> int:
return num_q_heads * head_dim
class NormalizedDeltaNetMetisLocalMemory(DeltaNetMetisLocalMemory):
"""DeltaNet memory with metis_modular-style key normalization.
Read path:
q = normalize(q)
y = q @ state
y = y / (q @ key_state + 1)
The paired hyper-memory class ``FullTokensKeyNormMetisHyperMemory`` writes
both ``state`` and ``key_state``.
"""
def __init__(self, config) -> None:
super().__init__(config)
self._key_state: torch.Tensor | None = None
def initialize(self) -> None:
self._state = None
self._key_state = None
def reset(self) -> None:
self._state = None
self._key_state = None
def _ensure_ready(
self,
batch_size: int,
device: torch.device,
dtype: torch.dtype,
) -> None:
if self._state is None or self._state.shape[0] != batch_size:
self._state = torch.zeros(
batch_size, self.kv_dim, self.kv_dim, device=device, dtype=dtype,
)
self._key_state = torch.zeros(
batch_size, self.kv_dim, 1, device=device, dtype=dtype,
)
def read(self, query_for_memory: torch.Tensor) -> torch.Tensor:
bsz, _h, seq_len, _d = query_for_memory.shape
self._ensure_ready(bsz, query_for_memory.device, query_for_memory.dtype)
query_for_memory = F.normalize(query_for_memory, dim=-1)
q_flat = query_for_memory.transpose(1, 2).reshape(bsz, seq_len, -1)
if self.num_kv_groups > 1:
q_2d = q_flat.view(bsz, seq_len * self.num_kv_groups, self.kv_dim)
out_2d = torch.matmul(q_2d, self._state)
# if self._key_state is not None:
norm_factor = torch.matmul(q_2d, self._key_state)
# print(norm_factor[0])
out_2d = out_2d / (norm_factor + 1.0)
return out_2d.view(bsz, seq_len, self.q_dim).contiguous()
out = torch.matmul(q_flat, self._state)
# if self._key_state is not None:
norm_factor = torch.matmul(q_flat, self._key_state)
# print(norm_factor.shape)
out = out / (norm_factor + 1.0)
return out.contiguous()
def write(self, new_state: torch.Tensor, key_state: torch.Tensor) -> None:
self._state = new_state
self._key_state = key_state
@property
def key_state(self) -> torch.Tensor | None:
return self._key_state
@property
def is_initialized(self) -> bool:
return self._state is not None and self._key_state is not None
class KernelizedDeltaNetMetisLocalMemory(NormalizedDeltaNetMetisLocalMemory):
"""DeltaNet memory read path with a kernel feature map on queries.
Pair this with ``StrideKernelKeyNormMetisHyperMemory`` so the same feature
map is applied to q and k before the key-state normalization:
phi(q) = kernel(q)
y = phi(q) @ state
y = y / (phi(q) @ key_state + 1)
"""
def __init__(self, config) -> None:
super().__init__(config)
self.qk_kernel_type = config.memory_configs.get("qk_kernel_type", "elu_plus_one")
def read(self, query_for_memory: torch.Tensor) -> torch.Tensor:
bsz, _h, seq_len, _d = query_for_memory.shape
self._ensure_ready(bsz, query_for_memory.device, query_for_memory.dtype)
q_flat = query_for_memory.transpose(1, 2).reshape(bsz, seq_len, -1)
if self.num_kv_groups > 1:
q_2d = q_flat.view(bsz, seq_len * self.num_kv_groups, self.kv_dim)
q_2d = _qk_kernel(q_2d, self.qk_kernel_type)
out_2d = torch.matmul(q_2d, self._state)
if self._key_state is not None:
norm_factor = torch.matmul(q_2d, self._key_state)
out_2d = out_2d / norm_factor
return out_2d.view(bsz, seq_len, self.q_dim).contiguous()
q_flat = _qk_kernel(q_flat, self.qk_kernel_type)
out = torch.matmul(q_flat, self._state)
if self._key_state is not None:
norm_factor = torch.matmul(q_flat, self._key_state)
out = out / norm_factor
return out.contiguous()
class L2NormalizedDeltaNetMetisLocalMemory(DeltaNetMetisLocalMemory):
"""DeltaNet memory with L2-normalized queries and no key-state denominator.
Pair this with ``StrideL2NormMetisHyperMemory``:
q = normalize(q)
y = q @ state
This keeps the read-side q scale controlled by L2 normalization while the
write-side hyper memory normalizes k.
"""
def read(self, query_for_memory: torch.Tensor) -> torch.Tensor:
bsz, _h, seq_len, _d = query_for_memory.shape
self._ensure_ready(bsz, query_for_memory.device, query_for_memory.dtype)
query_for_memory = F.normalize(query_for_memory, dim=-1)
q_flat = query_for_memory.transpose(1, 2).reshape(bsz, seq_len, -1)
if self.num_kv_groups > 1:
q_2d = q_flat.view(bsz, seq_len * self.num_kv_groups, self.kv_dim)
out_2d = torch.matmul(q_2d, self._state)
return out_2d.view(bsz, seq_len, self.q_dim).contiguous()
return torch.matmul(q_flat, self._state).contiguous()
class OneStepAblationMetisLocalMemory(MetisLocalMemoryBase):
def __init__(self, config) -> None:
super().__init__(config)
def initialize(self) -> None:
self.memory_state = None
def reset(self) -> None:
self.initialize()
def read(self, query_for_memory):
return self.memory_state
def write(self, new_info) -> None:
self.memory_state = new_info