Skip to content

Move vtable function pointers into the generic address space - #1376

Draft
pvelesko wants to merge 5 commits into
mainfrom
vtable-generic-as
Draft

Move vtable function pointers into the generic address space#1376
pvelesko wants to merge 5 commits into
mainfrom
vtable-generic-as

Conversation

@pvelesko

@pvelesko pvelesko commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Adds an LLVM pass that moves C++ vtable function pointer slots into the generic address space, the only address space SPIR-V permits function pointers in, and repairs the slot loads and indirect call sites that read them.

Also folds in the three follow-ups that make it usable on real C++: constant GEPs are rebuilt when a vtable is retyped (classes with virtual bases), unreferenced vtable globals are dropped before translation, and __cxa_pure_virtual is defined on the device.

Closes #1373
Fixes #1381
Fixes #1382

pvelesko added 2 commits July 28, 2026 12:25
Clang emits C++ vtables for SPIR-V targets with every slot in the global
address space, so each virtual function slot is a constant expression

  addrspacecast (ptr @_ZN7DerivedD2Ev to ptr addrspace(1))

SPIR-V only permits casts from a named storage class into Generic, never
into another named one, so any translation unit that dispatches virtually
on the device was rejected by the translator with

  InvalidModule: Invalid SPIR-V module: Casts from private/local/global
  address space are allowed only to generic

HipFunctionPointerASPass retypes such tables so their slots hold generic
pointers, then repairs the slot loads and the indirect call sites that
read them. The vptr stored inside the object is an ordinary data pointer
and stays in the global address space. Non function slots such as the
RTTI descriptor move to generic as well, which is the legal global to
generic direction.

The trigger is semantic rather than the Itanium _ZTV name prefix: any
global whose initializer casts a Function into a non generic address
space. That also covers hand written dispatch tables.

SPV_INTEL_function_pointers is already requested by the driver and the
translator only emits the OpExtension when the module actually contains
function pointers, so modules without virtual dispatch are unaffected on
targets lacking the extension.

Refs: #1373
Two parts, because executing an indirect call needs
SPV_INTEL_function_pointers and only Intel's stack implements it today.

tests/compiler is compile only and runs on every target. -c still performs
the device link that translates to SPIR-V, which is exactly where the
unfixed compiler aborted, so it catches a regression of the pass without
launching anything. Verified against a pre-fix install: it fails there
with the original address space diagnostic and passes with the fix.

tests/runtime additionally executes the dispatch and checks the values
numerically, exercising both a base pointer whose dynamic type comes from
a runtime value and one round tripped through memory. It gates itself on
device capability and reports the ctest skip code otherwise.

Support cannot be probed: Intel accepts such modules without advertising
cl_intel_function_pointers, rusticl rejects them only once the device JIT
runs, and PoCL neither runs nor rejects them, it hangs. Since the hang
rules out deciding from a failed build, the gate allow-lists the stacks
known to implement the extension, mirroring the isIntelGPU idiom the
OpenCL backend already uses. Requiring a GPU as well as an Intel vendor
matters: an Intel OpenCL CPU device reports the same vendor string.

Refs: #1373
@pvelesko
pvelesko force-pushed the vtable-generic-as branch from 42ccee9 to 283957b Compare July 28, 2026 10:03
pvelesko added 3 commits July 28, 2026 16:13
A constant getelementptr carries its source element type explicitly, so the
replaceAllUsesWith in replaceGlobalInitializer left GEPs claiming the old
value type while pointing at the retyped global.

Classes with virtual bases are where this shows: the VTT (_ZTT...) holds one
'getelementptr inrange(...) ({...}, ptr @_ZTC...)' per construction vtable,
and once a _ZTC table moves its slots to the generic address space the
SPIR-V writer aborts on the disagreement.

Rebuild those GEPs against the new value type, preserving indices, nowrap
flags and inrange.

Refs: #1373
Clang emits the vtable, VTT, construction vtables and RTTI of every
polymorphic class that merely appears in device code, even when no device
code dispatches through them. Trilinos is full of such classes:
Tpetra::RowMatrix is host-only polymorphic with virtual bases, so each HIP
explicit instantiation carries a dead _ZTV/_ZTT/_ZTC set whose slots are
almost all null and whose virtual base offsets are inttoptr constants.

Translating those aborts llvm-spirv:

  SPIRVWriter.cpp:1359: transConstantUse: Assertion
  '(C->getType()->isPointerTy() || ExpectedType->isTypeUntypedPointerKHR())
   && "Only pointer type mismatches should be possible"' failed.

which took out Tpetra_RowMatrix_{INT_INT,DOUBLE_INT,LONG_LONG_INT}_LONG_LONG_HIP
and several Xpetra/Galeri instantiations. GlobalDCE leaves them alone because
they sit in comdats.

A chipStar device module is self-contained, so nothing outside it can link
against these symbols and dropping the unreferenced ones is safe. Only the
Itanium vtable prefixes are matched, so runtime visible globals such as
__chip_var_* are untouched. Iterate to a fixed point: erasing a VTT releases
the construction vtables it pointed at.

Refs: #1373
Clang emits a reference to __cxa_pure_virtual in the vtable of every class
that still has an unoverridden pure virtual member. Nothing defined that
symbol on the SPIR-V platform, so the consumer rejected the whole module

  error : unresolved external symbol symbol #__cxa_pure_virtual
          at offset 56 in data segment #GLOBAL_VARIABLES

and every kernel in the program silently failed to build. Launches kept
reporting hipSuccess while results stayed at their initial values.

Define it next to __assert_fail with the same semantics ROCm's device libs
use: reaching a pure virtual slot is a hard error, so report and abort.

The check has to run, not just compile: the symbol resolves at the device
module build inside the driver, so a compile-only test passes either way.
TestDeviceVirtualDispatch.hip already carries the indirect-call capability
gate, so the abstract-base case goes there rather than duplicating it.

Fixes #1381
@pvelesko

Copy link
Copy Markdown
Collaborator Author

This branch as pushed regresses Trilinos STK from 8/14 to 7/14 on Arc B570: CL_OUT_OF_RESOURCES in desul::Impl::init_lock_arrays_hip, and stk_coupling goes from passing to a timeout. Do not merge as-is.

Folding in the two commits that fix it, a constant GEP repair for retyped vtables and dropping unreferenced vtable globals (#1382), plus the device __cxa_pure_virtual definition (#1381). With those, STK is at 13/14.

@pvelesko

Copy link
Copy Markdown
Collaborator Author

Necessary but not sufficient for RDC device virtual dispatch: with this applied, a -fgpu-rdc kynema-ugf build still leaves 40 _ZTV vtables and 18 virtual member bodies unresolved and the module build fails with ZE_RESULT_ERROR_INVALID_MODULE_UNLINKED. Filed as #1401 so this does not read as device virtual dispatch being finished.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant