Skip to content

v2.9.0

Choose a tag to compare

@github-actions github-actions released this 31 Oct 12:33

A performance release. --sketch quantification is up to 2.7× faster,
selective alignment up to 1.6× faster on isoform-rich references, and
salmon index up to 4.2× faster with less memory, and indexes are now
byte-for-byte reproducible. Two things change for users:

  • Indexes must be rebuilt. The index format moves to v3 (a new sshash
    dictionary format); salmon refuses older indexes with the command to rebuild
    them.
  • VBEM now decides convergence for each independent group of transcripts
    separately
    , and keeps iterating slowly mixing groups of near-identical
    isoforms until they have actually settled. Abundances shift, mostly within
    those families, and are more accurate on realistic simulations. Plain EM
    (--useEM) results are unchanged.

Speed

Full quantification (salmon quant --sketch, wall time in seconds, median
of 3 interleaved runs, 2 at one thread; one machine, AMD EPYC 9575F, all
runs pinned to the same socket), against
salmon 2.8.0 (built from its release tag) and kallistors 0.6.0 on the same
reads and reference:

workload threads 2.8.0 2.9.0 kallistors 0.6.0
muscle (SRR13638690, 4.4M × 2×170 bp, trimmed as in kallistors' benchmark), GENCODE v50 1 105.6 38.6 40.5
8 14.5 5.5 8.9
16 8.8 3.4 6.2
GEUVADIS (ERR188044, first 10M × 2×75 bp), Ensembl GRCh38 cDNA 1 81.9 47.9 59.1
8 10.8 6.9 11.2
16 6.4 4.0 7.1

Peak memory is unchanged from 2.8.0 (0.3–1.4 GB on these runs; kallistors
used 1.0–3.7 GB). Selective alignment at 8 threads: muscle 33.8 → 20.5 s,
GEUVADIS 29.3 → 24.2 s. Building the index at 16 threads: GENCODE v50
50.4 → 12.0 s (kallistors 11.8 s), Ensembl cDNA 17.1 → 4.5 s (kallistors
5.1 s).

Where the time went:

  • Inference. The EM and VBEM now work on the connected components of the
    equivalence-class graph: transcripts are renumbered densely by component,
    single-transcript classes are folded into constants, and each component is
    iterated in cache-sized blocks. This schedule is kallistors' (see
    Acknowledgements). VBEM's expectations exp(ψ(·)) are computed by a
    vectorized, branch-free routine instead of a library digamma, so a VBEM
    iteration now costs within 15% of a plain-EM one. Inference time (from the
    intermediate mapping file, 1 / 8 / 16 threads): muscle VBEM 46.6 s → 0.88 /
    0.15 / 0.13 s, GEUVADIS VBEM 3.7 / 0.76 / 0.55 s; plain EM costs about
    what kallistors' does (muscle 5.8 / 0.80 / 0.47 s vs 5.4 / 1.0 / 0.64 s;
    GEUVADIS 3.9 / 0.87 / 0.59 s vs 3.9 / 0.82 / 0.78 s).
  • Mapping (--sketch). piscem-rs 0.12 stops tracking a transcript as
    soon as it misses a hit (it could no longer be accepted), instead of keeping
    a hash-map entry for every occurrence of every k-mer hit; the accepted
    mappings are the same, and mapping is 12–20% faster. piscem-rs 0.12.1 and
    sshash-lib 0.8.1 then remove mispredicted branches and redundant
    Elias–Fano selects from the k-mer lookup and contig-table reads, worth
    another 7–9% at every thread count (found by profiling on Apple
    Silicon). Reading the intermediate mapping file back and building
    equivalence classes is also cheaper.
  • Single-threaded gzip input is decoded with
    sirocco, about 45% faster than the
    zlib-rs decoder it replaces (multi-threaded runs already decode in
    parallel and are unchanged).
  • Selective alignment. Grouping candidate anchors by transcript is now
    linear, and repeated hits that extend to an already-seen anchor are skipped:
    on the muscle run, full quantification at 8 threads goes from 33.8 s to
    20.5 s (Ensembl/GEUVADIS: 29.3 s → 24.2 s).
  • Index construction. cf1-rs batches and prefetches its MPHF lookups, uses
    a faster MPHF for in-memory builds, and skips work repeated across the
    isoforms of a gene; piscem-rs and salmon parse and hash their inputs in one
    parallel pass. GENCODE v50 at 16 threads: 50.4 s → 12.0 s; Ensembl cDNA:
    17.1 s → 4.5 s.

What changes in the results

VBEM convergence (default optimizer)

VBEM used to iterate every transcript until the slowest group of
transcripts converged. It now iterates each connected component — a set of
transcripts linked by shared reads — on its own, and stops it once the usual
criterion holds over the component (same tolerance, same minimum iterations)
and the movement still to come, projected from how fast its steps are
shrinking, is below 0.1% of the component's fragments. The second condition
matters for families of near-identical isoforms (e.g. TTN, NEB): their
steps are tiny long before they settle, so the criterion alone would stop them
far from converged. Components are independent problems, so this is still the
same model.

On six simulations with realistic expression profiles (three profiles,
GENCODE v50, selective alignment and --sketch), the total absolute error
against the true counts falls on every sample, by up to 31% (L1 error 0.145 →
0.101 of all fragments; 95% bootstrap intervals exclude zero), concentrated in
those isoform families; transcript-level Spearman and mean absolute relative
difference are unchanged (within ±0.001) and gene-level estimates are
unchanged. meta_info.json reports how the components ended
(em_components; see the output-format reference): a handful of very slowly
mixing families can reach the iteration cap after meeting the criterion,
which is reported there and in the log, and em_converged stays true. The
expert flag --stopRule global reproduces 2.8.0's VBEM results.

Results remain deterministic and identical at any -p. With --useEM, and
with --emAccel squarem or daarem, results are unchanged from 2.8.0 (to
within floating-point rounding, which does not show at the printed precision).

A note on --emAccel squarem

SQUAREM is not a faster route to the default answer. Its stopping test is
applied across each three-step cycle, extrapolation included, so at the
default tolerance it ends far closer to the optimum than unaccelerated
iteration does. On simulated data, converging that far makes
transcript-level rankings slightly worse for any algorithm (more truly
low-expressed transcripts are driven to zero), and even when SQUAREM is
stopped at the same distance from the optimum as the default, it is slower.
The default (no acceleration) is the recommended setting.

Mapping order within RAD records

