Skip to content

perf: rasterizer and pipeline optimizations - #8

Open
rayanht wants to merge 2 commits into
mainfrom
perf/rast-compaction
Open

perf: rasterizer and pipeline optimizations#8
rayanht wants to merge 2 commits into
mainfrom
perf/rast-compaction

Conversation

@rayanht

@rayanht rayanht commented Aug 25, 2026

Copy link
Copy Markdown
Owner

garden, 1535033 gaussians, 1297x840, SH degree 3, densification off.

BENCHMARK=1 PROFILE_STAGES=1 msplat garden --resume garden_nd0.ply -n 500 \
  --num-downscales 0 --warmup-length 999999 --sh-degree-interval 1

9s/run, +/-0.2%.

Landed

# change rast_fwd rast_bwd sum
0 v1.1.4 (6b81971) 1.674 4.453 12.50
1 threadgroup read instead of simd_broadcast 1.674 3.750 11.76
2 per-subtile candidate compaction 1.202 3.223 10.78
3 Morton-order gaussians at each densification 1.198 3.203 10.22

(3) is measured on an offline-reordered PLY to isolate it: proj_sh_fwd 0.853 -> 0.654,
sort 1.366 -> 1.206, grad_stats 0.120 -> 0.064, adam 2.884 -> 2.787. The rasterizers
do not move; they read packed_* in sorted order and were already coherent.

Full runs, v1.1.4 -> head of branch:

config before after PSNR SSIM
--num-downscales 0 82s 67.7s 25.72 -> 25.68 0.7838 -> 0.7835
--num-downscales 2 32.4s 29.4s 24.91 -> 24.93 0.7151 -> 0.7153

nd0 PSNR spread across runs is 0.12, nd2 0.05.

Breakdown

stage ms %
rast_bwd 3.223 29.9
proj_sh_bwd_adam 2.901 26.9
sort/scatter/pack 1.366 12.7
rast_fwd 1.203 11.2
loss_fwd_bwd 1.112 10.3
proj_sh_fwd 0.849 7.9
grad_stats 0.120 1.1
sum 10.776
ms/iter 11.42
unattributed 0.64

rast_bwd ablations (post-compaction, 3.199ms)

variant rast_bwd removed part
base 3.199
no atomics 2.910 atomics 0.289 (9.0%)
no simd reductions 2.507 reductions 0.692 (21.6%)
loop floor 1.105 per-lane work 2.094 (65.5%)

floor 1.105 (34.5%) / gradient math 1.113 (34.8%) / reductions 0.692 (21.6%) / atomics 0.289 (9.0%).
Compaction cut the floor 2.83 -> 1.105; atomics and reductions unchanged in absolute terms.

sort/scatter/pack ablations (1.376ms)

part ms
scatter_to_prealloc_bins + prefix_sum + bin load 1.139
pack (gather by gaussian id) 0.170
bitonic sort 0.067

1.88M atomic_fetch_add onto 4346 tile counters. Contention ruled out (see below);
the 1.139ms is bbox iteration plus 1.88M x 8B writes into prealloc_bins.

Queue

  • loss_fwd_bwd 1.11ms: loss_intermediates + ssim_h_buf are [H,W,15], ~65MB each, ~260MB round-trip vs ~0.65ms roofline.
  • compaction in the chunked kernels (4x-downscale phase only).
  • fold grad_stats into proj_sh_bwd_adam, ~0.1ms.

Ruled out

change result
transpose SH for coalescing proj_sh_bwd_adam 2.885 -> 4.67
16x16 rasterization threadgroups sum 11.815 -> 12.209
ballot-skip the 9 simd_sum when 1 lane is valid rast_bwd 3.228 -> 3.144, sum flat. Single-lane is only ~12% of cases.
radix instead of bitonic bitonic is 0.067ms of the 1.376ms stage. Nothing to win.
warp-aggregated scatter atomics spreading counters to remove contention: stage 1.364 -> 1.558. Contention is not the cost; tile-local counters cache well. Re-tested under Morton order (higher contention): 1.211 -> 1.342. Same answer.
deferred ("lazy") Adam for means/scales/quats/opacities dense geometry Adam costs 0.913ms (adam 2.832 -> 1.919 when ablated). Replaying the missed zero-gradient steps in registers is exact, and gave -6.2% wall. But the catch-up lands in the backward, after the forward already projected with stale parameters: SSIM 0.7820-0.7824 vs a 0.78355-0.78457 baseline band over 10 runs. Bounding staleness with a flush every 10 steps recovers SSIM to 0.78329 but halves the win to -3.4%. Correct fix is to catch up inside project_and_sh_forward, which costs back most of the saving.

proj_sh_bwd_adam is bandwidth bound: 507K visible x 1284B = 651MB/iter.

Notes

  • Full 7000-iteration runs: +/-4s spread, cannot resolve <2s. Quality checks only.
  • metal_tensor.hpp changes sizeof(MTensor). Incremental builds across it produce a mixed-ODR binary that trains to garbage. rm -rf build after any header change.

Two changes to nd_rasterize_forward_kernel and rasterize_backward_kernel.

Read batch data straight from threadgroup memory. Every lane reads the same
index and the hardware broadcasts that, so routing it through lane 0 plus four
simd_broadcast shuffles was overhead. Returns the same value bit for bit.

Compact candidates against the block's own pixel box. tile_bins is BLOCK_X/Y
(16x16) but a threadgroup covers RAST_BLOCK_X/Y (8x8), so each pixel was
testing every gaussian in a tile four times its footprint, ~94% of which fail
the per-pixel sigma test. Each gaussian is now tested once at load time:
sigma < 5.55 confines it to an ellipse with axis-aligned half-extents
sqrt(11.1 * cov_ii), cov = inverse(conic). Survivors are compacted in depth
order via simd_prefix_exclusive_sum. Only gaussians that fail for every pixel
in the block are dropped, so output is unchanged; degenerate conics (det <= 0)
fall through to the per-pixel test.

Pinned workload (garden, 1535033 gaussians, 1297x840, SH degree 3):
  rast_fwd  1.674 -> 1.202 ms   -28%
  rast_bwd  4.453 -> 3.223 ms   -28%
  total    12.50  -> 10.78 ms   -14%

Full 7000-iteration runs, quality unchanged:
  --num-downscales 0   77.7s -> 76.1s   PSNR 25.720 -> 25.714  SSIM 0.7838 -> 0.7841
  --num-downscales 2   32.4s -> 30.3s   PSNR 24.912 -> 24.921  SSIM 0.7151 -> 0.7151

The chunked kernels are untouched.
@rayanht rayanht changed the title perf: rasterizer wins (stacking) perf: rasterizer and pipeline optimizations Aug 25, 2026
Gaussian index order is whatever densification's compaction produced, so the
stages that address memory by gaussian id read unrelated cache lines from
neighbouring lanes, and visible/culled gaussians are interleaved inside a
simdgroup rather than uniform across it.

Sorting by Morton code of position fixes both. A counting sort over an 18-bit
Morton bucket (6 bits per axis, 262144 buckets) reusing the prefix-sum kernels
densification already runs, then the permutation is applied to all 18 buffers.
Reordering permutes an unordered set, so the scene is unchanged.

Pinned workload (1535033 gaussians, offline-reordered PLY, isolates the effect):
  proj_sh_fwd  0.853 -> 0.654  -23%
  sort/scatter 1.366 -> 1.206  -12%
  grad_stats   0.120 -> 0.064  -47%
  adam         2.884 -> 2.787   -3%
  rast_fwd/bwd unchanged (they read packed_* in sorted order, already coherent)
  sum         10.743 -> 10.221  -4.9%

Full 7000-iteration runs, 2 reps each:
  --num-downscales 0   71.0s -> 67.7s   PSNR 25.685 -> 25.684
  --num-downscales 2   30.1s -> 29.4s   PSNR 24.952 -> 24.927

Runs at each refine step, so it also keeps order fresh as split/dup children are
appended at the tail.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant