Skip to content

feat(model): dp balance - #355

Merged
meichangsu1 merged 8 commits into
modelscope:dev-swift-v5from
lincq2000:feat/dp-balance-v5
Nov 3, 2026
Merged

meichangsu1 merged 8 commits into
modelscope:dev-swift-v5from
lincq2000:feat/dp-balance-v5

Conversation

@lincq2000

@lincq2000 lincq2000 commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

PR type

  • Bug Fix
  • New Feature
  • Document Updates
  • More Models or Datasets Support

PR information

In RL training, every step waits for the slowest DP rank. Rollout lengths vary a lot, so the contiguous split slice_dp makes is often uneven. This PR adds an opt-in planner that reorders a mini-batch on the driver, before forward_backward, so that each rank gets a similar workload.

Feature

  • plan_dp_balance(...) returns a DPBalancePlan; apply it with plan.apply() to every sample-aligned field (inputs, old_logps, advantages, ...).
  • Each rank's cost is simulated with the model's own micro-batch planner, so it works for padded and packed inputs, with fixed or dynamic batching. The plan picks the cheapest of three orders (original, equal-size Karmarkar–Karp, snake by length) and reorders only if the slowest rank gets lighter.
  • Per-rank sample counts and slice_dp stay unchanged, so the gradient is unchanged for per-sequence-mean losses such as GRPOLoss. Adds dp_balance/* metrics.
  • Adds the cookbook/rl/grpo/dp_balance_grpo.{py,sh} example.

Bug fixes

  • TypeError: not a sequence with Qwen3.5 / Qwen-VL rollouts. dataset.encode stores mm_token_type_ids as a nested [1, L] list, and swift's append_and_finalize extends it flat, so its length no longer matches input_ids. The twinkle wrapper now converts it to an array first.
  • Packed samples attend to each other in forward_only. In eval mode use_cache is True and Transformers skips packed-sequence detection. forward_only now passes use_cache=False.

Experiment results

Environment: 4 × Ascend 910B3 (2 for training, 2 for vLLM), CANN 9.1.0, Python 3.12, torch 2.10.0 / torch_npu 2.10.0.post4, transformers 5.14.1, vLLM 0.26.0 / vllm-ascend 0.26.0rc2, flash-linear-attention 0.5.2.

Setup: Qwen3.5-4B GRPO on GSM8K, LoRA, FSDP2 with DP=2, dynamic batching, 50 steps. Each layout runs with balance_batch off and on, using the same seed.

layout balance_batch train ms / 1k tokens (median) iteration time mean reward mean loss mean grad norm
padded off 1155 161.9 s 0.556 0.027 0.063
padded on 913.5 (1.26x) 141.1 s (1.15x) 0.575 0.065 0.066
packed off 1395 194.9 s 0.552 0.017 0.063
packed on 923.4 (1.51x) 144.3 s (1.35x) 0.567 0.022 0.060

lincq2000 and others added 4 commits October 28, 2026 16:03
… packed inputs

- Add twinkle.model.plan_dp_balance: permute a batch before slice_dp so every
  DP rank gets a similar estimated cost. Each rank's cost is simulated with the
  model's own micro-batch planner (fixed or dynamic batching, common
  micro-batch count across ranks), padded micro-batches cost rows * w(longest)
  and packed ones sum(w(s)), with w(s) = 6*h*s + s^2. Candidates: original
  order, equal-size Karmarkar-Karp, and a length snake; the original order is
  kept unless the slowest rank gets lighter. Per-rank sample counts stay the
  same; DPBalancePlan.apply() reorders aligned fields. Adds dp_balance/*
  metrics, including micro-batch count and padding ratio.
- micro_batch: add plan_micro_batches_for_lengths and equal-size KK.
- Fix forward_only with padding-free inputs: pass use_cache=False so
  Transformers keeps detecting packed sequences in eval mode.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>EOF
git log --oneline -2
…mpletions

A dataset-encoded Qwen3.5 prompt stores the processor's [1, L] mm_token_type_ids as a nested list.
swift's append_and_finalize extends Python lists flat, giving a ragged [[0] * L, 0, 0, ...] that the
input processor cannot tensorize ("not a sequence"). Convert nested-list token fields to arrays first
so they are extended to [1, L + n], as numpy and torch fields already are.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Uses argparse (no twinkle.cli), scores rewards on new_state.messages, and installs the fla kernels on
NPU for packed inputs through apply_patch.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Keep the sequence-parallel context from dev-swift-v5 around forward_only's model call, with
use_cache=False inside it.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
return DPBalancePlan(order=identity)

# Ties keep the earlier candidate, so the batch is only permuted when the slowest rank gets lighter.
strategy = min(estimates, key=lambda name: (max(estimates[name][1].workloads), estimates[name][1].micro_batches))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

batch 不能被 DP 数整除时,重排会改变 GRPO 梯度权重

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

不能被 DP 数整除时,已改为不重排,保持原顺序

with self.sp_strategy.context() if self.sp_strategy is not None else contextlib.nullcontext():
outputs = self.model(**inputs)
# In eval mode use_cache defaults to True, and Transformers skips detecting
# padding-free packed sequences whenever a KV cache exists.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

这里为什么要use_cache=False

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

padding-free 时多条样本拼成一行,没有 attention_mask,样本边界靠 position_ids 区分,transformers 只有在 past_key_values is None 时才按 position_ids 切分。
forward_only 在 eval 模式下,默认use_cache=True,模型内部会 past_key_values = DynamicCache(上面的条件不成立)导致整行按一条长序列的普通 causal mask 计算。forward_only 用不到 KV cache。

lincq2000 and others added 4 commits October 30, 2026 17:59
GRPOLoss averages over a rank's samples and DP averages the ranks, so a sample weighs
1 / (dp_size * rank_size). When get_slice gives some ranks one sample more, moving samples
between ranks changes their weights, so plan_dp_balance now returns the original order there.
This also drops the placeholders and size sorting that only uneven splits needed.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
The uneven-batch check ran after the group-size check, so a grouped batch whose size is not a
multiple of dp_size raised ValueError instead of keeping its order as documented.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Without gradient checkpointing, use_cache stays True in the training
forward, so Transformers skips detecting padding-free packed sequences
and packed samples attend to each other. Pass use_cache=False as
forward_only already does.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Make the common micro-batch count test fail if ranks are estimated
  without it: use padded rows where the rank 1 cost changes.
- Reuse the default fla kernel choice in the cookbook.
- Reuse micro_batch._batch_cost for computed tokens.
- Drop the unused metric_prefix option.
- Add the copyright header to test_contract_append.py.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@meichangsu1
meichangsu1 merged commit c5d8e50 into modelscope:dev-swift-v5 Nov 3, 2026
0 of 5 checks passed
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