Skip to content

A8: the host ratchet, callgrind counts gated beside the QEMU legs - #91

Merged
tap merged 1 commit into
mainfrom
claude/sample-rate-solutions-comparison-mqc190
Oct 14, 2026
Merged

tap merged 1 commit into
mainfrom
claude/sample-rate-solutions-comparison-mqc190

Conversation

@tap

@tap tap commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

What this changes

This is the roadmap's A8 (row P10; Q6 decided a hard gate, two-sided at ±3 %). Each engine gets host workloads (bench/icount/host_main.cpp, binaries tap_sr_<engine>_hostcount_*):

  • The float converter streams 128-frame blocks at 2 and 12 channels.
  • scripts/icount.py --target host counts them under valgrind's callgrind and gates them against a new host key in each bench/baselines.json, with the same mechanics as the QEMU legs.
  • A new CI job, Instruction-count ratchet (host), runs the gate.

Why

The channel-parallel host path sat outside every ratchet. TAP_DSP_CHANNEL_PARALLEL is off under Helium, the M33's DSP extension and Hexagon, so no QEMU leg compiles it, and a host regression there could land unseen. async's 12-channel rows run that path.

Verification

What pins the count to the code, the compiler and the C library:

  • One function counted. Only tap_sr_host_counted(), the streaming loop, is counted (--toggle-collect). Construction, the fixture's sin() calls, process start-up and the environment stay outside.
  • Eager binding. LD_BIND_NOW=1, so no PLT resolution lands inside the counted window.
  • glibc pinned. GLIBC_TUNABLES holds glibc's CPU-dispatched routines to their x86-64 baseline variants and pins the copy thresholds glibc derives from cache sizes. Before the pin, this machine's memcpy resolved to the AVX variant, 1 % of the 12-channel async count.
  • No -march. The compiled code is the x86-64 baseline whatever the runner.

Recorded with g++ 13.3.0 (13.3.0-6ubuntu2~24.04.1), glibc 2.39 (2.39-0ubuntu8.9) and valgrind 3.22; the job prints the runner's versions. All of these count identically:

  • two runs of one binary;
  • a second build with another generator;
  • a replay of the CI job's own build and gate steps.

On CI: the runner reproduced all ten counts exactly, +0 against every baseline with the same checksums (job 115919836408, run 38583667902), and all 32 checks are green.

Engine Workload 2 ch 12 ch
async pipeline_float 109,526,808 362,078,536
bridge up_float 79,352,295 470,621,528
bridge down_float 99,664,796 593,181,679
rational up2_float 57,296,876 288,223,796
rational down2_float 29,983,981 161,054,642

async at 12 channels runs about 30 M instructions per channel over the stream, against 55 M at 2: that is what the channel-parallel path buys. bridge has no frame-major path and costs the same per channel at both counts.

Also checked:

  • clang 18 -Werror build, clang-tidy and clang-format are clean on the three new files.
  • check_retired_words.py passes.
  • The M33 cross configure defines no host workloads, and the README tables are regenerated.

Notes for the reviewer

  • The QEMU tables in the READMEs are byte-identical. The host figures are a second table from the same generator, between the same markers.
  • The book's icount chapter gains "The host, under callgrind"; the root CLAUDE.md and the roadmap (A8 landed, P10 green) describe it.

🤖 Generated with Claude Code

https://claude.ai/code/session_016GKzq1VSdcMYCRMhddiMaf

The roadmap's A8 (row P10, Q6: a hard gate, two-sided at ±3 %). The
channel-parallel host path sat outside every ratchet: no QEMU leg compiles
it (TAP_DSP_CHANNEL_PARALLEL is off under Helium, the M33's DSP extension
and Hexagon).

- Workloads (<engine>/bench/icount/host_main.cpp, binaries
  tap_sr_<engine>_hostcount_*, host builds only): each engine's float
  converter streams 128-frame blocks for about two seconds at the default
  profile, at 2 and 12 channels. async runs duplex push()/pull() (its 12
  channels take the frame-major channel-parallel path), bridge runs both
  directions, rational runs up and down by 2.
- scripts/icount.py --target host runs them under valgrind's callgrind
  and gates against the "host" key of each engine's bench/baselines.json
  with the same two-sided mechanics. The count is pinned to the code, the
  compiler and the C library:
  - Only tap_sr_host_counted(), the streaming loop, is counted
    (--toggle-collect). Construction, the fixture's sin() calls, process
    start-up and the environment stay outside.
  - Symbols bind eagerly (LD_BIND_NOW), so no PLT resolution lands in
    the window.
  - GLIBC_TUNABLES holds glibc's CPU-dispatched routines (memcpy, libm)
    to their x86-64 baseline variants and pins the copy thresholds glibc
    otherwise derives from cache sizes. Before the pin, this machine's
    memcpy was the AVX variant, 1 % of the 12-channel async count.
- CI: a host-ratchet job on ubuntu-24.04 that installs valgrind, records
  the image's g++-13, libc6 and valgrind versions, builds the workloads
  Release with g++-13 and no -march, and gates each engine in its own
  step.

Recorded here with g++ 13.3.0 (13.3.0-6ubuntu2~24.04.1), glibc 2.39 and
valgrind 3.22, the runner image's versions. Two runs of one binary, a
second build with another generator, and a replay of the CI job's steps
all count identically. The counts (instructions over the stream):

  async     pipeline_float  2ch 109,526,808  12ch 362,078,536
  bridge    up_float        2ch  79,352,295  12ch 470,621,528
            down_float      2ch  99,664,796  12ch 593,181,679
  rational  up2_float       2ch  57,296,876  12ch 288,223,796
            down2_float     2ch  29,983,981  12ch 161,054,642

async at 12 channels is about 30 M instructions per channel against 55 M at
2, which is what the channel-parallel path buys; bridge, with no frame-major
path, costs the same per channel at both counts.

The README tables gain a host table from the same generator; the QEMU
tables are unchanged. The book's icount chapter gains "The host, under
callgrind", and the root CLAUDE.md and the roadmap (A8 landed, P10 green)
describe it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GKzq1VSdcMYCRMhddiMaf
@tap
tap merged commit 3e10f68 into main Oct 14, 2026
32 checks passed
@tap
tap deleted the claude/sample-rate-solutions-comparison-mqc190 branch October 14, 2026 16:28
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.

2 participants