Skip to content

GPU BPSK demod never applies cancel_crossfade_isi — the shipped daemon takes the arm that decodes 12/96 at −2 dB #1433

Description

@dc0sk

The defect

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

  1. 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.
  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions