matchedfilter 0.1.0a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matchedfilter-0.1.0a1/LICENSE +21 -0
- matchedfilter-0.1.0a1/MANIFEST.in +11 -0
- matchedfilter-0.1.0a1/PKG-INFO +171 -0
- matchedfilter-0.1.0a1/README.md +147 -0
- matchedfilter-0.1.0a1/docs/design.md +72 -0
- matchedfilter-0.1.0a1/docs/hierarchical.md +284 -0
- matchedfilter-0.1.0a1/docs/machine-notes.md +881 -0
- matchedfilter-0.1.0a1/pyproject.toml +67 -0
- matchedfilter-0.1.0a1/python/matchedfilter/__init__.py +329 -0
- matchedfilter-0.1.0a1/python/matchedfilter/_core.c +268 -0
- matchedfilter-0.1.0a1/python/matchedfilter/benchmark.py +228 -0
- matchedfilter-0.1.0a1/python/matchedfilter/matchedfilter.h +165 -0
- matchedfilter-0.1.0a1/python/matchedfilter.egg-info/PKG-INFO +171 -0
- matchedfilter-0.1.0a1/python/matchedfilter.egg-info/SOURCES.txt +37 -0
- matchedfilter-0.1.0a1/python/matchedfilter.egg-info/dependency_links.txt +1 -0
- matchedfilter-0.1.0a1/python/matchedfilter.egg-info/requires.txt +7 -0
- matchedfilter-0.1.0a1/python/matchedfilter.egg-info/top_level.txt +1 -0
- matchedfilter-0.1.0a1/setup.cfg +4 -0
- matchedfilter-0.1.0a1/setup.py +74 -0
- matchedfilter-0.1.0a1/src/backend.h +36 -0
- matchedfilter-0.1.0a1/src/balanced.c +781 -0
- matchedfilter-0.1.0a1/src/be_avx512.c +151 -0
- matchedfilter-0.1.0a1/src/codelets.h +10992 -0
- matchedfilter-0.1.0a1/src/dispatch.c +153 -0
- matchedfilter-0.1.0a1/src/elemfft.h +156 -0
- matchedfilter-0.1.0a1/src/gen.py +351 -0
- matchedfilter-0.1.0a1/src/hmf.c +678 -0
- matchedfilter-0.1.0a1/src/hmf_table.h +364 -0
- matchedfilter-0.1.0a1/src/internal.h +60 -0
- matchedfilter-0.1.0a1/src/kernel1024.c +355 -0
- matchedfilter-0.1.0a1/src/matchfilt.c +210 -0
- matchedfilter-0.1.0a1/src/simd.h +215 -0
- matchedfilter-0.1.0a1/src/transform.h +49 -0
- matchedfilter-0.1.0a1/src/transpose16.h +27 -0
- matchedfilter-0.1.0a1/tests/test_api.py +523 -0
- matchedfilter-0.1.0a1/tests/test_internals.py +66 -0
- matchedfilter-0.1.0a1/tests/test_units.c +325 -0
- matchedfilter-0.1.0a1/tests/testutil.h +35 -0
- matchedfilter-0.1.0a1/tools/hmf_design.py +474 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Nitz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# setuptools only guesses at Python files, so every C source the build needs
|
|
2
|
+
# has to be named here. Without this the sdist builds fine and then fails to
|
|
3
|
+
# install: the extension sources are simply absent.
|
|
4
|
+
include README.md LICENSE
|
|
5
|
+
recursive-include src *.c *.h *.py
|
|
6
|
+
recursive-include python *.c *.h
|
|
7
|
+
recursive-include tests *.c *.h *.py
|
|
8
|
+
recursive-include tools *.py
|
|
9
|
+
recursive-include docs *.md
|
|
10
|
+
prune build
|
|
11
|
+
global-exclude *.pyc *.so *.o __pycache__
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: matchedfilter
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Fast single-threaded batched matched filter with peak-only output (x86-64)
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/ahnitz/matchedfilter
|
|
7
|
+
Keywords: matched-filter,correlation,fft,avx512,signal-processing
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: C
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: numpy>=1.20
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest; extra == "test"
|
|
21
|
+
Provides-Extra: bench
|
|
22
|
+
Requires-Dist: numpy>=1.20; extra == "bench"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# matchedfilter
|
|
26
|
+
|
|
27
|
+
A fast single-threaded matched filter for x86. You give it a batch of data
|
|
28
|
+
segments and a batch of templates; it correlates every pair and hands back only
|
|
29
|
+
the peaks.
|
|
30
|
+
|
|
31
|
+
Not returning the full correlation is the point. Most searches threshold the
|
|
32
|
+
output and throw the rest away, and once you say so up front the filter can
|
|
33
|
+
skip work that could not have produced a peak anyway.
|
|
34
|
+
|
|
35
|
+
> **Status: work in progress.** The API still moves, and there is a known
|
|
36
|
+
> calibration weakness in the hierarchical filter. See
|
|
37
|
+
> [Caveats](#caveats).
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import matchedfilter as mf
|
|
41
|
+
|
|
42
|
+
filt = mf.MatchedFilter(16384, ndata=16, ntemplates=64)
|
|
43
|
+
filt.set_data(data_spectra) # (16, 16384) complex64, already FFT'd
|
|
44
|
+
filt.set_templates(template_spectra) # (64, 16384) complex64
|
|
45
|
+
|
|
46
|
+
peaks = filt.run(binsize=1024, threshold=5.5)
|
|
47
|
+
peaks["index"], peaks["value"], peaks["magnitude"]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`import matchedfilter as mf` is the convention used throughout these docs.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install --pre matchedfilter
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`--pre` because this is an alpha release. Wheels are built for CPython 3.9 to
|
|
59
|
+
3.13 on manylinux x86-64; anywhere else pip falls back to the source
|
|
60
|
+
distribution, which needs numpy and a C compiler.
|
|
61
|
+
|
|
62
|
+
**x86-64 only for now.** The kernels are AVX2 and AVX-512 intrinsics with no
|
|
63
|
+
portable fallback, so a build on any other architecture stops with an error
|
|
64
|
+
rather than producing a slow one. AVX-512 is used when the CPU has it and AVX2
|
|
65
|
+
otherwise, decided at runtime.
|
|
66
|
+
|
|
67
|
+
## How it works
|
|
68
|
+
|
|
69
|
+
Inputs are **frequency domain**: the unnormalised forward transform of each
|
|
70
|
+
segment, in natural order. Produce them with whatever you already use (numpy,
|
|
71
|
+
MKL, FFTW); matchedfilter does not need to own that step.
|
|
72
|
+
|
|
73
|
+
The filter is built once and reused. Ingest conjugates the templates and
|
|
74
|
+
stores both sides in the layout the correlation loop walks, which costs a few
|
|
75
|
+
percent of a run and less as the batch grows.
|
|
76
|
+
|
|
77
|
+
`run` returns a structured array of shape `(ndata, ntemplates, nbins)` with
|
|
78
|
+
fields `index`, `value` and `magnitude`. Bins whose peak fell below the
|
|
79
|
+
threshold carry `index == -1`.
|
|
80
|
+
|
|
81
|
+
Supported lengths are 1024 and the powers of two from 4096 to 1048576.
|
|
82
|
+
|
|
83
|
+
### Performance
|
|
84
|
+
|
|
85
|
+
Per (data, template) pair, 8x32 batch, one core of a Zen 5 desktop:
|
|
86
|
+
|
|
87
|
+
| n | matchedfilter | numpy | |
|
|
88
|
+
|---:|---:|---:|---:|
|
|
89
|
+
| 1024 | 0.61 µs | 18.98 µs | 31x |
|
|
90
|
+
| 4096 | 2.08 µs | 37.81 µs | 18x |
|
|
91
|
+
| 16384 | 9.98 µs | 127.04 µs | 13x |
|
|
92
|
+
| 65536 | 48.87 µs | 568.56 µs | 12x |
|
|
93
|
+
|
|
94
|
+
numpy is a floor, not a rival. It is there so the comparison runs anywhere.
|
|
95
|
+
Against MKL or FFTW the margin is much smaller, and part of what is left comes
|
|
96
|
+
from computing peaks instead of a full correlation. Measure on your own box:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python -m matchedfilter.benchmark
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Hierarchical filtering
|
|
103
|
+
|
|
104
|
+
`HierarchicalFilter` adds a cheap pre-pass: correlate against a low-frequency
|
|
105
|
+
slice of the template, and only run the full-length filter where that slice
|
|
106
|
+
leaves a peak plausible.
|
|
107
|
+
|
|
108
|
+
**This helps only under an assumption about your templates**: that enough of
|
|
109
|
+
the matched-filter output power sits in the low band that a narrow slice gives
|
|
110
|
+
a usable bound on the full result. For chirp-like templates whose power is
|
|
111
|
+
concentrated at low frequency that tends to hold. For templates whose power is
|
|
112
|
+
spread flat across the band, or concentrated high, the slice bounds nothing
|
|
113
|
+
useful, and the pre-pass is pure added cost. It is worth checking against your
|
|
114
|
+
own templates before relying on it.
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
hf = mf.HierarchicalFilter(16384, ndata=16, ntemplates=64,
|
|
118
|
+
snr=6.0, # threshold you intend to use
|
|
119
|
+
fd=1e-3, # false-dismissal budget
|
|
120
|
+
band=2048) # width of the cheap slice
|
|
121
|
+
|
|
122
|
+
hf.set_reference(expected_output_power) # real frequency series, the OUTPUT
|
|
123
|
+
hf.set_templates(template_spectra)
|
|
124
|
+
hf.set_data(data_spectra)
|
|
125
|
+
peaks = hf.run(binsize=16384, threshold=6.0)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`set_reference` takes a real frequency series of length `n` holding the expected
|
|
129
|
+
power of the filter **output** in each bin. Only its shape is used; the overall
|
|
130
|
+
normalisation is divided out.
|
|
131
|
+
|
|
132
|
+
This is the output, not the template. The two differ whenever the data is
|
|
133
|
+
coloured, and passing the template's own power will mis-set the gate: a
|
|
134
|
+
broadband template reconstructing a narrowband signal is the case where it goes
|
|
135
|
+
wrong by the largest factor.
|
|
136
|
+
|
|
137
|
+
The gate is one-sided by construction: peaks it reports are bit-identical to
|
|
138
|
+
the flat filter's. It can only omit, never invent. `fd` is the budget for how
|
|
139
|
+
often it is allowed to omit one.
|
|
140
|
+
|
|
141
|
+
On pure noise at n=4096 the gate runs about **7x faster** than the flat filter.
|
|
142
|
+
The saving scales with how little survives, so it grows with your threshold and
|
|
143
|
+
falls toward 1x on data where most pairs trigger.
|
|
144
|
+
|
|
145
|
+
## Caveats
|
|
146
|
+
|
|
147
|
+
- **The false-dismissal budget is not currently met at low thresholds.**
|
|
148
|
+
`fd` is honoured well at snr 6 and above. At snr 5.0 to 5.5 with a coarse
|
|
149
|
+
band the gate omits more than it should: 1.4% against a 0.1% budget in a
|
|
150
|
+
418-template search. The cause is that the gate's recovery factors are
|
|
151
|
+
measured from a mean frequency series, which is not a bound on any
|
|
152
|
+
individual realisation. Tracked by an `xfail` test in `tests/test_api.py` and written up
|
|
153
|
+
in [docs/hierarchical.md](docs/hierarchical.md).
|
|
154
|
+
- Single-threaded by design. Parallelism is the caller's to arrange.
|
|
155
|
+
- x86-64 Linux only, as above. Other architectures are not implemented rather
|
|
156
|
+
than merely untested.
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
pip install -e .[test]
|
|
162
|
+
pytest # includes a C test that builds itself from source
|
|
163
|
+
python -m matchedfilter.benchmark
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
[docs/](docs/) holds the design notes: how the hierarchical gate is
|
|
167
|
+
calibrated, and measurements of the approaches that were tried and rejected.
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
MIT
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# matchedfilter
|
|
2
|
+
|
|
3
|
+
A fast single-threaded matched filter for x86. You give it a batch of data
|
|
4
|
+
segments and a batch of templates; it correlates every pair and hands back only
|
|
5
|
+
the peaks.
|
|
6
|
+
|
|
7
|
+
Not returning the full correlation is the point. Most searches threshold the
|
|
8
|
+
output and throw the rest away, and once you say so up front the filter can
|
|
9
|
+
skip work that could not have produced a peak anyway.
|
|
10
|
+
|
|
11
|
+
> **Status: work in progress.** The API still moves, and there is a known
|
|
12
|
+
> calibration weakness in the hierarchical filter. See
|
|
13
|
+
> [Caveats](#caveats).
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import matchedfilter as mf
|
|
17
|
+
|
|
18
|
+
filt = mf.MatchedFilter(16384, ndata=16, ntemplates=64)
|
|
19
|
+
filt.set_data(data_spectra) # (16, 16384) complex64, already FFT'd
|
|
20
|
+
filt.set_templates(template_spectra) # (64, 16384) complex64
|
|
21
|
+
|
|
22
|
+
peaks = filt.run(binsize=1024, threshold=5.5)
|
|
23
|
+
peaks["index"], peaks["value"], peaks["magnitude"]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`import matchedfilter as mf` is the convention used throughout these docs.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install --pre matchedfilter
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`--pre` because this is an alpha release. Wheels are built for CPython 3.9 to
|
|
35
|
+
3.13 on manylinux x86-64; anywhere else pip falls back to the source
|
|
36
|
+
distribution, which needs numpy and a C compiler.
|
|
37
|
+
|
|
38
|
+
**x86-64 only for now.** The kernels are AVX2 and AVX-512 intrinsics with no
|
|
39
|
+
portable fallback, so a build on any other architecture stops with an error
|
|
40
|
+
rather than producing a slow one. AVX-512 is used when the CPU has it and AVX2
|
|
41
|
+
otherwise, decided at runtime.
|
|
42
|
+
|
|
43
|
+
## How it works
|
|
44
|
+
|
|
45
|
+
Inputs are **frequency domain**: the unnormalised forward transform of each
|
|
46
|
+
segment, in natural order. Produce them with whatever you already use (numpy,
|
|
47
|
+
MKL, FFTW); matchedfilter does not need to own that step.
|
|
48
|
+
|
|
49
|
+
The filter is built once and reused. Ingest conjugates the templates and
|
|
50
|
+
stores both sides in the layout the correlation loop walks, which costs a few
|
|
51
|
+
percent of a run and less as the batch grows.
|
|
52
|
+
|
|
53
|
+
`run` returns a structured array of shape `(ndata, ntemplates, nbins)` with
|
|
54
|
+
fields `index`, `value` and `magnitude`. Bins whose peak fell below the
|
|
55
|
+
threshold carry `index == -1`.
|
|
56
|
+
|
|
57
|
+
Supported lengths are 1024 and the powers of two from 4096 to 1048576.
|
|
58
|
+
|
|
59
|
+
### Performance
|
|
60
|
+
|
|
61
|
+
Per (data, template) pair, 8x32 batch, one core of a Zen 5 desktop:
|
|
62
|
+
|
|
63
|
+
| n | matchedfilter | numpy | |
|
|
64
|
+
|---:|---:|---:|---:|
|
|
65
|
+
| 1024 | 0.61 µs | 18.98 µs | 31x |
|
|
66
|
+
| 4096 | 2.08 µs | 37.81 µs | 18x |
|
|
67
|
+
| 16384 | 9.98 µs | 127.04 µs | 13x |
|
|
68
|
+
| 65536 | 48.87 µs | 568.56 µs | 12x |
|
|
69
|
+
|
|
70
|
+
numpy is a floor, not a rival. It is there so the comparison runs anywhere.
|
|
71
|
+
Against MKL or FFTW the margin is much smaller, and part of what is left comes
|
|
72
|
+
from computing peaks instead of a full correlation. Measure on your own box:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
python -m matchedfilter.benchmark
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Hierarchical filtering
|
|
79
|
+
|
|
80
|
+
`HierarchicalFilter` adds a cheap pre-pass: correlate against a low-frequency
|
|
81
|
+
slice of the template, and only run the full-length filter where that slice
|
|
82
|
+
leaves a peak plausible.
|
|
83
|
+
|
|
84
|
+
**This helps only under an assumption about your templates**: that enough of
|
|
85
|
+
the matched-filter output power sits in the low band that a narrow slice gives
|
|
86
|
+
a usable bound on the full result. For chirp-like templates whose power is
|
|
87
|
+
concentrated at low frequency that tends to hold. For templates whose power is
|
|
88
|
+
spread flat across the band, or concentrated high, the slice bounds nothing
|
|
89
|
+
useful, and the pre-pass is pure added cost. It is worth checking against your
|
|
90
|
+
own templates before relying on it.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
hf = mf.HierarchicalFilter(16384, ndata=16, ntemplates=64,
|
|
94
|
+
snr=6.0, # threshold you intend to use
|
|
95
|
+
fd=1e-3, # false-dismissal budget
|
|
96
|
+
band=2048) # width of the cheap slice
|
|
97
|
+
|
|
98
|
+
hf.set_reference(expected_output_power) # real frequency series, the OUTPUT
|
|
99
|
+
hf.set_templates(template_spectra)
|
|
100
|
+
hf.set_data(data_spectra)
|
|
101
|
+
peaks = hf.run(binsize=16384, threshold=6.0)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`set_reference` takes a real frequency series of length `n` holding the expected
|
|
105
|
+
power of the filter **output** in each bin. Only its shape is used; the overall
|
|
106
|
+
normalisation is divided out.
|
|
107
|
+
|
|
108
|
+
This is the output, not the template. The two differ whenever the data is
|
|
109
|
+
coloured, and passing the template's own power will mis-set the gate: a
|
|
110
|
+
broadband template reconstructing a narrowband signal is the case where it goes
|
|
111
|
+
wrong by the largest factor.
|
|
112
|
+
|
|
113
|
+
The gate is one-sided by construction: peaks it reports are bit-identical to
|
|
114
|
+
the flat filter's. It can only omit, never invent. `fd` is the budget for how
|
|
115
|
+
often it is allowed to omit one.
|
|
116
|
+
|
|
117
|
+
On pure noise at n=4096 the gate runs about **7x faster** than the flat filter.
|
|
118
|
+
The saving scales with how little survives, so it grows with your threshold and
|
|
119
|
+
falls toward 1x on data where most pairs trigger.
|
|
120
|
+
|
|
121
|
+
## Caveats
|
|
122
|
+
|
|
123
|
+
- **The false-dismissal budget is not currently met at low thresholds.**
|
|
124
|
+
`fd` is honoured well at snr 6 and above. At snr 5.0 to 5.5 with a coarse
|
|
125
|
+
band the gate omits more than it should: 1.4% against a 0.1% budget in a
|
|
126
|
+
418-template search. The cause is that the gate's recovery factors are
|
|
127
|
+
measured from a mean frequency series, which is not a bound on any
|
|
128
|
+
individual realisation. Tracked by an `xfail` test in `tests/test_api.py` and written up
|
|
129
|
+
in [docs/hierarchical.md](docs/hierarchical.md).
|
|
130
|
+
- Single-threaded by design. Parallelism is the caller's to arrange.
|
|
131
|
+
- x86-64 Linux only, as above. Other architectures are not implemented rather
|
|
132
|
+
than merely untested.
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
pip install -e .[test]
|
|
138
|
+
pytest # includes a C test that builds itself from source
|
|
139
|
+
python -m matchedfilter.benchmark
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
[docs/](docs/) holds the design notes: how the hierarchical gate is
|
|
143
|
+
calibrated, and measurements of the approaches that were tried and rejected.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Batched matched filter: design
|
|
2
|
+
|
|
3
|
+
D data segments and T template segments arrive as spectra: complex vectors of
|
|
4
|
+
length N, the unnormalised forward transform of each segment. For every pair the
|
|
5
|
+
output is
|
|
6
|
+
|
|
7
|
+
z_dt[k] = IFFT( D_d[f] * conj(H_t[f]) )[k]
|
|
8
|
+
|
|
9
|
+
reported as a binned maximum over a search window, with a detection floor.
|
|
10
|
+
D and T are arbitrary.
|
|
11
|
+
|
|
12
|
+
## Where the time goes
|
|
13
|
+
|
|
14
|
+
Forward transforms number D+T. Pair work is D*T. At D=T=16 the pair loop is
|
|
15
|
+
~89% of the work before any optimisation, so every design decision is made about
|
|
16
|
+
the pair loop.
|
|
17
|
+
|
|
18
|
+
Per pair, done naively:
|
|
19
|
+
|
|
20
|
+
read D_d N complex
|
|
21
|
+
read H_t N complex
|
|
22
|
+
write product P N complex
|
|
23
|
+
read P N complex <- IFFT stage A
|
|
24
|
+
write intermediate N complex
|
|
25
|
+
read intermediate N complex
|
|
26
|
+
|
|
27
|
+
At 2^20 that is 48 MiB per pair, and 256 pairs is 12 GiB.
|
|
28
|
+
|
|
29
|
+
## What reuse buys
|
|
30
|
+
|
|
31
|
+
1. **Fuse the product into the IFFT's stage-A load.** The product never has to
|
|
32
|
+
exist in memory: stage A reads `D_d` and `H_t` and multiplies on the way in.
|
|
33
|
+
Removes 2 of the 6 passes, 16 MiB of 48 per pair.
|
|
34
|
+
|
|
35
|
+
2. **Pre-permute the stored spectra into stage-A order.** Stage A reads
|
|
36
|
+
`x[n2*N1 + n1]` walking n2, i.e. with stride N1, measured at 7.6 GB/s against
|
|
37
|
+
43.9 sequential on this core. Storing the spectra as `X'[n1*N2 + n2]` makes
|
|
38
|
+
that read contiguous. The permutation costs one transpose per segment (D+T of
|
|
39
|
+
them) instead of a strided read per pair (D*T of them): at D=T=16, 32
|
|
40
|
+
transposes to avoid 256 strided passes.
|
|
41
|
+
|
|
42
|
+
3. **Cache-block the (d,t) loop.** The matmul argument: a tile of nd×nt pairs
|
|
43
|
+
loads nd+nt spectra and does nd*nt work, so traffic falls by roughly the
|
|
44
|
+
harmonic mean where the spectra fit.
|
|
45
|
+
|
|
46
|
+
4. **Conjugate templates once at ingest**, not per pair.
|
|
47
|
+
|
|
48
|
+
## What does not work, and why
|
|
49
|
+
|
|
50
|
+
- **Pushing butterflies across the product.** The product is elementwise in
|
|
51
|
+
frequency and butterflies mix frequencies, so no part of the IFFT can be
|
|
52
|
+
pre-applied to `D` or `H` separately. Only the *permutation* commutes with an
|
|
53
|
+
elementwise product, which is why (2) works and a pre-butterflied store does
|
|
54
|
+
not.
|
|
55
|
+
- **Sharing IFFT work between pairs.** Different pairs have different inputs;
|
|
56
|
+
there is no common subexpression beyond what (1) to (3) already capture.
|
|
57
|
+
|
|
58
|
+
## Why the input is frequency domain
|
|
59
|
+
|
|
60
|
+
Taking time-domain segments would let the ingest rearrangement fuse into a
|
|
61
|
+
forward transform the library performed itself, making it free. Measured, that
|
|
62
|
+
rearrangement is only 1.9 to 4.5% of total, and shrinks as T grows:
|
|
63
|
+
|
|
64
|
+
| N | D × T | ingest | pair loop | share |
|
|
65
|
+
|---|---|---:|---:|---:|
|
|
66
|
+
| 2^12 | 16×16 | 26.7 µs | 652 µs | 3.9% |
|
|
67
|
+
| 2^12 | 16×256 | 228 µs | 11566 µs | 1.9% |
|
|
68
|
+
| 2^14 | 16×16 | 104 µs | 2940 µs | 3.4% |
|
|
69
|
+
| 2^16 | 16×64 | 2470 µs | 55566 µs | 4.3% |
|
|
70
|
+
|
|
71
|
+
Owning the forward transform is not worth that, and the caller's pipeline
|
|
72
|
+
generally has the spectra already.
|