Reshape instead of view in TiledFusedLogitsLoss - #8362
Conversation
deepspeedai#8348 fixed this in TiledMLP.backward: the flatten of batch and sequence into one axis needs a copy for a caller that hands in a non-contiguous activation, so it cannot be a view. TiledFusedLogitsLoss.forward does the same flatten, under the same comment, and is still on view: # flatten bs+seqlen to avoid having stride issues when narrowing into seqlen w/ bs>1 x = x.view(-1, *x.shape[2:]) y = y.view(-1, *y.shape[2:]) A transposed activation -- the layout deepspeedai#8348's test covers -- fails there: RuntimeError: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces) A channel slice happens to survive, because its row stride still admits the flatten, so only the transposed case is reachable today. y and mask go through the same flatten and are changed with it. The unflatten at the end stays a view: x_grad comes from zeros_like() of the already-flattened x, so it is contiguous by construction. Signed-off-by: alanhuangyoo <alanhuangyoo@gmail.com>
| loss = TiledFusedLogitsLoss.apply(self.loss_fn, model, x, y, None, shards, list(model.parameters()), "sum") | ||
| losses.append(loss) | ||
|
|
||
| torch_assert_close(losses[0], losses[1]) |
There was a problem hiding this comment.
The new test compares the two losses but never the gradient, and the flatten you changed feeds the gradient path as well: x_grad is a zeros_like of the flattened x, it is scattered per shard through x_grad.narrow(0, shard_offset, shard_step).view_as(x_shard) at ulysses_sp.py:1190, and unflattened at :1208. TestTiledMLPInputLayout, which your docstring names as the same caller contract, does assert x_tiled.grad against the reference.
The test also never calls backward(), so TiledFusedLogitsLoss.backward and that saved x_grad are not exercised at all.
Adding both passes at 307ff15, in a clean python:3.12-slim container with torch 2.14.0+cpu:
shards=2 loss_equal=True x_grad_equal=True max|dx|=0.000e+00
shards=4 loss_equal=True x_grad_equal=True max|dx|=0.000e+00
Concretely that is loss.backward() inside the loop, then comparing strided.grad with contiguous.grad after it. The parameter gradients match too, if you want the third assertion the MLP test makes.
There was a problem hiding this comment.
Both added in ac714ff — loss.backward() in the loop, strided.grad against contiguous.grad, and the parameter gradients as the third comparison, matching TestTiledMLPInputLayout.
Same numbers you measured, on torch 2.13.0:
shards=2 loss_equal=True x_grad_equal=True max|dx|=0.000e+00 max|dW|=0.000e+00
shards=4 loss_equal=True x_grad_equal=True max|dx|=0.000e+00 max|dW|=0.000e+00
One thing I checked while doing this, since it changes what the assertion is worth: reverting the reshape and running the strengthened test fails at the forward flatten (ulysses_sp.py:1150, view size is not compatible ...) before backward is ever reached. So the gradient assertion is not what catches this particular bug — the forward already did.
What it does cover is the part your comment named that the forward does not. reshape on a non-contiguous input returns a copy, so x_grad is a zeros_like of that copy rather than of the tensor the caller handed in, and the shard scatter at :1190 and the unflatten at :1208 have to put those values back on the original layout. Nothing was checking that they did. max|dx| = 0.000e+00 against the contiguous reference is what says the copy does not scramble it, and grad.abs().sum() > 0 says it is not passing by both sides being empty.
Thanks — the test was asserting the shallower half of its own docstring.
There was a problem hiding this comment.
Good catch on the revert check, and you are right that it changes what the assertion is worth. The forward flatten fails first, so the gradient comparison is not the guard against this particular regression and I should not have implied it was.
What it does pin is the part nothing was covering: the scatter at :1190 and the unflatten at :1208 putting values back on the caller's layout after reshape hands back a copy. grad.abs().sum() > 0 alongside it is the right guard against both sides being empty.
The flatten under test also feeds the backward: x_grad is a zeros_like of the flattened activation, scattered per shard through x_grad.narrow(...).view_as(x_shard) and unflattened on the way out. The test compared only the loss and never called backward, so that path was not exercised and the copy reshape makes for a non-contiguous input was never checked for putting the gradient back where it came from. shards=2 loss_equal=True x_grad_equal=True max|dx|=0.000e+00 max|dW|=0.000e+00 shards=4 loss_equal=True x_grad_equal=True max|dx|=0.000e+00 max|dW|=0.000e+00 Also asserts the parameter gradients, which is the third comparison TestTiledMLPInputLayout makes. Signed-off-by: alanhuangyoo <alanhuangyoo@gmail.com>
|
@stas00 — this is the sibling of your #8348 in the same file, and it has had no reviewer for a week, so flagging rather than letting it sit. #8348 moved If you would rather fold it into something else you have in flight, that is fine by me — I mostly want to make sure it did not get skipped for looking like a duplicate of #8348. |
There was a problem hiding this comment.
Thank you for covering this class as well and the heads up, @alanhuangyoo!
Follow-up to #8348, in the same file.
That PR moved
TiledMLP.backwardoffviewbecause the flatten of batch and sequence into one axis needs a copy when a caller hands in a non-contiguous activation.TiledFusedLogitsLoss.forwarddoes the same flatten, under the same comment, and is still onview:A transposed activation — one of the two layouts #8348's test covers — fails there:
What is and is not reachable
I ran the four input shapes through
TiledFusedLogitsLoss.applyon master:xcontiguousxtransposedxchannel slicey/maskstridedOnly the transposed case is reachable today. A hidden-dimension slice keeps the wider row stride, and that still admits merging batch into sequence, so it survives — same for the strided
yandmaskI tried.yandmaskgo through the same flatten and are changed with it rather than left on a spelling that happens to hold.The unflatten at the end of
forwardstays aview:x_gradcomes fromzeros_like()of the already-flattenedx, so it is contiguous by construction. Same reasoning for thex_grad.view(x_shape_orig)that #8348 left alone inTiledMLP.backward.TiledLosshas no such flatten, so nothing to do there.Test
TestTiledFusedLogitsLossInputLayoutmirrorsTestTiledMLPInputLayoutand checks the loss against the same input made contiguous, so it pins the value and not just the absence of a throw.On master:
With this PR: