CUDA: pass descriptor-table entry-point uniforms by reference (split from #11939; supersedes #11941) - #11992
Conversation
…d at parameter binding A CUDA compute entry-point uniform parameter that carries a descriptor table - a fixed-size array of resources or pointer-backed structs - is now laid out and reflected as an implicit ParameterBlock: the kernel receives one 8-byte device pointer in kernel-argument space and the payload lives in device global memory. A runtime index into the array becomes an ordinary dynamically-addressed global load instead of the serial .param-space load chain that made such kernels 10-30x slower than Vulkan/D3D12 (issue #11774). The decision is made exactly once, in computeEntryPointParameterTypeLayout at parameter-binding time, where reflection is computed; a post-link IR pass (reconcileCUDAByRefEntryPointParams) then retypes the parameter to match the recorded layout and rewrites its value uses into loads through addresses, reusing the constref pass's rewriteValueUsesToAddrUses (exported as a shared utility). The IR pass consumes the layout decision rather than re-deriving a predicate, so the emitted kernel signature and the reflected layout cannot disagree - the failure mode that sank earlier emit-stage attempts (#11747). Restricted to struct-typed parameters; bare descriptor arrays keep the by-value layout and are covered by the dynamic-index local-copy legalization this PR is stacked on. Ray-tracing stages, torch/ [AutoPyBindCUDA]/[CudaKernel] kernels, and source-written parameter groups are excluded. -cuda-entry-point-params-by-value restores the legacy ABI. Stacked on the dynamic-index floor pass (this repo's other CUDA PR); requires shader-slang/slangpy#1045 before slangpy bumps its Slang pin.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This supersedes #11941 (which couldn't be reopened)#11941 was previously merged into #11939's branch, and GitHub does not allow reopening a merged PR — so the by-reference layer is re-proposed here as a fresh PR, stacked on #11939 (base = Provenance of the results: all perf, nsys command-stream, and correctness numbers on record (#11774 and #11939) were measured with #11939 (floor) + this PR (by-ref) applied together — they were developed and validated as one stack and land as one. Specifically attributable to this PR (the memory-placement change):
Landing dependency: shader-slang/slangpy#1045 (host reference-safety) must merge before slangpy bumps its Slang pin — this PR changes the CUDA ABI that #1045 makes slangpy's cached-offset writers safe for. That is why the combined PR's SlangPy Tests check was red; it is gated on #1045, not on a defect here. The split is faithful: #11939 + this PR reproduce byte-for-byte the combined tree that was tested. |
|
Command-stream evidence for this by-reference layer (originally posted on the pre-split #11939; measured with floor + by-ref together). nsys A/B: command-stream composition with a by-ref-converted parameter (L40S, measured)Question: a shader that previously dispatched with a single CUDA command-stream entry gets one entry-point parameter converted to by-reference — is an extra cuMemcpy now needed? Answer: yes — exactly one
GPU-trace memops corroborate: Properties of that copy, matching the source line-by-line:
Repro for an independent Nsight session: |
|
Scaling follow-up to the above (originally on #11939 pre-split). Follow-up to the nsys A/B above — scaling measurement (L40S) confirming the upload is per command buffer, not per dispatch:
Separate submits scale 1:1 (the honest per-dispatch cost in slangpy's one-command-buffer-per-call pattern). Batched stays flat while launches double — the residual 3→5 is one copy per used global-pool page (verified via the GPU memop trace, transfers up to ~12 KB), i.e. the count tracks payload volume, not dispatch count. Repro: |
Fixes #11774 (with #11939).
Motivation
#11939's local-copy legalization is the floor: it makes runtime-indexed by-value parameters correct and O(1)-per-access, but the host still ships the full descriptor blob by value per dispatch and every thread pays a prologue copy. The measured regression — SlangPy
test_tensor_sum_indirect, 13–29× slower than Vulkan on L40S — is auniform TensorList<N>struct nestingRWTensor<float,2>[N]in.param. On every other target, indexed resource collections live in dynamically-indexable descriptor memory; this PR gives CUDA the same placement.Proposed solution
A compute entry-point
uniformstruct parameter that carries a descriptor table (a fixed-size array whose elements are or contain resources/pointers) is passed by reference — laid out and reflected as an implicitParameterBlock, so the kernel receives one 8-byte device pointer and the payload lives in pooled device global memory:The decision is made once, at parameter binding (
shouldPassCUDAEntryPointUniformParamByRef), where reflection is computed; the post-link IR passreconcileCUDAByRefEntryPointParamsconsumes the recorded layout rather than re-deriving a predicate — so the emitted signature and the reflected layout cannot disagree, eliminating by construction the ABI-desync class that sank the earlier__constant__-hoist attempt (#11747). Zero emit changes (ParameterBlockparams already render asT*), zero slang-rhi changes (entry-point-parented PB sub-objects are an existing, tested path).-cuda-entry-point-params-by-valuerestores the legacy ABI.Scope: struct-typed params only (bare descriptor arrays would reflect as
ParameterBlock<array>, a shape hosts don't yet represent — they keep the by-value layout and rely on #11939's floor). Excluded: RT stages, torch/[AutoPyBindCUDA]/[CudaKernel]kernels, explicit parameter groups, plain-data params.Change summary
slang-parameter-binding.cppslang-ir-cuda-byref-entry-point-params.{h,cpp};rewriteValueUsesToAddrUseshoisted toslang-ir-transform-params-to-constref.{h,cpp}; invocation inslang-emit.cppinclude/slang.h(CudaEntryPointParamsByValue),slang-options.cpp,docs/command-line-slangc-reference.mddocs/cuda-target.md(Binding rewritten),docs/user-guide/09-targets.mdtests/cuda/entry-point-uniform-*.slang— emit + reflection-JSON + opt-out + exclusions + a GPU execution testValidation
All perf/nsys numbers on record were measured with this PR + #11939 applied together (floor + by-ref), since the two land as a stack. Runtime-indexed descriptor tables 13–29× faster on L40S + RTX 5090, CUDA beating Vulkan (0.173 vs 0.199 ms at count=32); command stream adds exactly one async pooled
cuMemcpyHtoDAsyncper command buffer (nsys A/B 31:31:0 vs by-value control); slangpy CUDA canaries green with shader-slang/slangpy#1045. All 53tests/cudapass on the stacked branch (46 on #11939 alone).Landing order
Label pr: breaking change. Merge order: #11939 (non-breaking floor) → shader-slang/slangpy#1045 (host reference-safety, before slangpy bumps its Slang pin) → this PR → close #11774.
🤖 Generated with Claude Code