NucEngram / model_modernbert_engram.py
FreakingPotato's picture
Add NucEngram genomic language model (base, S123) + usage
cb634e7 verified
Raw
History Blame Contribute Delete
5.89 kB
"""ModernBERT-based encoder for genomic MLM, augmented with Engram injection.
This wrapper adds Engram (conditional N-gram memory) deltas to specific
ModernBERT encoder layers via forward pre-hooks. It does NOT modify HF's
ModernBERT code, the ``engram`` module, or the ``model_modernbert`` module —
all three are reused unchanged.
Backbone parameter names match ``ModernBertWrapper`` exactly so a checkpoint
trained as ``model_type='modernbert'`` can be warm-started into this class
with ``strict=False``: only ``engram_blocks.*`` keys are missing.
"""
from __future__ import annotations
from dataclasses import replace
from typing import Sequence
import torch
import torch.nn as nn
from .engram import Engram, EngramConfig
from .model_modernbert import (
ModernBertGenomicConfig,
build_modernbert,
)
def _validate_layer_inject_ids(ids: Sequence[int], num_layers: int) -> list[int]:
out: list[int] = []
for idx, i in enumerate(ids):
if isinstance(i, bool) or not isinstance(i, int):
raise ValueError(
f"layer_inject_ids[{idx}] = {i!r} is not an int (bools are rejected too)"
)
if i < 0 or i >= num_layers:
raise ValueError(
f"layer_inject_ids[{idx}] = {i} out of range [0, {num_layers})"
)
out.append(i)
if len(set(out)) != len(out):
raise ValueError(f"layer_inject_ids contains duplicates: {list(ids)}")
return out
def _engram_key(i: int) -> str:
return f"layer_{i:04d}"
class ModernBertEngramWrapper(nn.Module):
"""ModernBERT encoder for MLM with Engram modules injected at chosen layers.
The wrapper:
* Constructs a stock ``ModernBertForMaskedLM`` via :func:`build_modernbert`
and stores it at ``self.model`` (identical attribute layout to
:class:`ModernBertWrapper`, so state_dict keys are exactly the same
for every backbone tensor).
* Adds ``self.engram_blocks: nn.ModuleDict`` keyed by ``layer_{i:04d}``,
one :class:`Engram` per index in ``engram_cfg.layer_inject_ids``.
* Registers a ``forward_pre_hook(with_kwargs=True)`` on each target
encoder layer that adds ``engram_blocks[layer_{i:04d}](hidden, input_ids)``
to the layer's input ``hidden_states``.
``input_ids`` is stashed on ``self._current_input_ids`` at the start of
each ``forward`` and never cleared inside the same call, so layers
recomputed during backward (e.g. under ``torch.utils.checkpoint``) still
see the right ids.
"""
def __init__(self, cfg: ModernBertGenomicConfig, engram_cfg: EngramConfig):
super().__init__()
ids = _validate_layer_inject_ids(engram_cfg.layer_inject_ids, cfg.num_hidden_layers)
engram_cfg = replace(
engram_cfg,
hidden_size=cfg.hidden_size,
vocab_size=cfg.vocab_size,
pad_id=cfg.pad_token_id,
)
self.cfg = cfg
self.engram_cfg = engram_cfg
# IDENTICAL outer attribute layout to ModernBertWrapper:
self.model, self.hf_cfg = build_modernbert(cfg)
# Sibling engram modules — NOT inside self.model, so backbone keys are unchanged.
self.engram_blocks = nn.ModuleDict(
{_engram_key(i): Engram(engram_cfg, layer_id=i) for i in ids}
)
for i in ids:
self.model.model.layers[i].register_forward_pre_hook(
self._make_hook(i), with_kwargs=True
)
self._current_input_ids: torch.Tensor | None = None
@property
def attn_implementation(self) -> str:
impl = getattr(self.model.config, "_attn_implementation", None)
if impl:
return impl
return getattr(self.model.config, "attn_implementation", "unknown")
def num_params(self, only_trainable: bool = True) -> dict:
backbone = 0
memory = 0
for name, p in self.named_parameters():
if only_trainable and not p.requires_grad:
continue
n = p.numel()
if "engram" in name and "embedding.emb" in name:
memory += n
else:
backbone += n
return {"backbone": backbone, "memory": memory, "total": backbone + memory}
def _make_hook(self, layer_id: int):
key = _engram_key(layer_id)
# Cache to avoid one dict lookup per call.
block_ref = self.engram_blocks[key]
wrapper_self = self
def hook(module, args, kwargs):
ids = wrapper_self._current_input_ids
if ids is None:
return None # cold-call safety (e.g. tracing); do nothing
if args:
hidden = args[0]
delta = block_ref(hidden, input_ids=ids)
new_args = (hidden + delta, *args[1:])
return new_args, kwargs
hidden = kwargs["hidden_states"]
delta = block_ref(hidden, input_ids=ids)
kwargs = {**kwargs, "hidden_states": hidden + delta}
return args, kwargs
return hook
def forward(
self,
input_ids: torch.Tensor,
labels: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
) -> dict:
# Overwrite-only on every forward; do NOT clear before/after model() so
# that any backward-time recomputed forward (e.g. gradient checkpointing)
# still reads the same ids that the original forward used.
self._current_input_ids = input_ids
if attention_mask is not None:
attention_mask = attention_mask.to(torch.long)
out = self.model(
input_ids=input_ids,
attention_mask=attention_mask,
labels=labels,
output_hidden_states=False,
)
return {"loss": out.loss, "logits": out.logits, "hidden": None}