GPU Performance Audit Checklist¶
Intent¶
Provide a detailed, repeatable audit checklist for finding GPU performance problems in vibeSpatial. This document is meant to be used during deep dives, pre-land reviews, profiling sessions, and broad “does this code still think like CPU code?” investigations.
Request Signals¶
gpu performance audit
performance checklist
cupy review
cccl review
sync audit
transfer audit
host orchestration
cpu-shaped gpu code
stream audit
occupancy audit
Open First¶
docs/testing/gpu-performance-checklist.md
docs/architecture/runtime.md
docs/architecture/residency.md
docs/testing/performance-tiers.md
docs/testing/profiling-rails.md
src/vibespatial/cuda/_runtime.py
src/vibespatial/cuda/cccl_primitives.py
Verify¶
uv run python scripts/check_docs.py --checkuv run python scripts/profile_kernels.py --kernel all --rows 10000 --repeat 1uv run python scripts/health.py --gpu-coverageuv run python scripts/benchmark_pipelines.py --suite full --repeat 1 --gpu-sparkline
Risks¶
Fast-looking GPU code can still be host-bound if control flow, allocation, or grouping decisions happen in Python.
Scalar device reads can look harmless in review while silently forcing full stream or context synchronization.
“Device-resident” APIs can still pay hidden D2H costs if they eagerly build host mirrors for convenience.
Stream usage can be misleading when upstream CCCL or NVRTC launch helpers still force the null stream.
Small benchmarks can hide transfer, launch, and orchestration costs.
This checklist was built from a repo audit performed on April 7, 2026.
When To Use This Checklist¶
Use this document when any of the following is true:
a GPU path is slower than expected
a pipeline looks GPU-native in architecture docs but profiles as CPU-heavy
an operation uses CuPy, CCCL, NVRTC, or
cuda-pythona reviewer suspects excessive synchronization or transfer churn
a new kernel or dispatch path is being added and you want to avoid copying existing mistakes
Use it as both:
a discovery checklist while auditing existing code
a pre-merge checklist before landing new GPU work
Current Verdict¶
The current repo does have serious GPU-first infrastructure:
occupancy-based launch sizing exists in
src/vibespatial/cuda/_runtime.pyasync count-scatter total helpers exist in
src/vibespatial/cuda/_runtime.pyhardware-aware precision policy exists in
src/vibespatial/runtime/precision.pyparts of overlay already use real stream overlap
The current repo also still has multiple CPU-shaped GPU surfaces:
host-side orchestration in overlay microcells and grouped overlay
eager device-to-host metadata mirroring in pylibcudf device builders
Python loops driven by device-reduced maxima in WKB decode
unconditional same-stream synchronization in point-in-polygon helpers
CCCL wrappers that default to null-stream synchronization instead of caller- controlled completion
The conclusion is not “the framework is fake GPU.” The conclusion is:
the framework is capable
adoption is uneven
the worst problems are structural rather than cosmetic
Baseline Evidence¶
As of April 7, 2026:
uv run python scripts/profile_kernels.py --kernel all --rows 10000 --repeat 1selected CPU for both join and overlay at 10K rows on the local RTX 4090the profiler reported effectively 0% GPU utilization for those profiled runs
uv run python scripts/health.py --gpu-coveragereported:GPU available:
truetotal dispatches:
10134GPU dispatches:
400CPU dispatches:
9154fallback dispatches:
361GPU acceleration rate:
3.95%
Treat those numbers as a dated snapshot, not a permanent truth. Re-run them before claiming improvement.
Severity Model¶
Use this rubric while filing findings:
BLOCKINGA structural issue that prevents a path from scaling as a GPU path at all. Examples: host loops over device work units, repeated D2H reads inside a hot loop, unconditional same-stream syncs between stages, eager full metadata materialization on every “device” decode.HIGHA design or implementation issue that will materially cap throughput or destroy overlap on realistic workloads.MEDIUMA repeated anti-pattern that may not dominate every dataset, but should be cleaned up because it compounds with other costs.LOWA legitimate clean-up item or a suspicious pattern that needs measurement.
Audit Workflow¶
Run the audit in this order:
Confirm whether the path actually ran on GPU.
Confirm whether the path stayed on GPU after it got there.
Confirm whether the path avoided same-stream synchronization.
Confirm whether the path avoided Python-controlled batching or grouping.
Confirm whether kernel launch and primitive selection are appropriate for the hardware.
Confirm whether the benchmark or profiler surface is measuring the real path, not the planner’s intent.
If a path fails an earlier step, do not waste time micro-optimizing later steps first.
Physical Plan Review¶
Use this review for changes touching public dispatch, joins, overlay, dissolve,
IO, constructive operations, vsbench, or workflow shootouts.
[ ] Name the reusable physical shape that changed, such as semijoin, anti-semijoin, many-few overlay, mask clip, grouped geometry reduce, or area-filter-after-overlay.
[ ] Confirm the evidence reports actual backend, not only planner intent.
[ ] Confirm already-device-resident data stays on device until an explicit materialization or fallback boundary.
[ ] Confirm semijoin, anti-join, groupby, dissolve, and materialization steps are visible in benchmark or profiler output.
[ ] Confirm the change improves a real workflow canary or adds a shape-level benchmark that can catch regressions.
Mark as BLOCKING if:
the change only makes one workflow faster by adding a benchmark-specific shortcut
host materialization remains hidden behind pandas convenience operations
the result cannot explain which reusable shape got faster or slower
Fast Triage Commands¶
Use these search rails early:
rg -n "cp\\.asnumpy\\(|\\.get\\(\\)|\\.item\\(|runtime\\.synchronize\\(|cp\\.cuda\\.Stream\\.null\\.synchronize\\(" src/vibespatial -g'*.py'
rg -n "for .* in range\\(max_|for .* in cp\\.asnumpy|for .* in .*tolist\\(" src/vibespatial -g'*.py'
rg -n "count_scatter_total\\(|launch_config\\(|block = \\(256, 1, 1\\)|block=\\(256, 1, 1\\)" src/vibespatial -g'*.py'
rg -n "copy_device_to_host|to_shapely\\(|to_pandas\\(|to_numpy\\(" src/vibespatial -g'*.py'
Interpretation rules:
a grep hit is not automatically a bug
a hit inside a hot loop is much more suspicious than a hit at a terminal materialization boundary
runtime.synchronize()between same-stream stages is almost always wrong unless it is trace-only or required by a host read immediately afterward
Checklist¶
Runtime And Dispatch¶
[ ] Confirm the path records both requested runtime and selected runtime.
[ ] Confirm the profiler or benchmark reports actual execution device, not only planner intent.
[ ] Confirm
autodoes not demote already-device-resident workloads back to CPU only because row counts look small.[ ] Confirm explicit GPU mode fails loudly instead of silently falling back.
[ ] Confirm fallback events remain observable.
[ ] Confirm precision planning matches hardware class.
[ ] Confirm consumer GPUs are not doing unnecessary fp64 for predicate or metric kernels when
PrecisionPlanwould allow fp32.[ ] Confirm constructive kernels that stay fp64 do so by policy, not by accidental hardcoding.
Pass if:
the path actually executes on GPU when it should
the runtime record explains why
there is no silent fallback
Memory Pools And Allocation¶
[ ] Confirm the path uses the configured pool allocator instead of raw allocation churn.
[ ] Confirm temporary allocations are not repeatedly materialized and freed inside Python loops.
[ ] Confirm count-scatter paths size outputs once from totals instead of resizing incrementally.
[ ] Confirm eager pool trimming is not enabled in hot paths unless debugging.
[ ] Confirm long-lived and short-lived buffers are not interleaved in a way that obviously increases fragmentation.
[ ] Confirm managed memory is not silently used where a pool-backed VRAM path is expected.
Fail immediately if:
raw allocation or pool flushes happen per work unit
Python loops cause repeated alloc/free cycles that should have been batched
Streams And Synchronization¶
[ ] Confirm consecutive same-stream stages do not call
runtime.synchronize()between launches unless a host read follows immediately.[ ] Confirm CCCL helpers or CuPy wrappers are not forcing
Stream.null.synchronize()when the caller could defer completion.[ ] Confirm stream usage is real, not cosmetic.
[ ] Confirm a stream pool is not feeding work into helpers that still serialize on the null stream.
[ ] Confirm async transfer helpers use pinned memory where overlap matters.
[ ] Confirm stream synchronization is amortized per batch, not per item.
Mark as BLOCKING if:
null-stream synchronization is hardcoded in a reusable primitive wrapper
every kernel helper ends with an unconditional full-device sync
Transfers And Residency¶
[ ] Confirm device-backed builders do not eagerly copy structural metadata to host unless the public boundary requires it.
[ ] Confirm host materialization happens only at explicit surfaces such as
to_pandas,to_numpy,values,__repr__, or a visible fallback.[ ] Confirm D2H copies are not feeding more GPU work unless absolutely unavoidable.
[ ] Confirm small scalar reads are not happening repeatedly where a single batched transfer would work.
[ ] Confirm host mirrors are lazy and cached instead of rebuilt for every GPU path invocation.
[ ] Confirm zero-copy interop stays zero-copy when layouts already align.
Treat these as suspicious:
cp.asnumpy(...)in a mid-pipeline helper.get()or.item()on device scalars in a hot pathmultiple
copy_device_to_host(...)calls for related metadata that could be transferred together
CuPy Usage¶
[ ] Confirm operations are vectorized and bulk-shaped instead of Python loops over row ids, ring ids, or group ids.
[ ] Confirm
cp.flatnonzero,cp.searchsorted,cp.cumsum, andcp.concatenateare used on whole arrays, not in Python loops over groups.[ ] Confirm
cp.uniqueresults do not immediately trigger host loops over large work sets.[ ] Confirm boolean masks and family partitions stay on device when possible.
[ ] Confirm
cp.asarrayis not being used to bounce data host -> device -> host inside the same pipeline.[ ] Confirm any
cp.column_stackor dense gather step is justified and not a hidden quadratic memory move.
Mark as HIGH if:
Python owns the outer control flow and CuPy only performs the inner slices
device maxima are read to host just to drive
for range(max_...)
CCCL Usage¶
[ ] Confirm scan, sort, reduce, and binary-search wrappers accept a caller- controlled synchronization policy.
[ ] Confirm wrappers do not always synchronize the null stream.
[ ] Confirm wrappers can eventually accept streams when backend support exists.
[ ] Confirm count-returning primitives do not force immediate host scalar reads unless the pipeline truly needs a host integer to allocate.
[ ] Confirm cold-JIT avoidance logic does not lock the steady-state path into slower CuPy fallbacks after warmup.
[ ] Confirm sort and compaction primitives are used where they improve batch shape instead of falling back to host partition logic.
Repo-specific reminder:
the backend layer already exposes stream arguments in the CCCL cached call path
the wrapper layer is the current bottleneck
Kernel Launch And Occupancy¶
[ ] Confirm kernels use
launch_config()or another occupancy-aware sizing rule instead of hardcoded(256, 1, 1)without evidence.[ ] Confirm work is launched in bulk instead of many small per-group kernels dispatched from Python.
[ ] Confirm launch geometry matches the dominant work dimension.
[ ] Confirm launch count is not inflated by host-side binning or slicing that could happen on device.
[ ] Confirm shared-memory requirements are passed into block sizing when relevant.
Low-priority only if:
the hardcoded block size is on a cold path or a tiny helper
IO And Parsing Pipelines¶
[ ] Confirm parsers do not decode structure on GPU only to hand control back to Python for ring, part, or polygon walking.
[ ] Confirm offset construction is device-native count-scatter or segmented scan where possible.
[ ] Confirm format-family assembly does not immediately bounce through host family discovery unless the number of families is tiny and the cost is demonstrably irrelevant.
[ ] Confirm legacy count-scatter total sites use
count_scatter_total()rather thanruntime.synchronize()plus multiple.get()calls.[ ] Confirm “device decode” surfaces do not eagerly materialize host mirrors just because downstream host code currently expects them.
Mark as BLOCKING if:
the parser uses Python loops over per-geometry nested structure on the hot path
Predicate And Spatial Query Pipelines¶
[ ] Confirm coarse filtering, candidate compaction, and refine all stay on device once the workload is on device.
[ ] Confirm work estimation and binning do not require candidate rows on host.
[ ] Confirm dense and compacted helper launches do not synchronize before the caller needs the result.
[ ] Confirm candidate row assembly uses device primitives instead of host regrouping.
[ ] Confirm output scattering back into dense arrays does not force an early D2H read.
Overlay And Constructive Pipelines¶
[ ] Confirm grouped overlay does not materialize group boundaries to host unless the entire pipeline is already falling back.
[ ] Confirm stream pools are paired with stream-aware launches and primitive calls; otherwise treat them as cosmetic concurrency.
[ ] Confirm microcell labeling and contraction are not row-by-row host loops.
[ ] Confirm constructive helpers such as clip, shortest line, line buffer, and union-all do not synchronize between same-stream stages without a host dependency.
[ ] Confirm tree reductions do not split large GPU workloads into thousands of Python-managed one-row objects if a batched alternative is possible.
Mark as BLOCKING if:
overlay correctness may be fine, but topology assembly or contraction is fundamentally host-managed
Profiling And Evidence¶
[ ] Capture at least one profiler or benchmark run on the target machine.
[ ] Record actual GPU name.
[ ] Record the command, date, scale, and selected runtime.
[ ] Record whether the path stayed on device end to end.
[ ] Record the top 3 longest stages and whether they were CPU or GPU.
[ ] Record which findings are structural and which are incidental.
[ ] Record which suspicious host reads are legitimate allocation fences.
Do not accept:
“the code looks GPU-ish”
“the planner would select GPU”
“the kernel itself is fast in isolation”
Accept only:
measured device execution
stage-level timing
transfer-aware reasoning
Current Repo Hot Spots¶
Use this list as the first remediation queue, not as a substitute for fresh auditing.
Area |
Files |
Why It Matters |
Current Diagnosis |
|---|---|---|---|
Device WKB decode and OGA builders |
|
Foundational ingest path; hidden host mirrors poison downstream residency |
Eager D2H metadata copies and Python loops over nested geometry structure |
Point in polygon |
|
Core refine primitive used by predicates and constructive work |
Unconditional syncs and host-side work estimation in binned mode |
CCCL wrapper layer |
|
Shared by scan, sort, compaction, binary search, segmented reduce |
Null-stream synchronization is embedded in wrappers |
Overlay microcells |
|
Structural overlay path; expensive if host-managed |
Row-by-row host loops and host-side union-find |
Grouped overlay orchestration |
|
High-value constructive path |
Host-materialized grouping and Python-controlled per-group dispatch |
Legacy count-scatter totals |
|
Easy wins that remove avoidable syncs |
Old sync plus |
Hardcoded launch geometry |
|
Can cap occupancy or hide resource mismatches |
Hardcoded 256-thread block despite launch-config support |
Python tree reduction |
|
Correct but poor performance shape at scale |
One-row object splitting and host-managed reduction rounds |
False Positives To Avoid¶
Do not file these as bugs without stronger evidence:
a single scalar host read used strictly to size an output allocation
synchronization guarded only by hotpath tracing or profiling instrumentation
a tiny host loop over a small fixed set of geometry families when the data payload itself stays on device
explicit materialization at a public host boundary
Examples already considered legitimate or lower priority during the April 7, 2026 audit:
trace-only sync guards in overlay hotpath helpers
allocation-fence scalar reads in output sizing code
runtime precision detection based on actual hardware ratio
Sign-Off Checklist¶
Before closing a performance audit, confirm all of the following:
[ ] I identified whether the path was actually GPU or CPU at runtime.
[ ] I separated structural blockers from incidental cleanups.
[ ] I identified whether synchronization was caller-driven or wrapper-driven.
[ ] I identified whether host reads were terminal, allocation fences, or performance bugs.
[ ] I named the first 3 remediation targets in priority order.
[ ] I recorded at least one exact benchmark or profile command with date and scale.
[ ] I recorded whether the result supports or disproves the “CPU-shaped GPU code” hypothesis for the audited surface.
[ ] I answered the physical-plan review questions when the audited surface touched public workflow performance.
The audit is complete only when the final note answers two questions:
What is forcing this path to behave like CPU code today?
What is the smallest structural change that moves it back toward a true GPU execution shape?