Skip to content

Stage 2a: split-radix C++20 port beside the C, bit-identical, nothing routed - #28

Open
tap wants to merge 11 commits into
mainfrom
claude/wave2-stage2a-port
Open

tap wants to merge 11 commits into
mainfrom
claude/wave2-stage2a-port

Conversation

@tap

@tap tap commented Sep 18, 2026

Copy link
Copy Markdown
Owner

What this changes

Stage 2a of docs/audit-fft-and-code-smells.md (Part 3; spec in Part 4, provenance in Part 5). Adds tap::dsp::detail::split_radix_rdft<Sample> (include/tap/dsp/fft/split_radix.h), a statement-for-statement C++20 transliteration of the rdft path of Ooura's fftsg.c, beside the vendored C. Nothing is routed: fft.h is untouched, basic_real_fft still runs the C. The parity gate's engine_under_test alias is re-pointed at the port (the one place its header comment names), the bench builds every float scenario and the float size probe a second time against the port under a _port suffix and reports port/C without gating it, bench/README.md records the mechanism, and docs/fft-design.md records the measured numbers below from this PR's own CI logs (run and job ids on every number).

Eleven commits: the six of the original series (tables, bit reversal, complex kernel, real post-pass + transforms + gate re-point, bench, docs), a docs label correction (a9b53b4), and four review-fix commits after the two hostile reviews (d137a2e NOTICE.md provenance post-2a; 37ca717 bench: an informational port binary never reds the ratchet, size step pipefail and explicit failure on a missing .text, _port size probe under the same engine guard as icount, --merge drops nested _port keys; cb92d50 noise-floor TODO re-labelled to Stage 2b; d8eccf6 rule-2 sentence naming the intentional -Wdouble-promotion sites). Each of the first three compiles standalone; the class is complete in the fourth, which is where the parity target starts linking against it.

Why

The port is the long pole of the FFT plan: Stage 2b's routing flip (one line in fft.h), the MuTap bump with every float pin unchanged, and Stage 2c's removal of the C all rest on the port being bit-identical to the C for both precisions (Decision D10). This PR is where that identity is established and gated, with the C still in the tree to compare against.

How identity was achieved and how it is kept, stated in the header so nobody undoes it:

  1. Statement fidelity. Every Ooura arithmetic statement is textually intact (no cmul helpers, lambdas, std::complex or reassociation). Verified mechanically as well as by the gate: a normalizer that strips whitespace, maps Sampledouble and undoes only the documented differences finds all 26 mechanically ported kernels token-identical to the C (DIFFERENCES: 0).
  2. Table semantics per precision. The C's float build is #define double float, so makewt/makect's locals and the products fed to the trig calls are float arithmetic while cos/sin/atan stay the double libm functions with promoted arguments and the results converted on assignment (and 0.5 / cos(...), 0.5 / wk1r, 0.5 * cos(...) are double arithmetic converted on assignment). The port keeps the locals typed Sample, gives every libm call an explicit static_cast<double> argument and converts to Sample exactly where the C's assignment did. Breaking this on one line (std::cos of the float product, i.e. cosf) turns the three float gates red while the three double gates stay green.
  3. Tables built once in the constructor with rdft's first-call protocol; the lazy ip[0] = 0 init is gone, the transforms are const noexcept, and m_w.data() / m_ip.data() are loaded into locals once per transform so the helpers receive a, ip, w by parameter as the C functions do.
  4. Index types stay the C's int (Part 4 permits widening; not widening keeps the statements identical); the single size_tint narrowing is the constructor's.

Not ported: cdft, the DCT/DST family, the thread scaffolding.

Verification

The gate (-ffp-contract=off both sides, memcmp identity)

Green on the first CI run of the PR on every leg: linux (g++ 13.3.0), windows (MSVC 19.51.36256.0), macos (AppleClang 21.0.0, arm64), and at N ≤ 4096 against newlib's libm on cortex-m4-softfp, cortex-m4f, cortex-m33, cortex-m55 (arm-none-eabi-gcc 13.2.1, QEMU 8.2.2). Locally also clang++ 18.1.3. Every N from 4 to 65536 plus 2^20 (hosts), double and float, forward and inverse, five materials.

Informational max-ulp at default flags (CI run 35295206578 at 8afe6cf, 2026-09-18)

Platform (job) Toolchain Sizes max ulp double max ulp float
linux-ooura (105446230919) ubuntu-24.04, GNU 13.3.0, x86-64 no -march 4 … 65536, 2^20 0 0
windows-ooura (105446230835) windows-2025-vs2026, MSVC 19.51.36256.0 4 … 65536, 2^20 0 0
macos-vdsp (105446230934) macos-26-arm64, AppleClang 21.0.0.21000101 (FMA ISA) 4 … 65536, 2^20 0 0
cortex-m4-softfp (105446230884) arm-none-eabi-gcc 13.2.1 (15:13.2.rel1-2), QEMU 8.2.2, soft-float 4 … 4096 0 0
cortex-m4f (105446230771) same, fpv4-sp-d16 (VFMA) 4 … 4096 0 0
cortex-m33 (105446230920) same, single-precision FPU (VFMA) 4 … 4096 0 0
cortex-m55 (105446230931) same, MVE (VFMA) 4 … 4096 0 0
local g++ 13.3.0 and clang++ 18.1.3, x86-64 no -march 4 … 65536, 2^20 0 0

Zero everywhere because the statements are identical, so each compiler fuses both sides alike. Not measured and not claimed: x86-64 with -march (FMA), where the objdump probe shows g++ fusing the two sides differently after inlining. The gate stays at -ffp-contract=off.

objdump FMA probe (-O3 -march=haswell; reference C = fftsg.c + fftsg_float.c; port = a TU instantiating both precisions)

compiler default: C fused default: port fused -ffp-contract=off: C -ffp-contract=off: port
gcc / g++ 13.3.0 372 588 0 0
clang / clang++ 18.1.3 333 221 0 0

Bench, informational (bench run 35295262684 at 8afe6cf, arm-none-eabi-gcc 13.2.1, QEMU 8.2.2; nothing gated at the port, no seeding needed, all five icount jobs green with the C at +0.00 %)

