Single-Cell Foundation Models: What They Are, and How to Actually Use Them
Every claim here has a worked example with numbers you can check by hand.
Run code/demo_scgpt_pipeline.py alongside this — it prints every step.
0. The 60-second version
| Model | What it is, in one line |
|---|---|
| scGPT | BERT, but each token is a gene and “position” is replaced by expression level |
| Geneformer | Same, but instead of the expression value it feeds the expression rank |
| scFoundation | Same, but only encodes the genes that aren’t zero |
| UCE | Same, but a gene’s token comes from its protein sequence — so it works on any species |
| CellFM | Same as scGPT, 3× the data, different backbone |
| STATE | Two models: “where is this cell” + “how does it move when you push it” |
| Tahoe-100M | Not a model. A dataset: 100M cells, 1,100 drugs × 50 cell lines |
The thing to take from all of them: the gene embedding table. Not the model.
The thing that will break: the value encoder. Every single time.
1. What a cell is, as data
Forget biology for a second. Here is a liver cell:
gene: Alb Cyp1a1 Actb Tp53 Lcn2
count: 450 3 890 12 0
That’s it. A cell is a vector of counts. Real vectors are ~20,000 long; this is 5.
What the genes are, so the examples mean something:
| gene | what it does |
|---|---|
| Alb | albumin — liver’s signature protein. Massively expressed there, nowhere else |
| Cyp1a1 | a drug-metabolizing enzyme. Switches ON when toxicants appear. The one toxicology cares about |
| Actb | beta-actin — structural, in every cell, always on. A “housekeeping” gene |
| Tp53 | p53 — the DNA-damage alarm |
| Lcn2 | lipocalin-2 — injury/inflammation marker |
The zero problem
Lcn2 = 0. Is that gene off, or did the assay just miss it?
You cannot tell. In real single-cell data, ~80% of genes read zero this way — you only capture 10–20% of a cell’s RNA. This is called dropout and it is the central pathology of scRNA-seq.
NLP analogy: every sentence has 80% of its tokens randomly deleted, and you don’t get told which positions were deleted. You have millions of sentences, each mostly holes.
Hold onto this. It comes back in §8 as an argument that toxicogenomics data is better.
2. scGPT, step by step
2.1 Gene → token
Exactly a vocabulary lookup. scGPT’s vocab is ~60,000 genes instead of ~50,000 wordpieces.
Alb -> token id 4102
Cyp1a1 -> token id 88
Actb -> token id 17
Tp53 -> token id 991
Lcn2 -> token id 2043
2.2 Value → bin
A count of 450 isn’t a token. So scGPT bins it. Bin edges are fit on the training data — which is counts, so everything is :
bin edges: [0, 1, 5, 20, 60, 150, 400, 1000, ∞]
Alb count 450 -> bin 6
Cyp1a1 count 3 -> bin 1
Actb count 890 -> bin 6
Tp53 count 12 -> bin 2
Lcn2 count 0 -> bin 0
2.3 Embed and add
Two lookup tables, both 512-dimensional:
This is literally BERT.
token_embedding + position_embedding. Except “position” is replaced by “expression level” — because genes have no order. A cell is a set, not a sequence.
2.4 The rest
- 12 transformer layers, no positional encoding
- Pretraining: mask some genes’ values, predict them. Masked language modelling, with expression values instead of wordpieces
- 33M cells
Remember these two tables. They have completely different fates:
| table | shape | fate |
|---|---|---|
| ⭐ the thing worth stealing | ||
| 💥 the thing that breaks |
3. Where toxicogenomics breaks it — worked numerically
Here is a DrugMatrix profile. Same five genes. But these are fold-change:
Alb -0.02 no real change
Cyp1a1 +1.85 UP 71x vs untreated control <-- the signal
Actb +0.01 no real change
Tp53 +0.42 UP 2.6x
Lcn2 -0.71 DOWN 5.1x
Now push it through scGPT’s binner:
Alb value -0.02 -> bin -1 <-- CRASHES: no such bin
Cyp1a1 value +1.85 -> bin 1 <-- same bin as a COUNT of 1-5
Actb value +0.01 -> bin 0 <-- same bin as a COUNT of 0-1
Tp53 value +0.42 -> bin 0 <-- same bin as a COUNT of 0-1
Lcn2 value -0.71 -> bin -1 <-- CRASHES: no such bin
Two failures. The second is worse.
Failure 1 — negatives crash
lands in bin . There is no bin . Bin edges start at 0 because you cannot have RNA molecules. There is no embedding row for it. It errors — or silently wraps to the last row, which is worse.
Failure 2 — positives fail silently ⭐
Cyp1a1 = +1.85 means “this gene went up 71×, the loudest signal in the entire profile.”
It bins to 1. Bin 1 means “a count of 1 to 5 molecules” — i.e. “this gene is essentially off.”
No error. No warning. The most important signal in the profile has been silently relabelled as its opposite. And all 92% of DrugMatrix that sits in collapses into bin 0 right next to it.
You would get a number out. The number would be garbage. Nothing would tell you.
Why this matters more than it sounds
This is a type error, not a domain gap.
A domain gap is “trained on news, you have tweets.” More training fixes it. A type error is “the function expects an int, you passed a list.” No amount of fine-tuning fixes a type error.
This is the concrete, technical answer to “why don’t you just fine-tune scGPT?” — and it’s much stronger than a shrug.
It also means: anyone who tried it and got mediocre results may simply have been feeding it nonsense. The failure would look like “the foundation model didn’t help,” not like “we made a units mistake.” Worth remembering when reading the critique literature.
4. What survives: the gene embeddings ⭐
The type error lives entirely in . It does not touch .
That question has nothing to do with how you encode expression values. It’s learned from co-occurrence statistics across 33M cells — the same way word2vec learns that king and queen are related, from co-occurrence, regardless of how you later count the words.
So is:
- pretrained on 33M cells
- reusable
- modality-independent — doesn’t care about counts vs fold-change
- largely species-conserved — a gene’s function is mostly the same in rat and human
It is the part of scGPT that transfers. And nobody in the toxicogenomics literature has used it.
5. The connection: TransTissueFormer’s first layer is a gene table
TransTissueFormer’s first layer is a matrix . It computes:
Written out on a toy ( genes, slots so it fits on the page):
Read the rows. Row of is a vector describing gene .
And is an expression-weighted sum of gene embeddings — word2vec document embedding, where the weights are fold-changes instead of term frequencies.
So:
| shape | trained on | |
|---|---|---|
| TransTissueFormer’s | random init, then 425 examples | |
| scGPT’s | 33,000,000 cells |
And it dodges the type error completely, because never touches . TransTissueFormer has no value encoder — it multiplies the raw fold-change straight into the gene embedding. The broken part of scGPT is the part this architecture doesn’t have.
is 96.6% of TransTissueFormer’s parameters (07_TRANSTISSUEFORMER.md §7.6.1).
You’d never train word embeddings from scratch on 425 sentences. That is what this model does, on 96.6% of itself, and a matching pretrained table is sitting in a public checkpoint.
6. The other models, and what each one gives you
Geneformer — rank instead of value
Instead of binning the count, sort the genes by expression and feed the ranking:
counts: Actb 890, Alb 450, Tp53 12, Cyp1a1 3, Lcn2 0
ranks: Actb=1, Alb=2, Tp53=3, Cyp1a1=4, Lcn2=5
tokens: [Actb, Alb, Tp53, Cyp1a1, Lcn2] <- position IS the value
Clever: normalization-free, robust to batch effects and sequencing depth. If one sample is sequenced twice as deep, all counts double — but the ranking doesn’t change.
The cost: magnitude is gone. Rank 4 vs rank 5 could be a 1.01× or a 100× difference.
For toxicogenomics: ranking a fold-change profile is almost meaningful — you’d get “which genes moved most.” But it destroys the up/down distinction unless you rank by signed value, and 92% of the profile is tied at ~0 so the ranking is mostly noise. Not a good fit.
scFoundation — skip the zeros
Only encodes the non-zero genes. An asymmetric encoder-decoder: the encoder reads ~2,000 non-zero genes, the decoder reconstructs all 20,000.
Why: if 80% of your input is uninformative zeros, don’t spend attention on them.
For toxicogenomics: ⭐ the architecture idea transfers, and it transfers better than it works in single-cell. See §8.
UCE — the species fix ⭐
The problem: scGPT and CellFM are human. DrugMatrix is rat. You’d need to map rat genes → human orthologs. That’s ~80% clean — and the missing 20% is concentrated in exactly the genes toxicology cares about. Cytochrome P450s (Cyp1a1 and its family) have expanded and diverged differently in rodents, because rats and humans eat different things and evolved against different toxins. The dictionary fails precisely where you need it.
UCE’s move: don’t use a vocabulary at all. Get a gene’s token by running its protein sequence through ESM2 (a protein language model):
scGPT: Cyp1a1 -> look up "Cyp1a1" in a vocab -> row 88 -> 512-dim vector
(fails if the gene isn't in the vocab)
UCE: Cyp1a1 -> get its protein sequence -> ESM2 -> 5120-dim vector
(works for ANY protein-coding gene, ANY species,
including species never seen in training)
Trained on 36M cells across 8 species.
NLP analogy: this is byte-level or subword tokenization for an unseen language, versus a fixed vocabulary that OOVs everything. UCE never needs the bilingual dictionary because it reads the “spelling” directly.
For toxicogenomics: ⭐ the right structural fit for rat data. And it makes a sharp prediction: if you compare scGPT-embeddings-via-orthologs against UCE-embeddings-no-mapping, UCE’s advantage should concentrate in the non-ortholog genes — the P450s. If it does, that’s a mechanistic result, not a leaderboard bump.
CellFM — more of the same, bigger
100M human cells, ~800M params, ERetNet backbone. Same value-encoding problem. Same gene table to steal.
STATE — the architecture worth noticing
Arc Institute. Trained on 167M observational + >100M perturbational cells, 70 human cell contexts. Two modules:
- SE (State Embedding) — where is this cell in state space?
- ST (State Transition) — how does it move when you push it?
Notice the decomposition. “Where am I” + “how do I move” is the same split as:
- CPA’s
- ToxCompl’s
Three literatures, one idea. That convergence is evidence it’s the right decomposition — and it means the toxicogenomics program has been building a linear STATE without calling it that.
7. Tahoe-100M and the pseudobulk bridge ⭐⭐
What Tahoe actually is
Not a model. A dataset. 100M single cells, ~1,100 drugs × 50 cancer cell lines, ~60,000 drug-cell combinations. Open source, on HuggingFace. Released Feb 2025 — ~50× larger than all previously public drug-perturbed single-cell data combined.
Crucially: interventional. Someone dosed the cells and measured what happened.
The problem
Wrong modality (single-cell counts) for toxicogenomics (bulk fold-change). Two mismatches at once.
The fix, worked numerically
Tahoe has DMSO controls for every cell line. DMSO is the solvent — “we ran the experiment with no drug in it.”
So: average the cells, then divide by the control average.
500 cells treated with drug X, 500 DMSO control cells.
gene mean treated mean control log10 FC meaning
--------------------------------------------------------------
Alb 455.0 449.6 0.01 no change
Cyp1a1 29.9 3.0 0.88 UP 7.6x
Actb 879.6 889.7 -0.00 no change
Tp53 39.8 12.0 0.50 UP 3.1x
Lcn2 12.2 1.0 0.81 UP 6.5x
That last column is DrugMatrix’s format.
100,000,000 single cells
→ average within each (drug, cell line) [pseudobulk]
→ divide by that cell line's DMSO control [fold-change]
→ log10
→ ~60,000 bulk-equivalent fold-change signatures
Both mismatches solved by one operation. Single-cell → bulk (averaging). Counts → fold-change (dividing by control).
You deliberately threw away single-cell resolution. That’s the trade: lose per-cell detail, gain commensurability with every bulk toxicogenomics dataset ever collected.
And LINCS L1000 is already there
LINCS L1000 Level 5 ships z-scores — signed, centred at zero, structurally the same object as log fold-change. ~1.3M more signatures, free, already in the right modality.
Note the June 2026 paper that fine-tuned scGPT on 3M+ LINCS profiles used Level 3 (absolute expression). Nobody has used Level 5 as a native differential pretraining corpus.
The corpus that could exist
| source | native | → fold-change | scale |
|---|---|---|---|
| DrugMatrix | log10 FC | ✅ already | ~2,700 × 8 tissues |
| Open TG-GATEs | intensity | ÷ control | ~2,238 |
| LINCS L1000 L5 | z-scores | ✅ already | ~1.3M |
| Tahoe-100M | sc counts | pseudobulk ÷ DMSO | ~60,000 |
~1.4M interventional fold-change signatures. That corpus does not exist. Nothing prevents it existing.
8. The argument that toxicogenomics data is better ⭐
Both single-cell and fold-change data are ~90% zeros. The zeros mean completely different things.
| what a zero means | |
|---|---|
| single-cell | Lcn2 = 0 counts. Gene off? Or assay missed it? You cannot tell. ~80% of entries. This is missing data. |
| fold-change | Lcn2 = -0.02. The gene did not move under the drug. This is a measurement. |
Single-cell zeros are missing data. Fold-change zeros are signal.
The consequence: scFoundation’s “only encode non-zero genes” is a workaround for dropout — a hack around not knowing. In fold-change space the same trick is principled: you skip the 92% because nothing happened there, not because you’re uncertain.
So a fold-change-native foundation model gets scFoundation’s efficiency for a better reason, and gets a cleaner training signal, because its zeros are honest.
I have not seen this argued anywhere. It’s a small point. But it says the modality mismatch isn’t purely a disadvantage — on one axis, the tox data is better-conditioned.
9. How to actually use each one
Ordered by effort. All of these are concrete.
9.1 Steal the gene table 🟢 do this first
1. Download scGPT weights (public, HuggingFace)
2. Pull out E_gene -> (~60000, 512)
3. Map rat genes -> human orthologs (biomaRt / Ensembl)
4. Look up each of DrugMatrix's 8,565 probes
5. Load the result as TransTissueFormer's W
6. Train as normal
Ablate against — this is the part that matters:
| init of | tests |
|---|---|
| random | current baseline |
| PCA/co-expression from DrugMatrix itself | ⭐ the critical control — does a 33M-cell FM beat the data’s own structure? |
| scGPT (ortholog-mapped) | the obvious FM |
| UCE / ESM2 (no mapping) | species-agnostic |
| shuffled scGPT | content, or just some structure? |
| ortholog-only subset | isolates the mapping penalty |
Both outcomes publish. Wins → the FM bridge. Loses to DrugMatrix’s own co-expression → consistent with Kedzierska and Souza & Mehta, and converts “we plan to explore in future work” into “we tested it; here’s the evidence.”
9.2 Build the fold-change corpus 🟡
Pseudobulk Tahoe + LINCS Level 5 + TG-GATEs + DrugMatrix → ~1.4M interventional differential signatures. Pretrain natively in fold-change space, with a signed value encoder.
Why bother — the mechanistic argument: Ahlmann-Eltze says FMs underperform because pretraining data is observational. LINCS, Tahoe, and DrugMatrix are all interventional.
9.3 Don’t 🔴
- Don’t feed fold-change to scGPT’s value encoder. §3. It fails silently.
- Don’t fine-tune scGPT end-to-end on DrugMatrix. Same reason.
- Don’t expect zero-shot scGPT embeddings to work. Kedzierska: beaten by highly-variable-gene selection, a 2010 heuristic with no learning in it.
- Don’t skip the baselines. The Virtual Cell Challenge ran 1,200 teams and reported models “not yet consistently outperforming naive baselines.”
10. Summary
- A cell is a vector of counts. ~80% are zero and you can’t tell why.
- scGPT is BERT over genes.
token = gene_embedding + value_bin_embedding. No positional encoding, because a cell is a set. - Fold-change has negative values. There is no bin for those. And the positives get silently mis-binned — Cyp1a1 up 71× reads as “essentially off.” Type error, not domain gap.
- Gene embeddings never touch values → they transfer.
- TransTissueFormer’s (96.6% of it) is a gene embedding table, . scGPT’s is . Same shape. Load it.
- UCE tokenizes by protein sequence → any species, no ortholog dictionary. The right fit for rat.
- Pseudobulk + ÷ DMSO turns Tahoe-100M into DrugMatrix’s format. LINCS Level 5 is already there. ~1.4M signatures.
- Fold-change zeros are signal; single-cell zeros are missing data. On that axis, tox data is better.
Toy examples runnable in code/demo_scgpt_pipeline.py. Bin edges, token IDs, and embedding tables in the demo are invented for illustration — the structure is real, the specific numbers are not. scGPT’s 512-dim gene embeddings and ~60k vocab are verified; other model details are flagged in 17_SOURCES.md.