You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
BpskPlugin::demodulate dispatches to the GPU when a context exists (plugins/bpsk/src/lib.rs:112-116),
and the GPU demodulator never applies cancel_crossfade_isi. The non-RRC branch of bpsk_demodulate_with_gpu (plugins/bpsk/src/demodulate.rs:522-585) goes GPU timing search → bpsk_iq_demod_gpu → slice → differential_decode and stops.
The CPU path does cancel, at demodulate.rs:114, inside symbol_stream_with_expected.
The daemon ships with this on.crates/openpulse-daemon/Cargo.toml:28 is default = ["gpu"],
and server.rs:126/142 registers BpskPlugin::with_gpu(ctx) whenever GpuContext::init() finds an
adapter. rpi51 has one (Broadcom V3D via Vulkan, per the #1080 sweep).
Why it matters — an 84-frame swing, measured
#1428 step 1 measured the two arms end-to-end with real RS, paired over 96 seeds
(BPSK250 + Rs, 200 B). The cancelled arm is not a marginal win on AWGN, it is the difference
between decoding and not:
cell
uncancelled
cancelled
awgn −2 dB
12 / 96
96 / 96
awgn −1 dB
90 / 96
96 / 96
awgn 0 / 2 / 5 dB
96 / 96
96 / 96
moderate_f1 @ 8 dB
49 / 96
38 / 96
doppler-only @ 8 dB
64 / 96
83 / 96
So on a GPU-enabled daemon the coded BPSK receive takes the arm that decodes 12 of 96 at −2 dB
instead of 96 of 96. hpx_hf's SL2–SL5 are all BPSK, and SL2 is the rung every session starts on.
Note the second-order consequence: demodulate_soft has no GPU path at all and already skips the
cancellation by design (#832). So on a GPU daemon both arms are uncancelled, which makes the
coded/uncoded asymmetry recorded in #1429 not exist there — and makes the union proposed in #1428
degenerate on exactly the binary that runs on air.
Why no gate caught it
plugins/bpsk/tests/gpu_cpu_equivalence.rs → gpu_and_cpu_agree_under_noise compares decode outcome on a 27-byte uncoded payload at 4 / 8 / 12 / 20 dB. BPSK250 at 8 kHz is 32
samples/symbol (~15 dB of processing gain), so its lowest cell sits at ≈19 dB Eb/N0 — both arms
decode every seed, and the ~2.6× BER difference (#1429 measured 0.0127 cancelled vs 0.0336
uncancelled on #821's own fixture) is invisible by construction. The fixture is artificially
easy for the property it is named after.
The --no-default-features workspace gate never compiles the GPU path at all; gate.sh's --all-features pass is compile + lint only.
Chronology
The GPU path landed 2026-05-04 (664122b9, Phase 3.3). #821 added the cancellation 2026-07-13 and
touched symbol_stream_with_expected only. #1080's GPU-divergence sweep found three defects (the
timing search being I-only, psk8's reversed LLR bit order, the modulator drift) and did not find
this one.
Proposed fix
Apply the cancellation on the GPU non-RRC branch, on the i_syms/q_syms already in hand at demodulate.rs:556-575. It is O(symbols) of CPU work against a GPU FFT — negligible.
Extend the equivalence gate to a cell that can see it: 200 B payload + FecMode::Rs at
−2 … 0 dB, i.e. the #1363 follow-up: the engine-level coded A/B, and a decision-directed predicate that does not misfire on noise #1428 cell where the arms differ by 84 frames. Sabotage-verify by removing
the new cancellation call and watching the new cell fail while the existing 4–20 dB cells stay
green — that is what proves the cell is discriminating rather than merely added.
Check the sibling GPU demodulators for the same shape (qpsk, psk8, qam64 all have gpu
features). This is a twins question and I have not swept it yet.
Scope note
This is a prerequisite for #1428's union, not part of it: with both arms uncancelled on the
shipped daemon, a union of them decodes nothing extra and its tripwire would still pass on the
CPU-only gate.
Found by adversarial review of the #1428 union design; verified independently against the source
before filing.
The defect
BpskPlugin::demodulatedispatches to the GPU when a context exists (plugins/bpsk/src/lib.rs:112-116),and the GPU demodulator never applies
cancel_crossfade_isi. The non-RRC branch ofbpsk_demodulate_with_gpu(plugins/bpsk/src/demodulate.rs:522-585) goes GPU timing search →bpsk_iq_demod_gpu→ slice →differential_decodeand stops.The CPU path does cancel, at
demodulate.rs:114, insidesymbol_stream_with_expected.The daemon ships with this on.
crates/openpulse-daemon/Cargo.toml:28isdefault = ["gpu"],and
server.rs:126/142registersBpskPlugin::with_gpu(ctx)wheneverGpuContext::init()finds anadapter. rpi51 has one (Broadcom V3D via Vulkan, per the #1080 sweep).
Why it matters — an 84-frame swing, measured
#1428 step 1 measured the two arms end-to-end with real RS, paired over 96 seeds
(BPSK250 +
Rs, 200 B). The cancelled arm is not a marginal win on AWGN, it is the differencebetween decoding and not:
moderate_f1@ 8 dBSo on a GPU-enabled daemon the coded BPSK receive takes the arm that decodes 12 of 96 at −2 dB
instead of 96 of 96.
hpx_hf's SL2–SL5 are all BPSK, and SL2 is the rung every session starts on.Note the second-order consequence:
demodulate_softhas no GPU path at all and already skips thecancellation by design (#832). So on a GPU daemon both arms are uncancelled, which makes the
coded/uncoded asymmetry recorded in #1429 not exist there — and makes the union proposed in #1428
degenerate on exactly the binary that runs on air.
Why no gate caught it
plugins/bpsk/tests/gpu_cpu_equivalence.rs→gpu_and_cpu_agree_under_noisecompares decodeoutcome on a 27-byte uncoded payload at 4 / 8 / 12 / 20 dB. BPSK250 at 8 kHz is 32
samples/symbol (~15 dB of processing gain), so its lowest cell sits at ≈19 dB Eb/N0 — both arms
decode every seed, and the ~2.6× BER difference (#1429 measured 0.0127 cancelled vs 0.0336
uncancelled on #821's own fixture) is invisible by construction. The fixture is artificially
easy for the property it is named after.
The
--no-default-featuresworkspace gate never compiles the GPU path at all;gate.sh's--all-featurespass is compile + lint only.Chronology
The GPU path landed 2026-05-04 (
664122b9, Phase 3.3). #821 added the cancellation 2026-07-13 andtouched
symbol_stream_with_expectedonly. #1080's GPU-divergence sweep found three defects (thetiming search being I-only, psk8's reversed LLR bit order, the modulator drift) and did not find
this one.
Proposed fix
i_syms/q_symsalready in hand atdemodulate.rs:556-575. It is O(symbols) of CPU work against a GPU FFT — negligible.FecMode::Rsat−2 … 0 dB, i.e. the #1363 follow-up: the engine-level coded A/B, and a decision-directed predicate that does not misfire on noise #1428 cell where the arms differ by 84 frames. Sabotage-verify by removing
the new cancellation call and watching the new cell fail while the existing 4–20 dB cells stay
green — that is what proves the cell is discriminating rather than merely added.
gpufeatures). This is a twins question and I have not swept it yet.
Scope note
This is a prerequisite for #1428's union, not part of it: with both arms uncancelled on the
shipped daemon, a union of them decodes nothing extra and its tripwire would still pass on the
CPU-only gate.
Found by adversarial review of the #1428 union design; verified independently against the source
before filing.