Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces explicit bounds checking for target class indices in the PyTorch backend's sparse_categorical_crossentropy implementation to ensure consistent error behavior across CPU, GPU, and MPS devices, along with comprehensive unit tests. The review feedback highlights two critical issues: first, the check will crash with a RuntimeError on the 'meta' device during symbolic tracing, which can be resolved by returning early for 'meta' tensors; second, performing synchronous host-device transfers on GPU/MPS tensors introduces a severe performance bottleneck, and a non-synchronous device-side assertion should be considered instead.
| if target.device.type == "cpu" or target.numel() == 0: | ||
| return |
There was a problem hiding this comment.
During symbolic tracing or shape inference (e.g., inside 'compute_output_spec'), Keras runs operations eagerly using the 'meta' device. Since 'meta' tensors do not contain actual data, calling 'bool(torch.any(invalid))' on line 972 will raise a 'RuntimeError' (e.g., 'RuntimeError: Ky_meta_tensor cannot be cast to boolean'). To prevent crashes during symbolic tracing, we should return early if the target is on the 'meta' device.
| if target.device.type == "cpu" or target.numel() == 0: | |
| return | |
| if target.device.type in ("cpu", "meta") or target.numel() == 0: | |
| return |
There was a problem hiding this comment.
The old RuntimeError was caused by bool() on a meta tensor.
With the new approach, there is no bool() or host sync, so meta tensors work normally with masked_fill.
I also added a test for meta-device shape inference to make sure this doesn't regress.
I intentionally removed the "meta" special case since it is no longer needed.
| invalid = ~((target == -100) | ((target >= 0) & (target < num_classes))) | ||
| if bool(torch.any(invalid)): | ||
| raise IndexError(f"Target {int(target[invalid][0])} is out of bounds.") |
There was a problem hiding this comment.
Calling 'bool(torch.any(invalid))' and 'int(target[invalid][0])' on a GPU/MPS tensor forces a synchronous host-device transfer (GPU-to-CPU copy). This stalls the GPU pipeline and can introduce a severe performance bottleneck during training, as 'sparse_categorical_crossentropy' is called at every training step. While message parity with the CPU kernel is nice, forcing a synchronous barrier on every batch on GPU/MPS is highly discouraged in high-performance deep learning. Consider using a non-synchronous device-side assertion like 'torch._assert' with a static error message, or only performing this check when a debug/validation mode is explicitly enabled.
There was a problem hiding this comment.
I tested this on MPS, and bool() adds 0.42 ms because it waits for the GPU, causing a overhead on a small train step.
torch._assert has the same issue because it also calls bool().
So I removed the host sync and kept the check on the device. Invalid labels are safely changed to 0, and their losses are set to NaN.
The overhead is now reduced depending on the workload. CPU behavior is unchanged, and it also works with torch.compile.
I don't think this should be behind a debug flag because otherwise invalid labels can still silently produce a wrong loss.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #23669 +/- ##
==========================================
- Coverage 86.96% 84.41% -2.56%
==========================================
Files 484 484
Lines 71754 71762 +8
Branches 11840 11842 +2
==========================================
- Hits 62400 60576 -1824
- Misses 6224 8181 +1957
+ Partials 3130 3005 -125
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
2518a55 to
673793e
Compare
This PR fixes ,
keras.ops.sparse_categorical_crossentropyonly checks the shape of the inputs. It does not check that the label values are valid. It passes them straight toF.cross_entropy/F.nll_losson the Torch backend.Those PyTorch functions only check label values on CPU. So the same bad label behaves differently on each device:
[0, num_classes)IndexError: Target 5 is out of bounds.Example with 3 classes, where label
5is invalid:This is a real problem. If your labels and your number of classes do not match, training on MPS keeps running and slowly learns the wrong thing. Nothing tells you something is wrong.
The fix
On non-CPU devices, bad labels are changed to
0so the lookup stays in range. Then the loss for those rows is set toNaN:This runs after the class axis is moved to position 1. So
output.shape[1]is always the number of classes, no matter the rank or theaxisvalue. One call covers both the logits path and the probabilities path.Result for
target=[0, 5]with 3 classes:IndexErrorIndexError(no change)[0.1054, -0.0000], no warning[0.1054, nan][…, nan]InvalidArgumentErrorNaNWhy not just raise an error everywhere
To raise an error, we have to copy the labels from the GPU back to the CPU and look at them. That makes the GPU wait, on every training step.
I measured this on an M4 with MPS,
batch=4096,vocab=32000:The new check costs nothing. The error version does cost something, on every step.
The check also has no
ifstatement that depends on the data. That meanstorch.compilecan trace it withfullgraph=True, and it also works onmetatensors during shape inference.One small difference from TensorFlow
TensorFlow makes both the loss and the gradients NaN. Here, only the loss is NaN.
masked_fillblocks the gradient, so the gradient for a bad row is0.This means a bad label cannot push the weights toward the wrong class, and it cannot turn the weights into NaN. You see
nanin the loss right away, but the model is still usable. This is written in the docstring and tested bytest_invalid_target_contributes_no_gradient.Testing
Tested on an Apple M4 with a real MPS device, torch 2.12.1:
keras.losses.SparseCategoricalCrossentropy()returnsnan, andmodel.fitshowsloss: naninstead of a normal-looking number.keras/src/backend/torch/nn_test.py— 30 passed (12 on real MPS).keras/src/losses/losses_test.py -k sparse— 19 passed, nothing broken.ruff checkandruff formatare clean.Fixes: #23664
Contributor Agreement
Please review our AI-Assisted Contribution Policy and check all boxes below before submitting your PR for review:
Note: Failing to adhere to this agreement may result in your future PRs no longer being reviewed.