perf(cuda): size the ADM AIM CM launch by SM count instead of a fixed 8 rows (ADR-1226) - #1388
Draft
lusoris wants to merge 1 commit into
Draft
perf(cuda): size the ADM AIM CM launch by SM count instead of a fixed 8 rows (ADR-1226)#1388lusoris wants to merge 1 commit into
lusoris wants to merge 1 commit into
Conversation
lusoris
force-pushed
the
perf/adm-cm-launch-bounds
branch
3 times, most recently
from
September 7, 2026 15:41
2cc1560 to
59cd071
Compare
… 8 rows
`adm_cm_aim_line_kernel_8` was flagged for register pressure: 255 registers,
a 344-byte stack frame and 412/404 bytes of spill on sm_89, which caps a
128-thread block at two blocks per SM (16.7% occupancy).
Measuring the obvious fix first changed the fix. `__launch_bounds__(128, 5)`
does cut the allocator to 96 registers, and counter-intuitively spills *less*
(8 B stack) rather than more -- but it moved the kernel from 0.803 ms to
0.808 ms per call. Occupancy was never the constraint: a 1080p frame gives
buffer_h = 434, so at 8 rows per thread the launch is ceil(434/32) x 3 = 42
blocks against an RTX 4090's 128 SMs. Blocks-per-SM does not matter when
there is less than one block per SM to place.
The kernel's only parallelism is one block per BLOCKY * rows_per_thread rows,
times three orientation bands. There is no x-decomposition and there cannot
cheaply be one: each row's warp reduction must cover the whole row before the
single >> shift_inner_accum rounding, so splitting a row across blocks would
round differently from the CPU. So rows_per_thread is the knob, and lowering
it is arithmetically free -- each row is still reduced across all its columns
inside one block.
Instantiate rows_per_thread 2 and 4 and pick per launch from the device SM
count, because the optimum tracks block count rather than frame size:
rows=8 rows=4 rows=2 rows=1
1920x1080 0.801 0.553 0.586 0.622
640x480 0.299 0.203 0.159 --
576x324 0.300 0.178 0.141 --
Whole-feature `adm (CUDA)`, mean ms per frame on an RTX 4090:
1920x1080 1.31 -> 1.06 (-19.1%)
640x480 0.46 -> 0.33 (-28.3%)
576x324 0.39 -> 0.27 (-30.8%)
`vmaf_bench --validate` reports identical CPU-vs-CUDA max_diff values before
and after, to the last digit, for adm2_score and all four integer_adm_scale*
outputs.
`__launch_bounds__` is left off deliberately; stacked on this fix it is a 3.4%
regression. The kernel carries a comment saying so, so it is not helpfully
re-added.
Refs: ADR-1226
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris
force-pushed
the
perf/adm-cm-launch-bounds
branch
from
September 7, 2026 16:19
59cd071 to
d70159b
Compare
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.
Summary
adm_cm_aim_line_kernel_8was flagged for register pressure: 255 registers, a344-byte stack frame, 412/404 bytes of spill on sm_89, capping a 128-thread
block at two blocks per SM (16.7% occupancy).
Measuring the obvious fix first changed the fix, twice.
__launch_bounds__(128, 5)does not trade spill for occupancy — capped at 96registers it spills less than the unconstrained build (8 B stack vs 344 B),
because at 255 registers ptxas keeps the whole unrolled theta × row working set
live instead of rematerialising it.
And it bought nothing: 0.808 ms vs 0.803 ms per call. Occupancy was never the
constraint. A 1080p frame gives
buffer_h = 434, so at 8 rows per thread thelaunch is
ceil(434/32) × 3 = 42blocks — against an RTX 4090's 128 SMs.Blocks-per-SM is irrelevant when there is less than one block per SM to place.
The kernel's only parallelism is one block per
BLOCKY * rows_per_threadrows ×three orientation bands. There is no x-decomposition and there cannot cheaply be
one: each row's warp reduction must cover the whole row before the single
>> shift_inner_accumrounding, so splitting a row across blocks would rounddifferently from the CPU. So
rows_per_threadis the knob — and lowering it isarithmetically free, since each row is still reduced across all its columns
inside one block.
Measurements (RTX 4090, CUDA 13.3, 48 frames, warm)
Kernel time per call, host clock around the launch + stream sync:
The optimum tracks block count, not frame size — hence the SM-count-relative
rule (4 when that fills at least half the SMs, else 2) rather than a resolution
threshold.
rows=1regresses even at 1080p (327 blocks), past the point wheremore blocks pay for the per-thread setup.
Whole-feature
adm (CUDA), mean ms per frame:Numerics unchanged:
vmaf_bench --validatereports identical CPU-vs-CUDAmax_diffvalues before and after, to the last digit, foradm2_scoreand allfour
integer_adm_scale*outputs (5.42e-08 / 8.26e-08 / 3.15e-08 / 1.13e-07 /4.65e-08).
meson test --suite=fast: 0 failures.__launch_bounds__is left off deliberately — stacked on this fix it is a 3.4%regression (0.572 vs 0.553 ms). The kernel carries a comment saying so, so it is
not helpfully re-added.
Reproducer / smoke-test command
Discard the first three benchmark runs — the 4090 clocks up over roughly that
many.
Deep-dive deliverables (ADR-0108)
## Measurementssection, which carries the full register/spill sweep, the per-kernel timings and the whole-feature deltas.## Alternatives considered(five options, including the x-decomposition that would break CPU bit-exactness).AGENTS.mdinvariant note — no rebase-sensitive invariants beyond the rebase note below, which carries all four.changelog.d/changed/1226-cuda-adm-cm-aim-grid-occupancy.md,CHANGELOG.mdregenerated.docs/rebase-notes.md§ ADR-1226: the_8entry point is gone,__launch_bounds__is absent on purpose,rows_per_threadmust not go back up, andsm_count == 0is a supported state.Docs (rule 10)
No user-discoverable surface changes: no CLI flag, no public header, no build
option, no output-schema change. The performance delta is in the changelog.
State (rule 13)
no state delta: performance change, no bug opened or closed.
🤖 Generated with Claude Code