ray_implicit_intersection improvements#663
Draft
swahtz wants to merge 2 commits into
Draft
Conversation
Rewrite the per-ray SDF zero-crossing kernel for performance, precision,
and correctness, surface a shared cache-hint helper that other ops can
reuse, lift compile-time specialisations into `SampleRaysUniform.cu`,
and broaden the test surface with 7 new behavioural tests adapted from
the OpenVDB level-set / volume ray-intersector unit tests.
Indirectly benefits from `HDDAIterators.h` cleanups landed alongside
this change (branchless TimeSpan bookkeeping in `HDDASegmentIterator`,
bounded-loop level convergence in `HDDAVoxelIterator`), which the
ray-implicit, sample-rays-uniform, voxels-along-rays, and
segments-along-rays kernels all reach through the public iterator API.
## Source changes
`src/fvdb/detail/utils/cuda/Caching.cuh` (new):
- `_storeStreaming` / `_storeStreamingPair`: write-once stores via
`__stwt` (`.CS` qualifier in SASS) so write-once output tensors
don't get promoted into L1 and evict the voxel-data working set.
- `_loadReadOnly`: read-mostly load via `__ldg` (`.NC` qualifier) so
side-buffer SDF data shares cache capacity instead of competing
with the active-mask leaf data. Both fall back to plain
assignment/dereference on host and for types without a matching
intrinsic overload (e.g. `c10::Half`); NVCC fully inlines both
branches, so the CPU path is unaffected. Lives in
`fvdb::detail::ops` so any op `.cu` file can reach it via
unqualified name lookup from inside an anonymous namespace.
`src/fvdb/detail/ops/RayImplicitIntersection.cu`:
- Add a `bool EpsZero` template parameter on `rayImplicitCallback`.
The launcher branches once on `eps == 0.0f` (the overwhelmingly
common case) and dispatches the corresponding specialisation on
both CPU and CUDA paths, so NVCC drops the per-voxel
`if (deltaT < eps) continue;` branch and one register entirely
when `eps == 0`.
- Do all interpolation / time arithmetic in
`MathType = at::opmath_type<ScalarT>` so `c10::Half` rays compute
interpolation in fp32 and only cast back to `ScalarT` at the
streaming-store boundary.
- Route the per-voxel `gridScalars` load through `_loadReadOnly`
and every `outTimes` write (early-out, hit, miss) through
`_storeStreaming`.
- Detect sign flips with a single predicated check
(`scalarSign != voxelSign`) instead of nested `if`s.
- Track band continuity via `lastT1`. When the next valid voxel is
contiguous along the ray (`t0 == lastT1`), emit a sub-voxel
linear-interpolated hit time between the bracketing samples; when
there's a gap (inactive voxels in the iterator, or a run of NaN
tile values), fall back to the bracket-entry time, matching
`nanovdb::ZeroCrossing` precision in that case. This prevents the
kernel from interpolating across empty space between disjoint SDF
regions.
- Seed the sign reference from the FIRST valid (non-NaN) voxel along
the ray (matching `nanovdb::ZeroCrossing` semantics) instead of a
fixed sentinel. This handles both rays that start outside the
surface (first sample positive, hit on crossing into the negative
band) AND rays that start inside the surface (first sample
negative, hit on crossing back out) without baking a fixed
"positive = outside" SDF convention into the kernel.
- Add `c10::kHalf` to the `AT_DISPATCH_V2` types so half-precision
rays are actually dispatched on CUDA (previously raised
`NotImplementedError: "RayImplicitIntersection" not implemented
for 'Half'` at runtime once the unconditional fp16 test skip was
removed).
`src/fvdb/detail/ops/SampleRaysUniform.cu`:
- Adopt the shared `Caching.cuh` helpers (drops the local
`_storeStreaming` / `_storeStreamingPair` definitions).
- Factor `_emitSample` out of the per-launcher lambdas as a function
template. NVCC forbids extended `__device__` lambdas inside generic
lambdas (which the launchers use), so a free-function template is
the only place the streaming-store helpers can live without
redundant inline-at-call-site repetition. The template takes the
`(a, b)` pair as separately-deduced types `A, B` so callers don't
have to cast `at::opmath_type<Half> = float` HDDA times back to
`c10::Half` at every call site.
- Compile-time specialise `countSamplesPerRayCallback` and
`generateRaySamplesCallback` on three runtime predicates
(`ConeZero`, `IncludeEndpoints`, `ReturnMidpoint`). NVCC then
prunes the dead branches and, critically, hoists
`stepSize = minStepSize` out of the inner while-loops in the
`ConeZero` case, removing a `Clamp+mul` from the hot per-sample
body and dropping several live registers from this latency-bound
traversal.
`src/fvdb/detail/utils/nanovdb/HDDAIterators.h`:
- `HDDASegmentIterator::nextSegment`: replace the entering / leaving
`if`/`else` ladder around `mTimespan.t0` / `mTimespan.t1` with
predicated select expressions. Only the "active region just ended"
`break` remains a real control-flow branch; rays in the same warp
whose `active` state differs no longer diverge at the setter
level.
- `HDDAVoxelIterator::nextVoxel`: replace the three unrolled
`getDim` / level-update passes with a bounded `for (pass < 3)`
loop. Same worst-case behaviour (the level hierarchy stabilises
in <= 3 passes) but the body collapses to a single descent for
level-aligned iterations, which dominate the trace.
## Python docstrings
`fvdb/grid.py`, `fvdb/grid_batch.py`, `fvdb/functional/_ray.py`:
- Document the new convention-agnostic semantics of
`ray_implicit_intersection*`: the first valid (non-NaN) voxel along
each ray seeds the sign reference, and the first subsequent voxel
with the opposite sign is reported as the intersection. Both
"positive outside" and "negative outside" SDF conventions are
handled identically; rays that enter the bbox already inside the
surface are reported at the *exit* crossing along the ray.
## Tests
`tests/unit/test_basic_ops.py`:
- Drop the unconditional `if dtype == torch.float16: return` early
return in `test_ray_implicit_intersection` so the half-precision
dispatch is now actually exercised by the existing test.
- Two regressions for the algorithmic fixes in this commit:
* `..._starts_inside_surface`: ray whose origin sits inside the
SDF sphere must report the EXIT crossing — not -1, not the
bracket-entry of the very first active voxel. Pins the
`nanovdb::ZeroCrossing`-style "first valid voxel seeds the
reference" semantics.
* `..._two_disjoint_regions`: ray that crosses two separated SDF
spheres must report the FIRST surface, not a time inside the
empty gap between them. Pins the band-continuity gating.
- 7 new `@parameterized.expand(all_device_dtype_combos)` tests
adapted from `openvdb-jswartz/openvdb/openvdb/unittest/`
(`TestLevelSetRayIntersector.cc` + `TestNanoVDB.cc` for sign-of-
zero), giving 34 additional test instances:
| Test | Adapts from | Purpose |
|---|---|---|
| `..._sign_of_zero` | `TestLevelSetRayIntersector.cc:71-215` + `TestNanoVDB.cc:1520-1552` | `dir(1, +0, +0)` and `dir(1, -0, -0)` produce identical hit times across all 6 axis-aligned directions (`torch.equal` exact match). |
| `..._axis_aligned_analytic` | `TestLevelSetRayIntersector.cc:43-247` | Hit time for axis-aligned rays through sphere centre matches the analytic ray-sphere root within a voxel diagonal; covers `±x/±y/±z` (subsumes the OpenVDB negative-direction case). |
| `..._diagonal_analytic` | `TestLevelSetRayIntersector.cc:249-278` | Diagonal ray exercising 3-axis HDDA stepping; hit time within voxel diagonal of analytic root. |
| `..._explicit_misses` | `TestLevelSetRayIntersector.cc:311-389` (`testMissedIntersections`) | Four miss configurations all return `-1`: ray bypassing bbox, ray clipping bbox corner away from sphere, ray pointed away from bbox, ray grazing inside bbox outside sphere. |
| `..._non_trivial_transform` | `TestLevelSetRayIntersector.cc:99-216` | Non-unit `voxel_size=0.25` and non-zero `origins=(10, 20, 30)` — exercises `transform.applyToRay` (`RayImplicitIntersection.cu:82`), previously untested. |
| `..._high_resolution_sweep` | `TestLevelSetRayIntersector.cc:280-308` | 64×64 = 4096 ray sweep; geometric check `(hit_pt - center).norm() ≈ sphere_rad` per ray. fp16 explicitly skipped (precision insufficient for voxel-diagonal tolerance). |
| `..._single_voxel_bracket_entry` | (no direct OpenVDB analogue — pins `RayImplicitIntersection.cu:121-136`) | Symmetric `+1/-1` step SDF; verifies the linear-interp branch lands the zero exactly at the midpoint between two bracketing primal-voxel samples. |
- Convention pinned by the bracket-entry test: under fvdb's
`voxel_to_world`, primal voxel `i` is at world
`i*voxel_size + origin` (treated as a node, not a cell with a
`+0.5` offset). Combined with the dual transform's `+0.5` shift,
the kernel's linear interpolation between SDF samples at primal
voxels 3 (+1) and 4 (-1) produces a zero crossing at world
`x = 3.5`, not `x = 4.0` as you'd get under a cell-centred
convention. The test comment documents this for future readers.
## Test plan
- Build: `./build.sh install` (cp312, fvdb conda env).
- Targeted: `cd tests && pytest unit/test_basic_ops.py -v -k ray_implicit`
-> 49 passed, 1 skipped (deliberate fp16 skip in
`..._high_resolution_sweep`).
- Adjacent regressions (no behavioural changes expected, sanity
only):
* `pytest unit/test_basic_ops.py` -> 265 passed, 1 skipped.
* `pytest unit/test_basic_ops_single.py` -> 154 passed.
* `pytest unit/test_ray_marching.py unit/test_sample.py`
-> 455 passed, 4 skipped.
- C++: `RayImplicitIntersection` has no `gtest` coverage today, so
`./build.sh ctest` would not exercise this change.
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR optimizes ray traversal kernels and updates ray_implicit_intersection semantics to be sign-convention agnostic, including CUDA half dispatch and broader behavioral coverage.
Changes:
- Adds shared CUDA cache-hint helpers for read-only loads and streaming stores.
- Reworks
ray_implicit_intersection,uniform_ray_samples, and HDDA iterators with compile-time specialization and traversal optimizations. - Updates Python docs and adds/expands tests for ray implicit intersection edge cases.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
src/fvdb/detail/utils/cuda/Caching.cuh |
Adds reusable cache-hint load/store helpers. |
src/fvdb/detail/ops/RayImplicitIntersection.cu |
Rewrites zero-crossing traversal semantics and dispatch specialization. |
src/fvdb/detail/ops/SampleRaysUniform.cu |
Specializes sampling callbacks and reuses streaming-store helpers. |
src/fvdb/detail/utils/nanovdb/HDDAIterators.h |
Refactors HDDA segment/voxel iteration control flow. |
fvdb/grid.py |
Documents updated single-grid ray intersection semantics. |
fvdb/grid_batch.py |
Documents updated batch ray intersection semantics. |
fvdb/functional/_ray.py |
Documents functional ray intersection semantics. |
tests/unit/test_basic_ops.py |
Enables half coverage and adds ray intersection behavioral tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rewrite the per-ray SDF zero-crossing kernel (
ray_implicit_intersection) to improve performance (and in the process fixed a few correctness issues); surface a shared cache-hint helper that other ops can reuse; lift compile-time specialisations intoSampleRaysUniform.cu; clean upHDDAIterators.hto remove warp-divergence in the iterator setters; and broaden the test surface with 7 new behavioural tests adapted from the OpenVDB level-set / volume ray-intersector unit tests.This optimization started after being inspired by Efty's 'branchless getValue' work in NanoVDB and wanting to apply the same principles to the ray-marching kernels but as I pulled the thread, it lead to other fruit falling out of the tree (to branch metaphors).
End-to-end on my RTX PRO 6000 Blackwell, 1 M rays per workload: 1.29×–1.48× speedup for
ray_implicit_intersection(−25 to −28% kernel instructions, −2 registers/thread) and 1.05×–1.10× speedup foruniform_ray_samples(−6 registers/thread on the count pass).c10::Halfinputs are now dispatched forray_implicit_intersection; previously they raisedNotImplementedError.Motivation
ray_implicit_intersectionwas the dominant cost in SDF-driven rendering workflows for the recent simulation rendering work we were doing, and the current kernel had a few aspects I wanted to address for speed and correctness:eps. Every per-voxel iteration checkedif (deltaT < eps) continue;even though the overwhelmingly common caller passeseps = 0. NVCC kept the branch and the live register holdingeps.uniform_ray_sampleshad a related issue: the per-sample inner loop didstepSize = clamp(t * coneFactor, minStepSize, maxStepSize)on every step even whenconeAngle == 0.0(cone tracing disabled). That kept several extra live registers and aClamp+mulin this latency-bound traversal.Both kernels load read-only grid data (
gridScalars, leaf masks) and emit write-once outputs (hit times, sample intervals). Both were paying L1 write-allocate cost on the output stores, evicting active-mask data from L1.Changes
src/fvdb/detail/utils/cuda/Caching.cuh(new)Three inline helpers, host/device:
_loadReadOnly(ptr)— read via__ldg(.NCqualifier in SASS) so read-only SDF data shares the read-only data-cache capacity instead of competing with the active-mask leaf data._storeStreaming(dst, value)/_storeStreamingPair(dst, a, b)— write via__stwt(.CSqualifier) so write-once result tensors are not promoted into L1 and don't evict the working set.Both fall back to plain assignment / dereference on host and for types without a matching intrinsic (e.g.
c10::Half); NVCC fully inlines both branches so the CPU path is unaffected. Lives infvdb::detail::opsso any op.cufile can reach it via unqualified name lookup from inside an anonymous namespace.src/fvdb/detail/ops/RayImplicitIntersection.cubool EpsZerotemplate parameter onrayImplicitCallback. The launcher branches once oneps == 0.0fand dispatches the corresponding specialisation on both CPU and CUDA paths. WithEpsZero=trueNVCC drops the per-voxeldeltaT < epsbranch and one register entirely. ncu confirms the kernel is theRayImplicitIntersection<1>template instance on the wip path.MathType = at::opmath_type<ScalarT>soc10::Halfrays compute interpolation in fp32 and only cast back toScalarTat the streaming-store boundary.gridScalarsload through_loadReadOnlyand everyoutTimeswrite (early-out, hit, miss) through_storeStreaming.ifs detecting a sign flip with a single predicated check(scalarSign != voxelSign).lastT1. When the next valid voxel is contiguous along the ray (t0 == lastT1), emit a sub-voxel linear-interpolated hit time between the bracketing samples; when there's a gap (inactive voxels in the iterator, or a run of NaN tile values), fall back to the bracket-entry time, matchingnanovdb::ZeroCrossingprecision in that case. Prevents the kernel from interpolating across empty space between disjoint SDF regions.c10::kHalfto theAT_DISPATCH_V2types so half-precision rays are actually dispatched on CUDA. Previously raisedNotImplementedError: "RayImplicitIntersection" not implemented for 'Half'.src/fvdb/detail/ops/SampleRaysUniform.cuCaching.cuhhelpers (drops the local_storeStreaming/_storeStreamingPaircopies)._emitSampleout of the per-launcher lambdas as a function template. NVCC forbids extended__device__lambdas inside generic lambdas (which the launchers use), so a free-function template is the only place the streaming-store helpers can live without inline-at-call-site repetition. The template takes the(a, b)pair as separately-deduced types so callers don't have to castat::opmath_type<Half> = floatHDDA times back toc10::Halfat every call site.countSamplesPerRayCallbackandgenerateRaySamplesCallbackon three runtime predicates (ConeZero,IncludeEndpoints,ReturnMidpoint). NVCC then prunes the dead branches and, critically, hoistsstepSize = minStepSizeout of the inner while-loops in theConeZerocase, removing aClamp+mulfrom the hot per-sample body. ncu confirms the count-pass register count drops from 80 → 74.src/fvdb/detail/utils/nanovdb/HDDAIterators.hHDDASegmentIterator::nextSegment: replace the entering / leavingif/elseladder aroundmTimespan.t0/mTimespan.t1with predicated select expressions. Only the "active region just ended"breakremains a real control-flow branch; rays in the same warp whoseactivestate differs no longer diverge at the setter level.HDDAVoxelIterator::nextVoxel: replace the three unrolledgetDim/ level-update passes with a boundedfor (pass < 3)loop. Same worst-case behaviour (the level hierarchy stabilises in ≤ 3 passes) but the body collapses to a single descent for level-aligned iterations, which dominate the trace.These two cleanups are reached through the public iterator API by
ray_implicit_intersection,uniform_ray_samples,voxels_along_rays, andsegments_along_rays.Python docstrings
fvdb/grid.py,fvdb/grid_batch.py,fvdb/functional/_ray.py: document the new convention-agnostic semantics ofray_implicit_intersection*— the first valid (non-NaN) voxel along each ray seeds the sign reference, and the first subsequent voxel with the opposite sign is reported as the intersection. Both "positive outside" and "negative outside" SDF conventions are handled identically; rays that enter the bbox already inside the surface are reported at the exit crossing.Tests
tests/unit/test_basic_ops.py:if dtype == torch.float16: returnearly return intest_ray_implicit_intersectionso the half-precision dispatch is now actually exercised...._starts_inside_surface— ray whose origin sits inside the SDF sphere must report the EXIT crossing (not -1, not the bracket entry of the very first active voxel)...._two_disjoint_regions— ray crossing two separated SDF spheres must report the FIRST surface, not a time inside the empty gap.@parameterized.expand(all_device_dtype_combos)tests adapted fromopenvdb/openvdb/unittest/TestLevelSetRayIntersector.ccandTestNanoVDB.cc(sign-of-zero), giving 34 additional test instances: sign-of-zero equivalence across all six axis-aligned directions; axis-aligned and diagonal analytic-root checks; four explicit miss configurations; non-trivial transform with non-unitvoxel_size+ non-zeroorigins; 64×64 ray sweep with per-ray geometric check; single-voxel bracket-entry interpolation pinning the primal-voxel convention.Performance
Measured on RTX PRO 6000 Blackwell. Timing uses
torch.cuda.Eventover a 1024² = 1,048,576-ray camera-pinhole bundle aimed at each grid's active bbox; median of 20 iterations after trimming the top and bottom two outliers. ncu metrics use--profile-from-start offwith acudaProfilerStart/Stopwindow around a single 512² = 262,144-ray launch, fp32, on the optimization-engaged setting (eps = 0/cone_angle = 0).Wall-clock timing (median ms per call, 1 M rays)
ray_implicit_intersectionray_implicit_intersectionray_implicit_intersectionray_implicit_intersectionray_implicit_intersectionray_implicit_intersectionray_implicit_intersectionray_implicit_intersectionray_implicit_intersectionray_implicit_intersectionray_implicit_intersectionray_implicit_intersectionuniform_ray_samplesuniform_ray_samplesuniform_ray_samplesuniform_ray_samplesuniform_ray_samplesuniform_ray_samplesuniform_ray_samplesuniform_ray_samplesAll fp32 outputs are bit-equivalent main vs this PR branch (output checksum match within
atol=1e-3 * max(|main|, |wip|)).ncu microarchitecture (single launch, fp32, 262k rays)
Columns are main → wip. Lower-is-better for registers, instructions, cycles, and DRAM bytes; higher-is-better for occupancy, SIMD efficiency (
smsp__thread_inst_executed_per_inst_executed.ratio, out of 32), and L1 hit rate.ray_implicit_intersectionray_implicit_intersectionray_implicit_intersectionuniform_ray_samplesuniform_ray_samplesuniform_ray_samplesuniform_ray_samplesHow the numbers map onto the changes:
ray_implicit_intersection's −25 to −28% kernel instructions is the dominant cause of the 1.29×–1.48× speedup. It comes from the combination of (a)EpsZero=trueremoving the per-voxelepsbranch, (b) the new_emitSample-style streaming-store path eliminating the L1-bounce on output writes, and (c)HDDAIteratorspredicated setters cutting the per-step setter cost.uniform_ray_samplescount pass — the count pass is exactly where theConeZero=truehoist ofstepSize = minStepSizeout of the inner loop applies.ray_implicit_intersection(93.4 → 90.7 on crawler). That's the streaming-store path bypassing L1 deliberately — it's the intended behaviour, and the freed-up L1 capacity goes to the read-only side-buffer SDF data via_loadReadOnly. The wall-clock speedup confirms it's a net win.uniform_ray_samplesimproves (7.36 → 7.74, 4.68 → 4.86) — theHDDAIteratorspredication wins. Onray_implicit_intersectionSIMD efficiency dips slightly (10.62 → 10.06); the band-continuity check is a small new divergence path that's paid for many times over by the instruction-count reduction.Notes / risks
ray_implicit_intersection. The function is now SDF-sign-agnostic and reports the EXIT crossing for rays that start inside the surface (previously: returned-1or a spurious bracket-entry). New behaviour matchesnanovdb::ZeroCrossing. Two dedicated regressions pin it (..._starts_inside_surface,..._two_disjoint_regions). Existing callers that relied on the old "positive-outside-only" assumption will see different outputs on inside-surface rays, but those outputs were almost certainly being treated as no-hit anyway.c10::Halfis now dispatched forray_implicit_intersection. Code that previously caughtNotImplementedErrorand fell back to fp32 conversion at the Python boundary should drop that branch.