refactor(dispatch): Generate the dispatch surface from a single declaration - #372
refactor(dispatch): Generate the dispatch surface from a single declaration#372ahuber21 wants to merge 7 commits into
Conversation
|
@copilot new or modified files should have |
c2d57c2 to
fe66e35
Compare
|
Tick the box to add this pull request to the merge queue (same as
|
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>
dd2db8d to
5fafe1d
Compare
There was a problem hiding this comment.
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.
| "x86-64||ymm zmm mask vnni" | ||
| "haswell|ymm|zmm mask vnni" | ||
| "skylake-avx512|zmm|vnni" | ||
| "cascadelake|zmm|" |
There was a problem hiding this comment.
Will be fixed in 375 -- and all PRs will be merged together as a stack.
rfsaliev
left a comment
There was a problem hiding this comment.
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 @@ | |||
| /* | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 @@ | |||
| <!-- | |||
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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.
| # Distance kernels, and nothing else in the archive. Mangled names are used | ||
| # throughout: demangled ones carry `[clone .isra.0]` suffixes that differ between | ||
| # a local instantiation and an explicit one. | ||
| set(svs_kernel_regex "_ZN3svs8distance.*Impl") |
There was a problem hiding this comment.
Mangling rules are compiler-specific.
Are you sure that the pattern will work for all supported compilers/versions?
There was a problem hiding this comment.
It's a concern I raised with the agent. Don't have the exact reply, but it was along the lines of:
- This regex will work for everything we currently use
- Checking our own emitted mangled symbols gives us more visibility/control over what we release. A change here is probably not intentional.
The second point sounded quite reasonable to me. So I figured it's worth giving this a shot. If we find the maintenance cost is too high, we can improve the tests.
From my understand though, these symbols should have been stable for the last few months and I don't see any changes on the horizon.
|
@copilot, review the PR from readability/maintainability perspective, focus on cmake scripts and suggest changes to make scripts clearer and shorter, propose replacing complicated cmake code blocks with external tools if any or 'probing' sources for existing validation scripts. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Review: readability & maintainability of the CMake scriptsThe single-declaration → generated-surface design is sound, but the implementation reimplements a lot of general-purpose logic — C++ name demangling, header parsing, symbol tallying, list truncation, argument validation — in raw 1. Move the checkers out of CMake into one Python script (biggest win)
All of this is a handful of lines each in Python with 2. Extract the duplicated checker helpers into one shared moduleIf the scripts stay in CMake, factor the repeats into
# cmake/dispatch-checks/lib.cmake
include_guard(GLOBAL)
function(svs_require) # svs_require(SVS_NM SVS_ARCHIVE ...)
foreach(var IN LISTS ARGN)
if(NOT ${var})
message(FATAL_ERROR "${var} is not set.")
endif()
endforeach()
endfunction()
function(svs_require_files) # svs_require_files(SVS_ARCHIVE SVS_CONSUMER_OBJECT)
foreach(var IN LISTS ARGN)
if(NOT EXISTS "${${var}}")
message(FATAL_ERROR "${var} does not exist: ${${var}}")
endif()
endforeach()
endfunction()
# Mangled names of symbols in <file> matching <regex>.
function(svs_nm_symbols out_var file regex)
cmake_parse_arguments(arg "" "" "NM_ARGS" ${ARGN})
execute_process(COMMAND "${SVS_NM}" ${arg_NM_ARGS} "${file}"
OUTPUT_VARIABLE raw ERROR_VARIABLE err RESULT_VARIABLE status)
if(NOT status EQUAL 0)
message(FATAL_ERROR "${SVS_NM} failed on ${file}: ${err}")
endif()
set(symbols "")
string(REPLACE "\n" ";" lines "${raw}")
foreach(line IN LISTS lines)
if(line MATCHES "${regex}")
string(REGEX MATCH "[^ \t]+$" symbol "${line}")
list(APPEND symbols "${symbol}")
endif()
endforeach()
list(REMOVE_DUPLICATES symbols)
set(${out_var} "${symbols}" PARENT_SCOPE)
endfunction()
function(svs_report_first list_var limit)
set(shown ${${list_var}})
list(LENGTH shown count)
if(count GREATER limit)
list(SUBLIST shown 0 ${limit} shown)
endif()
foreach(entry IN LISTS shown)
message(" ${entry}")
endforeach()
if(count GREATER limit)
math(EXPR rest "${count} - ${limit}")
message(" ... and ${rest} more")
endif()
endfunction()That removes ~120 duplicated lines and gives one definition of "read symbols from a file." 3. Stop reconstructing mangled names in
|
The set of distance kernels compiled ahead of time -- extents x ISA levels -- was
written out by hand everywhere it was needed: three
extern templateblocks,two per-architecture translation units, and
supported_dim_list. Adding anextent meant editing six lists in agreement, and the preceding commit is what
happens when one of them disagrees.
cmake/dispatch-surface.cmakeis now the only place the extent list and the ISAlevels are written down. Everything else is derived from it:
include/svs/core/distance/dispatch_surface.h, which drives everyextern template, every explicit instantiation, andsupported_dim_listinstruction budget it is compiled at
The generated header is committed as well as generated, so that consuming the
headers with a bare
-I includeand no CMake keeps working.Configure now prints the surface in full rather than a count -- every extent,
and for each level its enumerator, its instruction budget and its translation
unit:
AVX_AVAILABILITY::NONEis a real hole and is now named as one: the entry pointsfall back to it, no translation unit compiles it, so every consumer builds those
kernels itself at whatever
-marchit happens to use. A baseline translationunit is its own change, not this one.
Seven
ctesttests replace what was a paragraph ofnmcommands in a reviewcomment. All of them derive their expectations from the declaration, so they hold
for an overridden surface too:
dispatch_surface_probe,dispatch_entry_probe-- consumers built at-march=x86-64. One names every kernel in the surface directly, the otherreaches them through the entry points; a declared-but-uninstantiated kernel is
an undefined symbol in the first, and a kernel the entry points cannot reach is
one in the second.
dispatch_surface_linkage-- the archive defines every kernel the probe names,nothing more, and the probe instantiated none of them itself: 864 kernels.
dispatch_surface_declaration-- the same 864, but counted from the threehand-written sources (the extent list, the type-pair macros, the enumerator
order) without consulting the generated header, so a generator bug has nowhere
to hide: 2 levels x 9 extents x 3 distances x {AVX2: 16, AVX512: 16} pairs.
dispatch_instructions_avx2,dispatch_instructions_avx512-- one per level,disassembling that level's object file against the instruction budget its
runtime predicate promises. The AVX2 object must contain
ymmand nozmm,mask or VNNI encodings; a level that emits what its predicate does not
guarantee is an illegal-instruction fault on a host the dispatcher considers
supported, which no symbol-table check can see.
dispatch_surface_execution-- the only check that observes a run rather thana symbol table:
gdbbreaks on every level's kernel for one extent and reportswhich one a call through the entry points actually enters. A specialization
that disappears behind an
#ifstill links and still counts.No behaviour change: the same 864 kernels, at the same instruction budgets, with
the same mangled names.
compute()is stillcompute(); nothing about the callchain or the runtime dispatch moves.
Part 2 of 4 of the ISA dispatching v2 milestone.