Skip to content

[AutoSP] Align cross entropy targets after an in-model causal label shift - #8811

Draft
jinyouzhi wants to merge 12 commits into
deepspeedai:masterfrom
jinyouzhi:autosp-label-shift
Draft

jinyouzhi wants to merge 12 commits into
deepspeedai:masterfrom
jinyouzhi:autosp-label-shift

Conversation

@jinyouzhi

@jinyouzhi jinyouzhi commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #8457. Only the last commit (6f7c57b9a) belongs to this PR; please review that commit. I will rebase once #8457 is merged.

Addresses #8457 (comment) (in-model causal label shift under AutoSP).

Problem

AutoSP shards the labels the same way as input_ids. A causal LM that shifts its labels inside the model (Hugging Face ForCausalLMLoss pads and shifts, labels = F.pad(labels, (0, 1), value=-100)[..., 1:]; Megatron-style code slices logits[..., :-1, :] / labels[..., 1:]) then shifts each shard on its own. Each rank pairs the last token of its shard with the wrong target, and the loss is silently wrong. #8457 documents pre-shifted labels as the workaround; this PR makes the in-model shift itself correct.

Approach

  • Labels are no longer sharded. Any shift the model applies to them runs on the full sequence, so the label subgraph computes exactly what it does without SP.
  • For each F.cross_entropy whose target derives from the labels:
    1. Walk from the CE input back to the sequence-sharded logits. Static sequence slices on the way (e.g. logits[..., :-1, :]) are removed, because each rank must keep its whole shard of logits. Their offsets give the window [start, S + stop) of global positions the CE input covered.
    2. Pad the full target with ignore_index back to the global positions of that window, then hand the CE this rank's shard [r*S/sp, (r+1)*S/sp). This needs no communication because the labels are replicated.
    3. The loss and gradient aggregation from [AutoSP] Aggregate the causal LM loss and gradients over the SP group #8457 is unchanged.
  • A static check makes sure the target lines up position by position with the logits window: the symbolic length of the target's sequence dimension must equal S + stop - start.
  • Anything outside this pattern raises instead of silently misaligning:
    • Only casts, contiguous, view/reshape/flatten (after any slices) and static sequence slices are allowed between the logits and the CE.
    • The labels may only flow into CE targets. They cannot be combined with sharded tensors such as input_ids, or returned from the module.
  • Padding uses narrow + full_like + cat instead of F.pad/constant_pad_nd, which specialize the symbolic sequence length during fake-tensor propagation.

Validation (8× Intel Arc Pro B70, torch 2.14.0.dev+xpu)

  • TestAutoSPCausalLoss compares the loss and the parameters after two optimizer steps against single-device training on the full sequence, over 4 ranks (sp=2, dp=2). New cases:

    • slice (logits[..., :-1, :] / labels[..., 1:])
    • pad (HF style)
    • each combined with ZeRO-1, sum/mean, an explicit mesh, and an empty shard
  • test_rejects_labels_it_cannot_align covers a strided logits slice and labels masked by input_ids.

  • test_compile_autosp.py: 45 sequential and 20 non-sequential tests pass. TestAutoSPCompile and TestAutoSPEngineBackward were excluded because they fail on this machine for environmental reasons (dynamo tracing of synchronize(); the XPU C++20 op build, fixed in Use C++20 for XPU op builders on torch 2.12+ #8803), identically on the base branch.

  • Tiny HF LlamaForCausalLM (transformers 5.17), unshifted labels, sp=2, also checked with dynamic=True + attention_mask:

    loss
    single device 4.840665
    [AutoSP] Aggregate the causal LM loss and gradients over the SP group #8457 alone 4.845766 (wrong)
    this PR 4.840665

Notes

  • Pre-shifted labels (shift_labels) remain supported and are still the simplest path.

Contribution checklist

Related work. This PR is stacked on #8457 and addresses its review thread linked above. A search for "AutoSP label shift" found no other PR.

Sibling paths checked. AutoSP exists only as a DeepCompile pass, so there is no eager variant to fix. #8457 rejects ZeRO-2/3, and the label-shift handling here inherits that restriction.

Commands and results. Hardware and software: 4× Intel Arc Pro B70 (XPU, XCCL), Level Zero driver 1.15.39122 (compute runtime 26.27.39122.11), torch 2.14.0.dev20260804+xpu, transformers 5.17.0.

pytest -p no:randomly -m sequential tests/unit/v1/compile/test_compile_autosp.py
pytest -p no:randomly tests/unit/v1/compile/test_compile_autosp.py

Results are listed under Validation above.

Numerics, performance, memory.

  • Numerics: the loss and the parameters after two optimizer steps match single-device training exactly. The HF Llama table above shows the end-to-end loss.
  • Memory: labels are now replicated instead of sharded. That adds [B, S] integer ids per rank, negligible next to the logits.
  • Throughput: the CE target is sliced locally, so no new collectives are added.

Compatibility matrix.

Validated Not validated
ZeRO stage 0, stage 1 stage 2/3 (rejected by #8457); offload
Precision fp32 bf16/fp16 for the label-shift cases (TestAutoSPCompile was excluded, see above)
Parallelism sp=2 with dp=2 (4 ranks); sp=2 with an explicit mesh sp>2 for the label-shift cases
Accelerator XPU CUDA, HPU

AI assistance. This PR was developed with AI assistance (GitHub Copilot CLI). I reviewed every changed line and ran the tests above.

jinyouzhi and others added 8 commits October 1, 2026 15:52
prepare_autosp_inputs() accepted seq_dim but only used it for
mark_dynamic: the graph passes always read the sequence length from
shape[1] and sliced dimension 1. Sequence-first [seq, batch] inputs
therefore sharded the batch dimension, or failed because that
dimension is static.

Dynamo copies only the `tag` attribute into node metadata, so record
seq_dim in the tag as (key, seq_dim). Tag lookup accepts both forms,
and a bare key keeps the previous default of dimension 1. The sequence
length and the slice now use the recorded dimension, and label_id is
bounds-checked like the other prepared inputs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
AutoSP sharded the labels before the model's causal shift. Each rank
therefore computed a local mean loss, which caused three problems:
- The last target of every shard was paired with a wrong token.
- Shards were weighted equally even when their numbers of valid
  (non-ignored) tokens differed.
- A shard with no valid tokens produced NaN.

Labels are now sharded where the cross entropy target still has the full
label shape, so the shift happens on the full sequence. The cross
entropy is rewritten to reduction="sum". autosp::aggregate_loss then
sums the loss and the valid-token count over the SP group and returns
the full-sequence loss. It supports both "mean" and "sum" reductions,
so the num_items_in_batch path of HF losses is covered too.

The engine averages gradients over every rank, SP ranks included.
Backward therefore scales the local gradient by sp_size / total_valid,
which makes the averaged gradient equal to the full-sequence gradient.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
… calls

Summing a half-precision cross entropy over a long shard overflows even when
the requested mean does not, so the per-token losses are now accumulated in
FP32 and the result is cast back to the logits dtype. Cross entropy calls
that pass input/target as keywords are now recognized instead of falling
back to sharding the labels before the shift.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
# Conflicts:
#	deepspeed/compile/custom_ops/all_to_all.py
#	deepspeed/compile/util.py
#	docs/code-docs/source/training.rst
#	tests/unit/v1/compile/test_compile_autosp.py
…he caller

Scope this PR to loss and gradient aggregation; label alignment under an
in-model causal shift moves to a follow-up PR.

- groups: with a (data_parallel, sequence_parallel) mesh and no mpu, the
  sequence-data-parallel group is the whole world, so gradients are summed
  over SP ranks. Previously it fell back to the DP-only mesh dimension
  while the engine still divided by dp/sp.
- initialize: create that mesh when the autosp pass is enabled with only
  sequence_parallel_size, so the implicit and explicit paths share the
  Ulysses convention (sum over SP, average over DP).
- autosp::aggregate_loss no longer scales the gradient by sp_size.
- engine: reject an mpu/mesh whose SP size differs from AutoSP's.
- sp_compile: drop the shape-based search for the full-length labels;
  labels are sharded as given and must be shifted on the full sequence.
- Tests: explicit mesh, an all-ignored shard, the SP-size mismatch, and
  the attention-mask gradient tests updated to the summed convention.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
@jinyouzhi
jinyouzhi marked this pull request as ready for review October 13, 2026 02:58
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-13T03:09:12.548972Z 6f7c57b Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f7c57b9a6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread deepspeed/__init__.py
logger.info(f"config to Initialize mesh device: {config}")
mesh_device = dist.initialize_mesh_device((config["data_parallel_size"], config["sequence_parallel_size"]), \
("data_parallel", "sequence_parallel"))
elif mpu is None and _autosp_sequence_parallel_size(config) > 1:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip SP mesh creation on the ZeRO fallback

When passes contains autosp, sequence_parallel_size > 1, and ZeRO stage is 2 or 3, this branch installs an SP mesh during initialization, but DeepSpeedEngine.get_autosp_backend() later returns None and uses ordinary torch.compile. The model is therefore not sequence-sharded while the engine/ZeRO gradient reduction still sums over the world group and divides only by the DP size, scaling the normal data-parallel update by sequence_parallel_size; reject these stages before installing the mesh or leave normal DP groups in place for the fallback.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the base PR #8457 (e17b176): deepspeed.initialize now raises when AutoSP (sequence_parallel_size > 1 with the autosp pass) is combined with ZeRO stage 2/3, before the SP mesh is installed, so it can no longer fall back to plain torch.compile with SP-scaled gradients. Covered by test_rejects_zero_stages_without_autosp[2,3]. This PR is rebased on it.

Comment on lines +356 to +358
valid_mask = gm.graph.call_function(operator.ne, args=(target_node, ignore_index))
with gm.graph.inserting_after(valid_mask):
valid_tokens = gm.graph.call_function(torch.sum, args=(valid_mask, ))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize probability targets by token count

When labels are floating class-probability targets (target.shape == input.shape), which F.cross_entropy supports, every class probability is unequal to the default ignore index, so this mask reports N*C valid tokens. Cross entropy's mean reduction divides by N/spatial positions, not by C as well, so aggregate_loss scales both the full loss and gradients down by the vocabulary size; detect this target form and count examples, or reject it, rather than counting target elements.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #8457 (e17b176): class-probability (floating point) targets are rejected with a clear error, since there is no ignore_index to count valid tokens. Covered by test_rejects_unsupported_cross_entropy.

Comment thread deepspeed/compile/passes/sp_compile.py Outdated
global_loss = gm.graph.call_function(operator.getitem, args=(aggregated, 0))
with gm.graph.inserting_after(global_loss):
# Return the loss in the dtype cross entropy would have produced.
output_loss = gm.graph.call_method("type_as", args=(global_loss, input_node))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve cross-entropy's autocast dtype

Under torch.autocast with FP16/BF16 logits, PyTorch executes cross entropy in FP32 and returns an FP32 loss, but this unconditional type_as casts the aggregated scalar back to the low-precision logits. That changes the model's output contract and can round or overflow a loss before downstream loss composition or scaling; preserve the dtype recorded on the original loss node instead.

AGENTS.md reference: AGENTS.md:L38-L38

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #8457 (e17b176): the aggregated loss is cast to the cross entropy output dtype (FP32 under autocast) instead of type_as(logits). Covered by TestAutoSPHalfPrecisionLoss::test_autocast_loss_keeps_cross_entropy_dtype (bf16 autocast).

"""
consumer = loss_node
target = _get_cross_entropy_arg(loss_node, "target", 1, None)
while _is_rank_preserving(target) or _is_reshape(target):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject target reshapes bound to the global sequence

When the CE target is reshaped with sequence-dependent explicit dimensions, such as labels.view(batch, seq_len) or reshape(batch * seq_len), this loop accepts the reshape, but _localize_cross_entropy_target inserts an S/sp shard before it without rewriting its size arguments. Fake propagation or the first forward then tries to reshape the local target to the original full-sequence size and fails; restrict accepted target reshapes to inferred dimensions or rewrite the sequence-sized arguments.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b166cd8: a reshape on the target path with explicit symbolic sizes (e.g. labels.view(batch, seq_len)) is now rejected with an error pointing to view(-1), instead of failing later with an opaque shape error on the shorter local target. view(-1)/reshape(-1) remain supported. Covered by test_rejects_labels_it_cannot_align[explicit_view].

weight = _get_cross_entropy_arg(loss_node, "weight", 2, None)
if weight is not None:
raise RuntimeError("AutoSP does not support class-weighted cross entropy on sequence-sharded labels")
reduction = _get_cross_entropy_arg(loss_node, "reduction", 6, "mean")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor legacy cross-entropy reduction overrides

When a model supplies F.cross_entropy's still-supported size_average or reduce arguments, those arguments override reduction inside PyTorch. This code only reads reduction and later writes "none", so, for example, size_average=False is parsed as mean here while PyTorch keeps producing a summed scalar that AutoSP divides by the token count; reduce=False is likewise silently converted from a per-token result to a scalar mean instead of being rejected. Resolve the legacy arguments before choosing the aggregate mode.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #8457 (e17b176): the deprecated size_average/reduce arguments are resolved with legacy_get_string (so e.g. size_average=False means sum and reduce=False is rejected as per-token), and cleared after the rewrite to reduction='none'. Covered by a size_average=False row in test_matches_full_sequence_reference and test_rejects_unsupported_cross_entropy.

jinyouzhi and others added 2 commits October 13, 2026 04:59
- Raise when AutoSP is combined with ZeRO stage 2/3, which falls back to
  plain torch.compile while the SP mesh is already installed.
- Reject class-probability targets, whose token count differs from the
  class-index path.
- Honor legacy size_average/reduce arguments when resolving the reduction.
- Cast the aggregated loss to the cross entropy output dtype instead of the
  logits dtype so autocast keeps the fp32 loss.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
…odel label shift

Keep the labels whole on every rank so that any shift the model applies to them,
such as the Hugging Face F.pad(labels, (0, 1), value=-100)[..., 1:], runs on the
full sequence. For each cross entropy on the labels, remove static sequence slices
between the sharded logits and the loss (e.g. logits[..., :-1, :]), pad the full
target to the global positions those slices covered, and hand the loss this
rank's shard of the padded target.

Only rank-preserving casts, reshapes after the slices, and static sequence slices
are allowed between the logits and the cross entropy, and the labels may only flow
into cross entropy targets. Any other pattern raises instead of silently pairing
logits with the wrong targets.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants