perf: remove device to host syncs in cross entropy forward and backward#1292
Open
kashif wants to merge 3 commits into
Open
perf: remove device to host syncs in cross entropy forward and backward#1292kashif wants to merge 3 commits into
kashif wants to merge 3 commits into
Conversation
The fused linear cross entropy and cross entropy wrappers called .item() on the non-ignore counts (and weight sums) before launching the Triton kernel. Each .item() forces a device->host synchronization that stalls the CPU until the GPU finishes the reduction, showing up as a large sync point on the critical path in profiles of LigerFusedLinearCrossEntropyFunction.forward. Keep these normalizers as 0-D device tensors and pass them into liger_cross_entropy_kernel as pointers, loading and casting them inside the kernel. This removes the sync while preserving the fused in-kernel mean reduction and identical numerics. The token-accuracy host-side division now uses the tensor directly (stays on device). Updated test_float32_internal, which calls the kernel directly, to pass n_non_ignore as a 0-D tensor to match the new kernel ABI.
The cross entropy and fused-linear-cross-entropy backwards guarded the grad_output multiply with torch.equal(grad_output, 1.0), which returns a Python bool and forces a device->host sync every backward (a second sync on top of the forward .item() one). Move that "skip when grad_output is 1.0" decision into element_mul_kernel: it already loads grad_output, so it now returns early on-device when the value is exactly 1.0 (bit-identical, since x * 1.0 == x). The wrappers launch the kernel unconditionally and drop the torch.equal() check. The reduction='none' path still branches on grad_output.ndim, which is a host side shape check and does not sync. Also tidies the forward-path kernel comments to explain the pointer/dtype choice.
Contributor
Author
|
cc @vaibhavjindal if you can kindly have a look thanks! |
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.
What
The fused linear cross entropy (and plain cross entropy) forward calls
.item()on the non-ignore token count before launching the Triton kernel, and the backward guards the grad scaling withtorch.equal(grad_output, 1.0). Both pull a value off the GPU, so each one forces a device to host sync. In a training profile these show up asaten::item,_local_scalar_denseandStreamSynchronizeblocks sitting on the critical path insideLigerFusedLinearCrossEntropyFunction.forward.This PR removes both syncs.
Forward. Keep the non-ignore count and the weighted sums as 0-D device tensors instead of Python scalars, and pass them into the kernel as pointers. The kernel loads and casts them internally, so the mean reduction and the numerics are unchanged.
Backward. Move the "skip the multiply when grad_output is 1.0" decision into
element_mul_kernel. It already loadsgrad_output, so it now returns early on-device when the value is exactly 1.0. This is bit identical becausex * 1.0 == x. The wrappers launch the kernel unconditionally and drop thetorch.equalcheck. Thereduction='none'path still branches ongrad_output.ndim, which is a host side shape check and does not sync.Why it matters
Dropping the syncs lets the CPU keep queuing work instead of stalling on the GPU. That helps compute and communication overlap under FSDP2 and DeepSpeed, and it unblocks CUDA and HIP graph capture, since a single
.item()breaks capture. The math is identical, so this is a pure latency and capture win.Benchmark
Measured on the pi052 CE path (
LigerFusedLinearCrossEntropyLoss, forward plus backward) on an AMD Radeon 890M, ROCm 7.2, withBT=2048 H=2048 V=131072.Wall time does not move on this iGPU because it is heavily compute bound, so a few ms of CPU stall hides behind the GPU work. The payoff shows on a fast GPU where the CE compute is quick and the stall was blocking the rest of the model from launching, which is the setting the original profile came from.
Testing
test_cross_entropy.pyandtest_fused_linear_cross_entropy.pypass (306 passed). The only failures are the pre existingtest_ampcases, which fail the same way onmainfrom an unrelated AMP addmm dtype issue.test_float32_internalwas updated to pass the count as a 0-D tensor to match the new kernel signature.Heads up for reviewers: this changes the internal signature of
liger_cross_entropy_kernel. Then_non_ignore,sum_non_ignore_weightandweight_sumarguments are now pointers to 0-D tensors rather than Python scalars, so any code calling that kernel directly must pass tensors.🤖 Generated with Claude Code