Skip to content

verify_dsp fails on a single-precision MicroPython build: six probes disagree with the CPython target #80

Description

@bdbarnett

What is failing

clean-build.yml's unix-usermod (float-precision) leg fails verify_dsp: six probes render differently on a single-precision MicroPython build than on the CPython target. The double-precision leg passes all 21 comparisons.

FAIL  dynamics_probe.py               cpython and micropython differ at output byte 2164
FAIL  dynamics_extras_probe.py        cpython and micropython differ at output byte 1999
FAIL  dynamics_options_probe.py       cpython and micropython differ at output byte 2011
FAIL  biquad_component_probe.py       cpython and micropython differ at output byte 137
FAIL  granular_pitch_shift_probe.py   cpython and micropython differ at output byte 586
FAIL  echo_filter_probe.py            cpython and micropython differ at output byte 167
21 comparisons, 6 failures, 0 pending

That cell is deliberate: -DMICROPY_FLOAT_IMPL=MICROPY_FLOAT_IMPL_FLOAT, and the workflow says why — it "mirrors single-precision MCU targets (ESP32-P4/RP2040), where mp_float_t is 32-bit". So this is a real target, not a hypothetical one.

It is not a regression — the gate was red from birth

Worth stating plainly, because the run history looks like something broke:

14:43  95cd1c2  CI ref moves to MicroPython v1.29.0   → clean-build still green
17:54  ef71b86  last SUCCESS                          → v1.29.0 in use, both legs pass
18:14  79ee874  adds the verify_dsp step              → FAILURE, and every run since

git merge-base --is-ancestor 95cd1c2 ef71b86 confirms the pin move precedes the last green run, and verify_dsp appears zero times in clean-build.yml at ef71b86. So the v1.29.0 pin move did not cause this. 79ee874 added a check that had never run before, and it found a pre-existing disagreement on its first attempt. Twelve consecutive failures since, all of them the same finding.

Likely mechanism, stated as a hypothesis

The bindings compute in mp_float_t and hand the result to kernel functions declared double. src/synthio/Biquad.c:

mp_float_t W0 = frequency * synthio_global_W_scale;      // float on this build
...
audioif_biquad_cp_configure(&self->coefficients, self->mode, W0, Q, A);

against

void audioif_biquad_cp_configure(audioif_biquad_cp_coefficients_t *coefficients,
    int mode, double W0, double Q, double A);

On the double-precision build mp_float_t is double, so the value reaching the kernel is the same one CPython computes. On the single-precision build it is rounded to float first and then promoted, so the kernel sees a different number — and Q15 coefficient rounding turns a last-bit difference into a different coefficient. That is consistent with biquad_component_probe failing at byte 137, very early.

audioif_dynamics_ms_to_coef(float ms, float sample_rate) takes float, so both builds truncate identically there; the dynamics failures therefore need their own explanation rather than this one, and the three of them failing together suggests a shared upstream input rather than the kernel boundary.

Not confirmed. The above explains the biquad case and does not explain dynamics, so at least one more mechanism is in play.

Reproduction

# from a MicroPython checkout at v1.29.0, with audioif's parent as USER_C_MODULES
make -C ports/unix CFLAGS_EXTRA="-DMICROPY_FLOAT_IMPL=MICROPY_FLOAT_IMPL_FLOAT" ...
python tests/parity/verify_dsp.py --micropython <that build>

A local verify_dsp run cannot see this: the desktop MicroPython in cmods/bin is double-precision, so the six local gates pass while this cell fails. That blind spot is why v0.4.0 was released on top of a red workflow.

Relationship to the other cross-target work

Same family as #79 (fused multiply-add across the remaining shared DSP files) and #55 (per-architecture differences), and it should be sequenced with them rather than fixed alone. All three are "the same C, different float behaviour per target", and the answer for each is either a code change that removes the freedom or a recorded per-target baseline — decided with measurements, not per-issue.

One ordering note: whatever fixes this will move board digests, exactly as #79 would, so audiocomponents' cost table is downstream of both.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions