Found while measuring the reg_flow TU split (#147). It is not caused by that change — it is pre-existing on main and affects the shipped CUDA library.
Claim
In libfastfields-cuda.so, every public entry point that computes
use_32bits = FF_CANUSE32BITS(...) and then dispatches has had the dispatch removed by the compiler. The function validates its arguments and returns without launching anything.
That is all 11 modules in src/lib-cuda/, 78 FF_CANUSE32BITS sites:
distance 19 posdef 10 pushpull 4 pushpull_backward 11 reg_field 11
reg_field_rls 4 reg_flow 12 reg_flow_rls 4 resize 1 restrict 1 splinc 1
src/lib-cpu uses the same macro and is unaffected: under the host compiler FF_CUDEV expands to nothing. That is precisely why the 59,886-check CPU gate has never seen this, and why no CUDA job has either — build-cuda is compile+link only and there is no GPU in CI.
Root cause
include/fastfields/impl/kernels/utils.h:
- line 192-194 — the dynamic-size
typed_prod is inline FF_CUDEV, i.e. __device__ only:
template <typenameOT, typenameIT, typenamesize_t>
inlineFF_CUDEVOTtyped_prod(constIT * x, size_t size)
- line 667-668 —
canUse32BitIndexMath is FF_CUHOST inline (__host__) and calls it:
FF_CUHOSTinlineboolcanUse32BitIndexMath(...)
{
...
int64_t numel = typed_prod<int64_t>(size, ndim);A __host__ function calling a __device__-only function template. nvcc does not reject it, and at -O1/-O2/-O3 the host object silently loses everything in the caller after that call.
prod(const T * x, size_t size) (~line 217) is FF_CUDEV too and forwards to the same typed_prod, so it is in the same class.
Minimal reproducer
Self-contained, ~40 lines, no project headers needed beyond the qualifier macros. refs counts undefined references to the three dispatch arms; 3 is correct, 0 means the switch was deleted.
#include<stdexcept>
#include<cstdint>
#include<limits>
#ifdef BUG
# definePROD_QUAL __device__ // as typed_prod is today
#else
# definePROD_QUAL __host__ __device__ // the fix
#endiftemplate <typenameOT, typenameIT>
PROD_QUALinlineOTmy_prod(constIT * p, int32_t n)
{ OT r = 1; for (int32_t i = 0; i < n; ++i) r *= (OT)p[i]; return r; }
template <classN, classS, classT>
__host__ inlineboolmy_canUse(N ndim, const S * size, const T * stride)
{
int64_t max32 = std::numeric_limits<int32_t>::max();
int64_t numel = my_prod<int64_t>(size, ndim);
if (numel >= max32) returnfalse;
if (numel == 0) return max32 > 0;
if (stride == nullptr) returntrue;
int64_t offset = 0, lin = numel - 1;
for (N i = ndim - 1; i >= 0; --i) { offset += (lin % size[i]) * stride[i]; lin /= size[i]; }
return offset < max32;
}
structTensor { int32_t ndim; int64_t * shape; int64_t * strides; };
namespacens {
__attribute__((visibility("hidden"))) voidf1(Tensor&, bool);
__attribute__((visibility("hidden"))) voidf2(Tensor&, bool);
__attribute__((visibility("hidden"))) voidf3(Tensor&, bool);
}
voidentry(Tensor & t, int ndim)
{
constbool u = my_canUse(t.ndim, t.shape, t.strides);
switch (ndim) {
case1: returnns::f1(t, u);
case2: returnns::f2(t, u);
case3: returnns::f3(t, u);
default: throwstd::invalid_argument("Only 1D, 2D and 3D");
}
}$ for O in 0 1 2 3; do for D in "" "-DBUG"; do
printf "%-6s -O%s refs: " "${D:-ok}" $O
nvcc -std=c++14 -O$O $D -x cu -Xcompiler -fPIC -c -o /tmp/r.o repro.cu 2>/dev/null
nm -u /tmp/r.o | c++filt | grep -c "ns::f"
done; done
ok -O0 refs: 3
-DBUG -O0 refs: 3
ok -O1 refs: 3
-DBUG -O1 refs: 0 <-- dispatch deleted
ok -O2 refs: 3
-DBUG -O2 refs: 0
ok -O3 refs: 3
-DBUG -O3 refs: 0
g++ compiling the same source directly keeps all 3 at every -O. It is the -x cu path.
nvcc 12.0.140 (nvidia-cuda-toolkit from Ubuntu apt — the exact package build-cuda installs).
Confirmation on real, unmodified code
src/lib-cuda/splinc.cpp at 85fdac7, compiled with the shipping flags and CI's -O1. DISPATCH_SPLINC computes use_32bits = FF_CANUSE32BITS(inp_out) and then switches on dtype/npoles/bound. Checking for the string literals of the throws on each side of that line:
| string literal | position | shipped | use_32bits = false | typed_prod fixed |
|---|
Unsupported spline order | before | 1 | 1 | 1 |
Unsupported npoles | after | 0 | 1 | 1 |
only floating point ... | after | 0 | 1 | 1 |
Everything after the FF_CANUSE32BITS line is gone from splinc.o, and comes back when either that one expression is neutralised or typed_prod is made host+device. The 360 ff::cuda::splinc:: template instantiations are still in the object — front-end instantiation happens regardless — so the object stays ~8 MB and the build looks entirely healthy.
Why nothing caught it
--no-undefined and the ldd -r step both pass: the deleted calls were intra-TU, so nothing became undefined.build-cuda is compile+link only.- The per-module
FFMEM budget is unaffected — the instantiations are still there. - The CPU suite cannot see it:
FF_CUDEV is empty under the host compiler.
Fix
One word, verified on the reproducer and on real splinc.cpp:
template <typename OT, typename IT, typename size_t>
-inline FF_CUDEV+inline FF_CUHOSTDEV
OT typed_prod(const IT * x, size_t size)
prod(const T *, size_t) should very likely go with it, and the rest of impl/kernels/ is worth auditing for other FF_CUHOST → FF_CUDEV call edges — that direction is the dangerous one, since nvcc does not diagnose it here.
Two things worth deciding alongside the fix, because the fix alone leaves the same trap set:
- A gate. The failure is invisible to every current job. The cheapest real check is a compile-time one: assert that a chosen
ff::cuda:: entry point still references its dispatch (e.g. nm -u on one object, or a linker check against a stub). Anything that would have gone red here. -Werror-style handling for nvcc's host/device call diagnostics, if it can be made to emit one for this case at all.
Scope note
I have deliberately not included this in #147 — it is a correctness fix in impl/kernels/, which triggers the whole CI matrix and overlaps #145/#146, and it deserves its own review rather than riding along with a compile-time perf change. #147's measurements are unaffected either way: the slices instantiate exactly what they are meant to instantiate, and the peak-RSS and CPU figures are the same with or without the fix.
Found while measuring the
reg_flowTU split (#147). It is not caused by that change — it is pre-existing onmainand affects the shipped CUDA library.Claim
In
libfastfields-cuda.so, every public entry point that computesuse_32bits = FF_CANUSE32BITS(...)and then dispatches has had the dispatch removed by the compiler. The function validates its arguments and returns without launching anything.That is all 11 modules in
src/lib-cuda/, 78FF_CANUSE32BITSsites:src/lib-cpuuses the same macro and is unaffected: under the host compilerFF_CUDEVexpands to nothing. That is precisely why the 59,886-check CPU gate has never seen this, and why no CUDA job has either —build-cudais compile+link only and there is no GPU in CI.Root cause
include/fastfields/impl/kernels/utils.h:typed_prodisinline FF_CUDEV, i.e.__device__only:canUse32BitIndexMathisFF_CUHOST inline(__host__) and calls it:A
__host__function calling a__device__-only function template. nvcc does not reject it, and at-O1/-O2/-O3the host object silently loses everything in the caller after that call.prod(const T * x, size_t size)(~line 217) isFF_CUDEVtoo and forwards to the sametyped_prod, so it is in the same class.Minimal reproducer
Self-contained, ~40 lines, no project headers needed beyond the qualifier macros.
refscounts undefined references to the three dispatch arms; 3 is correct, 0 means the switch was deleted.g++compiling the same source directly keeps all 3 at every-O. It is the-x cupath.nvcc 12.0.140 (
nvidia-cuda-toolkitfrom Ubuntu apt — the exact packagebuild-cudainstalls).Confirmation on real, unmodified code
src/lib-cuda/splinc.cppat85fdac7, compiled with the shipping flags and CI's-O1.DISPATCH_SPLINCcomputesuse_32bits = FF_CANUSE32BITS(inp_out)and then switches on dtype/npoles/bound. Checking for the string literals of thethrows on each side of that line:use_32bits = falsetyped_prodfixedUnsupported spline orderUnsupported npolesonly floating point ...Everything after the
FF_CANUSE32BITSline is gone fromsplinc.o, and comes back when either that one expression is neutralised ortyped_prodis made host+device. The 360ff::cuda::splinc::template instantiations are still in the object — front-end instantiation happens regardless — so the object stays ~8 MB and the build looks entirely healthy.Why nothing caught it
--no-undefinedand theldd -rstep both pass: the deleted calls were intra-TU, so nothing became undefined.build-cudais compile+link only.FFMEMbudget is unaffected — the instantiations are still there.FF_CUDEVis empty under the host compiler.Fix
One word, verified on the reproducer and on real
splinc.cpp:prod(const T *, size_t)should very likely go with it, and the rest ofimpl/kernels/is worth auditing for otherFF_CUHOST→FF_CUDEVcall edges — that direction is the dangerous one, since nvcc does not diagnose it here.Two things worth deciding alongside the fix, because the fix alone leaves the same trap set:
ff::cuda::entry point still references its dispatch (e.g.nm -uon one object, or a linker check against a stub). Anything that would have gone red here.-Werror-style handling for nvcc's host/device call diagnostics, if it can be made to emit one for this case at all.Scope note
I have deliberately not included this in #147 — it is a correctness fix in
impl/kernels/, which triggers the whole CI matrix and overlaps #145/#146, and it deserves its own review rather than riding along with a compile-time perf change. #147's measurements are unaffected either way: the slices instantiate exactly what they are meant to instantiate, and the peak-RSS and CPU figures are the same with or without the fix.