Skip to content

refactor(dispatch): Generate and validate the dispatch surface from a single declaration - #372

Open
ahuber21 wants to merge 25 commits into
dispatch/01-l2-d160-externfrom
dispatch/02-generate-surface
Open

ahuber21 wants to merge 25 commits into
dispatch/01-l2-d160-externfrom
dispatch/02-generate-surface

Conversation

@ahuber21

@ahuber21 ahuber21 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

The set of distance kernels compiled ahead of time is declared once, in
cmake/dispatch-surface.cmake, and the generated header, the per--march translation
units and the ctest checks all derive from it. Configure-time validation rejects a
malformed declaration, and VNNI becomes its own ISA level so that each level's -march
budget matches what it promises.

Gotchas:

  • This PR now contains feat(dispatch): Validate the dispatch-surface declaration at configure time #373 and fix(dispatch): Make VNNI its own ISA level and lower AVX512 to skylake-avx512 #375. They were stacked on it and the review cleanup
    touched the same files, so redoing that work here rather than folding them in would have
    meant resolving the same conflicts twice. Both are closed as merged here. The diff is
    large for that reason, not because the change grew.
  • The checkers moved. cmake/dispatch-checks/ is now
    tests/multi-arch/dispatch-checks/. They are test drivers; only generation stays under
    cmake/.
  • These checks are add_test, not Catch2. link_probe.cpp exists to fail to link
    when a declared kernel is uninstantiated, and entry_probe.cpp gets driven under gdb —
    neither works from inside the single tests binary. Porting the four cmake -P checkers
    to Catch2 is a follow-up.
  • include/svs/core/distance/dispatch_surface.h is generated but committed, so a bare
    -I include compile works without cmake. CI fails if it goes stale, and asserts that a
    build with an overridden surface leaves it alone.
  • Configure now rejects two ISA levels sharing one -march. Previously accepted, and it
    shipped a fault: the weaker level got compiled with instructions its runtime predicate does
    not guarantee, and the instruction checker missed it because it looks the budget up by
    -march. Not a review point; found while probing whether these checks can still fail.
  • Three points from the review were declined — Python for the checkers, dropping the
    declaration checker as redundant, and parallel lists for the ISA table. Reasons are in
    the review thread.

@ahuber21 ahuber21 added this to the ISA dispatching v2 milestone Aug 24, 2026
@ahuber21

Copy link
Copy Markdown
Contributor Author

@copilot new or modified files should have 2026 in their copyright header. Fix

Copilot AI commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

@copilot new or modified files should have 2026 in their copyright header. Fix

Updated the headers in 408513c.

@ahuber21 ahuber21 changed the title Generate the dispatch surface from a single declaration refactor(dispatch): Generate the dispatch surface from a single declaration Aug 24, 2026
@ahuber21
ahuber21 force-pushed the dispatch/02-generate-surface branch from c2d57c2 to fe66e35 Compare August 24, 2026 10:28
@ahuber21
ahuber21 marked this pull request as ready for review August 25, 2026 09:41
@ahuber21
ahuber21 requested a review from ethanglaser August 25, 2026 09:41
@mergify

mergify Bot commented Aug 25, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

ahuber21 and others added 6 commits August 25, 2026 17:23
The set of distance kernels compiled ahead of time -- extents x ISA levels
-- was written out by hand in every place that needed it: three extern
template blocks, two per-arch translation units, the `supported_dim_list`
array, and 48 near-identical `SPEC struct` lines in the instantiation
macros. Adding an extent meant editing all of them and hoping none was
missed. One had been: `euclidean.h` was missing d=160 for AVX2 (fixed in
the preceding commit), which silently made consumers instantiate that
kernel locally at their own -march.

Declare the surface once, in `cmake/dispatch-surface.cmake`:

    set(SVS_SUPPORTED_DIMS 64 96 100 128 160 200 512 768)
    set(SVS_ISA_LEVELS
        "AVX2|haswell|avx2"
        "AVX512|cascadelake|avx512"
    )

`cmake/generate-dispatch-surface.cmake` validates it and writes
`include/svs/core/distance/dispatch_surface.h`, which exports
`SVS_FOR_EACH_SUPPORTED_DIM(M)`, `SVS_FOR_EACH_DISPATCH_TARGET(M)` and
`SVS_SUPPORTED_DIM_COUNT`. Everything that used to spell the list out now
loops over one of those. 108 hand-written instantiation lines become 0.

Type pairs stay in C++, in `multi-arch/x86/preprocessor.h`. A pair exists
because an implementation exists for it -- sometimes a hand-written one --
so the list belongs beside those implementations, not in the build system.

`svs::Dynamic` is appended automatically and cannot be listed: it is what
serves every dimensionality without a fixed-extent kernel, and the library
is incorrect without it.

The generated header is committed as well as generated. The build always
compiles against the build-tree copy, placed ahead of the source include
directory, and installs it over the committed one; the committed copy is
refreshed only when the declaration is the default, so overriding the
surface for a one-off build cannot rewrite the tree. Committing it keeps a
bare `-I include` compile working without CMake -- which the downstream
repository relies on, since it compiles `multi-arch/x86/{avx2,avx512}.cpp`
by path with its own CMake.

No behaviour change: the static library exports the same 864 symbols with
the same sizes, and the two arch objects are symbol-identical before and
after, both here and in the downstream build. `[distance]` passes
(134402115 assertions, 13 test cases).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every comment this branch adds now says what the code cannot say for itself
and stops there. The block comments that restated the surrounding code, or
spent five lines on a hazard that takes two, are gone; the hazards themselves
stay, each naming its failure mode.

Comment-only. The non-comment diff against the previous tip is empty.
A kernel that is missing its `extern template` declaration does not
produce an error. The consumer instantiates it locally instead, from the
generic primary template -- and in a baseline consumer translation unit
the vectorized partial specializations are not even visible, since they
are guarded on SVS_AVX2 / SVS_AVX512_F. So the consumer silently gets a
scalar loop where the library has a vectorized kernel, compiled at
whatever -march the consumer happens to use. That is the bug that shipped
for L2 at d=160 with AVX2.

Nothing could catch it, because nothing referenced the whole surface at
once. This adds a consumer that does: tests/multi-arch/x86/link_probe.cpp
names every kernel the surface declares -- every (extent, ISA level) pair,
every element-type pair, all three distances -- and nothing else. It is
compiled at -march=x86-64, like an arbitrary consumer of the headers, and
two tests are run against it:

  dispatch_surface_probe    calls every kernel whose ISA level this host
                            satisfies, so a kernel compiled beyond what
                            its level guarantees faults here
  dispatch_surface_linkage  reads the object's symbol table and requires
                            the kernels it references to be exactly the
                            kernels the library defines

The linkage check is host-independent and covers the whole surface
everywhere; the run covers only what the host can reach.

On the default surface the two sets match exactly at 864 kernels, and on
the reduced surface used by the non-default-surface CI job, at 288. All
three failure modes were confirmed to fire: dropping the L2 extern block
reports 288 kernels instantiated by the probe itself, and checking against
an archive missing the AVX-512 translation unit reports its 432 kernels as
declared but never instantiated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"9 extents (8 fixed + svs::Dynamic) x 2 ISA levels" says nothing about which
extents, which levels, or what instruction budget each level compiles at, so
reading the log gave no way to tell a correct surface from a plausible one.

Also name the AVX_AVAILABILITY enumerators that are not in the surface, since
that is the question the old count invited and could not answer: NONE is
dispatched to but has no translation unit, so every consumer instantiates its
kernels itself, at the consumer's own -march.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four checks, each closing a failure mode the link probe cannot see.

dispatch_surface_declaration derives what the library must contain from the
three hand-written sources -- the extent list and ISA levels, the type-pair
lists, and the AVX_AVAILABILITY enumerator order -- and never reads the
generated header. The linkage check compares the archive against a probe built
from that header, so a generator that dropped an extent would drop it from both
and still agree; this one has nowhere to hide. It also checks the entry-point
consumer, whose kernels must all come from the archive: one it defines itself
is an extern declaration that is missing.

dispatch_instructions_<level>, one test per ISA level, disassembles the level's
object file and holds it to a budget table keyed by -march. A level guarantees
only what its runtime predicate tests, so an instruction outside that budget
faults on a host the dispatcher routes there -- and no symbol-table check can
see it.

dispatch_surface_execution is the only check that observes a kernel run rather
than exist: a specialization lost behind an `#if` still links and still counts.
It breaks on every level's kernel for one extent and confirms the run enters
the level this host satisfies. Weaker levels are covered by hosts that satisfy
only those.

dispatch_entry_probe reaches the kernels through the entry points rather than
by naming the Impl classes, which is what makes the consumer half of the
declaration check meaningful.

