Kilosort4: pipeline
The stages below follow the paper (Pachitariu et al. 2024, Methods) and the published defaults. Implemented stages show the dsp-kitchen API; the others are described so the remaining work is clear. Where the paper is silent and we had to choose, the choice is named. Measured results are in Benchmarks.
Input to every stage is preprocessed data in batches of batch_size = 60000 samples with halos
on each side, in whitened units (thresholds are multiples of the noise σ).
Running over a recording — implemented
Section titled “Running over a recording — implemented”use dspk_sorter_kilosort::ks4::{Kilosort4, Kilosort4Config};
let result = Kilosort4::new(Kilosort4Config::default()).run(&client, &recording, &probe)?;let sorting = result.to_sorting_output(Some(probe)); // one unit per cluster, with templatesOne runner serves Kilosort4 and EMUsort (run_plan with RunPlan::kilosort4 /
RunPlan::emusort). Batches are the windows of a dspk-core ChunkSchedule (batch_size, with
halos covering the filter’s settling, nt and, for EMUsort, the largest channel delay), streamed
by dspk_core::WindowLoader (the next window is read while the device works) through a
PipelineWorkspace, so memory is bounded whatever the recording length. The passes, each a stage
of the progress report:
- Fit: every
nskip-th window except the last (upstreamrange(0, n_batches − 1, nskip)), high-passed (and CAR): the whitening second moment and, for EMUsort, the channel-delay cross-correlation accumulate on the device and come back once. - Templates: every
nskip-th window (upstreamrange(0, n_batches, nskip): a recording ofnskipwindows or fewer is learned from window 0, as upstream). Each window comes down once for clip extraction on the host; only if too few clips are found are the other windows scanned. - Detection: every window, on the device; only spikes come back. Each window keeps its own
spikes (
HaloWindow::remap_event). - Clustering of the detected spikes, section by section (stage 4 below), and the learned templates (stage 5).
- Matching: every window again, matched against the learned templates (stage 6).
- Clustering of the matched spikes into the final units (stage 7), then the global merges (stage 8); duplicate spikes are removed and units labelled (stage 9).
Result (Kilosort4Result) |
|
|---|---|
fitted (FittedPreprocessing) |
the preprocessing Pipeline, SpatialWhitening, channel_delays (EMUsort), schedule and halos, and the FitSettings they were fitted with |
templates |
the universal templates (wPCA, wTEMP) |
detected, first_clusters |
the universal-template spikes and their first clustering |
learned |
the learned templates (LearnedTemplates) |
spikes, clusters |
the matched spikes (sample, template, amplitude, position, features) and their units |
merges, duplicates |
global merges applied, spikes removed as duplicates |
reproducible, device |
whether the run pinned its result-changing tuned choices, and the device it ran on |
to_sorting_output(probe) |
one unit per cluster: spike times, amplitudes (whitened σ), positions, and a waveform template (mean PC features × wPCA); primary channel where that template is largest; label good / mua from the unit’s auto-correlogram (stage 9); EMUsort’s composite score; no SNR in µV (amplitudes are whitened σ) |
A run reuses an earlier run’s preprocessing (RunPlan::fitted)
when the fit settings match, e.g. to compare learned and predefined templates without a second fit.
Reproducibility. With reproducible = true (the default) the autotuned choices that change the
numbers (IIR time blocks, matrix-product routine) are pinned, k-means++ draws come from a seeded
generator, and every reduction is ordered: the same input on the same device gives the same spikes
and units (checked run to run on both test recordings).
1. Preprocessing — implemented (approximation)
Section titled “1. Preprocessing — implemented (approximation)”Three switches build the filter chain, in this order: common average reference (do_car; a mean,
upstream may use a median), one Butterworth band-pass (do_bandpass, bandpass_low_hz …
bandpass_high_hz, BANDPASS_ORDER = 3 per edge, forward-backward), and a line-noise notch
(do_notch, notch_hz, notch_q). Kilosort4’s defaults: CAR on, the action-potential band
300–6000 Hz (NeuralBand::Ap), notch off. Upstream Kilosort4 high-passes at 300 Hz with no upper
edge; the 6 kHz upper edge is ours (decided 2026-10-09). Then local whitening over
the whitening_range = 32 nearest channels from the uncentred second moment X Xᵀ / n of each
fit window’s interior, averaged with equal weight per window, as upstream get_whitening_matrix
(SecondMomentAccumulator, SpatialWhitening::local_knn_from_covariance, WHITENING_EPSILON).
2. Universal templates — implemented
Section titled “2. Universal templates — implemented”Learned from the data (default):
- Clips. On every
nskip-th batch, a sample is a peak when|x|is the maximum over ±4 channel indices × ±5 samples (upstreamloc_range = [4, 5]over[channels, samples]) and aboveTh_single_ch; peaks with another peak within ±6 channel indices × ±nt / 2samples are dropped (only isolated ones are kept), as are the first and lastntsamples. Each clip isx[ch, t − nt0min .. t − nt0min + nt]; at most 500 000. - Scale (
ClipScaling): Kilosort4 scales each clip to unit norm, so k-means groups shapes alone; EMUsort’s fork scales all clips by one factor (sqrt(std of ‖clip‖²)), keeping relative amplitudes. wPCA: the topn_pcsright singular vectors of the clip matrix (not centred).wTEMP: k-means centres (n_templates, 10 initialisations), rows normalized to unit length.
Clips are found on the host (extract_clips); learning runs on the device from one upload of the
scaled clips: the nt × nt Gram matrix, its eigendecomposition, and k-means. Against Kilosort4’s
saved wPCA / wTEMP on the Neuropixels test recording, every learned row has a best |cosine| of
0.94–1.00.
Predefined (templates_from_data = false): UniversalTemplates::from_npz(path) reads
Kilosort4’s wTEMP.npz (fetched and verified through dspk-hub
with id kilosort4/wtemp-v1).
3. Universal-template detection — implemented (device)
Section titled “3. Universal-template detection — implemented (device)”- Template centres (once per probe): on each shank, a grid every
dmin / 2vertically (dmin= median vertical contact spacing unless set) and withround((xmax − xmin) / (dminx / 2)) + 1columns (dminx = 32µm). Each centre uses itsnearest_chans = 10contacts; centres farther thanmax_channel_distance = 32µm from every contact are dropped. Spatial weightsexp(−d² / σ²)fortemplate_sizes = 5widthsσ = min_template_size · (s + 1), normalized per centre. On the Neuropixels test probe the 1532 centres equal Kilosort4’s (xcup,ycup); their channel sets differ only where channels are equidistant. - Responses. Every channel is correlated with every
wTEMProw; for each centre, the weighted sum over its contacts is taken for every size and template, and the largest magnitude kept. - Local maxima over the
nearest_templates = 100neighbouring centres and ±nt0minsamples, aboveTh_universal = 9. Exact ties keep one spike (drop_tied_peaks): where contacts are far apart compared with the template sizes (an HD-EMG grid: 100 µm pitch, centres every 16 µm), several centres’ weights collapse onto one contact and report bit-identical responses; the paper asks that “no spike is detected twice”, so the lowest centre of a tie is kept. (On the HD-EMG test recording, 56 000 of 94 000 detections were such copies.) - Per spike: amplitude, template, centre, features (the centre’s
nearest_chanssnippets projected ontowPCA), and a position: the centre of mass of the centre’s contacts weighted by the rectified template response, in x and y (the paper’s spike position). Time is the waveform’s trough,t − nt/2 + nt0minfor a correlation peak att, as Kilosort4 reports spike times.
4. Clustering — implemented (device)
Section titled “4. Clustering — implemented (device)”kilosort4::clustering::cluster_spikes, written from the paper’s Graph-based clustering:
- Sections. Spikes are binned by vertical position (
section_um = 40), per shank, and embedded on the union of their centres’ channels ([slots · n_pcs]features, 0 elsewhere), built on the device from the compact features. - Bipartite k-NN graph (
dspk_synapse::sorting::bipartite): each spike links to its 10 nearest neighbours among a subset (everycluster_downsampling = 20-th spike, at most 25 000), so neighbourhood scales do not shrink as recordings grow. - Assignment. From 200 k-means++ seeds, right nodes then left nodes take the cluster maximizing
the paper’s bipartite modularity gain
n_tc − γ·k_t·K_c / 2m(γ = 1), until no spike moves. - Merging tree. The pair of clusters with the highest
γ̂ = 2m·K_ij / (k_i·k_j)is merged, repeatedly (host; at most 200 leaves). - Decisions from the top: a node splits when
γ̂ < 0.2or when its halves are bimodal along their weighted regression axis (score ≥ 0.6: 400 bins over[−2, 2], Gaussian smoothing of 4 bins, trough in bins 175–225, score1 − max(x_min/x₁, x_min/x₂)). Sections with fewer than 1000 spikes are one unit.
Choices of ours: the regression has a bias term (the paper’s axis passes through the origin; spike features are not centred, and without it a half sitting at the origin projects into the trough); a node only joins a cluster among its neighbours’; ties go to the smallest label.
5. Learned templates — implemented (device)
Section titled “5. Learned templates — implemented (device)”kilosort4::learned::learned_templates, from the paper’s Template learning: each unit’s template
(its mean PC features) is aligned in time by its best correlation with the wTEMP prototypes,
then templates whose correlation, maximized over lags, is ≥ 0.9 and whose norms differ by less than
20% are merged (largest units first, in passes until nothing merges). All pairs at all lags are
one matmul in PC space plus one kernel (see the GPU case study).
6. Learned-template matching — implemented (device)
Section titled “6. Learned-template matching — implemented (device)”kilosort4::matching::TemplateMatcher, from the paper’s Spike detection with learned templates and
matching pursuit and Extracting PC features with background subtraction:
- Scores: every channel correlated with the
wPCArows, then onematmulwith the unit-norm templates in PC form gives every template’s projectionc_j(t)at every sample. - Matching pursuit,
max_peels = 50rounds: per sample, the template explaining the most variance at its average norm,V = 2·μ·c − μ²; spikes whereVis the maximum over ±ntandc ≥ Th_learned = 8; their contributions are subtracted from the data and, through the template products at every lag, from the scores. - Features: the residual’s PC projections plus the spike’s own template contribution.
Choices of ours: a template’s average norm is its mean waveform’s norm; a matched spike is placed at its template’s position (centre of mass of its channel energies).
7. Clustering of the matched spikes — implemented (device)
Section titled “7. Clustering of the matched spikes — implemented (device)”The matched spikes’ features are clustered as in stage 4, giving the final units, with the paper’s
second criterion: before a node’s bimodality is tested, its halves’ cross-correlogram is
(dspk_synapse::metrics::compute_ccg_refractory). The paper’s Refractory auto- and cross-correlograms: 1 ms
bins over ±0.5 s; with n_k the coincidences in the central −k..k bins and R the baseline per
bin (the larger shoulder), R12 = min_k n_k / ((2k+1)·R) and Q12 = min_k ½(1 + erf((n_k − λ_k) / √(ε + 2λ_k))), λ_k = (2k+1)·R; a CCG is refractory when R12 < 0.25 and Q12 < 0.05.
A refractory pair is not split. The paper’s text says the opposite (“If the pair of units has a
refractory cross-correlogram, then the split is always performed”). Two halves that never fire
within a few ms of each other behave as one neuron, which is how the paper’s own global merges use
the same test (“A merge is performed if the CCG is refractory”), so we keep them together.
Choices of ours where the paper is silent: k up to 10 bins (10 ms) and shoulders beyond 250 ms
(RefractoryOptions).
8. Global merges — implemented
Section titled “8. Global merges — implemented”kilosort4::merges::global_merges, from the paper’s Global merges: units are taken in decreasing
spike count; every unit still in the pool whose waveform similarity (correlation maximized over
lags, computed in PC space on the device as for the learned templates) exceeds
global_merge.min_similarity = 0.5 is tested, most similar first, and merged when their CCG is
refractory; a merged unit is tested again, and a unit that merges with nothing is complete and
leaves the pool. Choice of ours: a merged template is the spike-count-weighted mean (no shift).
9. Duplicates and labels — implemented
Section titled “9. Duplicates and labels — implemented”- Duplicates: a unit’s spike within
duplicate_spike_ms = 0.25ms of its previous kept spike is removed (matching pursuit re-matching a template’s own residual). EMUsort’sduplicate_spike_bins = 7is the same setting in samples (0.23 ms at 30 kHz). - Labels: a unit is good when its auto-correlogram is refractory (
R12 < 0.2,Q12 < 0.2; the paper’s Methods say 0.1, Kilosort4 runs withacg_threshold = 0.2), else mua; written to Phy’scluster_KSLabel.tsvandcluster_group.tsv. - Composite score: EMUsort’s per-unit score (see the EMUsort pipeline),
written to
cluster_info.tsv(composite_score).
10. Not yet
Section titled “10. Not yet”- Drift correction. On the Neuropixels test recording Kilosort4’s own correction is ±0.5 µm
(
ops['dshift']), so it does not explain any difference there.