Skip to content

srdif: tune for Hexagon (clang) and gate Hexagon in the ratchet; no output bit moves - #44

Merged
tap merged 8 commits into
mainfrom
claude/srdif-hexagon
Sep 28, 2026
Merged

tap merged 8 commits into
mainfrom
claude/srdif-hexagon

Conversation

@tap

@tap tap commented Sep 28, 2026

Copy link
Copy Markdown
Owner

What this changes

  • A hexagon key for the instruction-count ratchet. Adds cmake/hexagon-linux-musl.cmake, the icount.py target, and a bench.yml job that builds a plugin-enabled qemu-hexagon from the pinned QEMU 8.2.2 release. It uses the same CodeLinaro clang 19.1.5 toolchain and flags as MuTap's Hexagon leg.
  • A tuning pass on the srdif engine for Hexagon's compiler. The Hexagon float and double scenarios now run below the engine Replace the Ooura-derived floating engine with srdif, a clean-room split-radix DIF engine #42 replaced, and every Cortex-M float key also falls.
  • No output bit moves at -ffp-contract=off.

Why

MuTap's bump to 72977aa measured srdif +12–13 % on its Hexagon chain workloads (fdkf, shadow, suppressor, chain). The float FFT alone read +27–32 % at N ≥ 512. #42's "must not regress" bar was checked only on the Cortex-M keys, because no key here measured Hexagon. The maintainer chose to fix srdif before MuTap takes it.

Verification

Instruction counts

The Hexagon counts come from the workflow_dispatch runs on this branch: 36357132506 for #42's engine and 36363710480 for the tuned engine. The predecessor figure is a local measurement with the same harness, converted by the runner's constant offset.

key scenario #42 srdif tuned predecessor tuned vs predecessor
hexagon rfft_f32_512 57,659,223 45,934,423 49,732,931 −7.6 %
hexagon rfft_f32_2048 65,684,295 51,608,903 54,834,714 −5.9 %
hexagon rfft_f64_512 110,683,597 99,939,789 103,239,537 −3.2 %
m4-softfp f32 512 / 2048 1,814,303,702 / 2,243,212,021 1,813,126,102 / 2,241,935,605 −0.06 %
m4f f32 512 / 2048 92,575,609 / 106,282,752 91,545,465 / 105,151,232 −1.1 %
m33 f32 512 / 2048 92,979,212 / 106,773,786 92,624,908 / 106,248,986 −0.4 / −0.5 %
m55-ooura f32 512 / 2048 83,933,381 / 97,602,563 83,628,229 / 97,109,507 −0.4 / −0.5 %
  • Unchanged to the instruction: m55 (CMSIS) and every fixed-point scenario.
  • Arm rows confirmed: the eight re-recorded Arm rows match run 36363710480 exactly (compare mode, +0.00 %).
  • .text: the f32 probes grow by 64–384 B and stay under every ceiling. The Hexagon ceilings are recorded new.

What changed, and what paid. Details are in docs/fft-design.md, "Hexagon (clang) tuning".

  • Forced inlining under clang only, not at -Os. clang had left the groups, leaf helpers and index-sequence lambdas as calls. Under GCC the same forcing costs the M4F / M33 / M55 keys 4–9 %, so GCC is left alone.
  • Each post-pass step loads both bin pairs before storing either.
  • Blocks of 16 and fewer run one split-radix level at a time in memory, instead of register leaves held in a stack array under clang.
  • Double runs one fused-pass group per loop step.
  • A multiply-subtract spelling that clang contracts. It is bit-neutral at -ffp-contract=off.

The Hexagon key counts packets, not instructions. qemu-hexagon reports one guest instruction per VLIW packet, as a per-address profile showed. bench/README.md says so.

Output bits. tap_dsp_srdif_fingerprint passes unchanged:

  • on the host (g++ 13 and clang 18, both glibc dispatches);
  • on all four QEMU legs;
  • on Hexagon/clang 19 at every N from 4 to 65536 in both profiles. The single row holds there, although CI does not run the Hexagon tests.

