Skip to content

fix: avoid ZeroDivisionError in compute_elastic_config return_microbatch path - #8162

Closed
Solaris-star wants to merge 1 commit into
deepspeedai:masterfrom
Solaris-star:fix/8156-elastic-return-microbatch-zerodiv
Closed

Solaris-star wants to merge 1 commit into
deepspeedai:masterfrom
Solaris-star:fix/8156-elastic-return-microbatch-zerodiv

Conversation

@Solaris-star

Copy link
Copy Markdown

Description

compute_elastic_config(..., return_microbatch=True) on elasticity version 0.1 (and any non-0.2 version) divided by the default world_size=0 and raised a bare ZeroDivisionError. Version 0.2 already has a dedicated microbatch path that does not need this division.

Changes

  • For non-0.2 versions, require a positive world_size argument or numeric WORLD_SIZE env before selecting a microbatch.
  • Raise ElasticityConfigError with a clear message instead of ZeroDivisionError.
  • Validate world_size against valid_gpus before dividing.
  • Add unit tests for the missing-world_size error path and the successful path with world_size=64.

Related issue

Fixes #8156

Tests

  • Added test_return_microbatch_requires_world_size_v01
  • Added test_return_microbatch_with_world_size_v01
  • Offline pure-logic verification of the fixed branch (full DeepSpeed suite needs torch/GPU CI)

…tch path

For elasticity versions other than 0.2, return_microbatch=True with the
default world_size=0 divided by zero. Require a positive world_size (or
WORLD_SIZE) and raise ElasticityConfigError instead.

Fixes #8156

Signed-off-by: Solaris-star <820622658@qq.com>
banxingmjj pushed a commit to openanolis/DeepSpeed that referenced this pull request Aug 12, 2026
…8237)

## Problem

`_get_compatible_gpus_v01` validates every micro batch against
`max_acceptable_batch_size`:

```python
if not all(mb <= max_acceptable_batch_size for mb in micro_batches):
    raise ValueError(...)
```

but the first heuristic scales the **LCM** of the micro batches, and the
LCM is never checked. It goes into `base_list` and reaches
`get_candidate_batch_sizes`, where the `base >=
max_acceptable_batch_size` branch appends it unscaled. So a batch size
the caller already said was too large becomes a candidate, and since the
LCM divides every micro batch it tends to win the most-valid-GPU-counts
vote in `get_best_candidates`.

The docstring says the heuristic produces "the largest batch size less
than the max_acceptable batch size", and `config-json.md` documents
`max_train_batch_size` as "Max acceptable batch size can be used in
training", so the returned value is not supposed to exceed it.

## Repro

```python
import deepspeed
from deepspeed.git_version_info import version as ds_version

ds_config = {"elasticity": {"enabled": True, "max_train_batch_size": 100,
                            "micro_batch_sizes": [8, 10, 12], "min_gpus": 1,
                            "max_gpus": 1500, "min_time": 20, "version": 0.1}}

print(deepspeed.elasticity.compute_elastic_config(ds_config=ds_config,
                                                  target_deepspeed_version=ds_version))
# (120, [...])    <- 120 against a declared max of 100
```

`DeepSpeedConfig.__init__` writes that return value straight into
`self._param_dict[TRAIN_BATCH_SIZE]`, so the job runs 20 percent over
the limit the user set, with the matching effect on the LR schedule and
step count.

It is not an exotic corner. `[8, 12]` with a cap of 16 gives 24, and a
brute-force sweep over micro batch sets of size 2 and 3 drawn from 1 to
32, against every cap up to 400, finds it in 946105 configurations.

The clearest evidence is in this repo:
`tests/unit/elasticity/test_elastic.py::test_proper_mbsz` sets
`max_train_batch_size` to 32 with micro batches `[1, 2, 3, 7]`, whose
LCM is 42, and gets 42 back today.

## Fix

Skip a base larger than the cap. Scaling one can only make it bigger, so
it can never yield a legal candidate, and every micro batch is already
validated against the cap, so the candidate list cannot end up empty.

## What this changes for the existing tests

`test_basic_10k` is unaffected: still 9792, still 23 valid GPU counts.

`test_proper_mbsz` needed one number changed, and I want to be upfront
about it rather than bury it. Its `world_size=7` was only reachable
because the batch size came back as 42, over its own cap of 32; at any
legal batch size for that config, 7 is not a valid GPU count. I changed
it to 4, where the batch per GPU is 6, so 7 is still correctly ruled out
and the assertion that 3 is chosen is unchanged. That keeps the test
doing what it was written to do, which is check the micro batch picked
for a given world size.

If you would rather keep `world_size=7` working, then the LCM overshoot
is load-bearing rather than a bug, and this PR is the wrong change; I
would want to hear that before going further. I could not find a config
for those micro batches that makes 7 valid without exceeding the cap.

`test_batch_size_within_max` is new and pins the actual contract.

## Test

```
                                                  before      after
test_basic_10k                                    PASS        PASS
test_proper_mbsz  (world_size=7, the old value)   PASS        ElasticityIncompatibleWorldSize
test_proper_mbsz  (world_size=4, the new value)   ElasticityIncompatibleWorldSize   PASS
test_batch_size_within_max  (new)                 FAIL: 120 exceeds 100             PASS
```

Run on CPU by driving the test bodies against the real
`compute_elastic_config`, once against `master` and once against this
branch; this path is pure Python and needs no GPU. `yapf --style
.style.yapf` and `flake8 --config .flake8` are clean on both changed
files, and clean on the unmodified tree as a control.

There is one other open PR touching this file, deepspeedai#8162, in
`compute_elastic_config`'s `return_microbatch` tail. It does not overlap
these lines.

Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>
@Solaris-star Solaris-star closed this by deleting the head repository Aug 12, 2026
banxingmjj pushed a commit to openanolis/DeepSpeed that referenced this pull request Aug 23, 2026
…non-0.2 elasticity (deepspeedai#8286)

## Root cause

With `return_microbatch=True`, `compute_elastic_config` takes the `else`
at `elasticity.py:372` for any elasticity version other than `0.2`, and
that branch divides `final_batch_size` by `world_size`. In practice that
means version `0.1`: `0.3` is rejected at line 301 and any other value
raises `NotImplementedError` at line 348, both before this point. The
branch is only reachable when `world_size` is unset, because the `if
world_size > 0:` block above it returns for every positive value. So on
master the division is always by zero and the caller gets a bare
`ZeroDivisionError` at line 375 rather than a configuration error.

Version `0.2` avoids this by resolving `world_size` from the
`WORLD_SIZE` environment variable at lines 321-334, and raising
`ElasticityConfigError` naming that variable when it cannot. Version
`0.1` never reads the environment, so a caller who follows the `0.2`
message's own advice, "set it as an environment variable", still
crashes:

| config | `WORLD_SIZE` in env | master |
|---|---|---|
| 0.2 | yes | returns `(9792, [...], 17)` |
| 0.2 | no | `ElasticityConfigError` naming `WORLD_SIZE` |
| 0.1 | yes | `ZeroDivisionError` |
| 0.1 | no | `ZeroDivisionError` |

## Fix

Resolve `world_size` from `WORLD_SIZE` in the non-`0.2` branch the way
`0.2` already does, and raise `ElasticityConfigError` with the same
guidance when it cannot be resolved. Then check the resolved value
against `valid_gpus` before dividing, matching the sibling block at
lines 352-355; without that check an out-of-range `WORLD_SIZE` would
reach the loop and fail on the `micro_batch_size is not None` assertion
instead of `ElasticityIncompatibleWorldSize`.

Both divisions by `world_size` in this function now run only on a value
that is positive and a member of `valid_gpus`. Nothing that works today
changes: on master this branch raised `ZeroDivisionError` for every
input, and the `0.2` path is untouched.

## Verification

- Three new tests in `tests/unit/elasticity/test_elastic.py` cover the
unset case, resolution from `WORLD_SIZE`, and an out-of-range
`WORLD_SIZE`. All three fail on master with `ZeroDivisionError` at
`elasticity.py:375` and pass here.
- `pytest unit/elasticity/` gives 23 passed, 3 skipped, on Python 3.12
with `torch==2.10.0+cpu` to match the `cpu-torch-latest` leg. The 3
skips are the `DistributedTest` classes that need `FusedLambBuilder`,
and they skip on master too.
- `pre-commit run --files` passes on both changed files, yapf, flake8,
codespell and `check-torchdist` included.
- Not checked: the GPU legs, and the `0.2` `return_microbatch` return at
line 371, which no test in the repo reaches either before or after this
change.

deepspeedai#8162 proposed the same resolution in July, and its author closed it
unmerged on 2026-08-12 without a review.

Fixes deepspeedai#8156

Signed-off-by: Ehsan Barkhordar <realbarkhordar@gmail.com>
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.

compute_elastic_config raises bare ZeroDivisionError for return_microbatch=True on non-0.2 elasticity versions without explicit world_size

1 participant