Share one masked-marginal engine across the masked models and score the leaderboard with the wild-type fill - #21
Open
Leo-T-Zang wants to merge 18 commits into
Open
Share one masked-marginal engine across the masked models and score the leaderboard with the wild-type fill#21Leo-T-Zang wants to merge 18 commits into
Leo-T-Zang wants to merge 18 commits into
Conversation
Every masked-marginal score masks a mutated position and sums a log-odds over the variant's mutations; the published conventions differ in what fills the variant's other mutated positions while one is masked, which Meier et al. 2021 (ESM-1v) supplement Appendix A defines as wild-type, mask, mutant and allele-matched fills. Add fitness/baselines/masked_lm, which builds a deduplicated context bank for all four at once and accumulates them in a single pass, so the four together cost about 19% more unique context examples than the mutant fill alone (2,929,196 against 2,458,521 on the 31 ncRNA assays). Compute the four together because match-fill cannot be recovered from the final mut-fill and wt-fill scores: it needs their per-position halves. Reduce each model script to an adapter of about 100 lines supplying an alphabet, a tokenization and one forward pass; the four scorers lose 1,559 lines of duplicated masking, batching and IO. Take the wild type from RAW_CONSTRUCT_SEQ and independently recover it by reverting each variant's mutations, and stop if they disagree, since three of the four strategies condition on it. Reject a tokenizer that maps a base to the unknown token, which is the bug the released RiNALMo scores carried, and assert that a scored position is masked, that context and token positions differ by a constant shift, and that all four strategies cover the same variants. Write a manifest beside each prediction file recording the strategies, columns, alphabet, dtype, checkpoint, counts, a hash of the scoring source and the GPU, because bfloat16 scores differ between GPU models. Add tests/test_masked_lm.py, which checks each strategy against a per-variant reference implementation on a stand-in model, needs no checkpoint or GPU, and pins each adapter's alphabet, column, batching and dtype defaults.
One scoring run now writes all four fill strategies into one folder as separate
columns, but combine_csv_data used the model name as folder, lookup key and
output column at once, so a folder could hold only one score.
Resolve each entry through resolve_source: a bare column name keeps the previous
meaning, and a {folder, column} entry lets several entries read different
columns of the same prediction files.
Register the eight masked checkpoints' four strategies with four_fill_entries,
kept out of ALL_MODELS so a default merge still produces one row per model.
Add --models to merge a subset, and fail on an unknown entry or on a prediction
file that lacks its configured column.
Adopt wt-fill, the convention the ESM and ProteinGym reference implementations use under the name masked-marginals, in place of the mutant fill the previous table used. Choose it on that provenance rather than on the scores: wt-fill has the highest observed macro on 7 of 8 checkpoints, but a paired assay bootstrap separates it from mask-fill on only 2 and from match-fill on only 1, while mut-fill is the weakest of the four on every checkpoint. Rescore AIDO.RNA, RNAGenesis, RiNALMo and RNA-FM, which all rise: AIDO.RNA takes second place from RNA-ERNIE and Evo 2 (7B), and RNA-FM passes Nucleotide Transformer. Note that RNA-ERNIE is the one masked model not on this convention, since it needs a paddlepaddle environment, so its score is carried over unverified. Publish all four strategies in four_fill_sensitivity.csv, and record that their differences sit almost entirely in the tRNA assays, which the macro weighting amplifies: under an unweighted assay mean wt-fill leads on only 3 of 8. Replace the claim that AIDO.RNA improves at every size step, since under wt-fill the 650M checkpoint scores above the 1.6B. Add fitness/analyze_fill_strategies.py, which reproduces the sensitivity table, the category spreads and the bootstrap intervals from the prediction files.
The page said to reproduce the aggregate with performance_fitness.py, but that script returns the absolute Spearman, a direction-folded AUC and an absolute MCC, so it cannot produce the signed values the leaderboard publishes. Say so, and point at analyze_fill_strategies.py, which computes the signed per-assay Spearman and the category macro from the prediction files. Leave performance_fitness.py itself alone: adding a signed option changes a shared aggregation used by the other tables and belongs in its own change.
The five AIDO.RNA sizes were split between the leaderboard, which carried only the 1.6B, and a side table, which is an odd division for five models scored on the same assays with the same convention. Fold all five into leaderboard_signed_3ncRNA.csv and drop aido_rna_scaling.csv. Say that the top three entries are a tie: AIDO.RNA (650M), Evo 2 (40B) and AIDO.RNA (1.6B) span 0.0093, inside the roughly 0.01 band that 31 assays cannot resolve, which the Notes section already states. Keep the observation that the size series stops improving after 650M, and that whether it is monotone depends on the fill strategy, as prose rather than as a second table.
Now that all five AIDO.RNA sizes are leaderboard entries, eight of the sixteen rows are masked checkpoints, so calling them the four rescored rows was ambiguous.
The script returned the absolute Spearman, so it credited a model whose scores anti-correlate with fitness exactly as much as one that correlates, which is the distinction the signed metric adopted in v0.1.1 exists to make, and it therefore could not produce the numbers the leaderboard publishes. Return the signed value. Its per-category means now reproduce this page's category columns for all eight masked checkpoints to floating-point precision, with the macro their unweighted mean. Leave AUC and MCC folded onto their better direction for now, and say so in the docstring: it is the same conflation, but unfolding them changes published numbers this change is not otherwise touching. Drop four_fill_sensitivity.csv, whose contents are the table on the leaderboard page and are regenerated by analyze_fill_strategies.py. Note that tests/test_fitness.py compares against result files published before this fix, so it will fail on any assay where a model anti-correlates until those artifacts are regenerated.
fitness/README.md still described the Spearman as absolute, which was the last place in the repository saying the active metric ignores direction. Add a test that a reversed prediction returns a negative Spearman equal in magnitude to the forward one, so that reverting to an absolute value fails rather than passing silently.
The canonical entries still pointed at the superseded prediction folders and
columns, so a default merge followed by performance_fitness.py reproduced the
old mutant-fill numbers rather than the leaderboard, which is the kind of quiet
disagreement this change set exists to remove.
Point RNA-FM, RiNALMo, AIDO.RNA at all five sizes and RNAGenesis at their
{name}_4fill folder and their wt_fill column. The {name}_{strategy} entries
still read the other three fills from those same files.
Verified end to end: merging the released predictions with these folders and
running performance_fitness.py --type all reproduces every one of the sixteen
leaderboard rows to at most 8.3e-17.
Note that the masked models were rescored on the 31 non-coding assays only, so
they now report NaN on the mRNA assays rather than a score from a superseded
convention.
The Spearman became signed in this branch, but the AUC was still folded with max(auc, 1 - auc) and the MCC was still absolute, so one row could report a model as badly wrong by Spearman and moderately good by the other two at the same time. On the non-coding assays 190 of 496 model and assay pairs rank variants backwards, and every one of them was credited with an AUC above 0.5 and a positive MCC; the worst reads Spearman -0.528 alongside AUC 0.747. Return the AUC and the MCC as computed. An AUC below 0.5 or an MCC below 0 now means the model ranks variants the wrong way round, higher stays better for all three, and the leaderboard is unaffected because it reports the Spearman. Extend the metric test to pin all three directions, so a reversed prediction has to score worse than random rather than the same as a correct one.
The AUC scores continuous predictions against median-binarised assay labels, while the MCC binarises both sides, which the shared phrase obscured. Note in the metric test why it uses an even, tie-free sample: an MCC of exactly -1 needs the median to split both classes evenly.
Two default paths could finish successfully while doing the wrong thing.
merge_scoring_files.py logged and continued past a missing prediction file, and
performance_fitness.py then dropped any model absent from every merged file, so
a misspelled folder produced a complete set of merged assays with a model
silently gone; a folder-name mistake during this work produced 31 merged files
carrying no scores at all and reported success. Count what each model
contributes and raise when a requested model contributes nothing, which is a
configuration error rather than the partial coverage the non-coding runs
legitimately have. Add --allow_incomplete for the permissive case, and log the
per-model assay counts either way.
The masked scoring scripts advertised the full 0-69 array while defaulting to
all four strategies, which the runner refuses on any assay needing windowing, so
the documented command failed on the mRNA assays. Point the scripts at the 31
non-coding rows the v0.2 leaderboard reports, and say why the four strategies
are offered only where the whole construct fits: a windowed context drops
mutations from the conditioning sequence, so the fills stop estimating the same
quantity. Document the same limit in the engine README.
Pin the registry to the leaderboard, since that disagreement nearly shipped: one
test asserts every masked model resolves to its {name}_4fill folder and wt_fill
column, another that every leaderboard row is a registered model and the table is
sorted. Reverting one entry to its old form fails the first.
Compare a windowed score against an independently computed reference rather than
only checking that masks survive and scores are finite, and stop tests/
test_fitness.py skipping a result file missing from the published archive, which
let it report success while checking nothing.
The page said RNA-ERNIE's convention was never verified. It can be read off its scorer: one forward pass over the unmasked wild type, a softmax rather than a log-softmax, and a sum of raw probability differences at the mutated positions. That is neither a masked marginal nor a log-likelihood ratio, and for multi-mutants no rank-preserving transformation relates the two, so its fourth place is not comparable with the checkpoints scored under wt-fill. Orthrus masks one position at a time in the variant's own sequence, which is mut-fill; say so rather than leaving it implicitly on the page's convention. Replace the claim that differences under about 0.01 are not meaningful, which nothing in the repository establishes: the bootstrap_se columns are standard errors of an assay-weighted difference from the best model, not of the equal-weight category macro. Give the measured interval for the top pair instead, +0.0043 with a 95% interval of [-0.0224, +0.0314].
Orthrus was excluded from the strategy comparison on the grounds that it has no mask token, so masking would mean zeroing continuous channels and would modify the convention rather than apply it. That premise was wrong. The antichronology/orthrus-mlm-6-track checkpoint is dual-objective pretrained, contrastive plus masked LM, its README describes predict_tokens as variant scoring, and its docstring specifies the masking convention directly: "Positions to score should be masked (nucleotide channels set to zero) before calling." Zeroing is therefore Orthrus's masking operation, not a substitute for one, and the published Orthrus score already relied on it for a single position. Score it through the shared engine, which leaves it on the same context construction, deduplication, batching and accumulation as every other masked model. Orthrus has no token vocabulary, so it is handed integer base codes, 0 to 3 for A, C, G, T with 4 for a masked position, and logits_at expands those into the 6-track tensor; no special case is needed in the engine. This also gives Orthrus the validation the other scorers have. Its own build_masking_tasks never checked that the variant sequence carries the declared mutant base, so a mismatch would have been scored silently rather than dropped. The CDS and splice channels remain zero throughout, because DMS constructs carry no transcript annotation. That is a limitation of applying the model to this data rather than of the masking, and it is unchanged from before.
The comment claimed lengths are passed so the backbone ignores padding, but this checkpoint's forward discards lengths and they affect pooled representations only. Padding is kept out by refusing mixed-length batches.
Orthrus was the last entry still published under mut-fill. Its four-strategy run puts wt-fill at 0.0717 against the 0.0384 it had, nearly double, and the whole difference is in tRNA, from -0.0068 to +0.0922: the deepest-mutation category, which is where the fills diverge. Move its leaderboard row and its registry entry to wt-fill, and add it to the sensitivity table as a ninth checkpoint. Correct two claims that this run breaks. mut-fill is no longer weakest on every checkpoint: for Orthrus match-fill edges below it by 0.0028, so it is lowest on 8 of 9. wt-fill has the highest observed macro on 8 of 9 rather than 7 of 8. Note also that none of Orthrus's own bootstrap intervals excludes zero, so it is consistent with the pattern rather than independent evidence for it, despite the size of the point estimate. Its rank is unchanged at 14 of 16.
The sensitivity section argued a case about the other three fills. Say instead that wt-fill has the highest observed macro on 8 of the 9 checkpoints and is the convention the leaderboard uses, and leave the table to be read. Keep the two caveats a reader needs: the differences sit almost entirely in the tRNA assays, which the equal-category weighting amplifies, and the model ordering is far more stable than the absolute numbers.
The stricter missing-model check turned a latent inconsistency into a failure: EVmutation is in ALL_MODELS, so a default merge requests it, but it has never been part of a prediction release and it never contributed a column to any output. Every run simply logged one warning per assay and moved on. It also has its own merge mode, --assays_with_MSAs_only, because it only scores assays with MSAs. Remove it from the default list and keep its SCORE_COLS entry, so that mode still works and the default pipeline no longer requests predictions that do not exist.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
The four masked language models (RNA-FM, RiNALMo, AIDO.RNA, RNAGenesis) were scored by four
near-duplicate scripts that hardcoded one masked-marginal convention. This replaces them with one
shared engine that implements the four conventions the literature defines, and moves the leaderboard
onto the one the reference implementations use.
The four fill strategies
Notation
For a variant with mutated positions$M$ , write $x^{wt}$ for the wild-type sequence and $x^{mt}$ for$i \in M$ , $wt_i$ is the wild-type base and $mt_i$ $x_{-i}$ for a sequence with a mask token at position $i$ , and $x_{-M}$ for$M$ . Finally $p(x_i = b \mid c)$ is the masked language model's$b$ at position $i$ given context $c$ .
the variant's own sequence. At a mutated position
the mutant base. Write
one with a mask at every position in
probability of base
Every masked-marginal score masks a mutated position, reads a log-odds between the mutant and$i$ is
wild-type base there, and sums over the variant's mutations. The four conventions differ in exactly
one thing: what the model sees at the variant's OTHER mutated positions while position
masked. All four are defined in Meier et al. 2021 (ESM-1v), supplement Appendix A.
1.
wt-fill, wild-type bases at the other mutated sitesBoth terms are read from the same context, so each summand is a genuine log-odds ratio. The context$i$ , so a multi-mutant's score is exactly the sum of its
never depends on the variant, only on
constituent single-mutant scores: the model is additive by construction and cannot express epistasis.
It is also by far the cheapest, needing one context per distinct mutated position of the whole assay
(1,697 across the 31 ncRNA assays) rather than one per variant.
This is what the ESM authors'
examples/variant-prediction/predict.pyand ProteinGym'sproteingym/baselines/esm/compute_fitness.pyimplement under the option namemasked-marginals, andit is the WT-LLR of the RNA zero-shot literature. The leaderboard adopts it on that provenance.
2.
mask-fill, masks at the other mutated sitesEvery mutated position is masked at once and the rest of the sequence is wild type. Both terms again
share one context, so this too is a log-odds ratio. The model is told that the other mutated
positions changed but not what they changed to, so it marginalises over them rather than conditioning
on them. One context per distinct mutated-position set. This is the formula written in the ESM paper,
and its supplement calls it strategy (a).
3.
mut-fill, mutant bases at the other mutated sitesPosition$i$ is masked in the variant's own sequence, so the variant's other mutations remain visible.$i$ depends on the actual genetic
Both terms share a context, so it is a log-odds ratio, and it is the only one of the four that can
express epistasis, since the conditional distribution at
background. It is also the most expensive, needing one context per (variant, position) pair
(2,458,521 across the 31 assays). Supplement strategy (c). This is the convention the previous
leaderboard used.
4.
match-fill, the fill matches the allele being scoredThe mutant term is read in the variant's context and the wild-type term in the wild type's, so the
two terms come from different contexts and this is not a log-odds ratio. It is a difference of two
conditionals, effectively a mutation-site-restricted difference between the variant's and the wild
type's pseudolikelihood contributions. Supplement strategy (b).
A worked example
Wild type
ACGUACGUACGUwith the double mutantA1C, G3U, so the variant isCCUUACGUACGU.Scoring position 1, with
#the mask token:wt-fill#CGUACGUACGUG, wild typemask-fill#C#UACGUACGU#, maskedmut-fill#CUUACGUACGUU, mutantmatch-fill#CUUACGUACGUand#CGUACGUACGUPosition 3 is then scored the same way, and the two contributions are summed.
Two properties that follow
All four are identical on single mutants. If$|M| = 1$ there are no other mutated positions, so
$x^{mt}{-i} = x^{wt}{-i} = x^{wt}_{-M}$ and the four expressions coincide term by term. This is used
as a fixture test: on
Pitt_2010_ribozyme, which is entirely single mutants, all four must returnidentical scores, and they do for all eight checkpoints.
They diverge on essentially everything else here, because 99.4% of the ncRNA variants are
multi-mutants.
Why the four are computed together
Write the four half-sums
so that$s_{\text{mut-fill}} = A - B$ , $s_{\text{wt-fill}} = C - D$ and $s_{\text{match-fill}} = A - D$ .$A - B$ and $C - D$ does not determine $A - D$ : adding a constant to both $A$ and $B$ leaves
Knowing
mut-fillunchanged while movingmatch-fill.match-filltherefore cannot be recovered from theother strategies' final scores, only from their per-position halves, which is why the engine
computes all four in one pass rather than offering them as separate runs.
Terminology hazard
The name is overloaded, and the overloading has already produced wrong numbers in this repository.
The ESM and ProteinGym code option called
masked-marginalsiswt-fill; the formula written in theESM paper is
mask-fill; andwt-marginalsis a different method entirely, a single unmaskedforward pass of the wild type with no masking at all, which is what the previously released RNA-FM
predictions turned out to be.
Leaderboard effect
Every masked model rises. AIDO.RNA (1.6B) 0.1824 to 0.2070, RNAGenesis 0.1498 to 0.1825, RiNALMo
0.1391 to 0.1690, RNA-FM 0.1014 to 0.1293, which passes Nucleotide Transformer, and Orthrus 0.0384 to
0.0717. The autoregressive models, NT and EVmutation are untouched, since masked marginals do not
apply to them, and their rows are byte-identical.
The five AIDO.RNA checkpoints were previously split between the leaderboard, which listed only the
1.6B, and a side table. They are five models scored on the same assays with the same convention, so
all five are now leaderboard entries and
aido_rna_scaling.csvis gone.The top three are a tie, not a ranking. AIDO.RNA (650M) 0.2163, Evo 2 (40B) 0.2120 and AIDO.RNA
(1.6B) 0.2070 span 0.0093, inside the roughly 0.01 band that 31 assays cannot resolve, which the
Notes section on that page already states. The substantive observation is not the order but that a
650M-parameter masked RNA model is level with a 40B autoregressive one.
The size series stops improving after 650M, and whether it is monotone depends on the fill strategy:
mask-fillandmut-fillincrease with size, whilewt-fillandmatch-fillput the 650M abovethe 1.6B. That is now stated as prose rather than as a second table.
All four strategies are computed for every masked checkpoint and published on the leaderboard page,
so the choice can be checked rather than taken on trust.
wt-fillhas the highest observed macro on8 of the 9 checkpoints, and it is the convention the leaderboard uses. The differences sit almost
entirely in the tRNA assays, which the equal-category weighting amplifies.
RNA-ERNIE is the one entry not on this convention. Its scorer runs a single unmasked forward pass
over the wild type, applies a softmax rather than a log-softmax, and sums raw probability differences,
so it is not comparable with the masked checkpoints. It needs a paddlepaddle environment we do not
have. The leaderboard page says so.
Orthrus joins the masked models. It had been excluded on the grounds that it has no mask token,
but its MLM checkpoint documents the convention directly, "positions to score should be masked
(nucleotide channels set to zero) before calling", and its published score already relied on that
operation. It now runs through the same engine as the others, and its scorer drops from 308 lines to
109 while gaining the mutant-base validation it lacked.
Implementation
fitness/baselines/masked_lmbuilds one deduplicated context bank covering all four strategies andaccumulates them in a single pass. The four share most of their contexts, so computing all four costs
about 19% more unique context examples than
mut-fillalone (2,929,196 against 2,458,521 over the 31assays).
Each model script is now an adapter of about 100 lines supplying an alphabet, a tokenization and one
forward pass; the four lose 1,559 lines of duplicated masking, batching and IO.
Guards, because a wrong masked score looks plausible: a tokenizer that maps a base to the unknown
token is rejected (that is the bug the released RiNALMo scores carried), every scored position is
asserted to be masked, the context-to-token offset is asserted rather than assumed, the four
strategies must cover the same variants, and the wild type is taken from
RAW_CONSTRUCT_SEQandindependently recovered by reverting each variant's mutations, with disagreement fatal.
Each prediction file gets a manifest recording the strategies, columns, alphabet, dtype, checkpoint,
counts, a hash of the scoring source and the GPU. The GPU matters: the same code and checkpoint in
bfloat16 on an L40S and an H100 differ by up to 0.2 in score and about 0.001 in per-assay Spearman.
Every number here was produced on H100s.
Verification
--strategies mut-fillreproduces the pre-refactor predictions to 1.5e-7 (RNA-FM), 4.7e-7(RiNALMo), 9.2e-7 (RNAGenesis) and 7.1e-7 (AIDO.RNA, on its original L40S), with identical NaN
masks, so the refactor changed no model's numbers.
mut-fillcolumn of the new runs reproduces the previously publishedmut-fillleaderboard towithin 0.0008.
Pitt_2010_ribozymefor all eight checkpoints,which is the fixture they must satisfy by construction.
tests/test_masked_lm.py: 26 tests, CPU only, no checkpoint needed. Each strategy is checkedagainst a per-variant reference implementation, and each adapter's alphabet, column, batching and
dtype defaults are pinned, since those are what the released predictions were produced with.
fitness/analyze_fill_strategies.pyreproduces the sensitivity table, the category spreads and thebootstrap intervals from the prediction files.
Reviewing
The three commits are separable: the engine and adapters, the merge registry change, then the
leaderboard.
fitness/merge_scoring_files.pykeeps its previous behaviour for every existing entry;a bare column name still means "a folder named after the model".