GPU density-function evaluation: working prototype, measured ceiling, and why lighting looks like the better target #2719
Replies: 2 comments 2 replies
|
I'd recommend moving this discussion to Discord, as it has a larger and more active community. However, since you can share more detailed content here, I won't close this thread. Also, your testing and implementation are incredibly helpful to us! |
|
"GPU density-function evaluation: working prototype, measured ceiling, and why lighting" is a good agent-runtime surface because the real cost is whether the workflow completes cleanly across tools, retries, and provider behavior. A compact provider matrix could track streaming, tool calls, structured output, usage reporting, timeout behavior, and fallback safety. That makes routing decisions easier to explain later. I am testing a multi-model OpenAI-compatible API layer around official Chinese models, so the routing and usage-accounting parts of this are directly relevant to me. Which provider difference causes the most friction here: tools, streaming, usage reporting, or fallback behavior? |
Uh oh!
There was an error while loading. Please reload this page.
Summary
I built a working prototype that evaluates Pumpkin's density-function graph on the GPU
via wgpu, and measured what it is actually worth. Sharing it because the infrastructure
may be more useful than the speedup, and because the measurement changed my mind about
where acceleration pays off.
Branch: https://github.com/gerofurlani07-gif/Pumpkin/tree/gpu-noise-acceleration
Nothing here is integrated into chunk generation yet — it is a validated prototype, not
a performance win you can run today. Numbers below are from an Intel Iris Xe iGPU.
What it does
A new opt-in crate,
pumpkin-world-gpu, excluded fromdefault-membersso it does notaffect normal builds:
BaseNoiseRouter::full_component_stackinto a flat instruction list. That stackis already topologically ordered with index-based inputs, so it maps onto GPU bytecode
almost directly.
point, with intermediates in
[node][point]layout to keep access coalesced.(217 nodes → 647 instructions) and nether.
compile_routeremits all ten named router outputs per point, so the aquifer andore-vein samplers can get what they need from one dispatch and keep their branchy,
stateful logic on the CPU.
Measured results
Overworld router, warm, best of ten iterations, ~1200 points per chunk:
But the honest number is the end-to-end one.
cargo bench -p pumpkin-world --bench chunk_genputs one overworld chunk at ~41.9 ms:So this targets 35.9% of chunk generation, which caps the whole-chunk gain at 1.56x
no matter how fast the GPU path gets. The measured 12-30x on the stage works out to
roughly 1.5x per chunk. Lighting is the larger share and is untouched — doing both would
put the ceiling near 3.7x. And chunk generation is only part of what a server does, so
none of this is a server-wide multiplier.
Two findings that may matter regardless
1. f32 is not uniformly safe. The shader runs f32 (WGSL has no f64). For smooth
density values that only costs precision, but
RangeChoiceandIntervalSelectcomparea selector against a threshold, so one ulp does not shift the result slightly — it
selects a different subgraph.
Seven overworld threshold nodes read the same selector: a
ClampedYGradientwhose valuerange equals its Y range, i.e. clamped identity on Y, compared against integer thresholds
(-60, 51, 321) that real block coordinates land on exactly. Vanilla computes it as a
lerp, and
start + t * (end - start)does not return an integer Y exactly in f32. Ilower that case to a clamp instead, which is exact and fixed the divergence.
Not every case is fixable:
EndIslandsdecides island contributions withsimplex < -0.9, and a genuine float computation near a threshold lands on oppositesides in f32 and f64 — about one point in 240 in my sweep.
2.
FindTopSurfacedoes not fit a single-pass design. It walks Y downward incell-height steps, re-evaluating the density subgraph at each position until density
turns positive — a data-dependent loop of nested evaluations at positions not known
ahead of time. Same shape that keeps the aquifer on the CPU. It would need a multi-pass
design (evaluate over a Y range, then search the result).
Why I think lighting is the better target
I originally dismissed lighting as a poor GPU fit because it is a BFS flood fill. That
was a judgement about algorithm shape made without measuring cost, and it was wrong on
both counts:
simply does not exist there.
passes by the light range, and each pass is a neighbourhood stencil, which is what GPUs
are good at.
The per-chunk "light from scratch" path during generation looks like the tractable part.
Incremental relighting when a block changes is small and latency-sensitive, and my
measurements suggest small dispatches lose to the CPU anyway.
Validation approach
Every node type is checked against its real CPU implementation, not against my own
reimplementation, since a transcription error would otherwise agree with itself. The
strongest check evaluates the full 217-node overworld router on the GPU against the CPU
interpreter. There is also a
threshold_auditexample that reports how exposed a routeris to the branch-flip issue, meant to catch a future node that reintroduces an inexact
selector.
Where this could go
If there is interest, the parts I think are reusable independent of the speedup:
Open questions I do not have good answers for: whether a maintained GPU backend is worth
the coupling to the router's internals, and whether the ~1.5x (or ~3x with lighting)
justifies it for the workloads Pumpkin cares about. Happy to be told this is not worth
pursuing.
Disclosure: this work was done with the assistance of an AI coding agent. The
measurements are real and reproducible via the benchmarks and examples in the branch;
the design decisions and their limitations are described above as accurately as I can
state them.
All reactions