With --writeRad (and in piscem's own RAD output), the mappings of a
single-end or orphaned read are now listed in transcript-id order (forward
before reverse for the same transcript) instead of hash order. The set of
mappings and their contents are unchanged, paired-end records were already in
this order, and quantification is unaffected.

Single-end reads and orphans

How a single-end read or an orphan (one mate mapped) is weighted by the
fragments that could have produced it was wrong in the default two-pass path,
and depended on how -l was spelled:

  • The intermediate RAD stored a reverse-strand read's right end where the
    quantifier expects its leftmost base (piscem's convention, which salmon now
    follows explicitly), so the "unseen mate must fit" bound and the read's
    5'-end sequence-bias context were a read length off. Reconstructed 5' ends
    on simulated reads: reverse single-end 0% → 98% exact, orphans 60% → 98%.
  • Whether that bound applied was decided by -l (-l A applied it, -l U,
    SF and SR did not); it now follows each read's actual mapping type, so
    -l A and an explicit single-end type give the same result.
  • Departures from C++ salmon. For single-end libraries the bound now uses
    the --fldMean/--fldSD default prior widened to an SD of at least 0.4×
    the mean, unless you pass --fldMean/--fldSD, which are then used as
    given; a narrow default that is too long for the library otherwise crushes
    every read near a transcript end. For orphans the bound is mixed 50/50 with
    "the mate lies off the transcript" (the reason it did not map), capping the
    penalty at ln 2.

Against simulated truth (fragment lengths 180±40, 250±25 and 320±60) at
default settings, single-end Spearman improves on every library type, e.g.
0.774 → 0.809 (stranded, -l SR) and 0.753 → 0.800 (unstranded, -l U),
and is never significantly worse than 2.8.0 with -l A; paired-end results
change only through orphans (MARD −0.0006). If you know your library's
fragment-length distribution, passing it improves single-end accuracy
further.

Everything else

Apart from the changes above, results are byte-identical to 2.8.0:
selective-alignment mapping output, the mappings themselves in --sketch
mode (only their order within single-end/orphan records changes), equivalence
classes, and quant.sf under --useEM (verified on GENCODE v50 and Ensembl,
paired-end without orphans).

Indexes

  • Format v3. sshash-lib 0.8 changes the dictionary format (6.0). Indexes
    built by salmon 2.7.0–2.8.0 (format v2) are refused with a rebuild command.
  • Reproducible builds. Contigs are numbered by their first appearance in
    the reference, so building the same index twice gives identical files.
  • Memory. Peak RSS of salmon index is lower than 2.8.0's: GENCODE v50
    6.5 → 4.9 GB, Ensembl cDNA 3.1 → 2.6 GB (16 threads). cf1-rs now uses
    sux's PHastR minimal perfect hash for both in-memory and external-memory
    builds; genome-scale (decoy) cDBG construction is about 1.5–1.9× faster.

Fixes

  • --noLengthCorrection takes length out of the model again (#1243).
    Since 2.0 the flag used each transcript's raw length as its effective
    length, so estimates still depended on length, just through a different
    divisor. This is the opposite of what the flag is for: 3' tagged-end
    protocols such as QuantSeq, where a transcript's read count does not grow
    with its length. It also moved reads between isoforms: with a short isoform
    sharing a long one's 3' end, 2.8.0 gave the short isoform 3750 of 5000
    reads where salmon 1.x gives it none. Every transcript now gets the same
    effective length, 100, as in salmon 1.x. This holds in the EM, in
    quant.sf's EffectiveLength and TPM, and in the online phase's
    position term, for reads, --sketch, --deterministic, --rad and -a
    input. On GEUVADIS (10M pairs, --noLengthCorrection), agreement with
    C++ salmon goes from Pearson 0.59 to 0.97 on read counts. Runs without the
    flag are unchanged.
  • --noEffectiveLengthCorrection (new, from C++ salmon) divides by each
    transcript's raw length instead of its fragment-length-adjusted effective
    length. It reproduces what --noLengthCorrection did in 2.0–2.8, for
    anyone who relied on that. Like --noLengthCorrection, it is refused
    together with bias correction, and --noLengthCorrection wins if both are
    given.
  • Security advisories in zstd. RustSec published advisories against the
    zstd crates on 2026-10-16. salmon now uses zstd 0.14.3 / zstd-safe 8.2.0 /
    zstd-sys 2.1.2 throughout: for RAD chunk compression (libradicl 0.21.1),
    for compressed reference FASTA (needletail 0.7.4) and for compressed reads
    (niffler 3.0.2), and its dependencies no longer pull in sux's unused zstd
    support. cargo audit reports no vulnerabilities.
  • FASTA reads. Uncompressed FASTA read files were refused in reads mode
    as truncated FASTQ (the truncation check assumed four-line records). They
    are accepted again, and give the same results as the equivalent FASTQ.
  • Read and fragment positions in selective alignment (#1231, Benjamin
    Demaille). Positions are now derived from each read's projected ends rather
    than from its first and last matching k-mer, which cut a fragment short
    whenever a mismatch or indel fell near a read end. On simulated reads with
    known coordinates, reverse-strand mate positions go from 70% to 98% exact,
    fragment lengths from −2.0 bp mean error to +0.02 bp, and positions of
    reads overhanging a transcript end from ~65% to 98.5% exact; dovetailed
    fragments are now detected at the true rate. Quantification accuracy
    against truth improves slightly (MARD −0.001, with or without --seqBias).
    Visible effect on real data: the estimated fragment-length mean is
    ~2–3 bp higher (e.g. 170.1 → 172.7 on GEUVADIS). This is the estimator,
    not the positions: salmon learns the distribution from uniquely mapping
    pairs, which are longer on average than all fragments; the old positions'
    downward bias happened to cancel that. --sketch is unaffected.
  • Other fixes from #1231 (Benjamin Demaille):
    • Paired-end sequence-bias contexts under --online used mate 1's
      orientation for the whole fragment, putting the reverse model at the
      fragment's leftmost base whenever mate 1 was the reverse read (half of
      IU pairs, all of ISR).
    • Reverse-strand single-end reads used an off-by-one (exclusive) end
      coordinate, and reverse orphans used their leftmost base as the 5' end.
    • Recovered orphan pairs (--recoverOrphans) now get correct positions.
    • The reference sequence store is checked against the index when loaded.
    • Ambiguity counts are accumulated in 64 bits.
    • --writeMappings no longer emits zero-length 0M CIGAR operations.
    • The bias convolutions guard a zero lower fragment-length bound.
    • Ensembl GFF3 transcripts resolve to genes through Parent.
    • The Gibbs sampler's prior honours --perNucleotidePrior.
    • An EM in which no transcript is above the cutoff counts as converged,
      and the convergence check uses the new iterate, as C++ salmon does.
    • Numeric quant arguments are validated, and C++'s negative --mp form
      is accepted.
    • Gibbs sampling reuses its buffers between rounds, and the
      fragment-length distribution is updated once per fragment (speed only).
  • Soft-clipping at transcript ends (#1238, fixed in #1239, Benjamin
    Demaille). With --softclip / --softclipOverhangs, a read overhanging
    the end of a transcript is now scored with the overhang clipped, as the
    option promises; default scoring (no soft-clipping) is unchanged. This
    matters most for short references: in the issue's miRNA example, reads of
    miR-21-5p with 4–6 nt untemplated 3' tails were all dropped; they now
    map.

Acknowledgements

This release was prompted by, and borrows from,
kallistors (Denis Demin), a Rust
reimplementation of kallisto whose 0.6.0 release reported quantification 2–3×
faster than salmon 2.8.0's --sketch mode on some datasets. We used its
published benchmark setup (SRR13638690 trimmed with its fastp settings,
GENCODE v50) to reproduce that comparison, and studied its implementation.

  • The new EM schedule is adopted from kallistors. Iterating each connected component in
    cache-sized tiles while reproducing the global stopping iteration, folding
    single-transcript classes into constants, and renumbering active
    transcripts densely — we found this was the whole of its EM speed advantage
    and adopted it in salmon-infer (components module).

Thanks to Denis Demin for making kallistors and its benchmarks openly
available, and to Benjamin Demaille for the #1231 and #1239 fixes. The remaining
optimizations (those for closing the mapping speed gap) are tailored to the salmon
index (piscem-rs, adopting sshash-rs) and are not adopted from elsewhere.

Dependencies

piscem-rs 0.12.1, sshash-lib 0.8.1 (sux 0.15 / epserde 0.13), cf1-rs
0.6, bramble-rs 0.2, libradicl 0.21.1 (with its faster lz4 paths; RAD files
are unchanged), noodles 0.119
(noodles-bam 0.98, noodles-sam 0.93, noodles-bgzf 0.52), paraseq-temp
0.6.2-pre.1, needletail 0.7.4, niffler 3.0.2, zstd 0.14.3, and new: sirocco 0.3 (single-threaded gzip
decoding), plus semver-compatible updates. Each noodles crate now appears
once in the dependency graph.

For library users

  • salmon-infer: the default EM/VBEM path runs on a new component engine
    (components module); the accelerated paths keep the sharded kernels.
  • salmon-eqclass: the class builder is a sharded hash table with lookups by
    borrowed label.
  • piscem-rs 0.12: MappingCache::hit_map is a HitMap (see the piscem-rs
    changelog).

Full details: docs/release-notes-2.9.0.md.


Install salmon-cli 2.9.0

Install prebuilt binaries via shell script

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/COMBINE-lab/salmon/releases/download/v2.9.0/salmon-cli-installer.sh | sh

Download salmon-cli 2.9.0

File Platform Checksum
salmon-cli-aarch64-apple-darwin.tar.xz Apple Silicon macOS checksum
salmon-cli-x86_64-apple-darwin.tar.xz Intel macOS checksum
salmon-cli-aarch64-unknown-linux-gnu.tar.xz ARM64 Linux checksum
salmon-cli-x86_64-unknown-linux-gnu.tar.xz x64 Linux checksum