Fix CUDA illegal memory access in backward with mixed requires_grad (#1056) - #1057
Fix CUDA illegal memory access in backward with mixed requires_grad (#1056)#1057nv-slang-bot[bot] wants to merge 3 commits into
Conversation
…#1056) Two IDiffTensor inputs to a [Differentiable] fn abort with CUDA_ERROR_ILLEGAL_ADDRESS in backward when only some inputs have requires_grad=True. Covers yes/yes, yes/no, no/yes under both torch bridge modes.
A [Differentiable] function with multiple IDiffTensor inputs called through the torch integration aborted the process with CUDA_ERROR_ILLEGAL_ADDRESS during backward when only some inputs had requires_grad=True. Every IDiffTensor param binds as a DiffTensor in the backward pass, so the compiled kernel scatters into its _grad_out unconditionally, but dispatch only allocated a grad buffer for requires_grad=True inputs. On CUDA the atomic scatter targets a raw device pointer, so the no-grad input's unbound _grad_out faulted; on other backends type resolution rejects a diff tensor with no output gradient outright. Bind a throwaway zeroed buffer for no-grad differentiable inputs so the scatter lands in valid memory. The buffer is discarded and None is still returned to torch for that leaf, so autograd reports no gradient for it. Closes #1056
On CUDA the pre-fix failure aborts the process (illegal address); on other backends type resolution rejects the unbound gradient with a TypeError. Addresses reviewer clarity nit on PR #1057.
|
Adding a potentially significant memory allocation to solve a null pointer seems overkill. Is there no way to deal with the null pointer correctly? |
|
@ccummingsNV The allocation is one buffer per no-grad input, sized like its primal and discarded after the backward pass — so for a large frozen/detached input it is wasted memory every backward. That cost is real; below is why the fix currently binds a buffer unconditionally, and the cleaner alternative. The fault has two different forms depending on backend:
A CUDA-side null-check on the atomic write would address only the first; the non-CUDA path fails earlier, at generation. So a null-guard alone isn't sufficient across backends. The fix that removes the allocation entirely is to bind a no-grad input as Why it isn't a one-liner: the That's more involved than this PR, but it's the correct fix and avoids both the allocation and the null pointer. I can switch this PR to the 🤖 Generated by an automated SlangPy coworker — may be inaccurate. A human maintainer should verify. |
Summary
Fixes a hard process abort (
CUDA_ERROR_ILLEGAL_ADDRESS) when a[Differentiable]Slang function with multipleIDiffTensorparameters is called through the PyTorch integration and only some of its inputs haverequires_grad=True.[Differentiable]function has two tensor params and only one requires grad #1056.Root cause
In the backward pass every
IDiffTensorparameter binds as a SlangDiffTensor— this is keyed on the call mode (backward), not on each tensor'srequires_grad(slangpy/builtin/tensorcommon.py). ADiffTensoralways carries an_grad_outatomic accumulator, and the generated backward path for its tensor loads scatters into_grad_outwith no per-tensorrequires_gradguard (slangpy/slang/difftensor.slang).But
NativeCallData::autograd_backward(src/slangpy_ext/utils/slangpy.cpp) only allocated a gradient buffer for inputs withrequires_grad=True, leavingpair->grad = Noneotherwise. The marshall then skips writing that parameter's_grad_outfields, so on CUDA — where the atomic target is a raw device pointer — the scatter writes through a dangling pointer and faults. This is whyyes/yesworked whileyes/noandno/yesaborted.Fix
In
autograd_backward, for an input pair whose primal hasrequires_grad=False, bind a throwaway zeroed buffer (instead ofNone) so the kernel's unconditional scatter lands in valid memory. The buffer is discarded, andNoneis still returned to torch for that leaf, so autograd correctly reports no gradient for it. This mirrors the reportedt.detach().requires_grad_(True)workaround, done internally.The buffer is bound on all backends, not just CUDA: on CUDA it prevents the illegal-address fault; on other backends type resolution rejects a diff tensor with no associated output gradient at kernel-generation time (see the
!= DeviceType.cudaTypeErrorguard inslangpy/builtin/tensorcommon.py), so the buffer is required there too — a CUDA-only gate would leave the non-CUDA mixed-grad path raisingResolveException. The bind is therefore unconditional.Testing
Adds
test_mixed_requires_grad_idifftensortoslangpy/tests/slangpy_tests/test_torchintegration.py, covering all three cases (yes/yes,yes/no,no/yes) with twoIDiffTensor<float,1>inputs and asserting gradients appear only forrequires_grad=Trueleaves. It runs under the autousetorch_bridge_modefixture, exercising both the native and Python-fallback bridge paths.Verified locally on an L40S (CUDA + Vulkan):
yes/noandno/yescases abort the process withCUDA_ERROR_ILLEGAL_ADDRESS.test_torchintegration.py: 342 passed, 84 skipped, 0 failed (matches the pre-change baseline).pre-commit runon the changed files: clean.Scope
Scoped to the torch-integration backward path. The plain-
Tensor(non-torch) CUDA guard-bypass attensorcommon.py(the!= DeviceType.cudagate that suppresses the missing-gradTypeErroron CUDA) is a separate latent hardening item and is intentionally not folded in here — naively un-gating it would raise at cache time and regress the valid mixed-grad case this fix enables.🤖 Generated by an automated SlangPy coworker — may be inaccurate. A human maintainer should verify.