Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions deepspeed/moe/sharded_moe.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ def top1gating(logits: Tensor,
new_capacity = torch.ceil(new_capacity / tp).mul(tp).to(new_capacity.dtype)
# Make sure the capacity value does not exceed the number of tokens.
capacity = min(new_capacity, torch.tensor(mask1.size(0)).to(new_capacity.device))
else:
# Same guard for the drop branch: capacity feeds torch.topk(..., dim=0) over the token
# dimension in _top_idx below, which requires capacity <= num_tokens.
capacity = min(capacity, torch.tensor(mask1.size(0)).to(capacity.device))

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 dispatch capacity when clamping top-k

When tensor-model parallelism is enabled, MOELayer.forward later calls drop_tokens on the capacity dimension, and drop_tokens asserts that this dimension is divisible by the TP world size (deepspeed/moe/mappings.py:64-65). For top-1 with drop_tokens=True, capacity_factor > num_experts, and a local token count that is not TP-divisible, this line sets the dispatch capacity to num_tokens; _top_idx succeeds, but the subsequent TP drop still fails. Clamp only the k passed to _top_idx (or repad the dispatch capacity) instead of reusing the clamped value for _one_hot_to_float.

Useful? React with 👍 / 👎.


# Compute l_aux
me = torch.mean(gates, dim=0)
Expand Down Expand Up @@ -403,6 +407,11 @@ def topkgating(
if drop_tokens:
# Calculate configured capacity and remove locations outside capacity from mask
capacity = _capacity(gates, torch.tensor(capacity_factor * k), torch.tensor(min_capacity))
# Make sure the capacity value does not exceed the number of tokens. The drop_policy=='probs'
# branch below selects tokens with torch.topk(..., k=capacity, dim=0) over the token
# dimension, which requires capacity <= num_tokens; #5353 added this same clamp to
# top1gating's no-drop branch but not to the drop branches.
capacity = min(capacity, torch.tensor(gates.size(0)).to(capacity.device))

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 Keep the clamp out of the position policy

When callers use topkgating(..., drop_policy='position') with a configured capacity or min_capacity greater than the number of tokens, this clamp changes the returned dispatch capacity even though that branch never passes capacity to torch.topk(..., dim=0). Previously, for example, min_capacity=8 with 4 tokens returned a capacity width of 8; now it returns 4, violating the min-capacity/padding behavior and removing padding that downstream code may require. Move the clamp into the probs branch or use a separate top-k selection limit.

Useful? React with 👍 / 👎.

# update mask and locations by capacity

if drop_policy == 'probs':
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/moe/test_moe.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,34 @@ def check_equal(logits, cap, sparse_truth, res):
check_equal(logits3, 4, sec_sparse, dispatch_res)


# #5353 clamped capacity to the number of tokens only in top1gating's no-drop branch. The drop
# branches feed capacity to torch.topk(..., dim=0) over the token dimension, which requires
# capacity <= num_tokens, so when capacity_factor * k > num_experts the capacity computed as
# ceil(num_tokens / num_experts * capacity_factor * k) exceeds num_tokens and torch.topk raises
# "selected index k out of range". These single-process gating checks fail on the unpatched
# branches and pass once the same clamp is applied there.
def test_topkgating_probs_capacity_exceeds_num_tokens():
# s=8, e=2, k=2, capacity_factor=2 -> capacity = ceil(8/2 * (2*2)) = 16 > 8.
num_tokens, num_experts, k = 8, 2, 2
logits = torch.randn(num_tokens, num_experts)
_, _, dispatch_mask, _ = topkgating(logits, k, capacity_factor=2.0, min_capacity=0, drop_policy='probs')
# Capacity is clamped to num_tokens, and no routed token is spuriously dropped
# (every token keeps all k experts because capacity now covers all tokens).
assert dispatch_mask.shape[-1] == num_tokens
assert int(dispatch_mask.sum()) == num_tokens * k


def test_top1gating_drop_capacity_exceeds_num_tokens():
# s=8, e=2, capacity_factor=4 -> capacity = ceil(8/2 * 4) = 16 > 8. This is the
# top1gating drop branch, which reaches torch.topk via _top_idx.
num_tokens, num_experts = 8, 2
logits = torch.randn(num_tokens, num_experts)
_, _, dispatch_mask, _ = top1gating(logits, capacity_factor=4.0, min_capacity=0, drop_tokens=True, use_rts=False)
assert dispatch_mask.shape[-1] == num_tokens
# top1 routes each token to exactly one expert; with capacity == num_tokens none are dropped.
assert int(dispatch_mask.sum()) == num_tokens


class TestExpertWeightGradWithZero(DistributedTest):
world_size = 2

Expand Down
Loading