Skip to content

[BUG] Curriculum data sampler doesn't restore its RNG on resume, so batches change after loading a checkpoint #8787

Open

Description

@Rakshit-gen

Describe the bug

If you train with curriculum learning and resume from a checkpoint, the data sampler doesn't pick up where it left off. The batches after the resume are different from the ones an uninterrupted run would have seen.

DeepSpeedDataSampler makes all of its random choices with its own generator:

self.np_rng = np.random.default_rng(self.data_efficiency_config[DATA_EFFICIENCY_SEED])

It uses it to shuffle a new cluster, to pick how many samples come from each cluster, to shuffle each global batch, and to reshuffle a cluster once it runs out.

But state_dict() saves numpy's global RNG, which the sampler never draws from:

CURRICULUM_LEARNING_NP_RNG_STATE: np.random.get_state()

and load_state_dict() puts that back into the global RNG:

np.random.set_state(state_dict[CURRICULUM_LEARNING_NP_RNG_STATE])

So after a resume, self.np_rng starts again from the seed, and the sampler repeats the random choices it made at the start of training instead of continuing them. Loading also rolls numpy's global RNG back to whatever it was at save time, which can surprise other code in the training script.

Nothing errors or warns. You only notice when a resumed run doesn't line up with one that never stopped.

To Reproduce

This runs on CPU in a single process. With one rank there's nothing to broadcast, so the two collectives are stubbed out.

import tempfile
import numpy as np
import deepspeed.comm as dist
from deepspeed.runtime.data_pipeline.data_sampling.data_sampler import DeepSpeedDataSampler

dist.barrier = lambda *a, **k: None
dist.broadcast = lambda *a, **k: None


class Group:

    def size(self):
        return 1


def sampler(path):
    config = {"seed": 1234, "data_sampling": {"num_epochs": 3, "curriculum_learning": {
        "enabled": True, "data_cluster_path": path, "curriculum_metrics": {"m": {
            "index_to_sample_path": "x", "index_to_metric_path": "x",
            "difficulty_type": "value", "clustering_type": "single_cluster",
            "min_difficulty": 2, "max_difficulty": 10, "schedule_type": "fixed_root",
            "schedule_config": {"total_curriculum_step": 8, "difficulty_step": 2, "root_degree": 1}}}}}}
    return DeepSpeedDataSampler(config, one_epoch_total_samples=40, micro_batch_size=4,
                                data_parallel_rank=0, data_parallel_size=1, data_parallel_group=Group(),
                                gradient_accumulation_steps=1, global_rank=0)


with tempfile.TemporaryDirectory() as d:
    it = iter(sampler(d))
    straight = [next(it) for _ in range(30)]

with tempfile.TemporaryDirectory() as d:
    s = sampler(d)
    it = iter(s)
    resumed = [next(it) for _ in range(15)]
    state = s.state_dict()
    s2 = sampler(d)
    s2.load_state_dict(state)
    it2 = iter(s2)
    resumed += [next(it2) for _ in range(15)]

print([i for i in range(30) if straight[i] != resumed[i]])

On master this prints [15, 16, 17, 19, 20, ..., 29], so 14 of the 15 batches after the resume differ. At first only the order inside each batch changes. With more than one data parallel rank, that already changes which rank trains on which sample. From batch 20 on, after the cluster gets reshuffled, the batches hold different samples altogether. With several difficulty clusters, the number of samples taken from each cluster drifts as well.

Expected behavior

A resumed run should get the same batches as a run that never stopped. That's what saving the sampler state is for.

A fix is to save self.np_rng.bit_generator.state and restore it in load_state_dict(). Older checkpoints only have the global state, so loading those can keep the current behavior and log a warning. I'll open a PR with this and a test.

ds_report output

torch version .................... 2.14.0
deepspeed info ................... 0.19.6, master at b7f51df

System info (please complete the following information):

  • OS: macOS 26.6
  • GPU count and types: none, this reproduces on CPU
  • Python version: 3.11

Launcher context

No launcher. The snippet above runs with plain python.

Docker context

No.

Additional context

Only people who use curriculum learning in data_efficiency and resume from checkpoints are affected.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions