kenga-embed-prophet-instruct / modeling_kenga_embed.py
GermannM's picture
42M kenga instruct/byte encoder for Giga-team eval
73f8503 verified
Raw History Blame Contribute Delete
6.48 kB
# -*- coding: utf-8 -*-
"""Self-contained Kenga instruct encoder. Byte UTF-8, bidirectional.
Usage (retrieval, Giga-style prefix on the query only):
from modeling_kenga_embed import KengaEmbed
m = KengaEmbed.from_pretrained("GermannM/kenga-embed-prophet-instruct")
q = m.encode_queries(["Where is the capital of Russia?"])
p = m.encode_documents(["Moscow is the capital of Russia."])
STS (prefix on both sentences):
a = m.encode_sts(["The cat sits on the mat."])
b = m.encode_sts(["A cat is sitting on a mat."])
"""
import json
import os
import torch
import torch.nn as nn
import torch.nn.functional as F
INSTR_RETR = "Instruct: Given a query, retrieve relevant passages\nQuery: "
INSTR_STS = "Instruct: Retrieve semantically similar text.\nQuery: "
class BL(nn.Module):
def __init__(self, in_f, out_f, k):
super().__init__()
kk = min(k, in_f, out_f)
self.U = nn.Parameter(torch.zeros(out_f, kk))
self.V = nn.Parameter(torch.zeros(in_f, kk))
self.S = nn.Parameter(torch.ones(kk))
self.b = nn.Parameter(torch.zeros(out_f))
self.register_buffer("mask", torch.ones(kk))
def set_active(self, k_t):
m = torch.zeros_like(self.mask)
m[: min(int(k_t), self.mask.numel())] = 1
self.mask.copy_(m)
def forward(self, x):
return ((x @ self.V) * (self.S * self.mask)) @ self.U.T + self.b
class BAttn(nn.Module):
def __init__(self, d, heads, k_attn):
super().__init__()
self.h = heads
self.dh = d // heads
self.scale = self.dh ** -0.5
self.q = BL(d, d, k_attn)
self.k_ = BL(d, d, k_attn)
self.v = BL(d, d, k_attn)
self.o = BL(d, d, k_attn)
def set_rank(self, ka):
for bl in (self.q, self.k_, self.v, self.o):
bl.set_active(ka)
def forward(self, x, pad):
B, T, _ = x.shape
Q = self.q(x).view(B, T, self.h, self.dh).transpose(1, 2)
K = self.k_(x).view(B, T, self.h, self.dh).transpose(1, 2)
V = self.v(x).view(B, T, self.h, self.dh).transpose(1, 2)
att = (Q.float() @ K.float().transpose(-2, -1)) * self.scale
att = att.masked_fill(pad, float("-inf")).softmax(-1)
return self.o((att @ V).transpose(1, 2).reshape(B, T, x.size(-1)))
class BBlock(nn.Module):
def __init__(self, d, dff, heads, k_attn, k_ff):
super().__init__()
self.ln1 = nn.LayerNorm(d)
self.ln2 = nn.LayerNorm(d)
self.attn = BAttn(d, heads, k_attn)
self.fc1 = BL(d, dff, k_ff)
self.fc2 = BL(dff, d, k_ff)
def set_rank(self, ka, kf):
self.attn.set_rank(ka)
self.fc1.set_active(kf)
self.fc2.set_active(kf)
def forward(self, x, pad):
x = x + self.attn(self.ln1(x), pad)
return x + self.fc2(F.gelu(self.fc1(self.ln2(x))))
class KengaEmbedEnc(nn.Module):
def __init__(self, d=768, layers=8, heads=12, dff=3072, seq=160,
v=256, k_attn=192, k_ff=512):
super().__init__()
self.d = d
self.seq = seq
self.tok = nn.Embedding(v, d)
self.pos = nn.Parameter(torch.zeros(1, seq, d))
self.blocks = nn.ModuleList(
[BBlock(d, dff, heads, k_attn, k_ff) for _ in range(layers)]
)
self.ln = nn.LayerNorm(d)
self.proj = nn.Linear(d, d)
def set_rank(self, ka, kf):
for b in self.blocks:
b.set_rank(ka, kf)
def forward(self, idx, lengths=None):
B, T = idx.shape
x = self.tok(idx) + self.pos[:, :T]
pad = idx.eq(0)
mask = pad[:, None, None, :].expand(B, 1, T, T)
for blk in self.blocks:
x = blk(x, mask)
x = self.ln(x)
m = (~pad).unsqueeze(-1).float()
pooled = (x * m).sum(1) / m.sum(1).clamp(min=1)
return F.normalize(self.proj(pooled), dim=-1)
class KengaEmbed:
def __init__(self, model, seq, instruct=True, device="cpu"):
self.model = model.to(device).eval()
self.seq = seq
self.instruct = instruct
self.device = device
@classmethod
def from_pretrained(cls, path, device="cpu"):
cfg = json.load(open(os.path.join(path, "config.json"), encoding="utf-8"))
m = KengaEmbedEnc(
d=cfg.get("d", 768),
layers=cfg.get("layers", 8),
heads=cfg.get("heads", 12),
dff=cfg.get("dff", 3072),
seq=cfg.get("seq", 160),
v=cfg.get("v", 256),
k_attn=cfg.get("k_attn", 192),
k_ff=cfg.get("k_ff", 512),
)
blob = torch.load(os.path.join(path, "pytorch_model.bin"),
map_location="cpu", weights_only=False)
sd = blob["model"] if isinstance(blob, dict) and "model" in blob else blob
m.load_state_dict(sd, strict=True)
rank = cfg.get("rank") or blob.get("rank")
if rank:
m.set_rank(*list(rank))
return cls(m, seq=cfg.get("seq", 160),
instruct=bool(cfg.get("instruct", True)), device=device)
def _ids(self, text):
raw = (text or "").encode("utf-8", errors="ignore")[: self.seq]
ids = [b if b else 1 for b in raw][: self.seq]
n = len(ids)
ids = ids + [0] * (self.seq - n)
return ids, n
def _prefix(self, text, kind):
if not self.instruct or not text:
return text
if text.startswith("Instruct:"):
return text
p = INSTR_STS if kind == "sts" else INSTR_RETR
return p + text
def encode(self, texts, kind="doc", bs=32):
vecs = []
with torch.no_grad():
for i in range(0, len(texts), bs):
chunk = texts[i : i + bs]
ids, ln = [], []
for t in chunk:
s = t if kind == "doc" else self._prefix(t, kind)
ii, n = self._ids(s)
ids.append(ii)
ln.append(n)
x = torch.tensor(ids, device=self.device)
l = torch.tensor(ln, device=self.device)
vecs.append(self.model(x, l).cpu())
return torch.cat(vecs, 0)
def encode_queries(self, texts, **kw):
return self.encode(texts, kind="retr", **kw)
def encode_documents(self, texts, **kw):
return self.encode(texts, kind="doc", **kw)
def encode_sts(self, texts, **kw):
return self.encode(texts, kind="sts", **kw)