nm, objdump and gdb are each optional: a missing tool skips its tests rather
than failing the build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Answer the review on the declaration's maintenance story: cmake/dispatch-surface.cmake
now states what to edit for an extent, a level, a type pair or an instruction budget,
and why AVX_AVAILABILITY::NONE has no row. Move the four checker scripts to
cmake/dispatch-checks/ with a README, and make the preprocessor.h type-pair comment
stand without the refactoring for context.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ahuber21 and others added 3 commits August 25, 2026 17:23
…e time

The declaration added in the preceding commit is only worth something if it
is checked rather than trusted, and if the knob that overrides it is
actually turned by something other than a person debugging.

Move the validation out of `generate-dispatch-surface.cmake` into
`validate-dispatch-surface.cmake`, which touches no build-system state and
so runs in script mode:

    cmake -DSVS_DISPATCH_SURFACE_FILE=<file> -DSVS_X86_SRC_DIR=<dir> \
          -P cmake/validate-dispatch-surface.cmake

`tests/cmake/dispatch-surface/` holds two declarations that must be accepted
and twelve that must be rejected, each carrying the substring its rejection
has to mention. `.github/scripts/check_dispatch_surface.sh` runs the lot --
fifteen cases, counting the default declaration -- in a fraction of a
second, needing no compiler and no build tree. It is a pre-commit hook and a
CI job.

Script mode has no `cmake_minimum_required`, so CMP0007 and CMP0057 default
to OLD there. Both matter: without CMP0007 an empty `|`-field disappears
when the entry is split, and without CMP0057 `IN_LIST` is not an operator.
Set both, scoped with cmake_policy PUSH/POP.

The new `Dispatch Surface` workflow adds what the script cannot check:

  - a configure with the default declaration must leave the committed
    `dispatch_surface.h` untouched. This catches a declaration changed
    without a reconfigure, and a generated header edited by hand.
  - a full build and test run against `valid-reduced.cmake`, which shares no
    fixed extent with the default declaration -- so a build that quietly
    fell back to the committed header would fail to compile rather than pass
    by accident. That build's archive holds 288 kernels at extents 32, 384
    and svs::Dynamic, against 864 at the default nine.
  - that same overridden build must leave the committed header alone.

Correctness does not depend on which extents have a fixed-extent kernel: an
extent without one is served by the svs::Dynamic kernel. `ctest -LE long`
against the reduced surface passes 153 of 154, the one failure being
`Testing Binary Reader Iterator`, which fails identically on the unmodified
default-surface build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…x512

The AVX512 level's translation unit was compiled at -march=cascadelake, which
enables AVX512-VNNI. That level promises AVX-512 F/BW/DQ about the host and
nothing more, so every kernel in that object file was compiled with permission
to use instructions a Skylake-SP does not have. The VNNI kernels that existed
guarded themselves with a runtime check, but the guard only covered the calls
that were written by hand; the compiler was free to emit vpdpwssd anywhere in
the TU on its own initiative.

Adding AVX_AVAILABILITY::AVX512_VNNI as a fourth level moves the check to the
one place a level is chosen -- the entry point -- and lets each TU be compiled
at exactly what its level promises. The int8/int8 and uint8/uint8 kernels move
to the new level; every other pair promotes to float before doing arithmetic,
where VNNI has nothing to offer, so those pairs have no kernel at this level.
That is what keeps a fourth level from costing a fourth of everything: 54 new
instantiations rather than 432.

Two consequences worth naming:

  - The pairs that move need an AVX512-level kernel to fall back to, and it has
    to live outside `#if SVS_AVX512_VNNI`. Inside, it would be absent from the
    AVX512 TU -- which is now compiled where that macro is 0 -- and silently
    replaced by the generic template. This is why the two halves of the change
    cannot land separately.

  - The entry points must not dispatch to a level that has no kernel for the
    pair in hand, for the same reason. `svs::distance::has_vnni_kernel` answers
    that, generated from the same list the kernels are, and it is `if constexpr`
    so it compiles away for the pairs that do not move.

The generated header now also defines SVS_ISA_LEVEL_<enumerator> per level, so
a surface that leaves a level out is visible to the code that dispatches on it.
Dropping the VNNI level degrades correctly -- those pairs stay on AVX512, and
the probe reports 864 kernels instead of 918. Dropping AVX2 or AVX512 is an
`#error` instead, because the entry points reach those two for every type pair.
ISA levels are not configuration the way the extent list is: a level exists
because kernels, a TU and a CPUID check exist for it.

The dispatch checks pick the change up on their own, which is what they were
written for. dispatch_instructions_avx512 now judges avx512.cpp.o at
skylake-avx512 and so forbids VNNI there, and it fails on the old object file;
the cascadelake row gains `vnni` as a requirement, because a VNNI level whose
object file has no VNNI in it is 54 instantiations of dead weight. Three
mechanical follow-ons: the per-level object libraries are named after the level
rather than the -march, since two levels now share neither; the execution check
breaks on int8/int8 rather than float/float, as a float-promoting pair has no
kernel at the top level and would route one lower; and the VNNI predicate joins
the other two in tests/multi-arch/x86/host_levels.h.

Verified on the default surface: 918 kernels declared, instantiated and
reachable with none instantiated by the consumer; all 456 vpdpwssd encodings in
vnni.cpp.o, zero in avx512.cpp.o and avx2.cpp.o, where before all 456 sat in
the AVX512 level's object file; the AVX2 object identical in symbol names and
sizes to before; the new L2Impl<128,int8,int8,AVX512> vectorized 16-wide float,
not scalar. `[distance]` passes with the same 134402115 assertions as before,
all eight dispatch tests pass, and ctest is otherwise unchanged. Also verified
with SVS_NO_AVX512=YES (both TUs compile generic, zero AVX-512 encodings, all
918 still linked) and with the reduced surface (306 kernels).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ahuber21
ahuber21 force-pushed the dispatch/02-generate-surface branch from dd2db8d to 5fafe1d Compare August 25, 2026 15:23
@rfsaliev
rfsaliev requested a balanced review from Copilot August 28, 2026 08:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Centralizes x86 distance-kernel extents and ISA levels into one generated dispatch declaration.

Changes:

  • Generates kernel declarations, instantiations, and supported dimensions from one CMake surface.
  • Adds linkage, instruction, declaration, and runtime dispatch checks.
  • Installs the generated header and documents maintenance workflows.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
CMakeLists.txt Installs the generated surface header.
cmake/AGENTS.md Documents dispatch ownership.
cmake/dispatch-surface.cmake Declares extents and ISA levels.
cmake/generate-dispatch-surface.cmake Validates and generates the surface.
cmake/multi-arch.cmake Builds ISA objects from the declaration.
cmake/templates/dispatch_surface.h.in Defines the generated-header template.
cmake/dispatch-checks/README.md Documents dispatch checks.
cmake/dispatch-checks/check-dispatch-declaration.cmake Validates declared kernel counts.
cmake/dispatch-checks/check-dispatch-execution.cmake Verifies runtime routing.
cmake/dispatch-checks/check-dispatch-instructions.cmake Inspects ISA instruction budgets.
cmake/dispatch-checks/check-dispatch-linkage.cmake Verifies kernel linkage.
include/svs/core/distance/cosine.h Generates cosine extern templates.
include/svs/core/distance/dispatch_surface.h Commits the default generated surface.
include/svs/core/distance/distance_core.h Generates supported dimensions.
include/svs/core/distance/euclidean.h Generates L2 extern templates.
include/svs/core/distance/inner_product.h Generates IP extern templates.
include/svs/multi-arch/x86/avx2.cpp Generates AVX2 instantiations.
include/svs/multi-arch/x86/avx512.cpp Generates AVX512 instantiations.
include/svs/multi-arch/x86/preprocessor.h Defines reusable instantiation macros.
tests/CMakeLists.txt Enables multi-architecture tests.
tests/multi-arch/CMakeLists.txt Registers dispatch probes and checks.
tests/multi-arch/x86/entry_probe.cpp Exercises public dispatch entry points.
tests/multi-arch/x86/host_levels.h Models host ISA predicates.
tests/multi-arch/x86/link_probe.cpp References every declared kernel.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cmake/AGENTS.md Outdated
Comment thread cmake/dispatch-checks/check-dispatch-instructions.cmake Outdated

@rfsaliev rfsaliev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The change is pretty big with high review/maintenance cost.
It seems like the huge AI generated change to be reviewed by AI.
Scripts should be simplified and well structured.

@@ -0,0 +1,72 @@
/*

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As I understand, this file is autogenerated from dispatch_surface.h.in.
Why do we need to track it in repository?
Suggesting to remove the file from repository but generate it in binary directory during config/build.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added a default version for reference. If someone researched the code on GH only, or on a fresh checkout without prior compilation there are no missing files.

@@ -0,0 +1,66 @@
<!--

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like files in this directory intended for tests.
IMHO it makes sense to move them to /test

@@ -0,0 +1,250 @@
# Copyright 2026 Intel Corporation

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like the cmake code here is pretty complicated.
Writing it in form of straightforward script leads high maintenance costs.
Please, modify the code to make it more structured, e.g. split to functions.

@ahuber21 ahuber21 closed this Sep 2, 2026
@ahuber21 ahuber21 reopened this Sep 2, 2026
The targets were dispatch_surface_probe and dispatch_entry_probe while the
sources are x86/link_probe.cpp and x86/entry_probe.cpp, so neither binary could
be found from the name of the file that produced it.

The ctest names keep the dispatch_ prefix -- dispatch_link_probe,
dispatch_entry_probe -- because `ctest -R dispatch` is how CI and the docs select
this group, and renaming the tests to match the binaries would drop two of the
eight out of that filter.

The object libraries follow from the target name via svs_add_dispatch_probe, so
the symbol-table checks pick up link_probe_objects and entry_probe_objects
without further change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Copilot reviewed 46 out of 46 changed files in this pull request and generated 4 comments.

Suppressed comments (3)

tests/multi-arch/dispatch-checks/check-dispatch-declaration.cmake:1

  • The error message references ${SVS_SURFACE_FILE}, but this script is driven by SVS_MANIFEST and never defines SVS_SURFACE_FILE, so the message will be missing the file context. Use ${SVS_MANIFEST} (or reword to “declared in the manifest”) so failures point to the correct input.
    tests/multi-arch/CMakeLists.txt:1
  • This else() triggers when either svs_nm or svs_gdb is missing, but the status message only mentions gdb. Make the message reflect the actual condition (e.g., mention missing nm and/or gdb) so skipped-test diagnostics are accurate.
    tests/multi-arch/CMakeLists.txt:1
  • This else() triggers when either svs_nm or svs_gdb is missing, but the status message only mentions gdb. Make the message reflect the actual condition (e.g., mention missing nm and/or gdb) so skipped-test diagnostics are accurate.

Comment thread cmake/generate-dispatch-surface.cmake
Comment thread cmake/generate-dispatch-surface.cmake
Comment thread cmake/generate-dispatch-surface.cmake
Comment thread cmake/multi-arch.cmake Outdated
ahuber21 and others added 3 commits September 2, 2026 06:23
The probes printed only their accumulated distance sum, which says nothing about
how many kernels ran: a macro list that expanded to fewer calls than the surface
declares still produced a plausible float. The count makes that visible, and it
cross-checks against a figure derived from a different source -- the declaration
checker independently computes 918 kernels, which is what link_probe now reports.

entry_probe reports 432 rather than 918 because each entry point picks one ISA
level at runtime, so it reaches nine extents by sixteen type pairs by three
distances, not the whole surface. Its count comes from a named constant next to
entry_one, since that function's body is what fixes the calls per expansion.

The comments explaining that printing defeats dead-code elimination are gone from
both files: the printed message now says it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The flag was only reached through the execution check's script, so a ctest run
never showed the three lines it parses and a probe that stopped honouring the
flag surfaced as a missing level rather than as itself.

The mode returns before the kernel sweep, so this is a second invocation rather
than an argument on the existing one, which keeps the kernel count visible too.
…est"

This reverts 669358e. The flag is reached again only through the execution
check's script, so a ctest run shows the kernel counts but not the three lines
that check parses.
@ahuber21

ahuber21 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

By the way, here is the verbose output what the new tests are doing.

# kernel linking
dispatch surface probe: called 918 kernels that computed a total distance sum of 76537.054688

# kernel entry, single ISA (the runner's ISA)
dispatch surface entry probe: called 432 kernels that computed a total distance sum of 36017.449219

# every declared kernel is defined in the library, and none of them in the consumer
dispatch linkage: 918 kernels declared, instantiated and reachable; none instantiated by the consumer

# the built surface counted from the declaration instead of from a probe
dispatch declaration: 918 kernels required by the manifest (3 levels x 9 extents x 3 distances x type pairs per level {AVX2: 16, AVX512: 16, AVX512_VNNI: 2}); the archive defines exactly those and the entry points reach all of them

# instruction budget per level: no wider vectors than the level's predicate guarantees
dispatch instructions: level AVX2 at -march=haswell has 21062 ymm, and no zmm, mask, vnni
dispatch instructions: level AVX512 at -march=skylake-avx512 has 19338 zmm, and no vnni
dispatch instructions: level AVX512_VNNI at -march=cascadelake has 3122 zmm, 456 vnni, and nothing forbidden

# runtime dispatch, observed under gdb: which level a call really enters
dispatch execution: a call to L2 at extent 64 entered AVX_AVAILABILITY level 3, the highest this host satisfies

I agree the cmake is still a bit bulky but it's quite self-contained. It delivers a status quo that is extremely well tested and if it ever breaks (for example because of the mangling assumptions you criticized), it's just a test failure, not a defect. We can update and fix the test. But I actually doubt the tests will start failing in the foreseeable future. claude explained quite convincing why the Itanium name scheme should be stable.

So what's left are the changes to existing headers and implementations. Those are very small. It's mostly just removing the now redundant instantiations and some updated dispatching logic because of the VNNI change.

So, honestly, I wouldn't be too concerned about "large AI generated PR".

ahuber21 and others added 3 commits September 2, 2026 11:30
The comment justified naming the object target after the ISA level by
asserting that more than one level can share an instruction budget. That
stopped being true when validate-dispatch-surface.cmake began rejecting
duplicate -march values as a hard error, so the stated reason no longer
holds. Record the constraint that does apply: the target name is unique
only because duplicate infixes are rejected too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SVS_SURFACE_FILE was never set anywhere in the tree, so the diagnostic
rendered as "ISA level 'X' is declared in  but is not an AVX_AVAILABILITY
enumerator" -- the file context the message exists to give was always
blank. The script is driven by SVS_MANIFEST.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard requires both tools, but the skip message named only gdb, so a
host missing nm reported a cause that was not the one that fired.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

# The build always compiles against the build-tree copy, and it is placed ahead
# of the source include directory so that it wins.
set(SVS_GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated/include")

@rfsaliev rfsaliev Sep 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am still afraid that there is possible include file collision between include/svs/core/distance/dispatch_surface.h and <build>/generated/include/svs/core/distance/dispatch_surface.h

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Would you remove the header instead or do something else entirely?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The intention is to generate the new header, and overwrite the existing include/svs/core/distance/dispatch_surface.h.
This header will be copied to <build> and used ... there should not be the possibility for confusion. Both headers should have identical content.

@rfsaliev
rfsaliev dismissed their stale review September 4, 2026 14:48

dismiss PR blocking

@ahuber21

Copy link
Copy Markdown
Contributor Author

Below is a very detailed and slightly sloppy benchmarking report. The tl;dr is

  • Recall byte-identical
  • All three distances were tested
  • Build time not benchmarked, because missing numa pinning and 224 threads (-> high noise)
    • It should be ok, there's nothing unique in building wrt. distance computations
  • QPS unchanged, except for VNNI, where small improvement is observed because we cut one run-time check (512F vs 512VNNI)
  • Extensive analysis confirms expected behavior: symbol counting, assembly analysis

Basically, check out the table under "QPS at a glance" and if anything is unclear you'll probably find it in the details below.


PR #372 A/B — results

PR #372, refactor(dispatch): Generate and validate the dispatch surface from a single declaration.
Design, provenance and every methodological choice are in METHOD.md; this file carries the numbers.

Host: 2 sockets × 56 physical cores. Search runs are pinned — numactl -N0 -m0,
num_threads = 56; build runs are unpinned at num_threads = 224 and are not comparable to the
search numbers, only across arms within themselves (METHOD.md explains why neither footprint works
for both). Throughout: one process at a time, arms interleaved run-by-run, arm order rotated per
replicate.

QPS at a glance

Median throughput, queries/second, at search_window_size = 50, replicate 1 and the warm-up sample of
each configuration excluded. Arm A′ is arm A's binary re-run, so the A vs A′ spread on a row is that
row's noise floor — read any A-to-B difference against it, never on its own.

cell data A · QPS A · recall B · QPS B · recall A′ · QPS A′ · recall reps
cohere_mip_768 fp16 37,949 0.968200 38,015 0.968200 37,516 0.968200 5
oi_mip_512 fp16 73,646 0.986560 73,429 0.986560 73,793 0.986560 5
oi_l2_512 fp16 85,523 0.994880 87,312 0.994880 86,189 0.994880 5
oi_cos_512 fp16 69,867 0.995880 70,571 0.995880 70,777 0.995880 5
laion_mip_512 fp16 88,430 0.990780 87,621 0.990780 88,274 0.990780 5
laion_l2_512 fp16 85,559 0.983120 85,276 0.983120 85,296 0.983120 5
laion_cos_512 fp16 85,450 0.983820 85,140 0.983820 84,409 0.983820 5
dbpedia_mip_dyn fp16 9,742 0.989700 9,792 0.989700 9,806 0.989700 5
dbpedia_l2_dyn fp16 7,554 0.992200 7,511 0.992200 7,544 0.992200 5
dbpedia_cos_dyn fp16 9,669 0.989800 9,652 0.989800 9,642 0.989800 5
bigann_l2_128 uint8 416,871 0.998240 420,770 0.998240 416,613 0.998240 10
spacev_l2_100 int8 347,639 0.979413 353,384 0.979413 347,797 0.979413 10

The two integer rows come from the 10-replicate deep-dive run, whose 100× larger query set costs ~9 %
absolute throughput against the 5-replicate run — uniformly on all three arms, so the effect estimate is
unaffected but the absolute figure is not on the fp16 rows' scale.

Each recall figure is the value that arm reported, not a copy; the generator asserts the identity rather
than assuming it. These are fixed sws points, not calibrated to a recall target — see METHOD.md
for why calibration is inadmissible in an A/B.

Effects are not derived from these medians: the estimates in this report are medians of paired
log-ratios over replicates, which is not the same statistic as a ratio of the medians above. The
per-cell effect and null columns are in "Throughput, per cell"; every sws point is in the appendix.

Verdict

Recall is bit-identical everywhere. Throughput is unchanged on the ten float cells and
reproducibly ~1.2 % faster on the two integer cells.
The integer gain is a real effect with a
measured mechanism, not noise: #372 compiles a dedicated AVX512-VNNI kernel that main does not, which
removes a per-distance runtime feature test and shrinks the hot integer L2 kernel from 614 to 201
bytes. Both arms compute the same VNNI arithmetic, which is why recall does not move.

quantity value
Δrecall, every cell, sws, arm and replicate exactly 0.0, same IEEE-754 bit pattern, 0 ULP
ten float cells — pooled effect +0.36 % (median) / +0.08 % (mean), 95 % CI ≈ ±0.9 pt
ten float cells — pooled null control, A vs A′ −0.10 % (median) / −0.06 % (mean)
ten float cells — cells exceeding their own null 0 of 10
two integer cells — pooled effect +1.20 %, null −0.01 %
two integer cells — B beats both baseline runs 109 of 120 replicate-points (chance: 40)
two integer cells — permutation p, arm labels exchanged < 1 × 10⁻⁵

On the float cells the effect and the null are the same size, and that is the result: #372 moves
float search throughput by less than the baseline moves against itself. On the integer cells the
effect is 100× the null and survives every adversarial check applied to it.

Both pooled estimators are quoted for the float cells because the centre shifts by ~0.3 points
between them — far inside the interval, so the conclusion does not depend on the choice, but a
single quoted number would hide that.

Build time is a separate measurement and it resolves nothing, on any cell. An early 2-arm run
suggested the integer cells built ~5 % faster; a 60-build follow-up with an A′ control withdrew that —
the two integer cells then disagreed in sign (+1.0 % and −4.7 %) while the null control itself read
±2.5–2.8 %. Build time is unchanged to this measurement's resolution, which is ±2.5 % on float cells and
about ±10 % on the integer ones. The search result does not depend on this; the two measurements are
independent.

For the PR this is a positive result, not merely a neutral one. The refactor was expected to be
performance-neutral; on integer search it is a small win, because generating the surface from one
declaration produces a dedicated kernel where the hand-maintained list left a runtime branch in the hot
path. Everywhere else it is neutral, which is what a refactor should be.

Recall — the control, not the measurement

Because all three arms search one shared saved graph per cell (see METHOD.md), Δrecall is an
assertion rather than a measurement, and it holds: across all 84 (cell, sws) pairs at five replicates
in the main matrix, and all 14 pairs at ten replicates in the VNNI re-measurement, every recall
value is byte-identical between all three arms — verified by comparing struct.pack('<d', x).hex(),
not by comparing rounded decimals. The VNNI cells additionally match the untiled baseline, which is
what licenses the enlarged query sets used there.

Independently, the shared-graph design was gated before any timing run: arm B loading an
arm-A-written index reproduced recall bit-for-bit across 12 (cell, sws) points spanning MIP, L2 and
cosine.

This is the strongest single statement the experiment makes. But state it precisely: what is proven is
that both arms return the same top-10 sets, not that every distance is bit-identical. The harness
saves recall, num_queries and latencies — no neighbor IDs and no distances — so a low-bit
difference that never reorders a top-10 result would be invisible here.

With 84 operating points × 5 replicates × ~5,000–20,000 queries each, a rounding difference large
enough to matter would almost certainly have flipped a tie somewhere and shown up as a nonzero Δrecall
at some sws. It did not, anywhere. So "computes identical distances" is a well-supported inference,
not a measurement, and confirming it outright would need a harness that dumps distances — which does
not exist and was not built.

Throughput, per cell

Robust per-cell estimates, sws ≥ 20, replicate 1 excluded (see "What was corrected" below).
effect is log(B / sqrt(A·A′)), null is log(A / A′); both are medians of paired log-ratios
with ±1σ. A cell's effect means nothing unless its own null is consistent with zero.

cell ISA reach effect null own null consistent with 0 verdict
bigann_l2_128 +VNNI +0.86 % ±0.59 −0.34 % ±0.48 yes superseded — see deep-dive
spacev_l2_100 +VNNI +1.11 % ±0.87 −0.58 % ±1.46 yes superseded — see deep-dive
oi_l2_512 AVX2/AVX512 +1.54 % ±1.16 −0.40 % ±0.64 yes null
oi_cos_512 AVX2/AVX512 +0.62 % ±1.09 −0.36 % ±0.38 yes null
oi_mip_512 AVX2/AVX512 −0.23 % ±2.51 −0.16 % ±0.22 yes null
laion_l2_512 AVX2/AVX512 +0.23 % ±0.25 +0.22 % ±0.22 yes null
laion_cos_512 AVX2/AVX512 +0.23 % ±2.93 +0.28 % ±2.66 yes null
laion_mip_512 AVX2/AVX512 −0.10 % ±2.95 −0.08 % ±3.30 yes null
cohere_mip_768 AVX2/AVX512 −0.00 % ±0.26 +0.28 % ±0.57 yes null
dbpedia_l2_dyn AVX2/AVX512 +0.14 % ±0.19 +0.14 % ±0.17 yes null
dbpedia_cos_dyn AVX2/AVX512 +0.13 % ±0.15 −0.20 % ±0.17 yes null
dbpedia_mip_dyn AVX2/AVX512 +0.56 % ±0.17 −0.43 % ±0.17 no null

Per (cell, sws) rows: analysis/effect_by_cell_sws.csv.

The three cells that look like they moved, and why none of them did

oi_l2_512, +1.54 %. Disproved by internal consistency. oi_l2_512 and laion_l2_512 compile
to the same specialization — {float32 query, float16 data, L2, 512} — and therefore execute
identical kernel code. laion_l2_512 reads +0.23 % against its own null of +0.22 %. A kernel-level
effect cannot appear on one and not the other, so oi_l2_512's excess is a property of the machine
during those runs, not of the code. Its own null is also −0.40 % ±0.64, i.e. the baseline was
drifting by a comparable amount on that cell.

dbpedia_mip_dyn, +0.56 % with a tight ±0.17. Its null is −0.43 % ±0.17, which fails the
consistency check: the baseline disagrees with itself by nearly as much as the claimed effect, and in
the opposite direction. That signature — a tight, oppositely-signed null — is machine drift across
the run order, not a kernel effect. No effect can be read off this cell in either direction.

bigann_l2_128 +0.86 % and spacev_l2_100 +1.11 %. These two are the exception: they are the
integer cells, the only two reaching the VNNI level, and at five replicates each was within 2σ of
zero — not dismissable, but not resolvable either. They were re-measured at ten replicates with
larger query sets, and there the effect resolves cleanly. See the deep-dive below; the numbers in
this row are superseded by it.

ISA coverage — what this experiment could and could not have detected

The compiled dispatch surfaces differ as expected. Flags read out of each tree's build.ninja,
symbol counts from nm -DC on each arm's libsvs_benchmark_library.so:

ISA level arm A -march arm B -march symbols A symbols B
AVX2 (avx2.cpp) haswell haswell 432 432
AVX512 (avx512.cpp) cascadelake skylake-avx512 432 432
AVX512_VNNI (vnni.cpp) TU absent cascadelake 0 54

Arm B's configure step reports the generated surface explicitly: 9 extents × 3 ISA levels; extents
64 96 100 128 160 200 512 768 svs::Dynamic
. The build-tree generated
dispatch_surface.h is byte-identical to the committed one, so no override was in play.

Why a flat result on the ten float cells was mechanistically predicted

cascadelake is skylake-avx512 plus VNNI, and VNNI is integer-only. So for float kernels arm
A's -march=cascadelake and arm B's -march=skylake-avx512 permit the same instruction set, and the
two arms' float codegen is equivalent by construction. The ten float cells reading flat is not weak
evidence of safety — it is the predicted outcome, and a non-flat float result would have been the
finding.

For the integer kernels both arms run the same arithmetic, but not the same function. Arm A is
compiled at -march=cascadelake, so SVS_AVX512_VNNI is set and its AVX512-level int8 kernel does
reach L2VNNIOp — the intuition that "main has no VNNI translation unit, therefore main never runs
VNNI" is wrong. The difference is how it gets there. Arm A has no AVX512_VNNI instantiation to
dispatch to (0 level-3 symbols against B's 54), so it dispatches to level 2, whose body tests a runtime
flag on every call:

template <size_t N> struct L2Impl<N, int8_t, int8_t, AVX_AVAILABILITY::AVX512> {   // arm A
    SVS_NOINLINE static float compute(...) {
        if (__builtin_expect(svs::detail::avx_runtime_flags.is_avx512vnni_supported(), 1)) {
            return simd::generic_simd_op(L2VNNIOp<int16_t, 32>(), a, b, length);
        }
        return simd::generic_simd_op(L2FloatOp<16>{}, a, b, length);   // dead on this host
    }
};

Arm B dispatches to a level-3 specialization whose body is the VNNI call and nothing else. The cost of
A's arrangement is visible in the object code. Disassembling only the kernel each arm actually
dispatches to for bigann (L2Impl<128, uint8, uint8>, objdump bounded to that symbol):

instruction class arm A, level 2 (614 B) arm B, level 3 (201 B)
vpdpwssd — the actual work 4 4
avx_runtime_flags load + cmpb 1 + 1, at function offset +4 0
vcvtdq2ps / vfmadd*ps — dead float fallback 16 / 8 0

Symbol sizes for spacev's kernel are the same story: 575 B at level 2 against 186 B at level 3.

So the VNNI arithmetic is bit-for-bit the same work, and arm A additionally loads a global and branches
on it as the first thing it does on every call, then carries a never-executed float path through the
same function body. Both sit inside an SVS_NOINLINE boundary, so neither can be hoisted out of the
caller's search loop. That is the mechanism: one fewer per-distance branch and a third of the
instruction footprint in the innermost function of a latency-bound graph traversal — which predicts a
small consistent gain rather than a large one, and that is what was measured.

Note also that this makes +1.2 % the narrow case. Arm A benefits from being compiled at
cascadelake; a main built with its AVX512 TU at skylake-avx512 would take the float fallback for
integer data and the gap would be far wider.

Two consequences that bound the strength of this result:

  • All 54 VNNI symbols are integer — 27 unsigned char, unsigned char and 27
    signed char, signed char, zero float16, because VNNI is vpdpbusd, an integer dot product. An
    fp16 matrix cannot reach that ISA level at all. Only 2 of the 12 cells (bigann uint8, spacev
    int8) exercise it. A flat result on the other ten is therefore expected and is not on its own
    evidence that refactor(dispatch): Generate and validate the dispatch surface from a single declaration #372 is safe at the VNNI level. The integer cells were added for exactly this reason.
  • Adding registrations to benchmark/src/vamana/uncompressed.cpp does not change ISA symbol
    counts. The surface is fixed at configure time from SVS_SUPPORTED_DIMS × SVS_ISA_LEVELS,
    independent of what for_standard_specializations exposes; the registration list only decides what
    the executable can dispatch to. B's VNNI count was 54 both before and after the int8 cells were
    added, which is correct and not a sign the edit failed.

Six of the twelve cells required registrations that do not exist upstream — the shipped list in
benchmark/src/vamana/uncompressed.cpp has no 512-extent and no cosine entry, so laion, open_images
and dbpedia would each have hard-failed with "Unimplemented specialization". The list was therefore
replaced, not extended: upstream's 7 entries (float16 at 96/100/200/768 and the two integer ones)
became 6 covering our fp16 cells, then the two integer entries and MIP at Dynamic were added back
for bigann, spacev and dbpedia_mip_dyn, giving 9. Both arms' post-edit file has the same git blob
hash and both arms' --help specialization lists are byte-identical, so the edit cannot bias the A/B.
It was left uncommitted.

The final compiled set is {uint8,uint8,L2,128} {int8,int8,L2,100} {float32,float16,MIP,768} {float32,float16,MIP,512} {float32,float16,L2,512} {float32,float16,Cosine,512} {float32,float16,L2,dynamic} {float32,float16,Cosine,dynamic} {float32,float16,MIP,dynamic}.

VNNI deep-dive — the one real effect in the experiment

The two integer cells were re-measured on their own: 10 replicates instead of 5, three arms, arm
order rotated on a 3-cycle, and query sets enlarged by tiling so a timed pass lasts ~0.27–0.32 s
instead of the ~2.6 ms that made the sws=10 points useless. 60 runs, all rc=0.

Recall is still exactly unchanged

Δrecall is 0.0 at all 14 (cell, sws) points across all three arms and all 10 replicates, and equals the
untiled baseline: bigann at sws=10 is 0.93284, spacev 0.8815142293140319. That the tiled and
untiled numbers agree matters, because tiling is what the enlarged query set depends on.

Throughput

cell effect B vs pooled baseline null A vs A′ permutation p
bigann_l2_128 +0.961 % ±0.127 +0.122 % ±0.147 < 1 × 10⁻⁵
spacev_l2_100 +1.435 % ±0.344 −0.060 % ±0.676 < 1 × 10⁻⁵
pooled +1.209 % ±0.189 −0.014 % ±0.346 < 1 × 10⁻⁵

The effect is ~100× the null. It is also present at every operating point from sws=20 to sws=200 on
both cells, with no trend in sws — consistent with a fixed per-distance cost, which is what the
mechanism above predicts.

Unlike the main matrix, replicate 1 shows no cold-cache penalty here (every one of the 42
(cell, arm, sws) groups is within 5 % of the later replicates), so it is kept. The verdict is identical
either way: +1.209 % with it, +1.187 % without.

Three adversarial checks, all passed

Run order cannot explain it. With arms ignored, slot position moves QPS by at most +0.13 % — an
order of magnitude too small. More directly, splitting log(B/A) by the slot gap between B and A gives
+1.15 % when B ran before A and +1.27 % when B ran after. Same sign, same size; a positional
artifact would have flipped sign.

A sign test needs no distributional assumption. A and A′ are the same binary, so under the null each
of the three arms is equally likely to be fastest. B was faster than both baseline runs at 109 of
120
replicate-points, against 40 expected — binomial p ≈ 1.4 × 10⁻³⁹ (bigann 50/60, spacev 59/60).

A permutation test gives the exact null. Exchanging which arm label is treated as "B" within each
replicate, 200,000 relabellings, none reached the observed +1.197 %: p < 1 × 10⁻⁵ pooled and per cell.

One caveat kept deliberately: a stricter criterion — requiring the effect's 2σ band to be disjoint
from the null's 2σ band at each individual sws — is met at 6 of 12 points, all on spacev. bigann fails
that stricter test only because its null sits at a consistent +0.12 to +0.25 % rather than at zero.
Aggregated per cell, bigann's effect still clears its null by 4.3σ, and the sign and permutation tests
do not depend on either band. The stricter figure is recorded here so the weaker points are not hidden
behind the pooled number.

Build timing

Separate measurement, unpinned at num_threads = 224, 10 cells × 2 arms × 3 replicates (dbpedia at 2)
= 58 builds, all rc=0. Two clocks agree throughout — the driver's wall time and the harness's own
build_time field never differ by more than ~0.6 s — so process startup is not contributing and the
numbers below are the harness clock. Read METHOD.md first: this first pass has no A′ null control,
so its noise floor comes from within-arm replicate spread, which is a weaker guarantee than the search
side has. The two integer cells were re-run with a control afterwards, and the result changed — the
subsection after next is the one to trust.

cell data type B/A within-arm spread verdict
bigann_l2_128 uint8 0.919 5.5 % superseded — not resolved
spacev_l2_100 int8 0.956 11.8 % superseded — not resolved
cohere_mip_768 float16 1.000 1.3 % not resolved
dbpedia_l2_dyn float16 0.997 4.9 % not resolved
laion_cos_512 float16 1.001 3.6 % not resolved
laion_l2_512 float16 1.009 3.5 % not resolved
laion_mip_512 float16 0.995 6.3 % not resolved
oi_cos_512 float16 1.004 7.4 % not resolved
oi_l2_512 float16 1.031 4.2 % not resolved
oi_mip_512 float16 0.995 2.5 % not resolved

Eight float cells: no resolvable difference. Pooled median B/A is −0.15 % with a 95 % CI of roughly
±2.5 %. The honest statement is that build time on float data is unchanged to the resolution this
measurement achieves
, which is about ±2.5 % — not that it is proven equal.

The two integer cells appeared to move — this reading is superseded and wrong; the 3-arm follow-up
below withdraws it.
It is kept because the correction is the point. Pairing by replicate, since build
is nondeterministic:

rep 1 rep 2 rep 3 direction
bigann_l2_128 0.917 0.986 0.893 B faster 3/3
spacev_l2_100 0.948 0.951 0.987 B faster 3/3

Integer replicate-pairs: 6/6 B-faster, median −5.06 %. Float replicate-pairs: median −0.04 %, B
faster in 12 of 23 — an exact coin flip, which is what a null looks like. An integer pair is below a
float pair in 129 of 138 cross-comparisons (93.5 %; 50 % under the null).

It looked expected rather than surprising, which is precisely why it needed the control: graph
construction is dominated by distance evaluations, so a cheaper integer kernel should shorten build on
exactly the two integer cells and leave the eight float cells alone, and the data matched that story.

Six replicate-pairs was thin, and the follow-up run withdrew this finding. See below.

The integer build-time effect did not survive its own control — withdrawn

The two integer cells were re-measured at 10 replicates × 3 arms = 60 builds, all rc=0, with the
A′ null control the first run lacked. The result contradicts the 6/6 above:

cell data type n effect, B vs pooled baseline null, A vs A′ sign test permutation p
bigann_l2_128 uint8 10 +1.01 % (B slower) −2.47 % 1/10 0.55
spacev_l2_100 int8 10 −4.67 % (B faster) +2.79 % 7/10 0.034
pooled 20 −0.52 %, 95 % CI [−4.8 %, +4.0 %] +1.78 % 8/20, p = 0.34 0.30

Negative means B builds faster. Excluding replicate 1 changes nothing material (pooled −0.52 %,
p = 0.35).

The two cells now disagree in sign, and the mechanism predicts they should agree. They differ only
in integer signedness; both dispatch to the same VNNI kernel that the search-side result measured. A
real kernel-level build effect cannot make one cell 1 % slower and the other 5 % faster. So the pattern
is noise, and the first run's 6/6 was a small-sample draw — bigann's own B/A across ten replicates is
0.82, 1.20, 0.93, 1.09, 1.01, 0.90, 1.05, 1.14, 1.03, 1.05, and the first run happened to catch three
of the low ones.

Three further reasons not to rescue this:

  • The null control is the size of the claimed effect. A against itself reads −2.5 % on bigann and
    +2.8 % on spacev. Per-run build time on these cells ranges 19.3–27.5 s and 32.0–40.4 s within a single
    arm, so the noise floor here is ~±10 %, not the ±5 % the first run's replicate spread suggested.
  • Slot position is not the explanation either. With arm labels ignored, the three execution slots
    read −1.4 % / +2.4 % / −1.5 %, and the rotation left arm B in the slow middle slot 8 times of 20
    against A's 6 — an imbalance biasing B slower, i.e. the wrong way to manufacture spacev's result.
  • spacev's nominal p = 0.03 is one cell of two. It is not significant after even the mildest
    accounting for having looked at both, and it has no mechanism that excludes bigann.

Corrected statement: build time is unresolved on all ten cells, integer included. The measurement's
resolution is ±2.5 % on the float cells and roughly ±10 % on the two integer cells, and no cell shows a
difference exceeding its own noise. A build-side effect of the ~1 % magnitude the search side measured
is entirely plausible mechanically — graph construction is distance-dominated — but this experiment
cannot see it, and it is not evidence for the PR either way.

Build recall confirms the nondeterminism the design assumes

Recall at sws=20 from these per-arm builds differs across replicates of the same arm — spans up to
8 × 10⁻⁵ (spacev 0.94027 / 0.94035 / 0.94027 on arm A alone). The 60-build follow-up shows the same
thing at scale: 30 builds of spacev_l2_100 produced 15 distinct recall values, and 30 of
bigann_l2_128 produced 3, all within 4 × 10⁻⁵. That is not a quality finding; it is the
direct evidence that per-arm graph builds would have injected build noise into Δrecall, which is why the
search comparison uses one shared graph per cell and can assert Δrecall = 0 exactly.

What was corrected during analysis

Two corrections changed the numbers materially. Both are in the analysis script's docstring so the
artifact carries them.

Replicate 1 is a cold-page-cache run and is excluded. The first run of each cell paid a page-cache
penalty — oi_l2_512 at sws=20 read 105,517 QPS in replicate 1 against ~180,000 in every later
replicate; spacev read 685,943 against ~925,000. The arm rotation put arm A first in replicate 1 on
every cell, so the penalty landed systematically on the baseline and inflated B/A. Dropping
replicate 1 collapsed the pooled null from −0.265 % to −0.058 %, which is the evidence that
replicate 1 was a contaminant rather than a fair sample. All replicate-1 data remains in the CSVs.

The central estimate is the median of paired log-ratios, not the mean. Mean-of-logs is dragged by
straggler runs: it reported +3.53 % on oi_l2_512 where the median reports +1.54 %.

The mechanism for the integer effect was wrong twice before it was right, and only disassembly settled
it.
First reading: the arms emit the same instructions and differ only by translation unit — which
made the ~1 % look like noise. Second reading, from the source alone: arm A takes the float fallback and
never executes VNNI — which would have made the gain large and was flatly wrong, since arm A's
avx512.cpp is compiled at cascadelake and so has SVS_AVX512_VNNI set. The settled answer needed
objdump bounded to the single dispatched symbol in each arm, and it is neither of those: same four
vpdpwssd, plus a runtime flag test and a dead float path in arm A only. Reading a #if-guarded
header without knowing each translation unit's -march is what produced the wrong middle answer.

The integer build-time speedup was retracted by its own follow-up. Three replicates per arm with no
null control gave 6/6 B-faster and a −5 % median, and it was mechanistically plausible, which is exactly
what made it convincing. Ten replicates with an A′ control flipped bigann's sign. The lesson is not
"n = 3 is small" — it is that the plausible mechanism made the thin result feel resolved, and only the
control that measures the baseline against itself could say otherwise. The one surviving effect in this
experiment is the one where the null was measured on the same cells at the same time.

Reading limits — carry these into any decision

  • Ten of twelve cells cannot reach VNNI, so the one positive finding rests on two cells — both of
    which show it, at every operating point, with a mechanism visible in the object code. Widening it
    would need more integer datasets, not more float ones.
  • The +1.2 % is specific to this pair of builds. It measures a dedicated VNNI kernel against a
    cascadelake-compiled AVX512 kernel that reaches VNNI through a runtime test. Against a main whose
    AVX512 TU is built for a non-VNNI target the gap would be larger; on a host without VNNI it would be
    zero. It is not a portable "refactor(dispatch): Generate and validate the dispatch surface from a single declaration #372 is 1.2 % faster" claim.
  • The NONE ISA level is untested. B's own configure output flags it: NONE is "dispatched to,
    but compiled by no translation unit, so every consumer instantiates those kernels itself, at its own
    -march"
    . Every run here was on a VNNI-capable host, so that path was never taken. Anything that
    depends on the scalar fallback — a pre-AVX2 host, or a consumer whose own -march differs — is
    outside this experiment.
  • The sws=10 operating points are untrustworthy and are excluded from every estimate above. Their
    timed passes ran ~2.6 ms, short enough that timer and scheduling noise dominate.
  • laion's recall caps near 0.99 (L2) / 0.995 (cosine) because its base is unit-normalized only to
    ~2 × 10⁻⁴. That is the data, not a defect, and it is identical on both arms.
  • dbpedia's search recall is computed over a different query subset than its build recall (the
    search runs use a 20× tiling of the 1,000-query set). Those two numbers are not comparable to each
    other; they remain fully comparable across arms, which is what the A/B needs.
  • QPS on a shared host is best-effort. Load average was recorded before every run, arms were
    interleaved rather than blocked, and A′ exists precisely so that host noise has a measured scale.
    The A′ column is the honest read on how much of any per-cell number is the machine.

Artifacts

All under /var/tmp/huberand/svs372/ (local ext4, nothing committed):

what path
absolute QPS tables (top table + appendix) analysis/qps_tables.py
per-sample parsed results, all arms/cells/reps runs/parsed_ar7.csv
VNNI deep-dive, per-sample, 10 replicates runs_vnni/parsed_vnni.csv
VNNI per-(cell, sws) effect vs null analysis/effect_by_cell_sws_vnni.csv
VNNI order-confound + per-sws analysis analysis/order_confound.py
VNNI sign test + permutation test analysis/vnni_permute.py
per-(cell, sws) recall + QPS comparison runs/compare_ar7.csv, runs/compare_ar7.txt
per-(cell, sws) effect vs null analysis/effect_by_cell_sws.csv
analysis script (prints the tables above) analysis/paired_effect.py
parser + its test suite analysis/parse_results.py, analysis/test_parse.py
shared-graph gate results gate/gate_results.csv
the 12 saved graphs, with build times and recall indexes/GRAPHS.md
dataset staging, verified metric/dim/count per dataset scratch/datasets/MANIFEST.md
arm provenance: SHAs, cmake lines, ISA TUs, symbol counts arms.txt
driver log for the 180 interleaved search runs runs/driver_ar7.log
build timing, 58 runs build_timing/build_times.csv, build_timing/driver_build.log
build timing analysis script analysis/build_effect.py
build timing contamination log build_timing/CONTAMINATION.md
integer-cell build follow-up, 10 reps + A′ build_timing2/build_times2.csv, build_timing2/driver_build2.log
3-arm build analysis (effect, null, sign, permutation) analysis/build_effect2.py

Appendix — QPS, every cell and operating point

Median queries/second, same exclusions as the table at the top. Recall gets one column here rather
than three: it is byte-identical across arms in every group of both runs — 84 in the 5-replicate matrix
and 14 in the 10-replicate deep-dive — so three copies of one number would only widen the table. The
generator asserts that identity rather than assuming it, and would fail loudly if it broke.

Rows marked * are sws = 10, excluded from every estimate in this report: its timed passes ran
~2.6 ms, short enough that timer and scheduling noise dominate, and the arm scatter on those rows shows
it.

cell sws A · QPS B · QPS A′ · QPS recall (all arms)
cohere_mip_768 10 * 133,154 138,138 135,117 0.829860
cohere_mip_768 20 82,668 82,902 82,771 0.906620
cohere_mip_768 30 59,434 59,094 59,052 0.940380
cohere_mip_768 50 37,949 38,015 37,516 0.968200
cohere_mip_768 80 23,825 23,742 23,715 0.982640
cohere_mip_768 120 15,872 15,738 15,818 0.989520
cohere_mip_768 200 9,370 9,390 9,374 0.993900
oi_mip_512 10 * 250,168 243,658 249,838 0.936640
oi_mip_512 20 156,484 152,753 156,557 0.970260
oi_mip_512 30 113,839 113,760 113,442 0.980260
oi_mip_512 50 73,646 73,429 73,793 0.986560
oi_mip_512 80 47,602 47,430 48,138 0.988560
oi_mip_512 120 31,295 31,503 31,394 0.989120
oi_mip_512 200 18,621 18,581 18,632 0.989520
oi_l2_512 10 * 290,568 290,817 287,056 0.932520
oi_l2_512 20 180,433 182,641 179,778 0.976740
oi_l2_512 30 130,401 133,777 131,646 0.988760
oi_l2_512 50 85,523 87,312 86,189 0.994880
oi_l2_512 80 55,409 57,150 56,294 0.997300
oi_l2_512 120 37,456 38,617 37,737 0.998280
oi_l2_512 200 22,378 22,945 22,555 0.999040
oi_cos_512 10 * 242,163 240,297 241,152 0.942680
oi_cos_512 20 151,519 150,648 150,692 0.979220
oi_cos_512 30 108,987 109,860 109,714 0.989600
oi_cos_512 50 69,867 70,571 70,777 0.995880
oi_cos_512 80 44,981 45,488 45,375 0.997960
oi_cos_512 120 29,287 29,624 29,531 0.998540
oi_cos_512 200 17,449 17,447 17,472 0.999060
laion_mip_512 10 * 282,375 275,023 232,735 0.947560
laion_mip_512 20 182,642 178,586 181,122 0.974740
laion_mip_512 30 134,742 133,309 134,647 0.984480
laion_mip_512 50 88,430 87,621 88,274 0.990780
laion_mip_512 80 58,006 57,541 57,706 0.994100
laion_mip_512 120 39,059 38,244 38,895 0.995580
laion_mip_512 200 22,808 22,772 22,853 0.996940
laion_l2_512 10 * 280,921 278,206 278,602 0.938820
laion_l2_512 20 176,545 176,566 176,466 0.968000
laion_l2_512 30 130,450 129,331 130,203 0.977000
laion_l2_512 50 85,559 85,276 85,296 0.983120
laion_l2_512 80 56,486 56,345 56,495 0.985580
laion_l2_512 120 37,581 37,880 37,609 0.986780
laion_l2_512 200 22,648 22,771 22,626 0.988180
laion_cos_512 10 * 278,715 277,928 275,632 0.942360
laion_cos_512 20 177,689 177,138 177,243 0.968000
laion_cos_512 30 130,163 130,061 129,579 0.977380
laion_cos_512 50 85,450 85,140 84,409 0.983820
laion_cos_512 80 55,337 55,410 55,041 0.986740
laion_cos_512 120 37,026 36,568 36,937 0.988200
laion_cos_512 200 21,316 21,429 21,278 0.989460
dbpedia_mip_dyn 10 * 35,634 35,798 35,742 0.932100
dbpedia_mip_dyn 20 21,506 21,574 21,477 0.972600
dbpedia_mip_dyn 30 15,375 15,477 15,437 0.982400
dbpedia_mip_dyn 50 9,742 9,792 9,806 0.989700
dbpedia_mip_dyn 80 6,206 6,274 6,253 0.995100
dbpedia_mip_dyn 120 4,166 4,194 4,178 0.996500
dbpedia_mip_dyn 200 2,475 2,490 2,480 0.997900
dbpedia_l2_dyn 10 * 28,117 27,829 27,994 0.942900
dbpedia_l2_dyn 20 16,771 16,884 16,837 0.977100
dbpedia_l2_dyn 30 12,044 12,041 12,024 0.986000
dbpedia_l2_dyn 50 7,554 7,511 7,544 0.992200
dbpedia_l2_dyn 80 4,776 4,763 4,760 0.996100
dbpedia_l2_dyn 120 3,160 3,159 3,155 0.997700
dbpedia_l2_dyn 200 1,858 1,856 1,858 0.998700
dbpedia_cos_dyn 10 * 34,967 34,980 35,012 0.932200
dbpedia_cos_dyn 20 21,077 21,176 21,203 0.972600
dbpedia_cos_dyn 30 15,246 15,275 15,223 0.982400
dbpedia_cos_dyn 50 9,669 9,652 9,642 0.989800
dbpedia_cos_dyn 80 6,163 6,157 6,160 0.995200
dbpedia_cos_dyn 120 4,102 4,110 4,127 0.996600
dbpedia_cos_dyn 200 2,435 2,449 2,444 0.998000
bigann_l2_128 10 * 1,855,003 1,870,928 1,852,761 0.932840
bigann_l2_128 20 1,015,845 1,026,399 1,014,138 0.980500
bigann_l2_128 30 689,540 694,401 687,726 0.992260
bigann_l2_128 50 416,871 420,770 416,613 0.998240
bigann_l2_128 80 259,558 262,334 259,375 0.999520
bigann_l2_128 120 170,921 172,196 170,294 0.999760
bigann_l2_128 200 99,269 100,037 99,125 0.999860
spacev_l2_100 10 * 1,516,464 1,549,135 1,493,327 0.881514
spacev_l2_100 20 829,435 839,613 827,815 0.940253
spacev_l2_100 30 568,601 579,433 570,402 0.961893
spacev_l2_100 50 347,639 353,384 347,797 0.979413
spacev_l2_100 80 216,383 219,229 216,583 0.988559
spacev_l2_100 120 140,979 142,619 140,939 0.992988
spacev_l2_100 200 81,164 82,132 81,200 0.995990

Per-sample values behind these medians: runs/parsed_ar7.csv and runs_vnni/parsed_vnni.csv. The
integer cells also have 5-replicate rows in parsed_ar7.csv, superseded here by the 10-replicate run.

@rfsaliev rfsaliev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Still some questions.

include("${CMAKE_CURRENT_LIST_DIR}/dispatch-levels.cmake")

set(SVS_DEFAULT_DISPATCH_SURFACE_FILE "${CMAKE_CURRENT_LIST_DIR}/dispatch-surface.cmake")
set(SVS_DISPATCH_SURFACE_FILE "${SVS_DEFAULT_DISPATCH_SURFACE_FILE}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is the motivation for this variable?

# CMake, and a one-off build with an overridden surface must not rewrite it.
# configure_file only touches the file when the content changes, so this neither
# dirties the tree nor forces rebuilds.
if(svs_surface_is_default)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like by default, the <root>/include/svs/core/distance/dispatch_surface.h is always overriden.
What is the reason having 2 identical autogenerated headers in different locations?

@@ -0,0 +1,32 @@
/*

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why this file is named vnni.cpp? Is it intended for avx-vnni or avx512-vnni support?

Comment on lines +26 to +27
#define SVS_DEFINE_FOR_DIM(DIM) SVS_INSTANTIATE_DISTANCES(template, DIM, AVX2)
SVS_FOR_EACH_SUPPORTED_DIM(SVS_DEFINE_FOR_DIM)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

SVS_INSTANTIATE_DISTANCES macro is defined in preprocessor.h
SVS_FOR_EACH_SUPPORTED_DIM - in dispatch_surface.h
but these headers are not included.

Seems like it breaks at least readability "... If someone researched the code on GH only,..."

Three human review comments on PR #372 named gaps the Copilot review agent did
not catch. Each is generalized here so a similar violation in a later PR is
flagged, rather than recorded as a one-off.

- copilot-instructions.md: a new file must satisfy the purpose line of the
  nearest AGENTS.md, in kind as well as in role.
- tests/AGENTS.md: the purpose line now admits harnesses written in any
  language, including build-level checks. Without this the rule above cannot
  decide where a ctest checker written in CMake belongs, because cmake/AGENTS.md
  and tests/AGENTS.md both had a claim on it. That ambiguity, not a weak rule,
  is why the reviewer's objection was derivable from neither.
- build-system.instructions.md: a value another module consumes is returned
  through function()/PARENT_SCOPE or a cache entry.
- library-code.instructions.md (new): a macro expanding into a translation
  unit's own definitions needs its defining header included directly.

Two rules were rejected at the wrong altitude before these survived. "Split
build logic into functions", the reviewer's own suggested example, would have
flagged 12 of 13 cmake modules on main: the established convention rather than
a deviation from it. The include rule scoped to include/svs/** would have
flagged 53 merged headers that use SVS_UNUSED or SVS_FWD transitively. Both
reached a usable form only after re-distilling to a rung at which main is clean
and this PR's additions are not.

The other six comments needed no rule. Five are answered by code or
configuration already on record, and one asks for a rationale that is present
at the comparison it serves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The AVX512 runtime predicate does not guarantee its compilation budget, and instruction tests break when AVX512 is disabled.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (6)

Previously missed (6) — in code that hasn't changed since the last review.

cmake/generate-dispatch-surface.cmake:74

  • This module publishes SVS_DISPATCH_TU_SPECS to multi-arch.cmake via a bare file-scope variable, and similarly publishes the generated-header path to the root install logic. That directly conflicts with the module-interface rule in .github/instructions/build-system.instructions.md:9; wrap generation in a function and return consumed outputs with PARENT_SCOPE (or use cache entries) so unrelated includes cannot silently overwrite them.
    include/svs/multi-arch/x86/avx2.cpp:27
  • This translation unit expands SVS_INSTANTIATE_DISTANCES and SVS_FOR_EACH_SUPPORTED_DIM but obtains their definitions only transitively through the distance headers. Include the defining preprocessor header directly so this code-generating dependency remains explicit and does not break when those headers stop re-exporting it.
    include/svs/multi-arch/x86/avx512.cpp:27
  • This translation unit expands SVS_INSTANTIATE_DISTANCES and SVS_FOR_EACH_SUPPORTED_DIM but obtains their definitions only transitively through the distance headers. Include the defining preprocessor header directly so this code-generating dependency remains explicit and does not break when those headers stop re-exporting it.
    include/svs/multi-arch/x86/vnni.cpp:20
  • This translation unit expands SVS_INSTANTIATE_DISTANCES and SVS_FOR_EACH_SUPPORTED_DIM but obtains their definitions only transitively through the distance headers. Include the defining preprocessor header directly so this code-generating dependency remains explicit and does not break when those headers stop re-exporting it.
    tests/cmake/dispatch-surface/valid-minimal.cmake:16
  • This comment calls the one-level fixture “still a library,” but distance_core.h explicitly rejects any x86 surface without both AVX2 and AVX512. The fixture is only structurally valid to the declaration validator, not buildable; describe that distinction so it is not mistaken for a supported minimal configuration.
    tests/multi-arch/x86/entry_probe.cpp:115
  • This reference still points to the checker's old location even though this PR moves it under tests/multi-arch/dispatch-checks/. Update the path so future changes can find the script that consumes this mangling.
  • Files reviewed: 50/50 changed files
  • Comments generated: 2
  • Review effort level: Balanced

# that the budget and the promise line up.
set(SVS_ISA_LEVELS
"AVX2|haswell|avx2"
"AVX512|skylake-avx512|avx512"
Comment on lines +104 to +107
foreach(class IN LISTS svs_required)
svs_count_class(count "${class}")
if(count EQUAL 0)
message("${SVS_OBJECT} contains no ${class} instructions.")
Sweeps every merged pull request below 300 on the public repo and writes the
recurring human review catches Copilot missed into the instruction surfaces.
18 principles, each seen in two or more distinct pull requests.

New surfaces: library-code (include/svs/**), bindings (bindings/**) and
documentation. Widens the testing glob to reach bindings/*/tests/**, which is
where two thirds of the fixture-reuse evidence lives, and the build-system glob
to reach .github/workflows/**.

Renames library-code.instructions.md to multi-arch.instructions.md: its glob is
include/svs/multi-arch/** and its title said Multi-Arch, so the name was
misleading and the library-code name was needed for the include/svs/** rules.

Resolves two contradictions. "Prefer smallest test surface" and "cover every
documented variant" pulled opposite ways, and are now ordered — smallest surface
per variant, then every variant. "Delete branches a change makes unreachable"
and "a conditional-compilation guard is load-bearing" both describe an #ifdef
path no CI job defines, so the first now says such a path is not dead merely
because no CI job selects it.

Five of the 18 could not be validated against their own evidence and land on the
strength of recurrence alone. Reuse-the-shared-helper, reuse-the-shared-fixture
and check-for-an-existing-flag describe duplication that is invisible inside a
single diff, which is all a reviewer sees; expect them to be quiet. Role-based
naming and compile-time-over-runtime had only post-fix evidence, so no violation
could be demonstrated. Each says so in its own record.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

ISA predicates do not fully match compile budgets, and declaration validation accepts configurations that cannot build.

Review details

Suppressed comments (8)

Previously missed (6) — in code that hasn't changed since the last review.

cmake/validate-dispatch-surface.cmake:115

  • Validation never checks that level names an AVX_AVAILABILITY enumerator. For example, BOGUS|haswell|avx2 is accepted because avx2.cpp exists, then the generated C++ fails to compile. Since this PR promises configure-time rejection of malformed declarations, validate levels against the enum (and add a rejection fixture) before accepting the row.
    include/svs/multi-arch/x86/avx2.cpp:27
  • This translation unit now expands code-generating macros without directly including either defining header. That makes its definitions depend on the distance headers' transitive includes, contrary to the multi-arch requirement and easy to break when those headers are reorganized. Include both macro owners explicitly.
    include/svs/multi-arch/x86/avx512.cpp:27
  • This translation unit now expands code-generating macros without directly including either defining header. That makes its definitions depend on the distance headers' transitive includes, contrary to the multi-arch requirement and easy to break when those headers are reorganized. Include both macro owners explicitly.
    include/svs/multi-arch/x86/vnni.cpp:20
  • This new translation unit expands code-generating macros without directly including either defining header. That makes its definitions depend on the distance headers' transitive includes, contrary to the multi-arch requirement and easy to break when those headers are reorganized. Include both macro owners explicitly.
    tests/cmake/dispatch-surface/valid-minimal.cmake:19
  • This fixture is labeled valid and described as a usable minimal library, but distance_core.h now emits an error unless both AVX2 and AVX512 are declared. The checker therefore blesses a surface that cannot build. Keep one extent, but include both mandatory levels (or classify the one-level surface as invalid).
    tests/multi-arch/x86/entry_probe.cpp:116
  • This comment contradicts the probe: int8_t/int8_t is one of the two pairs that does have an AVX512_VNNI kernel, and the checker path has moved out of cmake/. As written, it gives the opposite rationale for why expected_level() should match this call.

cmake/dispatch-surface.cmake:118

  • These -march budgets still exceed what the runtime predicates guarantee. haswell permits FMA (used by the AVX2 kernels), while dispatch tests only the AVX2 bit; more critically, skylake-avx512 permits BW/DQ/VL, while dispatch tests only AVX512F, and the integer conversion paths use BW/VL intrinsics. An AVX512F-only host such as KNL can therefore be routed into unsupported instructions. Extend the runtime feature detection/predicates to cover every feature enabled by each budget, or lower/split the budgets accordingly.
    "AVX2|haswell|avx2"
    "AVX512|skylake-avx512|avx512"
    "AVX512_VNNI|cascadelake|vnni"

tests/multi-arch/dispatch-checks/check-dispatch-instructions.cmake:108

  • The required-class assertion makes the test suite fail for the supported SVS_NO_AVX512=YES configuration: that option deliberately compiles the AVX512 and VNNI translation units without zmm/VNNI instructions, but these rows still require them. Pass the configuration into this checker and validate the generic fallback instead, or omit only these required-instruction assertions when AVX512 is disabled.
  • Files reviewed: 54/54 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

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.

4 participants