fourmodal-pathology-bind / 43_rnafm_se600m.py
Carpediem123's picture
Migrate release files 1-30
c2ede1f verified
Raw History Blame Contribute Delete
14.8 kB
"""Frozen Arc Institute STATE embedding (SE-600M) spot embeddings of the 5 RNA sets (each spot = one pseudo-cell).
python 43_rnafm_se600m.py [set ...] (default: all of rna_sets.SETS)
Model : arcinstitute/SE-600M (HF), revision 5a9a80f44f7ce32ce57059933ef0d735d7c10ce5, files model.safetensors
(metadata: converted from se600m_epoch16.ckpt, i.e. the final 16-epoch checkpoint), config.yaml and
protein_embeddings.pt (19,790 gene symbols -> ESM2-15B mean protein embedding, 5120-d; identical, in
the same order, to the checkpoint's pe_embedding.weight -- asserted). Licence: Arc Research Institute
State Model Non-Commercial License + Acceptable Use Policy (weights; non-commercial research use only),
code CC BY-NC-SA 4.0. Official code: github.com/ArcInstitute/state (commit 9bbfe78a, cloned to
ext/state/state, read but NOT imported / installed): the forward pass of src/state/emb/nn/model.py
(StateEmbeddingModel) + nn/flash_transformer.py is re-implemented below and loaded with a strict
state-dict load of all 238 tensors (the encoder / gene_embedding_layer tie is asserted); the unused
heads (binary_decoder, dataset_encoder, gene_embedding_layer) are dropped after loading.
16-layer post-LN transformer, d=2048, 16 heads, FFN 2048, GELU, no positional encoding, no attention mask.
Inputs : rna_sets.load(set) raw(-like) counts + HGNC symbols. Genes = the set's genes whose symbol is a key of
protein_embeddings.pt (official FilteredGenesCounts valid mask, set gene order kept); other genes dropped.
Preprocess: official `state emb transform` path = Inference.encode_adata -> create_dataloader ->
VCIDatasetSentenceCollator.sample_cell_sentences (is_train=False):
* counts are raw -> log1p (the official per-cell raw-vs-log1p heuristic, max>35 or sum(expm1)>5e6, is
bypassed because all inputs are known raw counts; how many spots it would misclassify is logged);
* expression weight w = log1p / sum(log1p) over the valid genes (uniform if the spot is all-zero);
* cell sentence (pad_length 2048) = [CLS] + 2047 genes ranked by descending expression with random
tie-breaking (every set has > 2047 valid genes, so: all detected genes in rank order, top 2047 if more,
then randomly drawn undetected genes without replacement); seeded numpy RNG (seed 0) instead of
torch.randperm;
* token count value = 100 * w[gene]; position 0 carries 100 * w[local gene index 3] exactly as the
official collator (cls_token_idx = 3 is written as a local gene index before the count lookup);
* token embedding = L2-normalised ESM2 embedding, position 0 replaced by the learned cls_token, the
learned dataset_token appended (count embedding 0) -> 2049 tokens.
Forward : encoder(Linear 5120->2048, LayerNorm, SiLU) * sqrt(2048) + soft-binned count embedding
(softmax(count_encoder(c)) @ bin_encoder) -> 16 layers -> decoder (SkipBlock + Linear).
Whole model in bf16 (official get_precision_config('cuda') casts the model to bf16 + bf16 autocast).
Readout : official embedding = L2-normalised decoder output at the CLS position (StateEmbeddingModel.forward
`embedding`), D = 2048. `state emb transform` additionally concatenates a 10-d dataset-token vector
(dataset_embedder(decoder output at the dataset token)) -> 2058-d X_state; that batch-identity part is NOT
saved here (only the 2048-d cell CLS embedding).
Output : rna_sets.save("se600m", set, E) -> cache/rnafm/se600m_<set>.npy (n_spot, 2048) float16.
Sanity : finite, mean pairwise cosine of 200 random spots, (hest) organ agreement of cosine 10-NN vs base rate,
and (hest, 300 spots) cosine between two runs with different tie-breaking seeds (stochasticity of the
undetected-gene filler tokens).
"""
import math
import os
import sys
import time
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.nn.functional as F
import yaml
from huggingface_hub import hf_hub_download
from safetensors.torch import load_file
from common import CACHE
from rna_sets import SETS, load, save
MODEL = "se600m"
REPO, REV = "arcinstitute/SE-600M", "5a9a80f44f7ce32ce57059933ef0d735d7c10ce5"
dev = "cuda"
DT = torch.bfloat16
# ----------------------------------------------------------------------------- model (re-implemented from ext/state)
class SkipBlock(nn.Module):
def __init__(self, d):
super().__init__()
self.intermediate_dense = nn.Linear(d, 2 * d)
self.dense = nn.Linear(2 * d, d)
self.layer_norm = nn.LayerNorm(d)
def forward(self, x):
return self.layer_norm(x + self.dense(F.relu(self.intermediate_dense(x))))
class Layer(nn.Module): # FlashTransformerEncoderLayer (post-LN, dropout 0 at inference)
def __init__(self, d, nhead, d_hid):
super().__init__()
self.nhead = nhead
self.qkv_proj = nn.Linear(d, 3 * d)
self.out_proj = nn.Linear(d, d)
self.norm1, self.norm2 = nn.LayerNorm(d), nn.LayerNorm(d)
self.linear1, self.linear2 = nn.Linear(d, d_hid), nn.Linear(d_hid, d)
def forward(self, x):
B, T, D = x.shape
q, k, v = self.qkv_proj(x).chunk(3, dim=-1)
q, k, v = (t.view(B, T, self.nhead, D // self.nhead).transpose(1, 2) for t in (q, k, v))
a = F.scaled_dot_product_attention(q, k, v, is_causal=False).transpose(1, 2).reshape(B, T, D)
x = self.norm1(x + self.out_proj(a))
return self.norm2(x + self.linear2(F.gelu(self.linear1(x))))
class TransformerEncoder(nn.Module):
def __init__(self, layers):
super().__init__()
self.layers = nn.ModuleList(layers)
class SE(nn.Module):
def __init__(self, token_dim, d, nhead, d_hid, nlayers, out_dim, n_pe, z_dim, n_ds):
super().__init__()
self.d = d
self.cls_token = nn.Parameter(torch.zeros(1, token_dim))
self.dataset_token = nn.Parameter(torch.zeros(1, token_dim))
self.encoder = nn.Sequential(nn.Linear(token_dim, d), nn.LayerNorm(d), nn.SiLU())
self.gene_embedding_layer = self.encoder # tied, as in the official model
self.transformer_encoder = TransformerEncoder([Layer(d, nhead, d_hid) for _ in range(nlayers)])
self.decoder = nn.Sequential(SkipBlock(d), nn.Linear(d, out_dim))
self.binary_decoder = nn.Sequential(SkipBlock(out_dim + d + z_dim), SkipBlock(out_dim + d + z_dim),
nn.Linear(out_dim + d + z_dim, 1))
self.bin_encoder = nn.Embedding(10, d)
self.count_encoder = nn.Sequential(nn.Linear(1, 512), nn.LeakyReLU(), nn.Linear(512, 10))
self.dataset_embedder = nn.Linear(out_dim, 10)
self.dataset_encoder = nn.Sequential(nn.Linear(out_dim, d), nn.SiLU(), nn.LayerNorm(d), nn.Dropout(0.1),
nn.Linear(d, n_ds))
self.pe_embedding = nn.Embedding(n_pe, token_dim)
def forward(self, tok, cnt):
"""tok: (B, L) global gene ids (position 0 = placeholder for CLS); cnt: (B, L) count values."""
x = F.normalize(self.pe_embedding(tok), dim=2)
x[:, 0, :] = self.cls_token.expand(x.size(0), -1)
x = torch.cat([x, self.dataset_token.expand(x.size(0), -1).unsqueeze(1)], 1)
x = self.encoder(x) * math.sqrt(self.d)
w = F.softmax(self.count_encoder(cnt.unsqueeze(-1)), dim=-1)
ce = w @ self.bin_encoder.weight
x = x + torch.cat([ce, torch.zeros_like(ce[:, :1])], 1)
for lyr in self.transformer_encoder.layers:
x = lyr(x)
return F.normalize(self.decoder(x)[:, 0, :], dim=1)
def build():
cfg = yaml.safe_load(open(hf_hub_download(REPO, "config.yaml", revision=REV)))
m, ds = cfg["model"], cfg["dataset"]
assert m["counts"] and m["rda"] and m["dataset_correction"] and ds["pad_length"] == 2048
assert ds["cls_token_idx"] == 3 and cfg["loss"]["name"] == "tabular"
pe = torch.load(hf_hub_download(REPO, "protein_embeddings.pt", revision=REV), map_location="cpu",
weights_only=True)
sd = load_file(hf_hub_download(REPO, "model.safetensors", revision=REV))
n_ds = cfg["dataset"][cfg["dataset"]["current"]]["num_datasets"]
model = SE(cfg["tokenizer"]["token_dim"], m["emsize"], m["nhead"], m["d_hid"], m["nlayers"], m["output_dim"],
len(pe), 1 + 10, n_ds)
for k in ["0.weight", "0.bias", "1.weight", "1.bias"]:
assert torch.equal(sd[f"encoder.{k}"], sd[f"gene_embedding_layer.{k}"])
model.load_state_dict(sd, strict=True)
assert torch.equal(model.pe_embedding.weight, torch.vstack(list(pe.values()))) # CLI's protein_embeddings.pt
del model.binary_decoder, model.dataset_encoder, model.gene_embedding_layer
model = model.eval().to(dev, DT)
print(f"[{MODEL}] loaded {len(sd)} tensors strictly; config pad_length={ds['pad_length']} "
f"{m['nlayers']}x{m['emsize']} heads={m['nhead']}", flush=True)
return model, cfg, {g: i for i, g in enumerate(pe)}
# ----------------------------------------------------------------------------- tokenisation (official collator)
def sentences(X, genes, g2i, pad_len, cls_idx, seed):
valid = np.array([g in g2i for g in genes])
Xv = X[:, valid].tocsr()
gid = np.array([g2i[g] for g in genes[valid]], dtype=np.int64)
n = Xv.shape[1]
assert n >= pad_len - 1 # -> official "top pad_len-1 genes" branch
rng = np.random.default_rng(seed)
T = np.zeros((Xv.shape[0], pad_len), np.int64)
C = np.zeros((Xv.shape[0], pad_len), np.float32)
with np.errstate(over="ignore"): # official is_raw_integer_counts heuristic
n_heur = int(sum((r.data.max(initial=0) <= 35) and (np.expm1(r.data.astype(np.float64)).sum() <= 5e6)
for r in (X.getrow(i) for i in range(X.shape[0])))) # (on the full row, as official)
n_trunc = 0
for r in range(Xv.shape[0]):
a, b = Xv.indptr[r], Xv.indptr[r + 1]
nz, raw = Xv.indices[a:b], Xv.data[a:b].astype(np.float64)
lv = np.log1p(raw)
s = lv.sum()
w = np.zeros(n)
if s > 0:
w[nz] = lv / s
else:
w[:] = 1.0 / n # softmax of an all-zero row
p = rng.permutation(len(nz)) # random tie-break among expressed genes
order = nz[p][np.argsort(-lv[p], kind="stable")][:pad_len - 1]
n_trunc += len(nz) > pad_len - 1
k = pad_len - 1 - len(order)
if k > 0: # random undetected genes, no replacement
zero = np.setdiff1d(np.arange(n), nz, assume_unique=True)
order = np.r_[order, rng.choice(zero, k, replace=False)]
loc = np.r_[cls_idx, order]
T[r] = gid[loc]
C[r] = 100.0 * w[loc]
stats = dict(valid=int(valid.sum()), n_gene=len(genes), heur_log=n_heur, trunc=n_trunc,
det_med=float(np.median(np.diff(Xv.indptr))))
return T, C, stats
def embed(model, T, C, bs=16):
E = np.zeros((len(T), model.decoder[1].out_features), np.float32)
i = 0
with torch.no_grad(), torch.autocast("cuda", dtype=DT):
while i < len(T):
try:
t = torch.from_numpy(T[i:i + bs]).to(dev)
c = torch.from_numpy(C[i:i + bs]).to(dev, DT)
E[i:i + bs] = model(t, c).float().cpu().numpy()
i += bs
except torch.OutOfMemoryError:
torch.cuda.empty_cache()
assert bs > 1, "OOM at batch size 1"
bs //= 2
print(f" OOM -> batch size {bs}", flush=True)
return E
def sanity(E, name):
rng = np.random.default_rng(0)
S = E[rng.choice(len(E), min(200, len(E)), replace=False)]
S = S / np.linalg.norm(S, axis=1, keepdims=True)
mc = float((S @ S.T)[np.triu_indices(len(S), 1)].mean())
msg = f"[{MODEL}] {name}: finite={np.isfinite(E).all()} std={E.std(0).mean():.4f} mean-pair-cos(200)={mc:.4f}"
if name == "hest":
org = pd.read_csv(os.path.join(CACHE, "hest", "meta.csv")).organ.values
assert len(org) == len(E)
Z = torch.from_numpy(E / np.linalg.norm(E, axis=1, keepdims=True)).to(dev)
nn_idx = []
for k in range(0, len(Z), 2048):
s = Z[k:k + 2048] @ Z.T
s[torch.arange(s.shape[0]), torch.arange(k, k + s.shape[0])] = -2
nn_idx.append(s.topk(10, dim=1).indices.cpu().numpy())
nn_idx = np.concatenate(nn_idx)
knn = float((org[nn_idx] == org[:, None]).mean())
p = pd.Series(org).value_counts(normalize=True).values
msg += f" 10-NN same-organ={knn:.3f} (base rate {float((p ** 2).sum()):.3f})"
print(msg, flush=True)
return mc
if __name__ == "__main__":
torch.cuda.set_per_process_memory_fraction(min(1.0, 8 * 2 ** 30 / torch.cuda.get_device_properties(0).total_memory))
t0 = time.time()
model, cfg, g2i = build()
PAD, CLS = cfg["dataset"]["pad_length"], cfg["dataset"]["cls_token_idx"]
for name in (sys.argv[1:] or SETS):
t1 = time.time()
X, genes = load(name)
T, C, st = sentences(X, genes, g2i, PAD, CLS, seed=0)
print(f"[{MODEL}] {name}: {X.shape[0]} spots; gene coverage {st['valid']}/{st['n_gene']} "
f"({st['valid'] / st['n_gene']:.1%}) in protein_embeddings; detected valid genes/spot median "
f"{st['det_med']:.0f}; spots with >2047 detected (truncated) {st['trunc']}; spots the official "
f"raw-vs-log1p heuristic would treat as log1p {st['heur_log']}", flush=True)
E = embed(model, T, C)
if name == "hest": # tie-breaking stochasticity check
idx = np.random.default_rng(1).choice(len(T), 300, replace=False)
T2, C2, _ = sentences(X[idx], genes, g2i, PAD, CLS, seed=1)
c = (embed(model, T2, C2) * E[idx]).sum(1)
print(f" seed-0 vs seed-1 tie-breaking: per-spot cosine median {np.median(c):.4f} min {c.min():.4f}",
flush=True)
sanity(E, name)
save(MODEL, name, E)
print(f" {name}: {time.time() - t1:.0f}s peak GPU {torch.cuda.max_memory_allocated() / 2 ** 30:.2f} GB",
flush=True)
print(f"[{MODEL}] done in {time.time() - t0:.0f}s", flush=True)