Skip to content

Add an AutoTP equivalence check for uneven TP size - #1008

Open
jinyouzhi wants to merge 1 commit into
deepspeedai:masterfrom
jinyouzhi:uneven_autotp
Open

Add an AutoTP equivalence check for uneven TP size#1008
jinyouzhi wants to merge 1 commit into
deepspeedai:masterfrom
jinyouzhi:uneven_autotp

Conversation

@jinyouzhi

@jinyouzhi jinyouzhi commented Aug 6, 2026

Copy link
Copy Markdown

Motivation

The existing AutoTP smoke tests check that a sharded run exits cleanly. That is a
weak signal: a shard cut on the wrong boundary also exits cleanly, it just trains
a different model. This adds a check that compares what the runs actually compute.

What it does

train.py trains one model for N steps and records every step's loss. Running it
at AutoTP=1 and AutoTP=3 and diffing the curves exercises the uneven split of
Qwen3's 16 attention / 8 KV heads (6/6/4 per rank), which is where a shard
boundary is most likely to be computed wrongly. Even splits divide every dimension
exactly and hide off-by-one bugs; an uneven split also forces the head count,
rather than the raw element count, to drive the partition. Splitting q_proj's
2048 output features as 683/683/682 would land mid-head and corrupt the attention
reshape.

Why there is a control run

A tolerance alone cannot settle whether an uneven split is accurate, only
whether it fits under a number chosen after the fact. So the runs also include
AutoTP=4 as a control. It divides evenly, so its gap to the baseline is pure
floating-point reassociation, which calibrates what the uneven split ought to cost.

Results — 500 steps, Qwen3-0.6B, fp32

Backend Sharding seq_len step 0 mean rel worst rel
NCCL, GPUs AutoTP=3 (uneven) 128 0.00e+00 7.69e-05 2.52e-03 (step 467)
NCCL, GPUs AutoTP=4 (even, control) 128 0.00e+00 7.58e-05 2.72e-03 (step 467)
gloo, CPU AutoTP=3 (uneven) 64 6.82e-08 2.75e-05 1.15e-03 (step 379)
gloo, CPU AutoTP=4 (even, control) 64 6.82e-08 2.79e-05 2.39e-03 (step 379)

The uneven split and the even control land on top of each other. Their means agree
to within a few percent, and on both backends the worst step is the same step
(467 on GPU, 379 on CPU), with the even control the further of the two from
the baseline there. The spike belongs to the training trajectory at that point,
not to how the heads were divided.

So the conclusion is not merely "AutoTP=3 stayed under the tolerance". It is that
splitting 16 heads unevenly across 3 ranks costs nothing in accuracy beyond what
an evenly-divisible tensor-parallel run already costs.

The GPU runs match exactly at step 0 while the CPU runs differ by 6.82e-08:
gloo reduces in a different order than a single rank does, so the sharded forward
is not bit-identical there. That is why the forward check uses a small tolerance
rather than demanding equality.

What is pinned, and why

Anything that could make the runs diverge for a reason other than sharding is fixed:

Data Batches come from a CPU generator seeded identically on every rank, so all ranks and all runs see the same tokens in the same order. AutoTP replicates the input across the tensor-parallel group, so ranks that disagreed on the batch would invalidate the comparison.
Precision fp32. bf16 rounding noise is orders of magnitude larger than the reassociation error being measured, and would mask a real bug.
Dropout Asserted to be zero at startup, so no RNG is consumed inside the forward.
Parallelism world_size must equal autotp_size. Spare ranks would silently become a data-parallel dimension, averaging gradients over more samples and changing what is being compared.
Threads The CPU runs cap OMP_NUM_THREADS identically at every width. Without it each rank sizes its thread pool for the whole machine and several ranks oversubscribe it: 27s per step instead of 1.5s.

The training data is random tokens, so the loss itself is meaningless. What matters
is only that differently-sharded runs agree on it.

How the comparison reads

compare_loss.py checks every step rather than the final loss, because
reassociation error jitters while a sharding bug compounds. It checks the first
step
far more tightly (--forward-rtol, default 1e-6): both runs start from the
same weights and no optimizer step has happened yet, so a gap there is a wrong
forward, not accumulated drift, and training dynamics cannot be blamed for it.

Usage

cd training/autotp_equivalence
bash run_gpu.sh 500 0,1,2,3   # NCCL
bash run_cpu.sh 500           # gloo
python -m pytest tests/ -v

Testing

  • 500 steps at AutoTP=1/3/4 on GPUs (NCCL) and on CPU ranks (gloo); results above,
    reproducible bit-for-bit across repeated runs.
  • 12 unit tests for compare_loss.py: accepts reassociation-scale noise, rejects a
    compounding gap, catches a wrong forward at the first step without mistaking later
    drift for one, reports the worst step rather than the last, and never hides the
    worst step when sampling a long run.

Existing AutoTP smoke tests check that a sharded run exits cleanly. That is a
weak signal: a shard cut on the wrong boundary also exits cleanly, it just
trains a different model. Compare the loss curves instead, which separates a
correct partition from a plausible-looking wrong one.

train.py trains one model for N steps and records every step's loss. Running it
at AutoTP=1 and AutoTP=3 and diffing the curves tests the uneven split of
Qwen3's 16 attention / 8 KV heads (6/6/4 per rank), where a shard boundary is
most likely to be computed wrongly -- an even split divides every dimension
exactly and hides off-by-one bugs.

A tolerance alone would not settle whether the uneven split is accurate, only
whether it fits under a number chosen after the fact, so the runs also include
AutoTP=4 as a control. It divides evenly, so its gap to the baseline is pure
floating-point reassociation and calibrates what the uneven split should cost.
Over 500 steps the two land on top of each other: means within a few percent,
and the same worst step on both backends (467 on GPU, 379 on CPU), with the even
control the further of the two from the baseline at that step. The spike belongs
to the training trajectory, not to how the heads were divided.

Everything that could make the runs diverge for another reason is pinned: same
seed and batch order across ranks, fp32, dropout asserted zero, world_size
required to equal autotp_size so spare ranks cannot silently add data
parallelism, and a fixed thread cap on CPU.

compare_loss.py checks every step rather than the final loss, because
reassociation error jitters while a sharding bug compounds. It checks the first
step far more tightly, since both runs start from the same weights there and
training dynamics cannot yet have amplified anything.

Verified on 3 and 4 GPUs with NCCL and on 3 and 4 CPU ranks with gloo, 500 steps
each.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: youzhiji <youzhiji@5090d-8.sh.intel.com>
@jinyouzhi

jinyouzhi commented Aug 17, 2026

Copy link
Copy Markdown
Author

test deepspeed #8185 c02df7ff84799c94f8decf50c2511811cfcf01df

===== AutoTP=3 (uneven 6/6/4) vs AutoTP=1 =====
  step      baseline     candidate         rel
     0     14.139614     14.139614    0.00e+00
   100     11.975559     11.975557    1.59e-07
   200     11.962918     11.962900    1.51e-06
   300     11.974757     11.975319    4.69e-05
   400     11.922600     11.921727    7.32e-05
   467     11.938583     11.968750    2.53e-03  <- worst
   499     11.925266     11.931254    5.02e-04

steps=500 first_rel=0.00e+00 mean_rel=7.69e-05 worst_rel=2.53e-03 at step 467
OK: 500 steps agree within 1e-02

===== AutoTP=4 (even 4/4/4/4, control) vs AutoTP=1 =====
  step      baseline     candidate         rel
     0     14.139614     14.139614    0.00e+00
   100     11.975559     11.975561    1.59e-07
   200     11.962918     11.962908    8.77e-07
   300     11.974757     11.975370    5.12e-05
   400     11.922600     11.921721    7.37e-05
   467     11.938583     11.971050    2.72e-03  <- worst
   499     11.925266     11.930954    4.77e-04

steps=500 first_rel=0.00e+00 mean_rel=7.58e-05 worst_rel=2.72e-03 at step 467
OK: 500 steps agree within 1e-02

Run on 4x RTX 5090

@jinyouzhi
jinyouzhi marked this pull request as ready for review August 17, 2026 17:45
@jinyouzhi
jinyouzhi requested a review from tjruwase as a code owner August 17, 2026 17:45
@PKUWZP

PKUWZP commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

test deepspeed #8185 c02df7ff84799c94f8decf50c2511811cfcf01df

===== AutoTP=3 (uneven 6/6/4) vs AutoTP=1 =====
  step      baseline     candidate         rel
     0     14.139614     14.139614    0.00e+00
   100     11.975559     11.975557    1.59e-07
   200     11.962918     11.962900    1.51e-06
   300     11.974757     11.975319    4.69e-05
   400     11.922600     11.921727    7.32e-05
   467     11.938583     11.968750    2.53e-03  <- worst
   499     11.925266     11.931254    5.02e-04

steps=500 first_rel=0.00e+00 mean_rel=7.69e-05 worst_rel=2.53e-03 at step 467
OK: 500 steps agree within 1e-02

===== AutoTP=4 (even 4/4/4/4, control) vs AutoTP=1 =====
  step      baseline     candidate         rel
     0     14.139614     14.139614    0.00e+00
   100     11.975559     11.975561    1.59e-07
   200     11.962918     11.962908    8.77e-07
   300     11.974757     11.975370    5.12e-05
   400     11.922600     11.921721    7.37e-05
   467     11.938583     11.971050    2.72e-03  <- worst
   499     11.925266     11.930954    4.77e-04

steps=500 first_rel=0.00e+00 mean_rel=7.58e-05 worst_rel=2.72e-03 at step 467
OK: 500 steps agree within 1e-02

Very cool work, can you elaborate your testing environment, e.g. how many GPUs did you use to test out AutoTP?

@jinyouzhi

Copy link
Copy Markdown
Author

test deepspeed #8185 c02df7ff84799c94f8decf50c2511811cfcf01df

===== AutoTP=3 (uneven 6/6/4) vs AutoTP=1 =====

step baseline candidate rel

 0     14.139614     14.139614    0.00e+00

100 11.975559 11.975557 1.59e-07

200 11.962918 11.962900 1.51e-06

300 11.974757 11.975319 4.69e-05

400 11.922600 11.921727 7.32e-05

467 11.938583 11.968750 2.53e-03 <- worst

499 11.925266 11.931254 5.02e-04

steps=500 first_rel=0.00e+00 mean_rel=7.69e-05 worst_rel=2.53e-03 at step 467

OK: 500 steps agree within 1e-02

===== AutoTP=4 (even 4/4/4/4, control) vs AutoTP=1 =====

step baseline candidate rel

 0     14.139614     14.139614    0.00e+00

100 11.975559 11.975561 1.59e-07

200 11.962918 11.962908 8.77e-07

300 11.974757 11.975370 5.12e-05

400 11.922600 11.921721 7.37e-05

467 11.938583 11.971050 2.72e-03 <- worst

499 11.925266 11.930954 4.77e-04

steps=500 first_rel=0.00e+00 mean_rel=7.58e-05 worst_rel=2.72e-03 at step 467

OK: 500 steps agree within 1e-02

Very cool work, can you elaborate your testing environment, e.g. how many GPUs did you use to test out AutoTP?

Thank you for your reviewing. That was completed on 4x RTX 5090.

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