Skip to content

Fix deepcopy bottleneck in state_dict() - #8387

Open
yuxin00j wants to merge 1 commit into
huggingface:mainfrom
yuxin00j:fix-deepcopy-state-dict
Open

Fix deepcopy bottleneck in state_dict()#8387
yuxin00j wants to merge 1 commit into
huggingface:mainfrom
yuxin00j:fix-deepcopy-state-dict

Conversation

@yuxin00j

@yuxin00j yuxin00j commented Aug 4, 2026

Copy link
Copy Markdown

Fix #8393
This PR significantly resolves a major CPU bottleneck in IterableDataset dataloading when buffer_size and max_buffer_input_shards are > 1.

The Bug:
Currently, _BaseExamplesIterable.state_dict() falls back to performing a full recursive copy.deepcopy(self._state_dict).
In nested iterator chains (like BufferShuffledExamplesIterable wrapping CyclingMultiSourcesExamplesIterable wrapping multiple ArrowExamplesIterable shards), the state_dict() is queried and updated on every yielded row.

Because copy.deepcopy relies on Python's pickle mechanics for object introspection and memoization, it incurs significant overhead. Worse, pickle often crashes when it encounters unpicklable objects like generators. When mbis is scaled up and the dictionary size grows, the deepcopy completely blocks the CPU, hiding underneath the shuffle buffer processing.

The Fix:
We replaced the expensive deepcopy() logic with a dynamic, lightweight _fast_copy() helper directly inside _BaseExamplesIterable.state_dict().

Because dataset state_dicts strictly consist of standard collections and primitive state objects (and importantly, contain no circular references), _fast_copy() manually recursively walks dict and list structures. For any other object, it natively returns the object (or uses its .copy() method if available), bypassing pickle and deepcopy entirely.

This allows us to dynamically serialize state without explicitly overriding state_dict in every single dataset subclass, restoring maintainability and drastically improving speed.

Impact:
We measured workloads using a high number of concurrent shards (max_buffer_input_shards=10):

import datasets
from torch.utils.data import DataLoader
from datasets.distributed import split_dataset_by_node

ds = datasets.load_dataset(
    "parquet",
    data_files="gs://your-bucket/benchmark_data/shard_*.parquet",
    split="train",
    streaming=True
)

ds = ds.shuffle(seed=0, buffer_size=10000, max_buffer_input_shards=10)
ds = split_dataset_by_node(ds, rank=0, world_size=8)
ds = ds.with_format("torch")

loader = DataLoader(
    ds,
    batch_size=8,
    num_workers=4,
    prefetch_factor=2,
    persistent_workers=True,
)

for epoch in range(3):
    ds.set_epoch(epoch)
    for batch in loader:
        pass
  • Before this PR: ~103.5 seconds per epoch.
  • After this PR: ~65.8 seconds per epoch.

This single PR shaves ~38 seconds of pure CPU overhead off the workload, generating a 36% overall speedup, entirely restoring the expected linear scaling of shuffle buffers when reading across many shards!

@yuxin00j
yuxin00j force-pushed the fix-deepcopy-state-dict branch 5 times, most recently from 2ebcc1a to ec9bb04 Compare August 4, 2026 05:51
This commit implements explicit state_dict serialization across the IterableDataset iterators, replacing the previous fallback logic that relied on copy.deepcopy() of python generators, which raised Pickling errors.
@yuxin00j
yuxin00j force-pushed the fix-deepcopy-state-dict branch from ec9bb04 to a28ff3e Compare August 4, 2026 05:53
@yuxin00j yuxin00j changed the title Fix deepcopy bottleneck in IterableDataset.state_dict() during shuffle Fix deepcopy bottleneck in state_dict() Aug 4, 2026
if self._state_dict:
return deepcopy(self._state_dict)

def _fast_copy(state):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

How about moving this func to module level for reusability?

return [_fast_copy(v) for v in state]
elif hasattr(state, "copy"):
return state.copy()
return state

@zhixiangli zhixiangli Aug 4, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We need to throw TypeError for unexpected types, such as those outside of the expected dict, list, tuple, primitive, numpy scalar, and numpy array types.

if not self._state_dict:
return {}
# Since ex_iterable.state_dict() returns a fresh isolated dict, we don't need a full recursive deepcopy.
return dict(self._state_dict)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

_fast_copy?

for i in range(len(state)):
state[i] = _inner_load_state_dict(state[i], new_state[i])
return state
return deepcopy(new_state)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could you confirm are there any other deepcopy needed to be replaced with _fast_copy?

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.

CPU Bottleneck in IterableDataset.state_dict() when shuffling multiple shards

2 participants