Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -648,6 +648,21 @@ jobs:
${{ matrix.indexflags && format('INDEXFLAGS="{0}"', matrix.indexflags) || '' }} \
NVCC="/usr/bin/time -f 'FFMEM %M kB %e s %C' nvcc" \
2>&1 | tee /tmp/build-cuda.log
- name: No host-pass exit(1) stubs in the CUDA objects
# fastfields-lib#150. nvcc diagnoses a host->device call only when the
# calling function is not a template; every host caller here is one, so
# instead of an error cudafe++ writes `::exit(1)` into the HOST object
# in place of the __device__-only callee's body, and -O1 then deletes
# everything after the call. Nothing else in this job can see that: the
# damage is intra-TU, so --no-undefined and `ldd -r` both pass, the
# objects keep their full size (front-end instantiation is unaffected),
# and the FFMEM budget below does not move either.
#
# An undefined `exit` in one of these objects has exactly one source --
# that stub. Nothing in the tree calls `::exit`.
#
# Runs before the hub link so that a red here is unambiguous.
run: ./tools/check-cuda-host-stubs.sh build/obj/lib-cuda/*.o
- name: Link the hub against the CUDA backend
# The actual --no-undefined gate: libfastfields.so is what references
# the FF_CUDA:: entry points, so this is where a module missing from
Expand Down
15 changes: 15 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,6 +184,21 @@ pushpull's fully-static order×bound compile is nightly
table lives above `MODULES` in `src/lib-cuda/Makefile`; read it before
changing `-j`, `-O`, the bound/spline policy, or anything that makes a
regulariser heavier. Do not recombine or "tidy" the split.
- **`FF_CUDEV` means "cannot run on the host", not "called from a kernel".**
nvcc diagnoses a `__host__` → `__device__` call **only when the calling
function is not itself a template**; every host caller in this tree is one
(the `FF_CUHOST` launchers in `impl/cuda/`, `canUse32BitIndexMath`, mesh.h's
`build_tree`). In that case it emits no diagnostic at all and `cudafe++`
replaces the callee's body *in the host object* with `::exit(1)` — which
links cleanly, passes `--no-undefined` and `ldd -r`, terminates the process
at the first call, and (because `exit` is `noreturn`) makes `-O1` delete
every statement after it. That shipped in `libfastfields-cuda.so` for months
with every job green; see fastfields-lib#150 and the qualifier table in
`MIGRATION.md`. **Everything in `impl/kernels/utils.h` is `FF_CUHOSTDEV` and
must stay that way**; two gates enforce it, the compile-time
`tests/impl-cuda/compile_probe_hostdev.cu` and the object-level
`tools/check-cuda-host-stubs.sh` in `build-cuda`. `FF_CUHOSTDEV` is the safe
default: identical device codegen, one extra inline host function.
- **Macros in installed headers are `FF_`-prefixed.** Anything `#define`d under
`include/` and not `#undef`'d before the end of that header is inherited by
every downstream translation unit, so it must be namespaced by prefix. Macros
Expand Down
60 changes: 60 additions & 0 deletions MIGRATION.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -231,6 +231,66 @@ per call, while **363 of the 391** `impl/cuda` upload sites use the
enable, and its cost may partly offset the register-pressure win. Measured
numbers and the exact benchmark that would settle it: #94 and #144.

## `__host__` / `__device__` qualifiers under nvcc (#150)

**nvcc checks a host→device call only when the calling function is not itself
a template.** That single sentence is the whole hazard, and it is not
documented anywhere in the CUDA guide as a limitation.

| caller | callee | nvcc 12.0 |
| --- | --- | --- |
| plain `__host__` function | `__device__` function | **error** |
| plain `__host__` function | `__device__` function *template* | **error** |
| `__host__` function *template* | `__device__` function | **accepted silently** |
| `__host__` function *template* | `__device__` function *template* | **accepted silently** |

In the two bottom rows nvcc does not diagnose anything. `cudafe++` instead
emits, into the **host** object, this in place of the callee's real body (the
real one is kept next to it under `#if 0`):

```c
{int volatile ___ = 1; (void)args; ::exit(___);}
```

So the caller compiles, links, passes `-Wl,--no-undefined` and `ldd -r` — the
damage is intra-TU, nothing becomes undefined — and **terminates the process
with status 1** the first time it runs. `exit` is `noreturn`, so from `-O1`
upwards the host compiler additionally deletes every statement after the call
as unreachable. `-O0` keeps those statements; it is not any less broken, the
process still exits.

Every host caller in this tree is a template, which is why this went unseen:

* `impl/kernels/utils.h`'s `canUse32BitIndexMath` → `typed_prod`, the edge
behind every `FF_CANUSE32BITS` in `src/lib-cuda`;
* every `FF_CUHOST` launcher in `impl/cuda/{reg_field,reg_flow,
distance_euclidean,distance_l1,distance_mesh}.h` → `prod(size, n)`, on its
first line, to size the grid;
* `impl/kernels/distance/mesh.h`'s `FF_CUHOST build_tree` → `max`.

**The rule.** Anything in `impl/kernels/utils.h` is `FF_CUHOSTDEV`, without
exception — the header holds only backend-agnostic helpers over scalars and
raw arrays, and host code calls them. More generally, `FF_CUDEV` means "this
genuinely cannot run on the host" (a device intrinsic, `__shared__`,
`threadIdx`), not "this is called from a kernel". When in doubt, `FF_CUHOSTDEV`
costs nothing: device codegen is identical and the host copy is one inline
function.

**The two gates**, added with the fix:

1. `tests/impl-cuda/compile_probe_hostdev.cu` — calls each helper from a
plain, non-template `__host__` function, i.e. the shape in the top two rows
of the table, so a re-qualified helper is an nvcc **error** again. It also
explicitly instantiates one representative launcher per affected header,
which type-checks the whole launcher body (an explicit instantiation
instantiates it in the device pass, where the call edge *is* reported).
Compiles in ~2 s in `compile-probe-cuda`; against the pre-fix header it
produces 22 errors.
2. `tools/check-cuda-host-stubs.sh` — run in `build-cuda` over
`build/obj/lib-cuda/*.o`, fails on any undefined `exit`. Nothing in this
codebase calls `::exit`, so that symbol has exactly one source: the stub
above. This is the catch-all for edges the probe does not enumerate.

## Porting pattern (per module)

Use `distance.{h,cpp}` at each level as the template.
Expand Down
84 changes: 56 additions & 28 deletions include/fastfields/impl/kernels/utils.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,34 @@
FF_NAMESPACE_BEGIN(FF_NS)
FF_NAMESPACE_BEGIN(FF_DEVICE)

// ============================================================================
// EVERY function in this header is FF_CUHOSTDEV (`__host__ __device__`), and
// that is a contract, not an accident. Do not "tighten" one back to FF_CUDEV.
//
// Nothing here is device-specific -- these are small generic helpers over
// scalars and raw arrays -- and host code genuinely calls them:
// * `canUse32BitIndexMath` (below) calls `typed_prod`;
// * every FF_CUHOST launcher in impl/cuda/{reg_field,reg_flow,distance_*}.h
// sizes its grid with `prod(size, n)`;
// * `distance/mesh.h`'s FF_CUHOST `build_tree` calls `max`.
//
// When one of these was `__device__`-only (fastfields-lib#150), nvcc did NOT
// reject those calls. It diagnoses a host->device call only when the CALLER
// is a non-template function; every caller above is a template, so the check
// was skipped and cudafe++ emitted, into the HOST object, a body of
//
// {int volatile ___ = 1; (void)args; ::exit(___);}
//
// in place of the real one. The callers therefore compiled, linked, and
// passed `--no-undefined` and `ldd -r`, and terminated the process with
// status 1 at the first call -- at -O1 and above the host compiler also
// deleted everything after it, because `exit` is `noreturn`.
//
// tests/impl-cuda/compile_probe_hostdev.cu turns that silence back into a
// compile error: it calls each helper from a plain (non-template) host
// function, which is the shape nvcc does diagnose.
// ============================================================================

// static check for floating types
template <typename T>
struct is_floating_point { static constexpr bool value = false; };
Expand All@@ -29,14 +57,14 @@ struct is_floating_point<half> { static constexpr bool value = true; };


template <typename T>
inline FF_CUDEV
inline FF_CUHOSTDEV
void swap(T& a, T& b)
{
T c(a); a=b; b=c;
}

template <typename T>
inline FF_CUDEV
inline FF_CUHOSTDEV
T square(T a)
{
return a*a;
Expand All@@ -45,26 +73,26 @@ T square(T a)
#ifdef __CUDACC__

template <typename T>
inline FF_CUDEV
inline FF_CUHOSTDEV
T sqrt(T a)
{}

template <>
inline FF_CUDEV
inline FF_CUHOSTDEV
float sqrt(float a)
{
return ::sqrtf(a);
}

template <>
inline FF_CUDEV
inline FF_CUHOSTDEV
double sqrt(double a)
{
return ::sqrt(a);
}

template <>
inline FF_CUDEV
inline FF_CUHOSTDEV
half sqrt(half a)
{
// hsqrt is not visible at global scope in every CUDA/arch combination;
Expand All@@ -75,7 +103,7 @@ half sqrt(half a)
#else

template <typename T>
inline FF_CUDEV
inline FF_CUHOSTDEV
T sqrt(T a)
{
return std::sqrt(a);
Expand All@@ -85,7 +113,7 @@ T sqrt(T a)


template <int N, typename T>
inline FF_CUDEV
inline FF_CUHOSTDEV
T pow(T a) {
T p = a;
# pragma unroll
Expand All@@ -95,7 +123,7 @@ T pow(T a) {
}

template <typename T>
inline FF_CUDEV
inline FF_CUHOSTDEV
T pow(T a, int N) {
T p = a;
# pragma unroll
Expand All@@ -105,36 +133,36 @@ T pow(T a, int N) {
}

template <typename T>
inline FF_CUDEV
inline FF_CUHOSTDEV
T min(T a, T b)
{
return (a < b ? a : b);
}

template <typename T>
inline FF_CUDEV
inline FF_CUHOSTDEV
T max(T a, T b)
{
return (a > b ? a : b);
}

template <typename T>
inline FF_CUDEV
inline FF_CUHOSTDEV
T abs(T a)
{
return static_cast<T>(a < 0 ? -a : a);
}

template <typename T>
inline FF_CUDEV
inline FF_CUHOSTDEV
signed char sign(T a)
{
return static_cast<signed char>(a == 0 ? 0 : a < 0 ? -1 : 1);
}

#ifdef __CUDACC__
template <>
inline FF_CUDEV
inline FF_CUHOSTDEV
half min<>(half a, half b)
{
// Compare via float: half has multiple implicit conversions to built-in
Expand All@@ -144,7 +172,7 @@ half min<>(half a, half b)
return (af < bf ? a : b);
}
template <>
inline FF_CUDEV
inline FF_CUHOSTDEV
half max<>(half a, half b)
{
float af = static_cast<float>(a);
Expand All@@ -159,7 +187,7 @@ template <typename T, typename U,
bool is_float_U = is_floating_point<U>::value >
struct _mod
{
inline FF_CUDEV static
inline FF_CUHOSTDEV static
T f(T x, U d)
{
signed char sx = sign(x);
Expand All@@ -174,22 +202,22 @@ struct _mod
template <typename T, typename U>
struct _mod<T, U, false, false>
{
inline FF_CUDEV static
inline FF_CUHOSTDEV static
T f(T x, U d)
{
return x % d;
}
};

template <typename T, typename U>
inline FF_CUDEV
inline FF_CUHOSTDEV
T mod(T x, U d)
{
return _mod<T,U>::f(x, d);
}

template <typename OT, typename IT, typename size_t>
inline FF_CUDEV
inline FF_CUHOSTDEV
OT typed_prod(const IT * x, size_t size)
{
if (size == 0)
Expand All@@ -201,7 +229,7 @@ OT typed_prod(const IT * x, size_t size)
}

template <typename OT, unsigned long size, typename IT>
inline FF_CUDEV
inline FF_CUHOSTDEV
OT typed_prod(const IT * x)
{
if (size == 0)
Expand All@@ -214,21 +242,21 @@ OT typed_prod(const IT * x)
}

template <typename T, typename size_t>
inline FF_CUDEV
inline FF_CUHOSTDEV
T prod(const T * x, size_t size)
{
return typed_prod<T>(x, size);
}

template <unsigned long size, typename T>
inline FF_CUDEV
inline FF_CUHOSTDEV
T prod(const T * x)
{
return typed_prod<T, size>(x);
}

template <int N, typename U, typename V>
inline FF_CUDEV
inline FF_CUHOSTDEV
void fillfrom(U out[N], const V * inp)
{
# pragma unroll
Expand All@@ -237,7 +265,7 @@ void fillfrom(U out[N], const V * inp)
}

template <int N, typename U, typename V, typename W>
inline FF_CUDEV
inline FF_CUHOSTDEV
void fillfrom(U out[N], const V * inp, W stride)
{
# pragma unroll
Expand All@@ -246,23 +274,23 @@ void fillfrom(U out[N], const V * inp, W stride)
}

template <typename U, typename V>
inline FF_CUDEV
inline FF_CUHOSTDEV
void fillfrom(int N, U out[], const V * inp)
{
for (int n=0; n < N; ++ n)
out[n] = static_cast<U>(inp[n]);
}

template <typename U, typename V, typename W>
inline FF_CUDEV
inline FF_CUHOSTDEV
void fillfrom(int N, U out[], const V * inp, W stride)
{
for (int n=0; n < N; ++n, inp += stride)
out[n] = static_cast<U>(*inp);
}

template <int N, typename U, typename V>
inline FF_CUDEV
inline FF_CUHOSTDEV
void fill(U * out, V inp)
{
auto val = static_cast<U>(inp);
Expand All@@ -272,7 +300,7 @@ void fill(U * out, V inp)
}

template <int N, typename U, typename V, typename W>
inline FF_CUDEV
inline FF_CUHOSTDEV
void fill(U * out, V inp, W stride)
{
auto val = static_cast<U>(inp);
Expand Down
Loading
Loading