Scenario m4-softfp m4f m33 m55 (C = CMSIS) m55-ooura
rfft_f32_512 (C, seeded baseline) 1,868,441,244 98,090,666 102,248,169 52,382,331 94,561,954
rfft_f32_512_port 1,864,904,581 97,126,274 100,833,265 89,407,453 89,047,005
ratio port / C 0.9981 0.9902 0.9862 1.7068 (vs CMSIS) 0.9417
output checksums identical identical identical differ (CMSIS ≠ Ooura) identical
rfft_f32_2048 (C, seeded baseline) 2,296,984,479 111,859,257 116,385,409 54,858,120 107,806,480
rfft_f32_2048_port 2,294,343,851 111,270,714 115,396,527 102,870,129 102,644,849
ratio port / C 0.9989 0.9947 0.9915 1.8752 (vs CMSIS) 0.9521
output checksums identical identical identical differ (CMSIS ≠ Ooura) identical

.text of the MinSizeRel float size probe (size -A), bytes:

key C port port / C
m4-softfp 51,729 53,289 1.0302
m4f 43,153 44,601 1.0336
m33 42,601 44,009 1.0331
m55 (C = CMSIS) 107,681 39,281 0.3648
m55-ooura 38,505 39,281 1.0202

Read against the ±3 % ratchet 2b applies: the port executes 0.1 – 5.8 % fewer instructions than the C on every Ooura key and is bit-identical to it at the bench's default flags (identical checksums, including on the VFMA cores); m55-ooura improves beyond 3 %, so 2b re-records that key rather than absorbing it. Against CMSIS-DSP Helium on the deployed m55 profile the port is 1.7 – 1.9× the count; m55 stays on CMSIS.

Local

g++ 13.3.0 and clang++ 18.1.3, Release, -DTAP_DSP_WERROR=ON -DTAP_DSP_BUILD_CAPI=ON -DTAP_DSP_BUILD_BENCH=ON: full builds warning-free, ctest 283/283 each (incl. the new _port smoke tests; host checksums C vs port identical: 0x662dd085b5b88325 at N = 512, 0x806d1c5dbb65c525 at N = 2048). Mutation check as described above (3 float gates red / 3 double green, restored 7/7). scripts/icount.py's informational path dry-run with a stubbed QEMU measure through compare, no-baseline, --update, --merge and ignored-baseline cases. scripts/tidy.sh full sweep (clang-tidy-18) clean; pre-commit run --all-files clean for this PR's files (it reformats the pre-existing tests/reference/frontend_vectors.h, not touched here).

