Uh oh!
There was an error while loading. Please reload this page.
cuda(mesh): add the sdt_naive host launcher; zero the vertex normals - #86
Conversation
Two of the four `distance_mesh::dt` branches on the CUDA side had a `__global__` kernel but no `CUHOST` launcher, so `dt` threw for them. This adds the launcher for the signed/naive one and fixes a defect in the already-dispatched signed/tree one. sdt_naive launcher (new) ------------------------ Structural mirror of cpu-impl's `sdt_naive` + `build_sdt_naive`: gather the mesh into contiguous device buffers (honouring the caller's real input strides), copy it to the host, build the face/vertex/edge pseudonormals with the shared kernels builder, upload them, size the grid from the batch *element* count and launch `sdt_naive_kernel` on the caller's stream. No BVH, no POD tree mirror, no per-lane traversal trace -- none of that exists on the brute-force path. `sdt_naive_kernel` gains `nearest_vertex` / `stride_nearest`, which `build_sdt_naive` has and it did not; without them the CUDA naive branch could not fill an output the CPU branch fills. Both are guarded on the pointer, exactly as `sdt_kernel` does, because `dt_mesh` leaves them null by default. Vertex-normal zeroing (bug fix in the existing `sdt`) ---------------------------------------------------- `MeshDistUtil::build_normals` ACCUMULATES into the vertex normals (`normvertices[v].add_(normal)` per incident face, normalised at the end) -- which is why cpu-impl allocates them with `new scalar_t[...]()`. The CUDA launcher used `allocHost` (`cudaMallocHost`), which does not zero, so the pseudonormals were accumulated on top of uninitialised pinned memory. That does not fail loudly: it perturbs the vertex/edge pseudonormals, i.e. the *sign* of the returned distance near vertices and edges. Face and edge normals are assigned rather than accumulated, so only this buffer needed it. Precomputed-tree `sdt` overload: still a throw, with the reason spelled out -------------------------------------------------------------------------- Deliberately not implemented. Its `const void * tree` cannot express what `sdt_kernel` requires (a *device* array of the POD `DeviceNode` mirror, not the polymorphic host `Node`), nothing in the tree produces such an array, the `treetrace` buffer's required size depends on a launch configuration only the launcher knows, and its `faces` must be the BVH-sorted order. The comment now states each of those and the two ways out (give it a real signature plus an in-repo caller by making `sdt` delegate to it, as cpu-impl's `sdt`/`build_sdt` split does; or drop it). Making that call unilaterally was out of scope. Validation ---------- Compile+link only, which is the standing bar for this layer -- there is no GPU in CI. `tests/impl-cuda/compile_probe_mesh.cu` now instantiates `sdt_naive` directly for all four (dim x dtype) combinations the dispatcher can select, plus the null-`nearest_vertex` shape; `dt`'s signed+naive branch was already probed and now resolves to a real launcher instead of a throw. The runtime behaviour of both signed launchers -- the tree walk, the stream plumbing, the normal buffers -- remains unexecuted and unverified. Refs #5 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016AjQcY78NgbagPSbPJRr6Z
balbasty
commented
Aug 19, 2026
CI status
The CPU gate was run locally instead, since the path filter (correctly) skips it here: The first
|
Uh oh!
There was an error while loading. Please reload this page.
distance_mesh.h conflicted with the sdt_naive launcher and vertex-normal fix from #86. Resolved by taking main's version wholesale and re-running tools/rename-macros.py over the tree rather than hand-editing: the script is idempotent and self-verifying, so its clean --check ('0 file(s) would change', include/ clean) is the evidence the rename is complete on the new base.
Refs #5. Does not close it — the acceptance bar there is real-hardware
validation, and nothing in this PR has ever been executed (see "What remains
unvalidated" below).
What was verified first
Both gaps named in #5's residual list are still present on
main(
include/fastfields/impl/cuda/distance_mesh.h):sdtoverload ends inthrow std::logic_error("distance_mesh::sdt (precomputed tree) not implemented");sdt_naive_kernelexists as a__global__but has noCUHOSTlauncher, sodt's signed+naive branch threw.1.
sdt_naivehost launcher (new)Structural mirror of cpu-impl's
sdt_naive+build_sdt_naive(
include/fastfields/impl/cpu/distance_mesh.h): gather the mesh intocontiguous device buffers (honouring the caller's real input strides), copy to
host, build the face/vertex/edge pseudonormals with the shared kernels
builder, upload, size the grid from
prod(size, nbatch)— the batch elementcount, not the rank — and launch
sdt_naive_kernelon the caller's stream.No new mathematics: the per-element work is
MeshDist::signed_dist_naivefromimpl/kernels/distance/mesh.h, the same function the CPU suite covers. Onlyloop-and-launcher glue is new. No BVH, no POD tree mirror, no traversal trace —
none of those exist on the brute-force path, which is exactly why this is the
reference
sdtshould be checked against on real hardware.sdt_naive_kernelgainsnearest_vertex/stride_nearest, whichbuild_sdt_naivehas and it did not; without them the CUDA naive branch couldnot fill an output the CPU branch fills. Both are guarded on the pointer, as in
sdt_kernel, becauseff::dt_meshleaves them null by default.2. Vertex normals were accumulated onto uninitialised memory (fix)
In the existing, already-dispatched
sdt.MeshDistUtil::build_normalsaccumulates into the vertex normals —
normvertices[v].add_(normal)onceper incident face, normalised at the end — which is why cpu-impl allocates them
with
new scalar_t[...](). The CUDA launcher usedallocHost(
cudaMallocHost), which does not zero. Face normals (copy_) and edge normals(built in a local map, then
copy_) are assigned, so only that one buffer wasaffected. Silent failure mode: it perturbs the vertex/edge pseudonormals, i.e.
the sign of the returned distance near vertices and edges — not something
compile+link could ever have caught.
3. The precomputed-tree
sdtoverload: still a throw, with the reason statedNot implemented, deliberately. The signature does not carry what a correct body
needs, and inventing the missing contract silently would have produced a
launcher that compiles and cannot be called correctly:
const void * tree—sdt_kernelneeds a device array of the PODDeviceNodemirror, not the polymorphic hostMeshDist::Node[]thatbuild_treeproduces. Avoid *cannot tell those apart (that ambiguity iswhat fastfields-cuda-impl#44 removed from
sdt_kernelby giving theparameter a real type), and nothing in the tree hands a caller a
DeviceNodearray —flatten_tree+ upload are private to the othersdt.void * treetrace/treesize— the trace is per lane and interleavedacross lanes, so the buffer must be
GET_BLOCKS(numel) * CUDA_NUM_THREADS * treesizebytes: a size that dependson the launch configuration this function itself picks.
facesmust be the BVH-sorted face list —build_treereorders faces inplace and its leaves index the sorted order.
The
TODO(host-launcher)comment (which recorded only the previous bad attempt)is replaced by the above plus the two ways out: give it a real signature and an
in-repo caller by making
sdtdelegate to it — the cpu-implsdt/build_sdtsplit — or drop the overload, which has no caller, no producer and no test.
Both are design changes with a reviewable blast radius, so neither is made here.
How CI actually exercises this
tests/impl-cuda/compile_probe_mesh.cunow callsM::sdt_naivedirectly forall four (dim × dtype) combinations the cuda-lib dispatcher can select, plus the
null-
nearest_vertexshape.probe_dtalready covered all four(
_signed×naive) flag combinations, sodt's signed+naive branch wasinstantiated before this PR and now resolves to a real launcher rather than a
throw. An uninstantiated template is not compiled, so without that addition a
green build would prove nothing about the new code.
Path filter: this touches
impl/cuda/andtests/impl-cuda/only, sobuild-cuda+compile-probe-cudarun and the CPU legs are correctly skipped.make test-lib-cpuwas run locally to confirm the gate is unmoved — result in acomment below.
What remains unvalidated
Everything at run time. There is no GPU in CI, so the evidence here is nvcc
accepting and linking the code, on the standing reasoning that the voxelwise
math is shared with the CPU backend and covered by
test_distance_mesh.cpp.Not verified by this PR: the tree walk, the atomics, the
streamplumbing, thenormal-buffer lifetimes, and — for the new launcher — whether
sdt_naiveandsdtactually agree on a real device. That agreement is #5's acceptance bar andis still open.
Unrelated and untouched, per #80:
src/lib-cuda'sMODULESomitsposdef/resize/restrict/splinc. Nothing here changesMODULESor thelink flags.
Generated by Claude Code