cudastereo: TMA path-aggregation kernels for StereoSGM on sm_100+ - #4169
Open
DmytroHilei wants to merge 3 commits into
Open
cudastereo: TMA path-aggregation kernels for StereoSGM on sm_100+#4169DmytroHilei wants to merge 3 commits into
DmytroHilei wants to merge 3 commits into
Conversation
check_consistency previously launched dim3(width/16, height/16); integer division floored away a right/bottom strip up to 15 px wide whenever an image dimension was not a multiple of 16, leaving those pixels never visited by check_consistency_kernel. They kept WTA's output and skipped L/R invalidation. On the common 1920x1080 case (1080 mod 16 = 8) this affected the bottom 8 rows of every frame. Switch the grid to cudev::divUp and add an in-kernel bounds-check so the now-overhanging threads return cleanly. Add a regression test (StereoSGM_NonAligned) in test_stereo.cpp that feeds unrelated random images of non-aligned size into cv::cuda::StereoSGM::compute and verifies the previously-uncovered strip is touched by check_consistency. Also fix a latent typo in store_uint8_vector<2u> (second lane was ptr[0] instead of ptr[1], breaking the dest[i] = ptr[i] contract held by the <1u>, <4u>, <8u>, <16u> specializations). The <2u> specialization is unreachable in current code; included as defensive hygiene against a future DP_BLOCK_SIZE = 2u variant.
asmorkalov
self-requested a review
July 14, 2026 09:58
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.
cudastereo: TMA path-aggregation kernels for StereoSGM on sm_100+
Motivation
The path-aggregation stage of
cv::cuda::StereoSGM::computeruns eight1-D DP recurrences (vertical U2D/D2U, horizontal L2R/R2L, four oblique
directions), one per direction, each on its own stream. Per-direction
ncu on the current legacy kernels (1920×1080, MD=128, RTX 5060 Laptop
sm_120) shows all of them DRAM-bound with the warp stalled on
long_scoreboardafter synchronous LDGs — the block-wideLDG + __syncthreadspattern pins compute order so the scheduler can'thide the load.
Hopper introduced TMA (
cp.async.bulk.shared::cta.global+mbarrier.try_wait.parity) precisely for this shape of workload:asynchronously stream bytes DRAM → smem while the warp keeps computing.
Blackwell (sm_100+) accepts the
.shared::ctacompletion form usedhere; a Hopper (
.shared::cluster) port is a separate follow-up.What this PR does
Safe fixes (carried forward from PR
cudastereo-sgm-consistency-fix/ commit b590fde):check_consistencylaunch bounds:dim3(width/16, height/16)→dim3(divUp(width, 16), divUp(height, 16))+ in-kernelx >= width || y >= heightearly-return. Previously the right/bottom strip upto 15 px wide was never visited on non-multiple-of-16 images; on
1920×1080 (1080 % 16 = 8) this affected the bottom 8 rows.
store_uint8_vector<2u>: fixuint8x2.y = ptr[0]→ptr[1]typo. Unreachable in current code (DP_BLOCK_SIZE ∈ {8, 16}) but
latent for any future 2-element variant.
StereoSGM_NonAlignedintest_stereo.cppthat verifies the strip is now touched by
check_consistency.TMA path-aggregation kernels, one per direction group:
aggregate_horizontal_path_kernel_tma— chunked prefetch,CHUNK_X = 128 pixels/phase, staging = 8 rows × 132 int32s ×
2 (L+R) × 2 (double-buffer) ≈ 16.5 KiB smem/block. 16 bulk copies
per phase transition (PATHS_PER_BLOCK threads issue L + R for
their own row).
aggregate_vertical_path_kernel_tma— per-row ping-pong, onebulk copy each for the per-iter right slice + left slice, issued
by thread 0. Two mbarriers,
try_wait.paritygate. Existingswizzled
right_bufferlayout preserved on the consumer side.aggregate_oblique_path_kernel_tma— same as vertical but withthe diagonal
right_x0 ± 1 per iteralignment quirk: per iterround the source pointer DOWN by
d = ((right_x0 % 4) + 4) % 4ints and shift the consumer read index UP by
d. Staging sizedwith
ALIGN_SLACK = 4for the slack.Runtime dispatch, added to the vertical + horizontal launcher
wrappers (4 of the 8) via a shared helper instead of a repeated
if/else at each call site:
canUseTma()caches `cudaDeviceGetAttribute(cudaDevAttrComputeCapabilityMajor)The 4 oblique launcher wrappers deliberately do not use this
dispatch — they always call the legacy kernel.
aggregate_oblique_path_kernel_tmais implemented, bit-exact, and a real isolated per-kernel win, but
oblique only runs in HH8 mode where it contends with 7 other
concurrent streams; under that contention its tail latency roughly
triples relative to legacy (see Performance below), which flips
HH8's end-to-end result from a win into a regression if oblique TMA
is dispatched. Leaving oblique on the legacy path and keeping
vertical + horizontal TMA on turns HH8 back into a (small) net win.
Compile-time guards: each
*_tmakernel body is wrapped in#if __CUDA_ARCH__ >= 1000so ptxas accepts the file when compiledfor older archs (empty body on those archs; runtime never dispatches
to it).
Shared device helpers: the mbarrier ping-pong init
(
tma_init_ping_pong_barriers) and the per-phase wait-then-flip-paritystep (
tma_wait_and_flip) are each defined once and reused by allthree
*_tmakernels, instead of being duplicated inline in each.Correctness
TMA kernels are bit-exact with their legacy counterparts on the
1920×1080 real-image cost volume (0 differing bytes / 265 MB per
direction; verified externally via the benchmark harness at
benchmarks/cuda_stereosgm/, not shipped with this PR).Since bit-exactness is designed in, no TMA-specific test is added — the
existing
CudaStereo_StereoSGM.regressiontest (which comparessgm.compute()output against a reference disparity image) alreadycovers this. Any TMA regression fails that test.
Cross-arch build verification
The
*_tmakernel bodies usecp.async.bulk.shared::cta/mbarrier.try_wait.parity, which ptxas only accepts for sm_100+. The#if __CUDA_ARCH__ >= 1000guards must therefore fully exclude thosebodies when compiling for older archs, or the build breaks on exactly
the targets CI uses (CI does not build sm_120). Verified by compiling
stereosgm.cuwith the same nvcc flags for each arch:The guard boundary was confirmed at the SASS level, not just by exit
code: the sm_90 / sm_86 objects contain zero bulk-copy
instructions (the Blackwell-only PTX is fully excluded), while the
sm_120 object contains 48
UBLKCPinstructions and 12*_path_kernel_tmasymbols — so the guarded code is emitted where it should be and absent
everywhere else.
Performance
Numbers below are RTX 5060 Laptop (sm_120, 26 SMs), 1920×1080,
MAX_DISPARITY = 128 (cudaEvent elapsed-time), warmup = 400, bench =
3000 for every kernel and the e2e pipeline (bumped from an earlier
200/1000 pass specifically to pin down whether the HH8 e2e result below
was noise — it wasn't). CSVs in
data/.Isolated per-kernel
Aggregate per-direction wins (mean of the direction rows above):
CHUNK_X(horizontal's TMA staging window, currently 128) was swept64/128/192 — 128 is the validated sweet spot (dominates on L2R
specifically, −19.7% isolated at that setting); 192 regresses
(−14.3% aggregate) and 64 is a noise-driven false peak. 256 overflows
the sm_120 static smem budget. Not an arbitrary first guess.
Isolated per-kernel: ncu (SpeedOfLight + MemoryWorkloadAnalysis)
Oblique's ncu is in the branch report (§6); horizontal's below
(both directions,
results/tma/horizontal/ncu/in the benchmarkharness) is new this round:
Horizontal's TMA win runs through the opposite mechanism from
oblique's. Oblique's L1/TEX hit rate rises with TMA (5.9%→42%,
better cache locality). Horizontal's collapses (29.1%→0.4-1.8%):
cp.async.bulkroutes the bulk read traffic DRAM/L2→smem directlythrough the TMA hardware unit, bypassing the L1TEX-mediated LDG path
that gave legacy its real 29% hit rate (a per-pixel register-shift
reuse trick). DRAM Throughput still rises even as L1TEX traffic
vanishes, and Warp Cycles/Issued Instruction drops — both confirm TMA
decouples the load from the warp's critical path rather than
improving what L1TEX sees. Achieved Occupancy drops ~5pp (ping-pong
staging smem cost) — the one thing this design spends to buy the rest.
End-to-end (
sgm.computepipeline)tmabelow = vertical + oblique TMA (no horizontal).tma_full= allthree.
tma_no_obl(HH8 only) = vertical + horizontal TMA, obliqueleft on the legacy path — this is what the PR actually ships.
Bit-exact vs legacy in every variant (0 / 2,073,600 differing pixels).
HH4 never invokes oblique, so it only has 3 rows.
HH8 with oblique TMA dispatched is a real, reproducible regression —
not noise. First pass at 200/1000 warmup/bench looked flat-within-1σ
(legacy std was 0.75ms, wide enough to plausibly cover the gap); at
400/3000 the std tightened to ~0.5ms and the same +2.7-3.3% gap held
in two independent runs. Every isolated oblique/vertical/horizontal
median improves with TMA, so a per-kernel view alone predicts an HH8
win that doesn't show up.
Root cause, confirmed via
nsys --report cuda_gpu_kern_sum(50profiled HH8 iterations/variant, isolated with
cudaProfilerStart/Stop+--capture-range=cudaProfilerApi): underHH8's 8 concurrent streams, TMA kernels' per-launch duration variance
is ~3× higher than legacy's (coefficient of variation ~20-32% for
TMA vs ~6-12% for legacy, oblique the worst offender at ~29-32%).
Tellingly, the legacy horizontal kernel — unmodified, unchanged code
— shows the same inflated variance (~20-25% CV) when co-scheduled
alongside TMA vertical+oblique, versus ~7-11% when it's the only thing
contending with other legacy kernels. That rules out "TMA kernels are
just noisier code" and points at real cross-stream contention: sharing
SMs/L2/DRAM with concurrent TMA streams fattens everyone's tail, TMA's
tail more than legacy's. WTA can't start until all 8 streams finish,
so HH8 e2e tracks
max()across those 8 durations, not the mean — afatter right tail raises that max even when the mean improved
(order-statistics behavior, not a contradiction).
Fix: since oblique showed the worst tail inflation and is HH8's
only TMA-exclusive contributor (vertical + horizontal both help in
HH4 too, where there's no 8-way contention), we tested dropping
oblique back to the legacy kernel while leaving vertical + horizontal
TMA on. That's the
tma_no_oblrow above: HH8 flips from +2.9%regression to −1.0%, a real if modest win, consistent with HH4.
This PR ships that configuration — the 4 oblique launcher wrappers
always call the legacy kernel;
aggregate_oblique_path_kernel_tmastays in the file, bit-exact and functional, as a documented base for
a future contention fix (e.g. CUDA stream priorities) rather than
being deleted.
the largest and most reproducible signal in the PR, and — unlike
oblique — it holds up end-to-end in both HH4 and HH8 because
horizontal only has 2 directions, not 4, and isn't exclusive to the
8-stream mode.
tma_no_oblincludes it and beats legacy at HH8), unlike oblique's −3.8%, which
doesn't.
Cross-device check: RTX 5090 (170 SMs vs the laptop's 26)
Reproduced via
run_pr_ab.shon a rented RTX 5090 (also sm_120), sameimage/params, same 400/3000 warmup/bench. Bit-exact in every variant
(0 differing pixels, confirmed in
run.log). CSVs indata/*_rtx5090.csv.Horizontal's win scales up sharply on the bigger chip — expected, more
SMs means more concurrent DRAM requests in flight, which is exactly
what
cp.async.bulk's decoupled prefetch is designed to exploit.The oblique finding doesn't fully transfer. On the 5090,
tma_full(oblique TMA dispatched) beats
tma_no_oblat HH8 — the opposite ofthe laptop result that justified shipping oblique on the legacy path.
170 SMs apparently gives HH8's 8 concurrent streams enough headroom
that oblique's contended tail latency (Performance/HH8 above) doesn't
dominate the way it does with 26 SMs. The shipped dispatch (no oblique
TMA) is not wrong here — it's still a real win on both devices
(−1.0% / −0.4%) — but it leaves ~1.4 points of additional HH8 win on
the table on high-SM-count Blackwell parts. Not making the dispatch
SM-count-aware in this PR: two data points isn't enough to pick a sane
threshold, and a wrong guess risks a device-dependent perf cliff worse
than the modest performance left on the table by staying conservative.
Compatibility
.shared::ctaisn't acceptedon sm_90 by ptxas; a
.shared::clusterport with differentcompletion semantics would unlock Hopper — follow-up, not this PR.
Files touched