Searching LIGO data for 20 cents a week


divyanshgolyan.com/posts/ligo-20-cents/

The next interesting thing in LIGO’s data might be hiding in a search nobody has run because it costs too much. LIGO, Virgo and KAGRA used about 700 million CPU core-hours in a single year of their last observing run. So I tried to make the search cheap. It hasn’t found anything new yet, but it’s now cheap enough to try what nobody has.

Mine searches a week of data from both LIGO detectors for about $0.20 on a rented GPU. PyCBC, one of LIGO’s standard pipelines, needs 182 CPU core-hours for the same week. Here’s how it got there, one step at a time.

Cost per week of data, log scale. PyCBC on CPUs $2.73; PyCBC on a tuned GPU about $0.72, projected; mine $0.52, $0.30, $0.142; 5x below the tuned GPU projection.
Each dot is one change, measured on real data. The hollow dot is a projection. The grey band is PyCBC at CPU prices from $0.01 to $0.03 a core-hour.

A search is a giant pattern match

Physicists can predict the chirp that two merging black holes make, so the search keeps a few thousand predicted chirps, called templates, and slides each one across every moment of the data, looking for a fit. The fit score is the signal-to-noise ratio, or SNR: how much louder the match is than noise alone would make it. Noise scores 1 to 3. Real mergers score 10 to 30.

But noise sometimes fakes a fit, so the search needs to know how often. A real signal reaches both LIGO detectors within milliseconds. So the search shifts one detector’s data by a few seconds, where nothing real can line up in both, and counts the fits it still finds (false positives).

Every search does three things to each stretch of data:

  1. Clean it. The raw data rumbles at low pitches and has spikes. Cleaning cuts the rumble, mutes the spikes and measures how noisy each pitch is, so every pitch counts fairly.
  2. Search it. Slide every template across the cleaned data and score the fit at each moment.
  3. Check the loud fits. A glitch can score high without looking like a chirp. So for each loud fit, the search splits the template into pieces by pitch and asks whether each piece fits as well as it should. A real chirp fits evenly. A glitch dumps its score into one piece.

PyCBC does step 2 one template at a time, on CPUs: 182 core-hours a week. Moving it to a GPU helps less than you’d think. In my test, its GPU mode searched 16x faster, but each job got only 2x faster, because the cleaning still ran on the CPU, repeated for every slice of the templates. Tuned well, I project it at about $0.72 a week, the hollow dot. Getting below that took three changes.

$0.52: one big matrix multiply

Step 2 is the expensive one. Sliding a template across every moment sounds slow, but a Fourier transform (FFT) does every slide at once. The standard way runs one FFT per template, thousands for every stretch of data.

Now, most templates look alike. Compress them with a singular value decomposition (SVD, a standard way to find the shapes a set of signals shares) and a few basic shapes can rebuild all of them, the way a JPEG stores a few patterns plus weights instead of every pixel. The search then needs one FFT per shape instead of one per template. Rebuilding every template’s score from the shapes’ scores is one large matrix multiply (which is exactly what GPUs are good at).

4,612 templates compress to 73 basis shapes; the search is one multiply of a 4,612 x 73 matrix by a 73 x moments matrix.
Templates from my main set, drawn as simple chirps (the last quarter second before the merger). The matrices are to scale, except the moments axis, which is cut short.

The multiply has a catch. It produces a score for every template at every moment: billions of numbers, almost all noise. Writing them to the GPU’s memory and reading them back takes longer than the math. So I (well, Claude) wrote a GPU kernel that does the multiply and, while the numbers are still on the chip, keeps only the best score in every 128 moments (about 1/16 of a second). It ran about 7x faster than the standard multiply, followed up by a separate pass to find the peaks.

Each small square is a block of scores. Standard: 128 blocks go out to GPU memory and 128 come back. Mine: only the best of every 128 is kept on the chip, so 1 block goes to memory; about 7x faster.
Each small square is a block of scores moved between the GPU’s chip and its memory.

That got a week down to $0.52. But now searching took 4.5 GPU-minutes, and checking took 34.

$0.30: exact checks only when loud

The checks ran on every fit above SNR 5, millions a week, most of them noise. But the compressed search already gives every fit a quick score, and for 99 fits in 100 it lands within 1% of the exact one. So the quiet fits keep their quick score, and only loud fits get the exact score and the glitch checks. On my test data, that cut the fits getting exact checks from 2.1 million to 0.33 million.

Each check was slow too, because it read a whole stored template from memory. Now the chip rebuilds the template from a few stored points, which made the checks 11x faster. This is a different compression from the SVD: that one shares shapes across templates, while this one shrinks each template itself.

Before: each template stored in full, about a million numbers. After: two smooth curves stored at a few points, about 7,500 numbers, and the chip redraws the template from them.
A template wiggles fast, but it is a smooth loudness curve and a smooth timing curve combined, and those take a few points to pin down. Simplified chirp, for illustration. With this and a cheaper way to set up the checks, they went from 200 ms to 17.7 ms per block of 1,024 templates.

Together, that brought a week to $0.30. Now every step was fast, but the GPU sat idle most of the time.

$0.142: keep the GPU busy

It was waiting: for downloads, for the CPU to clean the next batch, or for files to be written. It was busy 27% of the time, and I was paying for 100% (mom would be very disappointed).

The biggest fix by far: the GPU made 200 million weak fits per batch, then sat idle while the CPU sorted and threw most of them away. Now the GPU drops them itself, and 23 million reach the CPU. Cleaning also moved onto the GPU, from 26 to 39 seconds a piece to 3.

Share of time the GPU was busy: 27%, 32%, 59% after dropping weak fits on the GPU (+27, the biggest step), 66%, 71%.
Each step measured on an A40, one run each. At 71% busy, a week costs $0.142.

Looking back, every fix so far had the same thing in common.

Memory, not math

Each one moved less data, not less math. On an A40, the big FFTs used about 5% of the card’s math power. The rest of the time, the cores waited for data from memory.

That also decides which GPU to rent: the one with the most memory speed per dollar. In my tests, an RTX 4090, a gaming card, was 1.6 times cheaper per result than the A40. The H100 was the fastest card I tried, and 3.1 times more expensive per result.

Card GB/s per $ an hour Cost per result
RTX 4090 1,024 0.62x
A40 944 1x
H100 744 3.1x

But a cheap search is worthless if it misses things.

99 of 101

I ran mine over all of O4a and O4b, the 232 days in which both LIGO detectors were recording. PyCBC found 101 mergers there with high confidence, and mine found 99 of them.

Both misses were borderline. My search saw GW241116_151753 in both of its template sets, but running two sets costs a small penalty, and that pushed it just over 1 false alarm a year. It also had data from Virgo, the European detector, which I don’t use. PyCBC was the only pipeline sure about GW231119_075248: GstLAL and MBTA, two other LIGO pipelines, put it at 0.5 and 1.9 false alarms a year, and mine at 6.

The 101 mergers PyCBC found with high confidence in O4a and O4b. Mine found 99; missed GW231119_075248 (only PyCBC was sure) and GW241116_151753 (just over 1 false alarm a year).
Each line is a merger PyCBC is confident about, from May 2023 to January 2025.

Nothing new, yet

Sadly, it found nothing new in O4a and O4b. But O4c comes out on 16 December. At 20 cents a week, I can run the searches nobody has tried!

How I measured

My cost is the steady rate. Filtering one week of data from both LIGO detectors takes 868 GPU-seconds on an Nvidia A40 at $0.59 an hour, which is $0.142; ranking and the rest bring it to about $0.20. The steps in the first figure are from my lab notes: $0.52 is the first full week, $0.30 is one day scaled to a week, and $0.142 is the steady rate. The $0.52 and $0.30 steps searched only my 4,612 standard templates; $0.142 and PyCBC’s 182 core-hours cover all 5,880, including 1,268 for oval orbits. The full O4a and O4b run cost about $11 as billed, including restarts and tests, and O4b ran at $0.26 a week because its GPUs waited on downloads. PyCBC’s 182 core-hours is measured with PyCBC 2.8.3 on an Intel Xeon Gold 6342; it agrees with the published figure in Soni & Nitz, Table 4. The GPU projection comes from 8 PyCBC processes sharing one A40, with the repeated cleaning removed. The GPU comparison is from a kernel test, with the 4090 at RunPod’s $0.89 an hour; on O4b, an RTX 3090 at $0.19 an hour searched a week for $0.141, one machine over two weeks. “High confidence” means PyCBC’s false-alarm rate is below 1 per 100 years, from the GWOSC event pages on 29 October 2026. I used only the two LIGO detectors, not Virgo. Code, data and results are on GitHub and archived at doi:10.5281/zenodo.23761181.