File size: 11,816 Bytes
947f8cf 947ff9b 947f8cf 1a5e7a2 c4d1e75 1a5e7a2 c131cfd 947f8cf 12b8798 947f8cf 1a5e7a2 947f8cf c131cfd 1a5e7a2 6fb2512 c131cfd c4d1e75 6fb2512 c131cfd c4d1e75 c131cfd c4d1e75 c131cfd 1a5e7a2 947f8cf 1a5e7a2 c131cfd 947f8cf c131cfd 947f8cf 947ff9b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | """
daisychain.py -- self-contained inference for the DaisyChain genomic modular mind.
4 dense ~74M DNA/RNA specialists (eukaryote, prokaryote, mrna, mrna_splice), each
per-domain-distilled from Carbon-500M, behind a learned router (MLP on PCA(hidden)
+ per-specialist surprise). route() picks the home specialist; generate() / surprise()
expose the rest. No training/datasets dependency -- only model.py, specialist_presets.py,
spike_tokenizer.py, registry.py + the bundled tokenizer.json / *.safetensors / router2.pt.
"""
from __future__ import annotations
import os, math
import torch
import torch.nn.functional as F
HERE = os.path.dirname(os.path.abspath(__file__))
from model import SpikeWhaleLM
from specialist_presets import generic_specialist_config
from spike_tokenizer import SpikeTokenizer
import registry
TOK_JSON = os.path.join(HERE, "tokenizer.json")
_TRANS = str.maketrans({"U": "T", "u": "T", "a": "A", "c": "C", "g": "G", "t": "T"})
LN2 = math.log(2)
def clean(seq: str) -> str:
seq = seq.translate(_TRANS).upper()
return "".join(c if c in "ACGT" else "N" for c in seq)
class _RouterMLP(torch.nn.Module):
def __init__(self, dim, h=64):
super().__init__()
self.net = torch.nn.Sequential(torch.nn.Linear(dim, h), torch.nn.ReLU(),
torch.nn.Dropout(0.0), torch.nn.Linear(h, 4))
def forward(self, x): return self.net(x)
class DaisyChain:
DESCRIPTIONS = {
"eukaryote": "Eukaryotic genomic DNA",
"prokaryote": "Bacterial / prokaryotic DNA",
"mrna": "Mature mRNA (coding transcript)",
"mrna_splice": "Pre-mRNA / splice-site regions",
}
def __init__(self, root=HERE, device="cpu"):
self.dev = device
self.tok = SpikeTokenizer(vocab_file=os.path.join(root, "tokenizer.json"))
self.bos, self.eos = self.tok._vocab["<bos>"], self.tok._vocab["<eos>"]
self.models = {}
from safetensors.torch import load_file
for d in registry.ACTIVE:
ckpt = os.path.join(root, d, "model.safetensors")
if not os.path.exists(ckpt):
continue
cfg = generic_specialist_config(self.tok.vocab_size, position=registry.spec(d)["position"])
m = SpikeWhaleLM(cfg).to(device).eval()
sd = load_file(ckpt, device=device)
m.load_state_dict({k: (v.float() if v.is_floating_point() else v) for k, v in sd.items()})
for p in m.parameters():
p.requires_grad_(False)
self.models[d] = m
self.domains = list(self.models)
# FNS base-pair tables: map our 4096 6-mer tokens to their six per-position bases, so we
# can marginalize the 6-mer softmax into six 4-way nucleotide distributions and decode /
# score at the BASE level — the same factorization Carbon's FNS branch uses.
import itertools
_b2i = {"A": 0, "T": 1, "C": 2, "G": 3}
self._idx2base = "ATCG"
kmers = ["".join(t) for t in itertools.product("ACGT", repeat=6)]
self._kmer_ids = torch.tensor([self.tok._vocab[k] for k in kmers], device=device) # [4096]
self._base_at = torch.zeros(6, 4096, dtype=torch.long, device=device)
for i, k in enumerate(kmers):
for pos in range(6):
self._base_at[pos, i] = _b2i[k[pos]]
self._kmer_id_of = {k: self.tok._vocab[k] for k in kmers}
self.router2 = None
r2 = os.path.join(root, "router2.pt")
if os.path.exists(r2):
d = torch.load(r2, map_location="cpu")
if all(x in self.models for x in d["domains"]):
mlp = _RouterMLP(d["k"] + 4, d["h"]); mlp.load_state_dict(d["mlp"]); mlp.eval()
d["net"] = mlp; self.router2 = d
@torch.no_grad()
def _scores_hidden(self, seq):
ids = [self.bos] + self.tok.encode(clean(seq), add_special_tokens=False) + [self.eos]
t = torch.tensor([ids], device=self.dev)
scores, hids = {}, {}
for d, m in self.models.items():
hids[d] = m.model(input_ids=t)[0][0].mean(0)
scores[d] = float(m(input_ids=t, labels=t).loss)
return scores, hids
def surprise(self, seq):
"""Per-specialist bits/base (lower = more 'at home')."""
s, _ = self._scores_hidden(seq)
return {d: s[d] / 6 / LN2 for d in self.domains}
@torch.no_grad()
def route(self, seq):
"""Return (home_domain, bits_per_base_dict). Uses the learned MLP router."""
scores, hids = self._scores_hidden(seq)
bpb = {d: scores[d] / 6 / LN2 for d in self.domains}
if self.router2 is not None:
r = self.router2
hidden = torch.cat([hids[d] for d in r["domains"]])
bits = torch.tensor([scores[d] for d in r["domains"]])
z = (hidden - r["pca_mu"]) @ r["P"]
feat = ((torch.cat([z, bits]) - r["mu"]) / r["sd"])
best = r["domains"][int(r["net"](feat.unsqueeze(0)).argmax(1))]
else:
best = min(scores, key=scores.get)
return best, bpb
@torch.no_grad()
def generate_stream(self, domain, length=180, temperature=1.0, top_k=40,
repetition_penalty=1.3, prompt="", greedy=False, top_p=0.9):
"""Yield the growing continuation base-by-base (for live streaming UIs).
greedy=True takes the argmax 6-mer each step (deterministic — the model's single
best guess, same decoding as the sequence-recovery metric; the coding domains
produce in-frame ATG-start sequences, the low-complexity domains collapse to
homopolymers). Sampling (greedy=False) trades that for variety; repetition_penalty
then discourages the repeat loops these small specialists fall into."""
m = self.models[domain]
# frame-align + cap: our 6-mer tokens tile cleanly only when the context length is a
# multiple of 6 (else the model generates out of phase). Trim the leading remainder and
# cap to the context window — this is what makes generation match the offline tests.
p = clean(prompt) if prompt else ""
p = p[-1020:] # 1020 = 170*6, within the 1024-token window
p = p[len(p) % 6:] # drop leading remainder so the 6-mers align to the end
ids = [self.bos] + (self.tok.encode(p, add_special_tokens=False) if p else [])
t = torch.tensor([ids], device=self.dev)
bases, emitted = [], []
while sum(len(b) for b in bases) < length:
logits = m(input_ids=t).logits[:, -1, :].float()
logits[:, :4] = -1e9 # never emit specials
# Repetition control applies to BOTH decoders. Greedy without this falls into a
# self-reinforcing loop (argmax keeps re-picking the same 6-mer); the penalty plus
# a hard block on the last few emitted tokens forces it onward to its next-best,
# non-repeating guess — which is what actually de-degenerates the output.
if greedy:
# argmax with repeat penalty + a hard block on recent tokens (greedy alone loops)
if repetition_penalty and repetition_penalty != 1.0:
for tid in set(emitted[-12:]):
v = logits[0, tid]
logits[0, tid] = v / repetition_penalty if v > 0 else v * repetition_penalty
for tid in set(emitted[-8:]):
logits[0, tid] = -1e9
ti = int(logits.argmax())
else:
# nucleus (top-p) sampling — the same decoder Carbon uses. Adapts the candidate
# set to the distribution (keeps only tokens carrying mass), which escapes the
# low-complexity GC/AT loops that a fixed top-k falls into. No repetition penalty.
logits = logits / max(temperature, 1e-6)
sl, si = torch.sort(logits[0], descending=True)
cum = torch.cumsum(F.softmax(sl, dim=-1), dim=-1)
rm = cum > top_p
rm[1:] = rm[:-1].clone(); rm[0] = False
logits[0, si[rm]] = -1e9
ti = int(torch.multinomial(F.softmax(logits[0], dim=-1), 1))
emitted.append(ti)
t = torch.cat([t, torch.tensor([[ti]], device=self.dev)], dim=1)
bases.append(self.tok._ids_to_tokens[ti])
yield "".join(bases)[:length]
def generate(self, domain, length=180, temperature=1.0, top_k=40,
repetition_penalty=1.3, prompt="", greedy=False):
out = ""
for out in self.generate_stream(domain, length, temperature, top_k, repetition_penalty, prompt, greedy):
pass
return out
# ---- FNS base-pair-level decode + score (Carbon's factorized-nucleotide approach) ----
def _bp_marginals(self, logits):
"""Marginalize a 6-mer logit vector into six 4-way per-position base distributions [6,4]."""
p = F.softmax(logits[self._kmer_ids], dim=-1) # [4096] over the full 6-mers only
bp = torch.zeros(6, 4, device=logits.device)
for pos in range(6):
bp[pos].scatter_add_(0, self._base_at[pos], p)
return bp
@torch.no_grad()
def generate_baselevel_stream(self, domain, length=180, temperature=1.0, top_p=0.9, prompt=""):
"""Base-pair-level generation: at each step marginalize the 6-mer softmax to six 4-way
nucleotide distributions and sample each base with temperature/top-p (Carbon FNS-style).
Deciding each base over 4 options — not 6 bases at once over 4096 — avoids whole-6-mer loops."""
m = self.models[domain]
p = clean(prompt) if prompt else ""
p = p[-1020:]; p = p[len(p) % 6:]
ids = [self.bos] + (self.tok.encode(p, add_special_tokens=False) if p else [])
t = torch.tensor([ids], device=self.dev)
out = ""
while len(out) < length:
logits = m(input_ids=t).logits[0, -1].float() / max(temperature, 1e-6)
bp = self._bp_marginals(logits) # [6,4]
six = []
for pos in range(6):
probs = bp[pos]
s, si = torch.sort(probs, descending=True)
rm = torch.cumsum(s, -1) > top_p
rm[1:] = rm[:-1].clone(); rm[0] = False
q = probs.clone(); q[si[rm]] = 0; q = q / q.sum()
six.append(self._idx2base[int(torch.multinomial(q, 1))])
six = "".join(six)
t = torch.cat([t, torch.tensor([[self._kmer_id_of[six]]], device=self.dev)], dim=1)
out += six
yield out[:length]
@torch.no_grad()
def score(self, domain, seq):
"""Carbon `score_sequence` equivalent: mean per-base log-prob of the observed sequence under
the base-level (marginalized) distribution. Higher = more likely. bits/base = -score/ln2."""
m = self.models[domain]
s = clean(seq); s = s[len(s) % 6:]
if len(s) < 12:
return float("nan")
ids = [self.bos] + self.tok.encode(s, add_special_tokens=False)
t = torch.tensor([ids], device=self.dev)
logits = m(input_ids=t).logits[0] # [L,V]; position i predicts the next 6-mer
b2i = {"A": 0, "T": 1, "C": 2, "G": 3}
tot, n = 0.0, 0
for i in range(len(ids) - 1):
bp = self._bp_marginals(logits[i].float()) # [6,4] predicted bases of next 6-mer
nxt = s[i * 6:(i + 1) * 6]
for pos, ch in enumerate(nxt):
tot += math.log(max(float(bp[pos, b2i[ch]]), 1e-12)); n += 1
return tot / n # mean per-base logp
|