Builds that contract (the bench's default flags) move the float checksums on the FPU keys and on Hexagon float. The fp-contraction policy allows that.

Implementer's local runs

  • Host Release 442/442 (GCC and clang) and Debug 438/438.
  • ASan+UBSan on the FFT suites 225/225.
  • clang-format and scripts/tidy.sh clean.
  • All four Arm QEMU legs as ci.yml builds them.
  • Hexagon tap_dsp_tests 423/426. The three failures are the dlopen ABI-tag tests, which a static musl binary cannot run.
  • Host timings: no cell got more than 5 % slower.

I independently re-measured the Hexagon counts at the head of the tuning commits.

Clean room. The tuning was written by an implementer barred from the port, the package, pre-hand-off history, the MuTap trees, the provenance records and every PR. The orchestrator, which had read the port, passed the port's Hexagon counts as numbers only, and deleted its own port build products first. The access statement is in docs/fft-design.md, "The Hexagon tuning pass, clean-room". It includes single lines of forbidden sections that broad greps printed: routine names and one-line prose, no code, reported unused.

Notes for the reviewer

  • Submodule pin moved: none. MuTap's bump (branch claude/dsptap-srdif-bump, held) will move to this PR's squash once it merges. That re-records MuTap's Hexagon ratchet and all nine fingerprint legs from one CI run.
  • New required check candidate: icount hexagon, once this is on main.
  • The main-SHA cells of the new bench rows read pending until the squash, per bench/README.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy


Generated by Claude Code

tap and others added 7 commits September 27, 2026 22:59
The srdif engine (#42) met "must not regress" on every key this
ratchet measured, but none of them is Hexagon, the third target of MuTap,
DspTap's main consumer: MuTap's Hexagon ratchet read +12 ... +13 % on its
chain workloads at the bump to 72977aa. This adds the target so DspTap gates
it directly:

- cmake/hexagon-linux-musl.cmake, adapted from MuTap's (the same CodeLinaro
  clang 19.1.5 toolchain, -mv68 -mhvx -mhvx-length=128b, static musl), so the
  two repositories' Hexagon counts are comparable.
- scripts/icount.py: target `hexagon`, run under qemu-hexagon user mode.
- bench.yml: a `hexagon` key in the matrix, which builds a plugin-enabled
  qemu-hexagon from the pinned QEMU 8.2.2 release (cached on its digest),
  downloads the toolchain, and measures the size probes with llvm-size. As a
  hosted target it also builds and gates rfft_f64_512.

The key has no baselines yet, so its job refuses on a pull request until a
workflow_dispatch run on this branch seeds them (bench/README.md, "Seeding");
the .text ceilings are 0 (not recorded) until then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
…he Arm keys

The srdif engine (#42) executed more instructions on the new
`hexagon` ratchet key (clang 19.1.5, -mv68 -mhvx, qemu-hexagon 8.2.2) than
the engine it replaced: +15.9 % / +19.8 % (float, N = 512 / 2048) and
+7.2 % (double). Now -7.64 % / -5.88 % and -3.20 % below it, and every
Cortex-M float scenario falls as well (-0.06 ... -1.11 %).

- Under clang (not at -Os / -Oz), the butterflies, groups, compile-time
  blocks, their index-sequence lambdas, the post-pass pairs and the swaps
  are forced inline (TAP_DSP_SRDIF_INLINE). clang had kept them out of
  line on Hexagon: calls per group and register leaves through a stack
  array. GCC keeps its own choices (forced there, the FPU keys cost 4.5 -
  9.1 % more).
- The post-pass runs two bin pairs with every load before any store
  (post_two), so the pairs' arithmetic can share Hexagon packets.
- Blocks of 16 and fewer run one split-radix level at a time in memory
  down to pairs (level, level_bf) instead of register leaves: a leaf of
  16 holds 32 values and spills on every target measured.
- Double runs the run-time fused pass one group per loop step (two groups
  of doubles are twice Hexagon's register file).
- mul_sub spells a b - c d so clang's statement-scoped contraction fuses
  a multiply-subtract instead of negating a product (float only).

No output bit moves at -ffp-contract=off: the srdif fingerprints pass
unchanged on the host (g++, clang++), the four QEMU legs and Hexagon.
Contracting builds move last bits (bench checksums on the FPU keys and
Hexagon float). Contract, tables and heap unchanged. The measurements and
what was tried are in docs/fft-design.md, "Hexagon (clang) tuning".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
… tuning

The srdif engine's Hexagon tuning lowers every float scenario on the four
keys that run it, inside the +-3 % band (m4-softfp -0.06 / -0.06 %, m4f
-1.11 / -1.06 %, m33 -0.38 / -0.49 %, m55-ooura -0.36 / -0.51 %, N = 512 /
2048); the exact counts are recorded, one row per scenario in
bench/README.md. Fixed-point scenarios and the m55 (CMSIS) key are
unchanged to the instruction and not re-recorded. The m4f DONE line in the
README carries the new checksum (GCC fuses the unchanged operations
differently; the fingerprints at -ffp-contract=off do not move). The
hexagon key is seeded from CI, not here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
A new subsection of "The floating engine (srdif)": what the hexagon key
counts (qemu-hexagon counts packets, not instructions), where the count
went at 72977aa, the before/after per key and scenario, each step and each
change measured alone, what was tried and not kept, the output bits (the
fingerprints hold at -ffp-contract=off, Hexagon included; which contracting
builds' checksums move and why), the .text probes, the Hexagon test
battery, host timings, and a finding for the class: on Hexagon a third of
every floating scenario is basic_real_fft's out-of-place copy through
musl's memcpy. The "Structure" bullet on register leaves notes what the
tuning replaced; README's CMSIS ratio (1.78x -> 1.77x at N = 2048) follows
the new m55-ooura count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
- bench/README.md: the `hexagon` row of the key table, and why the key
  exists. It counts packets, not instructions: qemu-hexagon reports one guest
  instruction per VLIW packet. Its absolute counts carry a per-machine
  constant. Six required `icount <key>` jobs once seeded.
- docs/fft-design.md, provenance: "The Hexagon tuning pass, clean-room".
  What the orchestrator measured of the port, and how that was passed on
  (numbers only). The brief's exclusions. The implementer's access
  statement, including the single lines of forbidden sections its broad
  greps printed (routine names and one-line prose, no code, reported unused).
  The result.
- CLAUDE.md: the engine no longer has register leaves. Blocks of 16 and
  fewer run one level at a time in memory, with forced inlining under clang.
  It also names the hexagon key beside the five Cortex-M ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
…ed engine

Baselines: the counts of workflow_dispatch run 36363710480 on this branch
(2a5fc69, seed mode), the tuned srdif engine.
- rfft_f32_512: 45,934,423
- rfft_f32_2048: 51,608,903
- rfft_f64_512: 99,939,789
- Q15 / Q31: 159,976,886 / 184,890,581 / 157,303,237

For the record, the engine as #42 shipped it read 57,659,223 / 65,684,295 /
110,683,597 on the first seeding run (36357132506). The engine it replaced
read 49,732,931 / 54,834,714 / 103,239,537 (local, the same harness,
converted to the runner's constant).

.text ceilings for the key: measured + 3 %, rounded up to 64 bytes, from
llvm-size -A on the statically linked probes (musl and libc++ included):
245,824 / 251,968 / 251,456.

The eight Cortex-M float rows re-recorded by the tuning pass are confirmed
to the instruction by the same run (compare mode, +0.00 %). Their main SHA
cells, like the hexagon rows', stay pending until the squash.

Co-Authored-By: Claude Opus 5.5 <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 of #44 at 2bd44bb (srdif Hexagon tuning + hexagon ratchet key)

Verdict: no correctness defect in srdif.h. No output bit moves where the fingerprints are defined. Every headline number I re-measured reproduces, and I found no provenance problem. The findings below are one supply-chain gap in bench.yml, gaps in the clean-room record, and wording in the docs. This is a COMMENT review: I neither approve nor request changes.

What I ran, and what held

All of it was run on the PR head in a detached worktree, with builds in my scratch space.

  • Output bits (tap_dsp_srdif_fingerprint, -ffp-contract=off): the single pinned row matches in every configuration below.
    • Host g++ 13 Release: 437/437 tests pass.
    • Host clang++ 18 Release (TAP_DSP_WERROR=ON): 437/437 tests pass.
    • cortex-m33 and cortex-m4-softfp QEMU legs, built exactly as ci.yml builds them (MinSizeRel, -DTAP_DSP_WERROR=ON): 367 + 2 + 3 selected tests pass, and both fingerprints match.
    • Hexagon (clang 19.1.5, Release, so the forced-inline path is active) under qemu-hexagon at every N from 4 to 65536: both profiles match.
  • Hexagon battery. I built the whole Hexagon suite with the Hexagon toolchain file and ran it through ctest under qemu-hexagon, with sweeps capped at 4096. 423 of 426 pass. The three failures are fft_abi_tag.*, whose dlopen reports "Dynamic loading not supported" in a static musl image. This matches the PR's report.
  • UB and memory, over the 241 FFT / srdif / fingerprint tests:
    • g++ 13 with ASan+UBSan (-fno-sanitize-recover=all): 241/241 pass.
    • clang++ 18 with -fsanitize=undefined,bounds -fsanitize-trap=all, Release, so the always_inline path is the one checked: 241/241 pass.
    • clang's ASan runtime is not installed in this container, so I could not run ASan under clang.
  • post_two aliasing. The loop only reaches pairs k, k+1 ≤ m/2 − 1. Their four indices {k, k+1, m−k−1, m−k} are therefore pairwise distinct: m−k−1 ≥ m/2+1 > k+1. Loading all four before storing any cannot observe a store, so the reorder is exact.
  • level<L> / block<L>. Checked by hand against the removed leaf_blocks<L>:
    • the same butterflies with the same twiddles, including J == L/8 as the eighth turn;
    • J = 1 and J = 3 at L = 16 use W16^1/W16^3 and W16^3/−W16^1;
    • the same sub-block recursion (L/2 at a, then L/4 at a+L and at a+3L/2 Samples);
    • block<1> is never instantiated.
  • Portability of TAP_DSP_SRDIF_INLINE, including the attribute on the templated lambdas:
    • clang 18 (host) and clang 19 (Hexagon) compile it at -O0, -O2 and -Os with -Wall -Wextra -Wpedantic -Wconversion -Wshadow and no warning;
    • clang-cl (clang 18 in --driver-mode=cl, /W4) compiles it;
    • clang-cl defines __OPTIMIZE_SIZE__ under /O1 and /Os and not under /O2 or /Od, so the size gate carries over to Windows. AppleClang uses the same front end; I could not run it here.
    • GCC never sees the attribute, so no compiler is left that would reject it on a lambda.
    • noexcept and allocation-free behaviour are unchanged: the new code is only stack aggregates and by-reference lambdas.
  • Symbols. On the Hexagon Release binary, llvm-nm shows that only kernel, fused_pass, permute, post_pass and the table builders survive out of line. Without the attribute, group<…>, block<16|32|64>, level<16>'s lambda and group_first are all separate functions.
  • Hexagon counts, via icount.py --target hexagon as bench.yml runs it:
    • My machine reads a constant +1,596 against CI on all six scenarios, fixed point included. The PR head reproduces the seeded baselines exactly after removing that offset.
    • 28befbd (#42's engine) reproduces run 36357132506's counts exactly after the same offset: 57,659,223 / 65,684,295 / 110,683,597.
    • PR run 36364049440 (pull_request) reads the same counts as dispatch run 36363710480, to the packet.
  • Arm keys, m33 and m4f built exactly as bench.yml builds them: the float counts equal the new baselines exactly (92,624,908 / 106,248,986 and 91,545,465 / 105,151,232).
  • Ablations, each change undone on the head tree:
change undone measured claimed
forcing off (Hexagon f32 512 / f32 2048 / f64 512) 54.13 / 62.12 / 107.12 M, +17.8 / +20.4 / +7.2 % same
forcing under GCC (m4f) +9.06 / +6.80 % +9.1 / +6.8 %
post_two (Hexagon, three scenarios) 47.47 / 53.17 / 100.97 M same
mul_sub (Hexagon float) +0.65 / +0.43 % same
forcing at MinSizeRel (Hexagon f32 probe .text) 263,204 B same
  • The three Hexagon .text figures (238,628 / 244,580 / 244,132) and the three ceilings, each measured + 3 % and rounded up to 64 B, check out.
  • Packets, not instructions. I wrote a second plugin that also sums qemu_plugin_insn_size() per executed unit. On rfft_f32_512:
    • units: 45,935,997 (the ratchet's count);
    • bytes: 382,797,700, which is 95,699,425 32-bit words;
    • unit sizes: 1 word 18.5 M, 2 words 12.1 M, 3 words 8.5 M, 4 words 6.9 M.
    • A plugin "instruction" is therefore a whole packet of up to four words. The hexagon key counts packets, and a word count would be about 2.08× higher. The claim holds.
  • Provenance (item 4 of my brief; the port was read at 7a58ebe for this item only): no move toward the port. The side-by-side comparison is at the end.

Findings, most severe first

1. [medium] bench.yml:197-205: the Hexagon toolchain is downloaded without a digest check.

  • The workflow checks the SHA256 of the plugin header and of the QEMU source. It does not check the one input that determines every hexagon count, the CodeLinaro compiler tarball.
  • Nothing asserts the compiler version either: "Toolchain versions" only prints it.
  • Artifactory can replace 19.1.5/clang+llvm-19.1.5-cross-hexagon-unknown-linux-musl.tar.zst in place. If it did, the ratchet would move or silently re-baseline, and nothing would say why. bench/README.md promises the counts are "comparable with MuTap's Hexagon ratchet: same toolchain", which depends on this input being fixed.
  • Failure modes I checked:
    • A failed download is loud: curl -f → zstd "unexpected end of file" → tar exits 2, reproduced locally under bash -e.
    • A wrong archive layout is loud: test -n "$cxx" fails.
    • A missing llvm-size is loud: pipefail, then command not found.
    • An empty .text is loud: the case guard catches it.
    • A different but valid tarball is silent.
  • What would satisfy me: add a HEXAGON_TOOLCHAIN_SHA256 beside QEMU_SRC_SHA256. Download to a file, check it with sha256sum -c, then extract. Optionally, grep -q 'clang version 19.1.5' on the --version line.

2. [medium-low] docs/fft-design.md:2855-2864: the clean-room record understates what crossed from the port measurements.

  • The record says the implementer received "counts as numbers only, per scenario, plus a per-transform figure", and was barred from "the orchestrator's scratch except the brief".
  • The brief (scratchpad/srdif-hex/BRIEF.md) actually carried more than that:
    • (a) per-transform-pair figures for both profiles: float 14.4 k vs 18.3 k and double 31.4 k vs 35.0 k at N = 512;
    • (b) a size-localized comparison, "at N = 64 srdif was already 1–2 % below". This comes from measuring the port at a second size, and it tells the reader where the gap is;
    • (c) permission to read clean-engine/TARGETS.md and clean-engine/targets/**;
    • (d) the statement that "the ratchet counts instructions, not packets", which the implementer later showed to be wrong.
  • None of this is expressive content of the port, and I do not think it taints the work. But this section exists to be an exact disclosure, and "a per-transform figure" is not exact.
  • What would satisfy me: list every figure that crossed, verbatim (a–b). Name the clean-engine allowance (c). Note (d).

3. [low] bench/README.md:160-163 and the predecessor cells (bench/README.md:230-235, docs/fft-design.md:2337-2358): the Hexagon offset comes from the process, not the machine.

  • The count moves with the binary's path and the environment qemu-hexagon passes to the guest. The same tap_dsp_icount_rfft_q15_512 binary, on one machine, read:
    • 159,978,121 from one directory;
    • 159,978,852 from a directory whose path is 44 characters longer (+731);
    • 1,584 fewer packets under env -i than with the normal environment.
  • "A few hundred to about a thousand between machines" is also low: the implementer's machine reads +1,723 against CI and mine +1,596.
  • As a consequence, the "CI-equivalent" predecessor cells cannot be reproduced to the packet. I built 7a58ebe with its own harness, at the same path length as my head build: its fixed-point scenarios equal the head's, and its float scenarios read 28 packets below the cited 49,732,931 / 54,834,714 / 103,239,537, on every scenario.
  • The README's predecessor fixed-point cells (159,976,914 / 184,890,609 / 157,303,265) sit exactly 28 above #42's. That contradicts fft-design.md's own premise that the fixed-point scenarios "run the same code on both trees". The 28 is an artifact of the orchestrator's build path or harness.
  • The percentages do not move: 28 packets is 6e-7.
  • CI itself is stable: the PR run and the dispatch run agree to the packet.
  • What would satisfy me: say "process environment and binary path" instead of "machine". State the ~1.6–1.7 k offsets actually observed. Either mark the predecessor cells as ±28 or give their fixed-point cells as equal to #42's, so the table does not imply a fixed-point difference.

4. [low] docs/fft-design.md:2342: "The Cortex-M counts reproduce bench/baselines.json exactly" holds for the float rows only.

  • In the PR's own dispatch run 36363710480, icount m33 reads:
    • rfft_q15_512 758,994,136 against a baseline of 752,189,619 (+0.90 %);
    • rfft_q31_2048 +0.49 %;
    • rfft_q31_512 +0.69 %.
  • m4f reads +0.61 … +0.90 %. I get the same numbers locally, and the same at 28befbd. So this drift predates this PR (the fixed-point baselines were seeded at #32 and have not been re-recorded) and is not caused by it.
  • The PR body's "Arm rows confirmed … +0.00 %" is correct for the eight float rows it names.
  • What would satisfy me: qualify the sentence, for example "the float rows reproduce exactly; the fixed-point rows read the same in-band drift as on main". Filing a follow-up to re-record the fixed-point rows would be welcome, but not in this PR.

5. [low] include/tap/dsp/fft/srdif.h:716-722: mul_sub's bit-identity claim needs FLT_EVAL_METHOD == 0 as well as "without contraction".

  • g++ -m32 -mfpmath=387 -O2 -ffp-contract=off -fexcess-precision=standard -S on the two spellings:
    • the split form rounds ab to float (fstps/flds) before the subtraction;
    • the plain form keeps it in extended precision.
  • So on an x87 build the output bits do move. The fingerprints are already scoped to FLT_EVAL_METHOD == 0 (a static_assert in the test), so no pinned claim breaks; the docstring is simply broader than what holds.
  • What would satisfy me: add "and FLT_EVAL_METHOD == 0" to the sentence.

6. [low] CLAUDE.md:43: "forced inlining under clang for Hexagon" misdescribes the gate.

  • The gate is defined(__clang__), so the forcing applies to every clang: AppleClang (the double profile on macOS), clang-cl, IntelLLVM, and clang-based Arm toolchains (armclang, ATfE).
  • Only Hexagon and x86 were measured. I measured one more target to see whether the choice is right elsewhere: the M33 icount harness compiled with clang++ 18 (--target=thumbv8m.main-none-eabihf -mcpu=cortex-m33 -O3) and linked with arm-none-eabi-g++ as the toolchain file does. Forced: 83,478,506 / 98,999,250. Not forced: 89,827,306 / 105,224,146. That is −7.1 % / −5.9 %, so the choice helps there too.
  • What would satisfy me: "forced inlining under clang (every target; measured on Hexagon)". Optionally add the clang-on-M33 figure to the srdif.h comment.

7. [low] bench.yml:208 and the "Toolchain versions" step: a missing qemu-hexagon passes both checks.

  • … -plugin help 2>&1 | grep -q "unknown option" does detect a build without plugins: linux-user prints qemu: unknown option 'plugin', which I checked in linux-user/main.c. If the binary is absent, though, the output is "No such file or directory", grep finds nothing, and the step passes.
  • qemu-hexagon --version | head -1 then passes as well, because the default shell is bash -e without pipefail (the job log confirms shell: /usr/bin/bash -e {0}).
  • The job does fail in the end, but as a Python FileNotFoundError traceback from icount.py, not as an annotated error.
  • What would satisfy me: test -x "$HOME/qemu-hexagon-plugins/qemu-hexagon" || { echo "::error::…"; exit 1; } before the probe, and set -o pipefail in that step. The plugin probe would also read better in its own step than inside "Download the Hexagon toolchain".

8. [nit] srdif.h:112: TAP_DSP_SRDIF_INLINE depends on __OPTIMIZE_SIZE__, which is decided per TU.

  • A consumer that mixes -Os and -O2 TUs gets token-different definitions of the same inline members. This is benign in practice, since the mangling is unchanged.
  • But the one surviving weak kernel<…> is whichever the linker keeps. "Not forced under -Os" is therefore a per-link outcome, not a per-TU guarantee: a size-built target can end up with the 12 KB forced kernel.
  • What would satisfy me: one sentence saying so.

9. [nit] Host test count. The PR says "Host Release 442/442". At this head I get 437/437 with g++ 13 and with clang 18 (default options plus TAP_DSP_WERROR=ON). Which configuration gives 442?

10. [nit] Cache scope. The qemu-hexagon cache saved by the workflow_dispatch run on the branch is not visible to refs/pull/44/merge: PR run 36364049440 rebuilt QEMU and saved again. Nothing is wrong. It will settle once main saves the cache, and the key (QEMU digest + -1) is sound as long as the configure line changes only together with a bumped suffix. A comment saying so would help the next editor.

The five Arm keys are unaffected by the workflow changes: matrix.arch is empty for them, so every Hexagon step is skipped, and SIZE_TOOL stays arm-none-eabi-size. Run 36363710480 confirms this.


Provenance: the tuned srdif.h against 7a58ebe:include/tap/dsp/fft/split_radix.h

None of the five changes moves srdif toward the port's structure, and three move it further away.

  • Small blocks. The port handles them as register leaves: cftf161 and cftf081 load all 16 or 8 complex values into scalars, then store them, as in x0r = a[0] + a[8]; … y5r = wn4r * (x0r - x0i); … a[8] = y1r + y5r;. Below that come cftf040 and cftx020. The PR removes srdif's register leaves. level<L> loads, transforms and stores one butterfly (J, J+q, J+2q, J+3q) at a time, in memory, and the recursion block<L/2>(a); block<L/4>(a+L); block<L/4>(a+L+L/2) goes down to pairs. The one intermediate step that looked like the port was "first level of 16 in memory, then register leaves of 8", the shape of cftmdl1 + cftf081, and it was not kept.
  • Post-pass. The port's rftfsub / rftbsub handle one pair per iteration, updating in place: a[j] -= yr; a[j + 1] -= yi; a[k] += yr; a[k + 1] -= yi;. post_two takes two consecutive pairs, reads all eight values before any store, and computes through a value-returning post_math. The port has no such form.
  • Run-time level. The port's cftmdl1 pairs j with its mirror j0 = m - j in one iteration, swapping wk1r and wk1i. The PR's double path now runs one group per step and no pairing at all. The float path keeps #42's j, j+1 pairing, which was already not mirror pairing.
  • Permutation. Unchanged, apart from swap2 being forced inline. There is no move toward bitrv2's ip[] table.
  • mul_sub. The port writes wk1r * x0r - wk1i * x0i as a single expression. The split statement exists only to steer clang's contraction.
  • Scratch. I found no leftover Hexagon build product of the port in the session scratchpad (checked with file for Hexagon ELF objects newer than the brief). The Hexagon build of 7a58ebe that I made for finding 3 is deleted with this review.

Generated by Claude Code

Comment thread .github/workflows/bench.yml Outdated
Comment thread .github/workflows/bench.yml Outdated
Comment thread docs/fft-design.md Outdated
Comment thread bench/README.md Outdated
Comment thread docs/fft-design.md Outdated
Comment thread include/tap/dsp/fft/srdif.h Outdated
Comment thread include/tap/dsp/fft/srdif.h
Comment thread CLAUDE.md Outdated
…cords

bench.yml:
- The CodeLinaro toolchain tarball is digest-verified (HEXAGON_TOOLCHAIN_SHA256,
  sha256sum -c before extraction), and its version is asserted, like the QEMU
  source and plugin header.
- qemu-hexagon's plugin probe has its own step: a missing binary fails there,
  not later as an icount.py traceback. The version step sets pipefail.

Records:
- docs/fft-design.md, the clean-room record: everything that crossed to the
  implementer, verbatim. That is the port's scenario counts, the per-pair
  microbenchmark figures for float and double, and the N = 64 comparison.
  It also names the #42 harness allowance, and notes the brief's wrong
  "instructions, not packets".
- The Hexagon offset belongs to the process (binary path and environment),
  not the machine: +731 for a longer path, -1,584 under env -i, and +473 to
  +1,723 observed against CI. The predecessor cells read +-28. The
  README's predecessor fixed-point cells now equal #42's (the same code).
- "The Cortex-M counts reproduce exactly" is for the float rows. The
  fixed-point rows carry the pre-existing in-band drift from #32.
- srdif.h comments only (no code change; fingerprints unchanged):
  - The forced-inline gate is every clang. The review's clang-18 Cortex-M33
    measurement (-7.1 / -5.9 %) is cited.
  - The gate is per translation unit.
  - mul_sub's bit identity needs FLT_EVAL_METHOD == 0.
  - Hexagon figures are packets.
- CLAUDE.md: "every clang, tuned on Hexagon".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
@tap
tap merged commit d9c1e33 into main Sep 28, 2026
25 checks passed
tap added a commit that referenced this pull request Sep 30, 2026
…1e33

The rows #44 recorded left the main SHA pending until the squash. The
push-to-main bench run on d9c1e33 (run 36367784465, compare mode) confirms
them:
- The eight Cortex-M float rows reproduce to the instruction.
- The six hexagon rows read +22 packets on every scenario, fixed point
  included. That is the process-environment offset bench/README.md
  describes, inside the band, and the baselines are not re-recorded.

The cells now name d9c1e33 and the run. No baseline or ceiling changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZPTzNxo5Fe4EtpXXKf7Sy
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