Skip to content

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

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

refactor(dispatch): Generate and validate the dispatch surface from a single declaration#372
ahuber21 wants to merge 23 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
"x86-64||ymm zmm mask vnni"
"haswell|ymm|zmm mask vnni"
"skylake-avx512|zmm|vnni"
"cascadelake|zmm|"

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.

Will be fixed in 375 -- and all PRs will be merged together as a stack.

@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 and others added 7 commits September 2, 2026 04:19
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…heir helpers

The checkers are test drivers, so they belong under tests/multi-arch/ with
the probes they run. Extract three divergent copies of symbol-reading logic
into a shared lib.cmake, eliminating duplication of svs_nm_symbols,
svs_require, svs_require_files, and svs_report_first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Route the hand-rolled ISA level and TU spec splits through centralized
parsers that validate field counts, so malformed rows now fail where they
are written instead of as empty variables later. The validator now shares
the parser with the other two users, and its malformed-row fixture was
updated to the shared message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two levels cannot share one -march instruction budget because the compiler
then emits instructions from the weaker level at that budget, giving hosts
routed to the weaker level instructions its runtime predicate does not
guarantee. The instruction checker cannot catch this because it looks the
budget up by -march and would check the weaker level against the (stronger)
budget row, which forbids nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A CMake function captures the policy stack where it is defined, so the parser's
behaviour depended on which caller included it first. Under CMP0007 OLD an empty
field vanishes when the spec is split, which turns a row with an empty middle
field into a malformed-row error and silently invalidates the premise of the
invalid-level-empty-field fixture. Setting the policy in the parser's own file
makes the two error paths stable regardless of the caller.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every other test under tests/ is a Catch2 case, so a reader finding no Catch2
macros here has no way to tell whether that is deliberate. It is for the probes,
whose tests are the link step and a gdb session, and provisional for the four
cmake -P checkers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The generator emits dispatch_surface.manifest.cmake with the extent list and the
ISA level enumerators, and the declaration check reads that instead of including
the declaration itself. The check now depends on the surface's contents rather
than on that file's format, location, or what else it happens to set.

The AVX_AVAILABILITY read from distance_core.h stays: it is what derives the
mangled enum digit rather than assuming it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ahuber21
ahuber21 force-pushed the dispatch/02-generate-surface branch from 44dd9a6 to 882b761 Compare September 2, 2026 12:32
@ahuber21
ahuber21 requested a review from homksei as a code owner September 2, 2026 12:32
@ahuber21 ahuber21 changed the title refactor(dispatch): Generate the dispatch surface from a single declaration refactor(dispatch): Generate and validate the dispatch surface from a single declaration Sep 2, 2026
@ahuber21

ahuber21 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — this is a useful review and most of it is going in. One piece of context that changes several items: #372 is the middle of a stack, and #373/#375 already resolve some of what you raised. To make that reviewable rather than confusing, I've folded #373 and #375 into this PR, so what you see now is the whole dispatch-surface change. The diff is correspondingly larger; #373 and #375 are closed as merged here.

Taken

2 — shared checker helpers. Done. svs_require, svs_require_files, svs_nm_symbols, svs_report_first now live in one lib.cmake. The two divergent svs_symbols() definitions and the third inlined copy in the execution check are all one function now.

7 — checker location. Done. cmake/dispatch-checks/ moved to tests/multi-arch/dispatch-checks/. Only generation stays under cmake/.

4 — one ISA-level parser. Done. On the count: there are six string(REPLACE "|" ";" ...) sites, but only five parse the ISA-level/TU-spec encoding — those five now go through svs_parse_isa_level() / svs_parse_tu_spec(), which fail on a wrong field count instead of silently producing empty variables. The sixth, in check-dispatch-instructions.cmake, parses a different three-field table (<-march>|<required classes>|<forbidden classes>) with one reader and no duplicate, so it stays local; routing it through the ISA-level parser would be wrong.

5 — manifest instead of header re-parsing. Partly done, and I'd rather be precise about which part. The generator now emits dispatch_surface.manifest.cmake with the extent list and the ISA-level enumerators, and the declaration checker reads that instead of include()-ing the declaration and re-deriving both. The AVX_AVAILABILITY read from distance_core.h stays, for the reason under item 3: it is what derives the mangled digit instead of assuming it.

The SVS_TYPE_PAIRS_ macro parser — line-continuation folding and one-level alias following — is still there, and it is the most fragile thing left. It can't move to the manifest, because the generator doesn't know the count either: the pairs are a C++ preprocessor macro, so cmake would just be parsing the same header from a different file. The right fix is to have the probe print the count under --report, letting the preprocessor do the counting, and read a number instead of a regex. That's a follow-up I'd rather do properly than rush into this PR.

Not taken, with reasons

1 — port the checkers to Python. Declining, on your own escape clause ("if a Python dependency is unacceptable"). SVS's C++ build and test suite have no Python dependency today: no find_package(Python3) anywhere, bindings/python/ is isolated behind its own pyproject.toml and is never add_subdirectory'd, no .py file is invoked by any CTest test, and none of the C++ CI workflows set up Python. Making the C++ test suite need a Python interpreter is a bigger change than the duplication it removes, and 2/4/5 together take out most of what motivated it.

I do agree with the diagnosis, though — emulating dicts via set(prefix_HAVE_${key} ...) was the worst pattern in here, and item 5 is what removes the need for it.

3 — the hardcoded mangling. I tried this and measured it, and it doesn't work. The plan was to match nm -C output nstead, but demangling renders an enum template argument as (svs::distance::AVX_AVAILABILITY)1 — a number, not the numerator name. So the positional digit survives demangling, and 18446744073709551615 survives it too, because svs::Dynamicis aconstexpr size_t` and demangling prints its value. Of the three things you named, only ength-prefixing goes.

That isn't worth the trade: it swaps a dependency on the Itanium mangling, which is a published ABI, for a dependency on the demangler's own rendering, which is a libstdc++ implementation detail and less stable. Worse, the first cut of it deleted the code that reads the enum out of distance_core.h and hardcoded "NONE is 0, so declared levels start at 1" instead — which turns a reordered enum from a hard error into silently wrong matching. Keeping the header parse: it is the only thing that derives the digit rather than assuming it.

So the mangling stays, and I'd rather say that than claim the item is closed. What did improve is that it is now concentrated in one file with the enum read from the source of truth, instead of also being spread across a surface re-parse.

3, second half — "consider whether both checkers are needed." This one I think is wrong. The linkage check compares the probe's references against the archive's definitions, but both are generated from the same header — so a generator that drops an extent drops it from both, and they still agree. The declaration check derives its expectation from three hand-written sources (dispatch-surface.cmake, preprocessor.h, distance_core.h) and is the only thing that catches that class of bug. tests/multi-arch/dispatch-checks/README.md says as much. Reducing it to a count would drop the coverage the check exists for.

4, second half — parallel lists instead of the |-joined encoding. Declining. SVS_ISA_LEVEL_NAMES/_ARCHS/_INFIXES replaces "this row has three fields", which a parser can validate and fail on, with "these three lists are equal length and correspond positionally", which nothing checks and which breaks silently when someone appends to one. The parser keeps the error where the mistake is.

6 — split generate-dispatch-surface.cmake into functions. Largely already done, in #373 (now part of this PR): validation moved out into validate-dispatch-surface.cmake and the generator shrank by ~100 lines. Your point isn't fully answered — that new file is still straight-line — but it has 14 accept/reject fixtures pinning its error messages, and restructuring it for readability alone risks that coverage. On the duplicate foreach: the generator only has one pass over SVS_ISA_LEVELS; the duplication was across files, between it and the validator, which is what item 4 removes. I'd rather leave the rest and let you re-raise it.

7 — obj_name keyed by arch. Already fixed in #375 (now part of this PR): it reads set(obj_name ${infix}_obj). Worth flagging that #375 is exactly the case you predicted — it adds a third level, AVX512_VNNI|cascadelake|vnni, alongside AVX512|skylake-avx512|avx512, so the collision was one shared -march away from being real.

7 — committed generated header. Also already addressed in #373 (now part of this PR). .github/workflows/dispatch-surface.yml has a job that configures and then fails if include/svs/core/distance/dispatch_surface.h comes out with a diff — the staleness gate you asked for — plus a second job asserting that a build with an overridden surface does not rewrite the committed copy. Keeping the committed file itself, for the reason in the inline thread: a fresh checkout or a bare -I include compile has no missing headers.

7 — comment volume in dispatch-surface.cmake. Keeping this one as is, deliberately. That file is the single lever for configuring the surface and is meant to be read more often than edited; the prose-to-code ratio is the point rather than an accident. The other files' comments are within normal range — for the record, the measured figures are 92% comments for dispatch-surface.cmake and 22–28% for the checkers, and multi-arch.cmake is 37%, not over half.

Found while doing this

Not one of your points, but worth flagging since it's in the same file. The validator rejected a duplicate ISA level and a duplicate TU infix but not a duplicate -march, and that one ships a fault: declaring AVX512|cascadelake|avx512 compiles the AVX512 object with VNNI while the AVX512 runtime predicate still admits hosts without it. The instruction checker can't catch it — it looks the budget up by -march, so it checks that object against the cascadelake row, which requires VNNI and forbids nothing, and passes. Configure was silent. There's now a duplicate--march check alongside the other two and a fixture pinning it.

@ahuber21
ahuber21 requested review from rfsaliev and a balanced review from Copilot September 2, 2026 12:47
@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>
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