Notes for the reviewer

  • Documented exception. The port header's banner is the four house lines plus a // prose block carrying Ooura's notice verbatim and the derivative statement, exactly the text docs/fft-design.md and NOTICE.md settled in wave 1; SPDX LicenseRef-Ooura AND MIT.
  • No contract change. No documented contract point moves; the port is bit-identical to what basic_real_fft runs today and nothing is routed at it. No consumer pin moves.
  • Files deliberately not touched: fft.h (2b flips routing), README.md, tests/CMakeLists.txt (the parity targets already existed), ci.yml, third_party/**, the taphouse style files. scripts/icount.py is touched, per the bench brief's "documented informational prefix in icount.py" option: the mechanism lives where the QEMU invocation already is, so the ratio prints next to the counts.
  • The m55 key's C sibling is CMSIS-DSP, so its printed port/C ratio is port-vs-CMSIS; the port-vs-Ooura number on the M55 is the m55-ooura key's.
  • For the 2b flip agent: basic_real_fft can hold a split_radix_rdft<Sample> by value; its transforms are const, so forward_inplace/inverse_inplace on the wrapper can become const too. The engine's constructor allocates (two vectors) and asserts the geometry the same way fft.h does. Expect icount.py to flag m55-ooura as IMPROVED beyond tolerance at the flip (re-record, as the policy says).

🤖 Generated with Claude Code

https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy

tap and others added 7 commits September 18, 2026 01:24
Stage 2a of docs/audit-fft-and-code-smells.md, first step: the C++20 engine
that will replace the vendored Ooura C, added beside it and routed nowhere.
This commit carries the banner (SPDX LicenseRef-Ooura AND MIT, Ooura's notice
verbatim, the derivative-work statement), the transliteration rules that the
parity gate depends on, the class surface (constructor from size, size(); the
transforms land with the last kernel group), and the table builders makewt,
makeipt and makect run once from the constructor with rdft's first-call
protocol, so the lazy ip[0] = 0 initialization is gone.

Table semantics per precision (Decision D10): the locals are typed Sample,
every libm call takes an explicit static_cast<double> argument (the C's float
build promotes its float argument to the double cos/sin/atan; std::cos(float)
would call cosf) and the result is converted to Sample exactly where the C's
assignment converted it, including 0.5 / cos(...), 0.5 / wk1r and
0.5 * cos(...) being double arithmetic converted on assignment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
bitrv2, bitrv2conj, bitrv216, bitrv216neg, bitrv208 and bitrv208neg as
private static members, statement for statement from fftsg.c. The only
changes are the ones the header's rules allow: double -> Sample in the local
declarations, const on the ip table pointer, and the local prototype
declarations dropped (class scope resolves the names).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
cftfsub and cftbsub with everything they reach: cftf1st, cftb1st, cftrec4,
cfttree, cftleaf, cftmdl1, cftmdl2, cftfx41, cftf161, cftf162, cftf081,
cftf082, cftf040, cftb040 and cftx020, statement for statement from fftsg.c
(the USE_CDFT_THREADS blocks in cftfsub/cftbsub and the local prototypes
dropped, double -> Sample, const on the w table pointer; nothing else).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
…inted at the port

rftfsub and rftbsub (the one double literal each, 0.5 - c[nc - kk], gets the
explicit narrowing the C's assignment performs), and forward_inplace /
inverse_inplace: rdft's body minus the table build, const and noexcept, the
tables loaded into locals once per transform so the helpers receive a, ip and
w by parameter as the C functions do.

tests/test_fft_parity_ooura.cpp: engine_under_test re-pointed from
basic_real_fft at detail::split_radix_rdft, the one place its header comment
names, and that comment updated. The memcmp gate is unchanged. With the alias
re-pointed and rule 2 of the header deliberately broken on one makewt line
(std::cos on the float product, i.e. cosf), the three float gates fail and
the three double gates pass; with the header as committed, all seven parity
tests pass on g++ 13.3 and clang++ 18.1 (-ffp-contract=off gate: identity at
every N from 4 to 65536 plus 2^20, both precisions, forward and inverse; the
informational default-flags run reports 0 ulp on this x86-64 host, which has
no FMA without -march).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
…, informational

From Stage 2a until 2c the icount job reports the C and the port side by
side without ratcheting the port (nothing is routed yet; audit Part 3):

- bench_common.h: TAP_DSP_BENCH_ENGINE gains split_radix; the port is
  measured through split_radix_bench_adapter, which presents basic_real_fft's
  out-of-place forward()/inverse() with the same copy loop and the same 2/N
  arithmetic, so a port-vs-C delta is the transform's alone. backend_name()
  prints split_radix for the port (it has no backend behind it).
- bench/icount/CMakeLists.txt: every float scenario builds twice, the second
  binary keyed <scenario>_port with the port; both get the host smoke test.
  bench/CMakeLists.txt: the float size probe likewise (rfft_f32_512_port).
- scripts/icount.py: a documented INFORMATIONAL_SUFFIX (_port). Such a
  scenario is counted and printed after the gated ones beside its C sibling
  with the ratio port/C and whether the two DONE checksums agree, but it can
  never fail the run, --update never writes it to the baselines, a baseline
  naming it is reported and ignored, and --record files it under a separate
  top-level "informational" key that --merge skips. A pull request therefore
  shows the ratios in the job log with no seeding and no gate change.
- bench.yml: the size step builds and prints the port probe's .text and the
  port/C ratio beside the C's; the ceiling still applies to the C probe only.
- bench/README.md: the two _port scenarios, the mechanism and its limits (on
  the m55 key the sibling is CMSIS; port-vs-Ooura on the M55 is m55-ooura's).

On this x86-64 host the C and port icount binaries print identical checksums
(0x662dd085b5b88325 at N = 512, 0x806d1c5dbb65c525 at N = 2048) under both
g++ 13.3 and clang++ 18.1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
From the CI and bench logs of #28 at 8afe6cf (2026-09-18),
labelled with run and job ids and toolchains, never typed in:

- fp-contraction policy, item 2: the informational default-flags max-ulp
  per platform (linux g++ 13.3, windows MSVC 19.51, macos AppleClang 21 on
  arm64, the four QEMU legs on arm-none-eabi-gcc 13.2.1 at N <= 4096): 0 ulp
  everywhere, with the reason and the one case not measured (x86 -march).
- transliteration rules: what the port did with them (int indices kept, the
  mechanical fidelity check over 26 kernels, the cosf mutation that turns the
  float gates red, the objdump FMA counts 372/588 gcc and 333/221 clang at
  default and 0/0 with the flag, the gate's coverage).
- .text per key, C vs port (+2.0 % to +3.4 % on the Ooura keys; the m55 row
  is CMSIS) and the instruction counts per scenario with the port rows,
  ratios (0.94 to 0.999 vs the C; 1.7 to 1.9 vs CMSIS on m55) and the
  checksum agreement, read against the 2b ratchet.
- shareability row and provenance section: the port's const transforms and
  where it landed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
Spot-checked every number in the Stage 2a tables against the CI logs of
run 35295206578 (max-ulp per platform) and bench run 35295262684
(instruction counts, .text); all match. Two labels did not:

- the sentence under the max-ulp table counted "three" FMA-capable legs
  while listing four (macOS arm64, M4F, M33, M55); now "four".
- the instruction-count preamble called the bench's Release build "-O2".
  No toolchain file sets an optimisation level (they set only the
  _INIT target flags), so Release is CMake's GNU default, -O3 -DNDEBUG;
  the text now says so. (bench.yml line 140 carries the same "-O2" in a
  comment; a workflow file is out of this docs commit's scope.)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy

@tap tap left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hostile review A — numerics and transliteration fidelity (head 9df52d2)

Reviewed in a detached worktree of the branch; every claim below was checked against source, a local build, a local run, or the cited CI job log. Toolchains: g++ 13.3.0, clang++ 18.1.3, clang-tidy-18, clang-format 18.1.3 (the pinned pre-commit rev), python3.

Findings

Blockers: none. Should-fix: none. Three nits, all documentation/consistency; none touches the transform, the tables, the gate or the ratchet.

  1. nit — docs/fft-design.md:172. "the three FMA-capable legs (macOS arm64, M4F, M33, M55)" names four legs; the table above it has four FMA rows. Fix: "four". (Line comment.)

  2. nit — bench/README.md:114 vs bench/icount/CMakeLists.txt:61 vs bench/CMakeLists.txt:49. The README says the _port binaries are always built "whatever the variable says"; the icount block skips them when TAP_DSP_BENCH_ENGINE=split_radix, while the size-probe list adds rfft_f32_512_port unconditionally (two identical port probes in that configuration, and no port icount pair). Verified by configuring the host bench with -DTAP_DSP_BENCH_ENGINE=split_radix and listing the built targets. Fix: make the two CMake lists agree (the icount guard is the sensible one; apply it to the probe) and qualify the README sentence. CI never sets the variable, so no measured number is affected. (Line comment.)

  3. nit — scripts/icount.py:31 (and the same phrase in bench/README.md and the bench.yml header). "it cannot fail the run" holds for the verdict but not for the process: a _port binary that hangs, faults or prints ok=0 hits the raise SystemExit in measure() (lines 86/90) before the gated scenarios are judged, so the informational binary can still turn the job red. Probably the desired behaviour; the wording should say so, or measure() should catch it for informational binaries and print did not complete. (Line comment.)

Checked and found correct (the fix pass need not re-verify)

Transliteration fidelity (attack 1). Wrote an independent normalizer (strip comments/whitespace, drop the C's local prototypes and USE_CDFT_THREADS blocks, map Sampledouble, const int*/const Sample*→bare pointer, drop static/noexcept, then tokenize) and compared all 28 routines against fftsg.c. Result: 24 routines token-identical (makeipt, bitrv2, bitrv2conj, bitrv216, bitrv216neg, bitrv208, bitrv208neg, cftfsub, cftbsub, cftf1st, cftb1st, cftrec4, cfttree, cftleaf, cftmdl1, cftmdl2, cftfx41, cftf161, cftf162, cftf081, cftf082, cftf040, cftb040, cftx020). rftfsub/rftbsub differ by exactly the one documented cast (static_cast<Sample>(0.5 - c[nc - kk]), the C's assignment narrowing); makewt/makect differ only by inserted static_cast<double>(/std::/) tokens — no operand reordered, no regrouping, no type of a local changed. The rdft glue (xi = a[0] - a[1]; a[0] += a[1]; a[1] = xi;, a[1] = 0.5 * (a[0] - a[1]); a[0] -= a[1];, the n > 4 / n == 4 conditions, the nw/nc first-call protocol with ip[0] = ip[1] = 0 on entry) matches rdft statement for statement; m_nw/m_nc equal what the C re-reads from ip[0]/ip[1] on every later call, including the N = 4 case where makect is never called and nc stays 1, and N = 8 where makewt(2) writes nothing to w and makect(2) writes c[0], c[1]. Branch coverage of the sweep: cftfsub's five size branches (n > 512, > 128, ≤ 128, == 32, < 32, == 8, == 4), bitrv2's l == 8 / else (alternating powers of two), makewt's nwh == 4 / nwh > 4, makeipt's l > 32 loop (from N = 256 up) and the deep cftrec4/cfttree path (2^20) are all reached by 4 … 65536 + 2^20.

Table semantics per precision (attack 2). Every libm call in makewt/makect/the constructor takes a double argument via explicit cast (std::atan(1.0), std::cos(static_cast<double>(...)), std::sin(...), std::sqrt(static_cast<double>(n) / 2.0)); the float products delta * static_cast<Sample>(nwh), delta * 2, delta * 6, 3 * delta * static_cast<Sample>(j) are Sample arithmetic with the C's left-to-right grouping; 0.5 / cos(...), 0.5 / wk1r, 0.5 / wk3r, 0.5 * c[0], 0.5 * cos(...), 0.5 * sin(...), 0.5 * (a[0] - a[1]), 0.5 - c[nc - kk] are double arithmetic converted on assignment, exactly where the C converts. No std:: overload resolves to a float function. Mutation reproduced: in a scratch copy, w[j] = static_cast<Sample>(std::cos(delta * static_cast<Sample>(j))) (i.e. cosf) turns exactly the three float gates red (first difference at N = 2048, 128 ulp fwd / 96 ulp inv; 2^20: 131072 / 262144 ulp) and leaves the three double gates green.

Constness, aliasing, UB (attack 3). Every transform helper takes const int* ip / const Sample* w, so nothing can write a table after construction (compiler-enforced); makewt/makect are reached only from the constructor. Built the gate under ASan + UBSan (-fno-sanitize-recover=all, Debug so the constructor assert is live): 6/6 pass over 4 … 65536 + 2^20, both precisions, both directions — no out-of-bounds read at N = 4 / 8 (ip = 4 ints, w = 2 / 4 samples), no signed overflow or shift UB in bitrv2/makeipt at 2^20 (largest ip write is index 511 of 727). -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werror full builds (capi + bench on) warning-free on both g++ 13.3 and clang++ 18.1; ctest 283/283 on each.

Parity gate integrity (attack 4). expect_identical_all_sizes runs every size × every material for the given direction; the four gates cover {double, float} × {forward, inverse}; the 2^20 test covers both directions per material; the inverse is fed the reference forward's output on both sides. sweep_sizes() clamps at TAP_DSP_PARITY_MAX_N with a static_assert that it is a power of two ≥ 4 (no vacuous pass); the 2^20 test is compiled out, not skipped, below the cap (bare-metal main counts SKIP as a failure). tap_dsp_fft_parity gets -ffp-contract=off on its own TU (which is where the header-only port is compiled) and links tap_dsp_fft_reference_nocontract; tap_dsp_fft_parity_default_flags links tap_dsp_fft_reference_default with no flag on either side. Neither parity target links tap::dsp, so no backend define reaches them.

Bench and ratchet (attack 5). split_radix_bench_adapter::forward/inverse/copy are line-for-line basic_real_fft::forward/inverse/copy (if (input != output) copy loop; const Sample scale = Sample(2) / static_cast<Sample>(m_size); output[i] *= scale;); member init order matches declaration order. Host checksums C vs port identical on both compilers: 0x662dd085b5b88325 (512), 0x806d1c5dbb65c525 (2048). icount.py driven with a stubbed measure through compare / port-key-in-baseline / no-baseline / --update --record / --merge: _port never enters failures, never reaches baselines[target], is filed under top-level informational in the record file, and --merge skips that key — so the seeding path on a push to main cannot pick it up. The m55 sibling is CMSIS (documented three times: bench.yml, README, design note), and the port-vs-Ooura M55 figure is taken from m55-ooura.

Docs and numbers (attack 6). Pulled all twelve job logs. Every number in the PR body and docs/fft-design.md matches: the ten _port counts, the ten C counts (+0.00 % vs baselines.json), the ten port/C ratios, the five checksum verdicts (identical × 4 keys, DIFFER on m55), the ten .text figures and five size ratios, and the max over all N and materials = 0 ulp lines for double and float on all seven CI legs with the stated compiler ids (GNU 13.3.0, MSVC 19.51.36256.0, AppleClang 21.0.0.21000101, GNU 13.2.1 × 4). The derived figures (+2.0 … +3.4 % .text, 0.1 … 5.8 % fewer instructions, 0.4 % m55-vs-m55-ooura port delta, 1.7 – 1.9× vs CMSIS) recompute correctly. objdump FMA probe reproduced exactly (-O3 -march=haswell, vfn?m(add|sub)): gcc 372 / 588, clang 333 / 221, both 0 / 0 at -ffp-contract=off. The header banner is the design note's block verbatim plus the one modifications line, SPDX LicenseRef-Ooura AND MIT, and is consistent with NOTICE.md, LICENSES/LicenseRef-Ooura.txt and the Part 5 / Part 13 corrections (derivative-work statement, modification grant, readme path fixed by D6). The final commit 9df52d2 touches only docs/fft-design.md, so the 8afe6cf attribution of the measurements is right.

Style gates (attack 7). clang-format-18 --dry-run --Werror and pre-commit run --files clean on split_radix.h, test_fft_parity_ooura.cpp, bench_common.h; scripts/tidy.sh tests/test_fft_parity_ooura.cpp (which pulls the header through HeaderFilterRegex) clean, also with --warnings-as-errors=readability-*. (Note, pre-existing and not this PR's: the tidy compile database is configured without TAP_DSP_BUILD_BENCH, so bench/** is never tidied in CI or by tidy.sh.)


Generated by Claude Code

Comment thread docs/fft-design.md
Comment thread bench/README.md Outdated
Comment thread scripts/icount.py Outdated

@tap tap left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hostile review B (process, build, CI, downstream) of head a9b53b4. Numerics and transliteration fidelity are the other reviewer's. Every finding below was reproduced on a detached worktree of the branch or read from the named CI log; nothing is from memory. No blockers.

Findings

  1. should-fix — NOTICE.md (canonical provenance statement) still calls the port "planned". NOTICE.md:39 "What DspTap relies on going forward: the planned C++20 port of rdft (include/tap/dsp/fft/split_radix.h …)" and :31 "What DspTap ships today: one source file of the package, fftsg.c, plus its readme.txt". On merge DspTap ships the derivative header, so the file that README.md and docs/fft-design.md both defer to ("This file is the canonical statement") is wrong on the one fact this PR changes. docs/fft-design.md's provenance section was updated in this PR to say the port landed; NOTICE.md was not, and it is not a Part 12 contention file. Fix: two sentences ("landed at Stage 2a, #28, beside the C and routed nowhere until 2b; the decision on asking the author is still recorded here at 2c"). README.md:436-440 ("planned C++20 port", "Nothing from either has shipped yet") and :449 say the same thing; README is 2b's file, so that is a hand-off to the 2b README rewrite, not a change here.

  2. should-fix — .github/workflows/bench.yml:168-169: the size step pipes size through tee under bash -e {0} with no pipefail. Inline comment. A failing size on the port probe yields ratio port/C = 0.0000 and a passing step; the C line has the same pre-existing hole. The Count step in the same file sets set -o pipefail; Part 13 says every tee step must.

  3. nit — -Wdouble-promotion. Six lines in the float instantiation (split_radix.h:187, 245, 246, 290, 2531, 2554) fail under -Wdouble-promotion -Werror on g++ 13.3.0 and clang++ 18.1.3. Not in any Tap or MuTap flag set today; inline comment explains why it is still worth a thought for M4 soft-float consumers, and why it is the numerics reviewer's call.

  4. nit — bench/CMakeLists.txt:49 has no NOT TAP_DSP_BENCH_ENGINE STREQUAL "split_radix" guard on the _port size probe while bench/icount/CMakeLists.txt:61 has one; with the non-default engine the port is compared against itself. Inline.

  5. nit — bench.yml:140 comment "Release (-O2)" is contradicted by this PR's own last commit (a9b53b4: Release is CMake's GNU default -O3 -DNDEBUG, no toolchain file overrides it). The commit message says a workflow file was out of that commit's scope, but the PR touches bench.yml, so the one-word fix belongs here rather than in a later PR.

  6. nit — docs/fft-design.md:48 still carries TODO(stage 2a) / "pinned at Stage 2a" for the noise-floor row; this is the Stage 2a PR and it does not do it. Inline; re-label to the owner.

  7. nit — PR body says "Six commits"; there are seven (a9b53b4, the docs label fix, was added after the body was written). The commit list in the body is otherwise accurate.

  8. note — scripts/icount.py:108 --merge filters only the top-level informational key, not a _port key nested under a real target; no pipeline path produces one. Inline, no change requested.

Verified correct

  • Scope. Nine files; none of fft.h, README.md, CLAUDE.md, ci.yml, tests/CMakeLists.txt, test_fft.cpp, test_fft_oracle.cpp, test_fft_rt.cpp, third_party/**, or the style configs. The only edit to tests/test_fft_parity_ooura.cpp is the engine_under_test alias plus its comment; the fft.h include there is still load-bearing (it supplies the rdft/rdft_f prototypes ooura_ref calls). Nothing routed, nothing of 2b/2c.
  • Header-only, standalone. A TU including only split_radix.h, and one instantiating both precisions, compile clean under -std=c++20 -Wall -Wextra -Wconversion -Wsign-conversion -Wshadow -pedantic -Werror on g++ 13.3.0 and clang++ 18.1.3, also with -fno-exceptions -fno-rtti -DNDEBUG. Five standard includes, no macro defined, no statics or globals (every helper is a private static member), clang-tidy-18 with the repo .clang-tidy clean over the header, clang-format --dry-run --Werror clean on the three C++ files, each of the first three commits' header compiles on its own as the body claims. The constructor's assert is the same idiom as fft.h:340; Stage 6 owns TAP_EXPECTS.
  • Compile time. Both precisions at -O2: g++ 1.72 s, clang++ 1.10 s (this host; fftsg.c as C++ 0.94 / 0.73 s, the two C files as C 1.67 s gcc). Same order as Part 4's 1.3 s; D8 stands.
  • Test registration. tap_dsp_fft_parity links tap_dsp_fft_reference_nocontract + tap_dsp_warnings only (tests/CMakeLists.txt:221-223), not tap::dsp; gtest names unchanged, so ci.yml's -L parity -R default_flags -V step still matches (ran on linux, job 105446230919). QEMU legs at 9df52d2 (run 35295707940): all four report TAP_DSP_TESTS_COMPLETE rc=0 with selected=223/4/1 skipped=0; the parity binaries ran whole at N ≤ 4096 (the two 2^20 tests are compiled out under the cap, as documented); M55 tap_dsp_tests .text 1,077,672 + heap 2,087,896 fits AN547.
  • CMake/CI. bench/icount second loop goes through the same function, so the smoke test stays host-only and the _port binaries build on every leg (the QEMU jobs need them); the _port size probe builds on Generic because bench.yml builds it. TAP_DSP_BENCH_ENGINE default unchanged and only visible under TAP_DSP_BUILD_BENCH.
  • icount.py. Exercised compare / no-baseline / --update / --record / --merge with a stubbed QEMU: _port never affects the exit code, a baseline naming a _port key prints IGNORED and --update drops it, --record files it under informational, --merge skips that key; the seed-only-on-main logic is untouched and baselines-<key> artifacts come from --update, which never writes _port. The informational lines appear in the job log (m33 105446394276, m4-softfp 105446394297, m55 105446393983) and are appended to the step summary via teeicount.txt; the size line is echoed and appended too.
  • Provenance. The header's notice block is token-identical to LICENSES/LicenseRef-Ooura.txt and to third_party/ooura/readme.txt:141-145; NOTICE.md's quoted grant matches; the derivative-work sentence is present; no "unmodified", "public domain" or "permissive" wording was added; docs/fft-design.md's banner block matches the header except the documented extra "Modifications (…) September 2026" sentence.
  • Numbers traced to logs. m33 counts 100,833,265 / 115,396,527 and .text 42,601 / 44,009 (105446394276); m4-softfp 1,864,904,581 / 2,294,343,851 (105446394297); m55 .text 107,681 / 39,281 (105446393983); ulp 0 / 0 on linux (105446230919), macos (105446230934), cortex-m55 (105446230931); runner images ubuntu-24.04 / macos-26-arm64 / windows-2025-vs2026, compilers GNU 13.3.0 / AppleClang 21.0.0.21000101 / MSVC 19.51.36256.0 / arm-none-eabi-gcc 13.2.1 (15:13.2.rel1-2), QEMU 8.2.2 (1:8.2.2+ds-0ubuntu1.18) — all as the design note states. bench/README.md's tables agree with bench/baselines.json (unchanged) and the new _port semantics. The .text deltas (+776 … +1,560 B, +2.0 … +3.4 %) and the "0.1 – 5.8 % fewer instructions" arithmetic check out.
  • Downstream (MuTap). Scratch copies of MuTap configured with -DMUTAP_WERROR=ON against DspTap origin/main and against this tree: CMakeCache.txt, target list and configure output identical; mutap_tests builds warning-free with -Werror against this tree (its suite was 123/197 green and still running at post time; nothing in it can see the port). Nothing in MuTap includes the new header; MuTap's CI gcc -c submodules/dsptap/third_party/ooura/fftsg*.c path is untouched; no new cache variable, option, CMake minimum or default. The "no pin bump needed" claim holds.
  • Hygiene. All seven commits carry both trailers; PR footer present; no other model identifiers in the diff; PR template sections filled; each commit message matches its diff. Local: full build -DTAP_DSP_WERROR=ON -DTAP_DSP_BUILD_BENCH=ON -DTAP_DSP_BUILD_CAPI=ON, ctest 283/283. Head a9b53b4: all 24 check runs green.

Generated by Claude Code

Comment thread .github/workflows/bench.yml
Comment thread include/tap/dsp/fft/split_radix.h
Comment thread bench/CMakeLists.txt Outdated
Comment thread docs/fft-design.md Outdated
Comment thread scripts/icount.py
tap and others added 4 commits September 18, 2026 01:57
Review B1 on #28: the canonical statement still called the C++20
port "planned" and described the vendored fftsg.c as everything DspTap
ships. Post-2a truth, stated in the same bullet: the port exists at
include/tap/dsp/fft/split_radix.h beside the vendored C, is bit-identical
to it for both precisions under the parity gate, and is routed nowhere
until Stage 2b flips basic_real_fft at it; the C leaves the shipping tree
at Stage 2c. "SPDX plan" becomes "SPDX" now that the header carries it.

No licensing fact moves: the grant quotation, the derivative-work and
modification-grant sentences, the readme.txt-stays-permanently point and
the LicenseRef-Ooura AND MIT identifier are as wave 1 settled them.
README.md's matching "planned" sentences belong to the Stage 2b README
rewrite (audit Part 3) and are not touched here.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
…step fails loud

Reviews A2/A3 and B2/B4/B5/B8 on #28.

- bench.yml, size step (B2): set -eo pipefail (Part 13: every step that
  pipes through tee sets it; the Count step already did), and an explicit
  "<C|port> .text unavailable" error when size -A yields no numeric .text
  row for either probe. Before, a failing arm-none-eabi-size printed an
  empty figure, a 0.0000 ratio and a green step, and the C ceiling
  comparison [ "" -gt ceiling ] was silently skipped inside the if.
- bench.yml (B5): the workloads comment said "Release (-O2)"; Release is
  CMake's GNU default, -O3 -DNDEBUG, as a9b53b4 already corrected in the
  design note. Header comment states precisely what "informational" holds
  for the icount and size paths.
- bench/CMakeLists.txt (A2/B4): the _port size probe now follows the same
  guard as the _port icount pairs and is skipped when TAP_DSP_BENCH_ENGINE
  is already split_radix (the probe would be the port compared to itself).
  Verified by configuring both ways: default builds the two icount _port
  pairs and the _port probe; split_radix builds none of the three.
- scripts/icount.py (A3): measure() raises MeasurementError instead of
  SystemExit. A gated binary that yields no count still aborts the run as
  before; an informational (_port) binary that times out, faults or prints
  ok=0 is reported as "<key>: informational binary failed: <reason>" in the
  informational block and the run continues to the gated verdict, so the
  docstring's "never a gate entry" is now true of the process as well as
  the verdict. (B8): --merge drops a _port key nested under a real target,
  not only the top-level "informational" key, so a hand-edited record file
  cannot seed one. Driven with a stubbed measure() through compare,
  no-baseline, port-key-in-baseline, --update --record, informational
  timeout, informational ok=0, gated failure and --merge with a smuggled
  key; outputs in the PR comment.
- bench/README.md: the _port sentence says the actual rule (built at the
  default; not when the variable is already split_radix), the informational
  paragraph says exactly what holds, and the Sizes policy notes the pipefail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
Review B6 on #28: the contract table's noise-floor row and the
split-radix rationale paragraph still said the float 1.105e-7 probe value
and the double figure against the compensated-DFT oracle are "re-measured
and pinned at Stage 2a". Stage 2a did not and could not: its scope is the
port and its parity gate, and tests/test_fft.cpp is untouched by it.

Labelled Stage 2b, not later: the audit (Part 3) does not name the stage
that pins these two numbers, and 2b is the PR that re-points the existing
battery at the port and rewrites the README, so a number measured then is
measured on what ships; 3b's widening of test_fft.cpp is over the
fixed-point profiles, which these two rows are not about.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
Review B3 on #28. In the float instantiation -Wdouble-promotion
(g++ 13.3 and clang++ 18.1, not in any Tap flag set) fires on six lines:
0.5 * (a[0] - a[1]), 0.5 / wk1r, 0.5 / wk3r, 0.5 * c[0] and the two
0.5 - c[nc - kk]. Those are the intended rule-2 doubles, where the C's
float build promoted to a double literal in exactly the same places, and
the statements stay textually intact (rule 1; the reviewers' fidelity
normalizer and the cosf mutation check are evidence attached to the
current text). One sentence added to rule 2 in the banner saying so and
that no cast is to be added to silence it. Comment-only: the parity label
re-run 7/7 on g++ and clang++.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy

tap commented Sep 18, 2026

Copy link
Copy Markdown
Owner Author

Fix pass for reviews A (#pullrequestreview-5243359055) and B (#pullrequestreview-5243370615)

Head a9b53b4d8eccf6, four commits, seven files; the port header change is comment-only (git diff --stat: +7 lines in the banner; parity label 7/7 re-run on g++ 13.3.0 and clang++ 18.1.3). Every finding was re-verified against source before it was changed.

finding fix commit
B1 should-fix — NOTICE.md calls the port "planned", describes the C as everything shipped provenance bullet states the post-2a truth: the port exists at include/tap/dsp/fft/split_radix.h beside the vendored C, bit-identical under the parity gate, routed nowhere until 2b, the C leaves at 2c; "SPDX plan" → "SPDX". No licensing fact moved (grant quotation, derivative-work and modification-grant sentences, readme.txt stays, LicenseRef-Ooura AND MIT all as wave 1 settled them) d137a2e
B2 should-fix — size step pipes size through tee without pipefail set -eo pipefail at the top of the step; each of $text / $text_port checked to be a non-empty digit string, else ::error::<key>: C probe .text unavailable … / … port .text unavailable … and exit 1. Both holes (C line included) closed; no more 0.0000 ratio or skipped ceiling comparison 37ca717
A2 / B4 nit — _port size probe unguarded while the icount pairs are guarded; README says "whatever the variable says" bench/CMakeLists.txt applies the icount guard (NOT TAP_DSP_BENCH_ENGINE STREQUAL "split_radix"); README states the actual rule. Verified by configuring both ways: default builds both _port icount pairs and the _port probe; split_radix builds none of the three 37ca717
A3 nit — "cannot fail the run" overstated; B note 8 — --merge only filters the top-level key catch-and-print: measure() raises MeasurementError; a gated binary still aborts the run, a _port binary that times out / faults / prints ok=0 prints <key>: informational binary failed: <reason> in the informational block and the gated verdict runs. --merge drops _port keys nested under a real target with a DROPPED line. Docstring, bench/README.md and the bench.yml header say exactly what holds 37ca717
B5 nit — bench.yml comment "Release (-O2)" "CMake's Release default for GCC, -O3 -DNDEBUG; no toolchain file overrides it" 37ca717
B6 nit — docs/fft-design.md noise-floor row TODO(stage 2a) both cells and the rationale paragraph re-labelled TODO(stage 2b) with the reason (2a's scope was the port and its gate; test_fft.cpp untouched; 2b re-points the battery at the port, so a number pinned there is measured on what ships; 3b's widening is over the fixed-point profiles) cb92d50
B7 nit — PR body "Six commits" body now says eleven and names the five added since (PR body)
B3 nit — -Wdouble-promotion on lines 187, 245, 246, 290, 2531, 2554 documented, not cast: reproduced on both compilers (exactly those six, nothing else); rule 2 in the banner now names the sites, states they are the rule-2 contract (the C's float build promoted in the same places), that no cast is added (rule 1 keeps the statements textually intact; both reviews' normalizers and the cosf mutation are evidence attached to the current text), and that no Tap flag set enables the warning d8eccf6
A1 nit — "three FMA-capable legs" already fixed at a9b53b4 (review A was written against 9df52d2)

Hand-off to Stage 2b (not changed here)

README.md:436-440 ("planned C++20 port", "Nothing from either has shipped yet") and :449 say what NOTICE.md used to say. README is the Stage 2b README rewrite's file (audit Part 3) and another agent's this wave, so it is left as is; the 2b agent should bring those sentences to the same post-2a wording NOTICE.md now carries (port landed at 2a, bit-identical, routed at 2b).

Local verification of d8eccf6

  • g++ 13.3.0 and clang++ 18.1.3, Release, -DTAP_DSP_WERROR=ON -DTAP_DSP_BUILD_CAPI=ON -DTAP_DSP_BUILD_BENCH=ON: zero warnings, ctest 283/283 each; ctest -L parity 7/7 each.
  • -DTAP_DSP_BENCH_ENGINE=split_radix vs default: target lists as in the A2/B4 row.
  • python3 -m py_compile scripts/icount.py; pre-commit run (clang-format 18.1.3) clean; scripts/tidy.sh tests/test_fft_parity_ooura.cpp (clang-tidy-18, pulls the header) clean.

scripts/icount.py driven with a stubbed measure()

Fake build dir with the four tap_dsp_icount_rfft_f32_{512,2048}{,_port} binaries, counts = the real m33 numbers, measure() replaced by a stub that can raise MeasurementError per key. <dir> = the scratch directory.

$ icount.py --target m33 --build-dir <dir>/build-m33 --plugin <plugin>   # compare
rfft_f32_2048: 116385409 insns vs baseline 116385409 (+0.00%) ok
rfft_f32_512: 102248169 insns vs baseline 102248169 (+0.00%) ok
--- informational: '_port' scenarios (counted, never gated, never baselined; Stage 2a until 2c) ---
rfft_f32_2048_port: 115396527 insns [engine=split_radix backend=split_radix]; rfft_f32_2048: 116385409 insns [engine=reference_c backend=ooura]; ratio rfft_f32_2048_port/rfft_f32_2048 = 0.9915; output checksums identical
rfft_f32_512_port: 100833265 insns [engine=split_radix backend=split_radix]; rfft_f32_512: 102248169 insns [engine=reference_c backend=ooura]; ratio rfft_f32_512_port/rfft_f32_512 = 0.9862; output checksums identical
-> exit 0
   baselines file now: {"m33": {"rfft_f32_2048": 116385409, "rfft_f32_512": 102248169}}

$ icount.py --target m33 --build-dir <dir>/build-m33 --plugin <plugin>   # no-baseline
rfft_f32_2048: 116385409 insns (NO BASELINE — commit this value)
rfft_f32_512: 102248169 insns (NO BASELINE — commit this value)
--- informational: '_port' scenarios (counted, never gated, never baselined; Stage 2a until 2c) ---
rfft_f32_2048_port: 115396527 insns [engine=split_radix backend=split_radix]; rfft_f32_2048: 116385409 insns [engine=reference_c backend=ooura]; ratio rfft_f32_2048_port/rfft_f32_2048 = 0.9915; output checksums identical
rfft_f32_512_port: 100833265 insns [engine=split_radix backend=split_radix]; rfft_f32_512: 102248169 insns [engine=reference_c backend=ooura]; ratio rfft_f32_512_port/rfft_f32_512 = 0.9862; output checksums identical
-> exit 1
   baselines file now: {"m33": {}}

$ icount.py --target m33 --build-dir <dir>/build-m33 --plugin <plugin>   # port-key-in-baseline
rfft_f32_2048: 116385409 insns vs baseline 116385409 (+0.00%) ok
rfft_f32_512: 102248169 insns vs baseline 102248169 (+0.00%) ok
rfft_f32_512_port: baseline 100833265 IGNORED (informational scenario; remove it from bench/baselines.json)
--- informational: '_port' scenarios (counted, never gated, never baselined; Stage 2a until 2c) ---
rfft_f32_2048_port: 115396527 insns [engine=split_radix backend=split_radix]; rfft_f32_2048: 116385409 insns [engine=reference_c backend=ooura]; ratio rfft_f32_2048_port/rfft_f32_2048 = 0.9915; output checksums identical
rfft_f32_512_port: 100833265 insns [engine=split_radix backend=split_radix]; rfft_f32_512: 102248169 insns [engine=reference_c backend=ooura]; ratio rfft_f32_512_port/rfft_f32_512 = 0.9862; output checksums identical
-> exit 0
   baselines file now: {"m33": {"rfft_f32_2048": 116385409, "rfft_f32_512": 102248169, "rfft_f32_512_port": 100833265}}

$ icount.py --target m33 --build-dir <dir>/build-m33 --plugin <plugin> --update --record <dir>/measured-m33.json   # update-record
rfft_f32_2048: 116385409 insns (NO BASELINE — commit this value)
rfft_f32_512: 102248169 insns (NO BASELINE — commit this value)
--- informational: '_port' scenarios (counted, never gated, never baselined; Stage 2a until 2c) ---
rfft_f32_2048_port: 115396527 insns [engine=split_radix backend=split_radix]; rfft_f32_2048: 116385409 insns [engine=reference_c backend=ooura]; ratio rfft_f32_2048_port/rfft_f32_2048 = 0.9915; output checksums identical
rfft_f32_512_port: 100833265 insns [engine=split_radix backend=split_radix]; rfft_f32_512: 102248169 insns [engine=reference_c backend=ooura]; ratio rfft_f32_512_port/rfft_f32_512 = 0.9862; output checksums identical
recorded <dir>/measured-m33.json
updated <dir>/update-record.json
-> exit 0
   baselines file now: {"m33": {"rfft_f32_2048": 116385409, "rfft_f32_512": 102248169}}
   measured-m33.json: {"informational": {"m33": {"rfft_f32_2048_port": 115396527, "rfft_f32_512_port": 100833265}}, "m33": {"rfft_f32_2048": 116385409, "rfft_f32_512": 102248169}}

$ icount.py --target m33 --build-dir <dir>/build-m33 --plugin <plugin>   # port-timeout-informational (new path)
rfft_f32_2048: 116385409 insns vs baseline 116385409 (+0.00%) ok
rfft_f32_512: 102248169 insns vs baseline 102248169 (+0.00%) ok
--- informational: '_port' scenarios (counted, never gated, never baselined; Stage 2a until 2c) ---
rfft_f32_512_port: informational binary failed: <dir>/build-m33/bench/icount/tap_dsp_icount_rfft_f32_512_port: timed out after 1200 s under QEMU
rfft_f32_2048_port: 115396527 insns [engine=split_radix backend=split_radix]; rfft_f32_2048: 116385409 insns [engine=reference_c backend=ooura]; ratio rfft_f32_2048_port/rfft_f32_2048 = 0.9915; output checksums identical
-> exit 0

$ icount.py --target m33 --build-dir <dir>/build-m33 --plugin <plugin>   # port-ok0-informational (new path)
rfft_f32_2048: 116385409 insns vs baseline 116385409 (+0.00%) ok
rfft_f32_512: 102248169 insns vs baseline 102248169 (+0.00%) ok
--- informational: '_port' scenarios (counted, never gated, never baselined; Stage 2a until 2c) ---
rfft_f32_2048_port: informational binary failed: <dir>/build-m33/bench/icount/tap_dsp_icount_rfft_f32_2048_port: workload did not complete cleanly
rfft_f32_512_port: 100833265 insns [engine=split_radix backend=split_radix]; rfft_f32_512: 102248169 insns [engine=reference_c backend=ooura]; ratio rfft_f32_512_port/rfft_f32_512 = 0.9862; output checksums identical
-> exit 0

$ icount.py --target m33 --build-dir <dir>/build-m33 --plugin <plugin>   # gated-binary-fails (unchanged: aborts)
rfft_f32_2048: 116385409 insns vs baseline 116385409 (+0.00%) ok
-> exit SystemExit(<dir>/build-m33/bench/icount/tap_dsp_icount_rfft_f32_512: workload did not complete cleanly)

$ icount.py --merge <dir>/measured-m33.json <dir>/hand-edited-m4f.json   # merge; the second file smuggles a _port key under "m4f"
m33: 2 scenario(s) from <dir>/measured-m33.json
m4f: rfft_f32_512_port from <dir>/hand-edited-m4f.json DROPPED (informational scenario; never a gate entry)
m4f: 1 scenario(s) from <dir>/hand-edited-m4f.json
merged into <dir>/merge.json
-> exit 0
   baselines file now: {"m33": {"rfft_f32_2048": 116385409, "rfft_f32_512": 102248169}, "m4f": {"rfft_f32_512": 98090666}}

CI on d8eccf6 is being watched; not merging.


Generated by Claude Code

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant