Repository navigation
perf(relu): fuse the GPU backward into one select kernel - #15
Merged
Merged
Conversation
ReLU's backward, dvalues * (output > 0), ran as two kernels: the compare wrote a 1-byte bool mask, and the multiply read it back as a mixed fp32 x bool operand, which on its own took 0.26 ms where an fp32 x fp32 multiply of the same shape takes 0.17 ms. Nothing branches, so warp divergence was not the cost; the extra pass and the bool operand were. relu_kernel.py adds one ElementwiseKernel, `output > 0 ? dvalues : 0`, with dvalues and output typed separately (T, U) so a fp16 output with an fp32 gradient doesn't raise. ReLU binds it in _compile_for_device and keeps the multiply as _backward_fallback. Forward has no kernel (maximum already runs at memory bandwidth), but is bound as _forward_fallback like the other pointer-swap layers. Being a select, the GPU path turns a NaN gradient at an inactive unit into 0 instead of NaN * 0 = NaN. The fallback is unchanged. RX 9070 XT, CIFAR-10 benchmark: 7.26 -> 6.78 ms/step (-6.6%); ReLU backward over the 5 layers 0.90 -> 0.45 ms. Also strips trailing whitespace in activations.py.
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.
Context
After PR #14, a breakdown of the CIFAR-10 step on the RX 9070 XT showed conv at ~40 % of GPU time and BatchNorm + ReLU at ~42 %. ReLU's backward stood out: it took 0.90 ms, 3× its forward (0.28 ms), though both should cost about one pass over memory. This PR moves the backward into a single elementwise kernel. One commit,
e5f2787. It also strips trailing whitespace inactivations.py, so review withgit diff -w.ReLU backward
dvalues * (self.output > 0)ran as two kernels. The compare wrote a 1-byte bool mask, and the multiply read it back as a mixed fp32 × bool operand. Timed alone at 256×32×32×32 fp32 (RX 9070 XT):maximumout > 0d * bool_maskd * fp32_mask, for comparisonoutput > 0 ? d : 0Nothing in it branches, so warp divergence wasn't the cost. The time went to the extra pass and the slow bool operand. The fused kernel reaches ~590 GB/s for its 12 B per element, close to the card's bandwidth (the forward reaches ~640 GB/s).
aether/custom_kernels/relu_kernel.pyadds the kernel in the same form asloss_kernel.py: a module-levelElementwiseKernel, anis_gpu_relu_backward_available()check and a launch function. There's no kernel cache, because there's nothing to key it on. CuPy already caches the binary per dtype pair.dvaluesandoutputare typed separately (T,U), so an fp16 output with an fp32 gradient doesn't raise.ReLU._compile_for_devicebinds it as_backward_gpuand keeps the multiply as_backward_fallback. Forward has no GPU kernel, sincemaximumalready runs at bandwidth. It's bound as_forward_fallback, like the other pointer-swap layers, whichtest_device_migrationexpects.RX 9070 XT,
benchmarks/cifar10_breakdown.py(synthetic data, batch 256):That's −6.6 % per step. A second run gave 6.81 ms.
Testing
python3 -m unittest discover tests: 1142 tests pass on NumPy and CuPy (65 skipped).test_backward_mixed_dtypes_and_nan: an fp16 output with fp32dvalueskeeps the gradient dtype, and the NaN case has hand-written expected values for each path. On CuPy it fails onmain(NaN where 0 is expected).test_device_migration.test_pointer_swap_binds_gpu_paths_after_to_cupynow covers ReLU too.Follow-ups (out of scope)
ElementwiseKernelwith nothing vendor-specific, but the speedup there is unconfirmed.