streaming-community 0.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simulation/__init__.py +4 -0
- simulation/core/__init__.py +4 -0
- simulation/core/create_index.py +86 -0
- simulation/core/last_used_ordered_set.py +37 -0
- simulation/core/main.py +243 -0
- simulation/core/node_tracker.py +224 -0
- simulation/core/shard_downloads.py +153 -0
- simulation/core/shuffle_quality.py +164 -0
- simulation/core/sim_dataset.py +573 -0
- simulation/core/sim_spanner.py +36 -0
- simulation/core/sim_time.py +321 -0
- simulation/core/sim_world.py +55 -0
- simulation/core/utils.py +167 -0
- simulation/core/yaml_processing.py +207 -0
- simulation/interfaces/__init__.py +4 -0
- simulation/interfaces/interface_utils.py +111 -0
- simulation/interfaces/sim_cli.py +122 -0
- simulation/interfaces/sim_script.py +113 -0
- simulation/interfaces/sim_ui.py +410 -0
- simulation/interfaces/widgets.py +448 -0
- simulation/launcher.py +14 -0
- streaming/__init__.py +30 -0
- streaming/_install_check.py +34 -0
- streaming/_version.py +6 -0
- streaming/base/__init__.py +15 -0
- streaming/base/array.py +110 -0
- streaming/base/batching/__init__.py +45 -0
- streaming/base/batching/device_per_stream.py +226 -0
- streaming/base/batching/per_stream.py +158 -0
- streaming/base/batching/random.py +73 -0
- streaming/base/batching/stratified.py +184 -0
- streaming/base/compression.py +258 -0
- streaming/base/constant.py +36 -0
- streaming/base/converters/__init__.py +9 -0
- streaming/base/converters/dataframe_to_mds.py +328 -0
- streaming/base/dataloader.py +100 -0
- streaming/base/dataset.py +1531 -0
- streaming/base/distributed.py +129 -0
- streaming/base/format/__init__.py +42 -0
- streaming/base/format/base/__init__.py +8 -0
- streaming/base/format/base/reader.py +400 -0
- streaming/base/format/base/writer.py +533 -0
- streaming/base/format/index.py +15 -0
- streaming/base/format/json/__init__.py +9 -0
- streaming/base/format/json/encodings.py +86 -0
- streaming/base/format/json/reader.py +123 -0
- streaming/base/format/json/writer.py +119 -0
- streaming/base/format/mds/__init__.py +9 -0
- streaming/base/format/mds/encodings.py +800 -0
- streaming/base/format/mds/reader.py +149 -0
- streaming/base/format/mds/writer.py +144 -0
- streaming/base/format/xsv/__init__.py +9 -0
- streaming/base/format/xsv/encodings.py +127 -0
- streaming/base/format/xsv/reader.py +275 -0
- streaming/base/format/xsv/writer.py +270 -0
- streaming/base/hashing.py +68 -0
- streaming/base/local.py +78 -0
- streaming/base/partition/__init__.py +70 -0
- streaming/base/partition/orig.py +181 -0
- streaming/base/partition/relaxed.py +93 -0
- streaming/base/registry_utils.py +199 -0
- streaming/base/sampling.py +80 -0
- streaming/base/shared/__init__.py +17 -0
- streaming/base/shared/array.py +66 -0
- streaming/base/shared/barrier.py +167 -0
- streaming/base/shared/memory.py +128 -0
- streaming/base/shared/prefix.py +305 -0
- streaming/base/shared/scalar.py +38 -0
- streaming/base/shuffle/__init__.py +48 -0
- streaming/base/shuffle/naive.py +40 -0
- streaming/base/shuffle/py1br.py +93 -0
- streaming/base/shuffle/py1e.py +144 -0
- streaming/base/shuffle/py1s.py +131 -0
- streaming/base/shuffle/py2s.py +146 -0
- streaming/base/spanner.py +59 -0
- streaming/base/storage/__init__.py +36 -0
- streaming/base/storage/download.py +907 -0
- streaming/base/storage/upload.py +1296 -0
- streaming/base/stream.py +522 -0
- streaming/base/util.py +605 -0
- streaming/base/world.py +163 -0
- streaming/multimodal/__init__.py +8 -0
- streaming/multimodal/convert/__init__.py +4 -0
- streaming/multimodal/convert/laion/__init__.py +4 -0
- streaming/multimodal/convert/laion/laion400m/__init__.py +4 -0
- streaming/multimodal/convert/laion/laion400m/convert_and_upload.py +290 -0
- streaming/multimodal/convert/webvid/__init__.py +4 -0
- streaming/multimodal/convert/webvid/crawl_webvid.py +165 -0
- streaming/multimodal/convert/webvid/crawl_webvid_subsets.py +233 -0
- streaming/multimodal/convert/webvid/extract_webvid_videos.py +97 -0
- streaming/multimodal/webvid.py +409 -0
- streaming/py.typed +0 -0
- streaming/text/__init__.py +10 -0
- streaming/text/c4.py +162 -0
- streaming/text/convert/__init__.py +4 -0
- streaming/text/convert/c4.py +162 -0
- streaming/text/convert/enwiki/__init__.py +0 -0
- streaming/text/convert/enwiki/mds/__init__.py +0 -0
- streaming/text/convert/enwiki/mds/create_pretraining_data.py +453 -0
- streaming/text/convert/enwiki/mds/make_train_parallel.py +122 -0
- streaming/text/convert/enwiki/mds/merge_shard_groups.py +74 -0
- streaming/text/convert/enwiki/mds/pick_eval_samples.py +72 -0
- streaming/text/convert/enwiki/mds/tokenization.py +408 -0
- streaming/text/convert/enwiki/tfrecord/__init__.py +0 -0
- streaming/text/convert/enwiki/tfrecord/count_samples.py +35 -0
- streaming/text/convert/enwiki/tfrecord/create_pretraining_data.py +428 -0
- streaming/text/convert/enwiki/tfrecord/make_train_parallel.py +37 -0
- streaming/text/convert/enwiki/tfrecord/pick_eval_samples.py +125 -0
- streaming/text/convert/enwiki/tfrecord/tokenization.py +408 -0
- streaming/text/convert/enwiki_text.py +126 -0
- streaming/text/convert/pile.py +222 -0
- streaming/text/enwiki.py +145 -0
- streaming/text/pile.py +164 -0
- streaming/vision/__init__.py +11 -0
- streaming/vision/ade20k.py +141 -0
- streaming/vision/base.py +176 -0
- streaming/vision/cifar10.py +73 -0
- streaming/vision/coco.py +135 -0
- streaming/vision/convert/__init__.py +4 -0
- streaming/vision/convert/ade20k.py +183 -0
- streaming/vision/convert/base.py +68 -0
- streaming/vision/convert/cifar10.py +88 -0
- streaming/vision/convert/coco.py +247 -0
- streaming/vision/convert/fake_cifar10.py +51 -0
- streaming/vision/convert/imagenet.py +169 -0
- streaming/vision/imagenet.py +73 -0
- streaming_community-0.14.0.dist-info/METADATA +475 -0
- streaming_community-0.14.0.dist-info/RECORD +133 -0
- streaming_community-0.14.0.dist-info/WHEEL +5 -0
- streaming_community-0.14.0.dist-info/entry_points.txt +2 -0
- streaming_community-0.14.0.dist-info/licenses/LICENSE +201 -0
- streaming_community-0.14.0.dist-info/licenses/NOTICE +9 -0
- streaming_community-0.14.0.dist-info/top_level.txt +2 -0
simulation/__init__.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Copyright 2022-2024 MosaicML Streaming authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Create a dataset index file from input parameters."""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import random
|
|
10
|
+
import string
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from streaming.base.format import get_index_basename
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
logging.basicConfig(level=logging.INFO)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_random_foldername() -> str:
|
|
20
|
+
"""Generate random folder name to store the index file in.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
str: random alphanumeric folder name.
|
|
24
|
+
"""
|
|
25
|
+
return ''.join(
|
|
26
|
+
random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits)
|
|
27
|
+
for _ in range(16))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create_stream_index(shards: int, samples_per_shard: int, avg_raw_shard_size: int,
|
|
31
|
+
avg_zip_shard_size: Optional[int]) -> str:
|
|
32
|
+
"""Create dataset index file from input parameters.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
shards (int): Number of shards.
|
|
36
|
+
samples_per_shard (int): Number of samples per shard.
|
|
37
|
+
avg_raw_shard_size (int): Average raw shard size.
|
|
38
|
+
avg_zip_shard_size (int): Average compressed shard size.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
local path to created index file for stream.
|
|
42
|
+
"""
|
|
43
|
+
index_data = {'version': 2, 'shards': []}
|
|
44
|
+
|
|
45
|
+
shards_list = []
|
|
46
|
+
for _ in range(shards):
|
|
47
|
+
shard_data = {
|
|
48
|
+
'column_encodings': [],
|
|
49
|
+
'column_names': [],
|
|
50
|
+
'column_sizes': [],
|
|
51
|
+
'format': 'mds',
|
|
52
|
+
'raw_data': {
|
|
53
|
+
'basename': '',
|
|
54
|
+
'bytes': avg_raw_shard_size,
|
|
55
|
+
'hashes': {}
|
|
56
|
+
},
|
|
57
|
+
'hashes': [],
|
|
58
|
+
'samples': samples_per_shard,
|
|
59
|
+
'size_limit': avg_raw_shard_size,
|
|
60
|
+
'version': 2,
|
|
61
|
+
'zip_data': None,
|
|
62
|
+
'compression': None
|
|
63
|
+
}
|
|
64
|
+
if avg_zip_shard_size is not None:
|
|
65
|
+
shard_data['zip_data'] = {'basename': '', 'bytes': avg_zip_shard_size, 'hashes': {}}
|
|
66
|
+
shard_data['compression'] = ''
|
|
67
|
+
shards_list.append(shard_data)
|
|
68
|
+
|
|
69
|
+
index_data['shards'] = shards_list
|
|
70
|
+
|
|
71
|
+
# Try making the directory for the stream's index.json file
|
|
72
|
+
foldername = get_random_foldername() + '_indexcreated'
|
|
73
|
+
try:
|
|
74
|
+
os.mkdir(foldername)
|
|
75
|
+
except FileExistsError:
|
|
76
|
+
logger.warning(' Folder already exists, trying again...')
|
|
77
|
+
foldername = get_random_foldername()
|
|
78
|
+
os.mkdir(foldername)
|
|
79
|
+
|
|
80
|
+
index_basename = get_index_basename()
|
|
81
|
+
index_path = os.path.join(foldername, index_basename)
|
|
82
|
+
|
|
83
|
+
with open(index_path, 'w') as f:
|
|
84
|
+
json.dump(index_data, f)
|
|
85
|
+
|
|
86
|
+
return index_path
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright 2022-2024 MosaicML Streaming authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""An ordered set that can be used as an LRU cache."""
|
|
5
|
+
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LastUsedOrderedSet(OrderedDict):
|
|
11
|
+
"""An ordered dict that can be used as an LRU cache.
|
|
12
|
+
|
|
13
|
+
This is a subclass of OrderedDict, with some LRU-specific functions and all values as ``None``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def setitem(self, key: Any, move_to_end: bool = True):
|
|
17
|
+
"""Set/add an item.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
key (Any): key to be added.
|
|
21
|
+
move_to_end (bool, optional): whether to move the item to the end, signifying most
|
|
22
|
+
recent access. Defaults to ``True``.
|
|
23
|
+
"""
|
|
24
|
+
super().__setitem__(key, None)
|
|
25
|
+
self.move_to_end(key, last=move_to_end)
|
|
26
|
+
|
|
27
|
+
def popLRU(self):
|
|
28
|
+
"""Pop the least recently used item (located at the front)."""
|
|
29
|
+
return self.popitem(last=False)[0]
|
|
30
|
+
|
|
31
|
+
def setuse(self, key: Any):
|
|
32
|
+
"""Mark an item as used, moving it to the end.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
key (Any): key of element to move to the end, signifying most recent access.
|
|
36
|
+
"""
|
|
37
|
+
self.setitem(key)
|
simulation/core/main.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Copyright 2022-2024 MosaicML Streaming authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Main simulation function, simulating bytes downloaded and time taken each training step."""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from typing import Generator, Union
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
|
|
13
|
+
from simulation.core.node_tracker import NodeTracker
|
|
14
|
+
from simulation.core.shard_downloads import run_cache_limit, simulate_shard_downloads
|
|
15
|
+
from simulation.core.sim_dataset import SimulationDataset
|
|
16
|
+
from simulation.core.sim_time import Time
|
|
17
|
+
from simulation.core.utils import bytes_to_time, get_batches_epochs, time_to_bytes
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
logging.basicConfig(level=logging.INFO)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def simulate(
|
|
24
|
+
dataset: SimulationDataset,
|
|
25
|
+
time_per_sample: float,
|
|
26
|
+
node_network_bandwidth: int,
|
|
27
|
+
max_duration: Time,
|
|
28
|
+
generator: bool = False
|
|
29
|
+
) -> Generator[Union[tuple[int, float, int], tuple[float, int], tuple[NDArray, NDArray, float,
|
|
30
|
+
int]], None, None]:
|
|
31
|
+
"""Simulates step time and downloads using streaming for the specified input parameters.
|
|
32
|
+
|
|
33
|
+
At each training step, the simulation does the following:
|
|
34
|
+
* gets the shards containing the current batch's samples.
|
|
35
|
+
* for each node, downloads shards if they are not present (round-robin through workers).
|
|
36
|
+
* predownloads more shards during model process time plus extra time from previous step.
|
|
37
|
+
* tracks the time the the step took as well as downloaded bytes.
|
|
38
|
+
|
|
39
|
+
Key Notes and Assumptions:
|
|
40
|
+
* assume that batch time is solely made up of two things: batch shard download wait time
|
|
41
|
+
and batch processing time
|
|
42
|
+
* loop through workers in a node round-robin style for batches and for downloads
|
|
43
|
+
* assume each node has a separate, uniform network bandwidth
|
|
44
|
+
* the batch has to wait until all nodes have downloaded the shards containing batch samples.
|
|
45
|
+
* for shard eviction itself, use LRU shard eviction to take out the least recently used
|
|
46
|
+
shard, per node.
|
|
47
|
+
* shards are shared across devices on a single node, but nodes do not share shards between
|
|
48
|
+
each other.
|
|
49
|
+
* if a shard is unavailable, we download shards round-robin until we have it.
|
|
50
|
+
* if a shard is available in a node, we just use it.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
dataset (SimulationDataset): SimulationDataset object created based on input params/yaml
|
|
54
|
+
time_per_sample (float): time to process one sample on one device (seconds)
|
|
55
|
+
node_network_bandwidth (int): network bandwidth per node (bytes/s)
|
|
56
|
+
max_duration (Time): max duration of simulation. Defaults to ``None``.
|
|
57
|
+
generator (bool): True if we yield throughput and shard_download one step at a time.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Generator[Union[Tuple[int, float, int],
|
|
61
|
+
Tuple[NDArray, NDArray, float, int],
|
|
62
|
+
Tuple[float, int]], None, None]: either a tuple of step number, step time, and
|
|
63
|
+
downloaded bytes, a tuple of startup time and min needed cache limit,
|
|
64
|
+
(both when generator=True), or a tuple of all step times, downloaded bytes,
|
|
65
|
+
startup_time, and min needed cache limit.
|
|
66
|
+
"""
|
|
67
|
+
# tracking startup time, which includes SimulationDataset instantiation time.
|
|
68
|
+
start_time = time.time()
|
|
69
|
+
startup_time = dataset.get_instantiation_time()
|
|
70
|
+
|
|
71
|
+
# Get batches, epochs, total batches from dataset and provided time info.
|
|
72
|
+
batches_per_epoch, epochs, total_batches = get_batches_epochs(dataset, max_duration)
|
|
73
|
+
|
|
74
|
+
# Retrieve streaming and dataset information from SimulationDataset.
|
|
75
|
+
physical_nodes = dataset.get_nodes()
|
|
76
|
+
devices = dataset.get_devices()
|
|
77
|
+
workers = dataset.get_workers()
|
|
78
|
+
device_batch_size = dataset.get_batch_size()
|
|
79
|
+
predownload = dataset.get_predownload()
|
|
80
|
+
cache_limit = dataset.get_cache_limit()
|
|
81
|
+
total_shards = dataset.get_num_shards()
|
|
82
|
+
raw_shard_sizes = dataset.get_raw_shard_sizes()
|
|
83
|
+
zip_shard_sizes = dataset.get_zip_shard_sizes()
|
|
84
|
+
# dataset's spanner object maps global sample id to shard id.
|
|
85
|
+
sample_to_shard = dataset.get_spanner()
|
|
86
|
+
|
|
87
|
+
# Initialize NodeTracker objects for each node. These keep track of shards, worker downloads,
|
|
88
|
+
# cache usage, shard usage ranges, etc. for each node.
|
|
89
|
+
nodes = []
|
|
90
|
+
for _ in range(physical_nodes):
|
|
91
|
+
nodes.append(
|
|
92
|
+
NodeTracker(workers, devices, predownload, device_batch_size, total_shards,
|
|
93
|
+
cache_limit))
|
|
94
|
+
|
|
95
|
+
# Time for the global batch is just device batch size * time per sample.
|
|
96
|
+
# We assume all devices process their microbatch perfectly in parallel.
|
|
97
|
+
avg_batch_process_time = device_batch_size * time_per_sample
|
|
98
|
+
|
|
99
|
+
notification_batches = int(batches_per_epoch) / 20
|
|
100
|
+
|
|
101
|
+
# Simulate training by looping over epochs and batches.
|
|
102
|
+
|
|
103
|
+
# Track time and downloads at each step.
|
|
104
|
+
step_times = []
|
|
105
|
+
step_downloads = []
|
|
106
|
+
|
|
107
|
+
for epoch in range(epochs):
|
|
108
|
+
|
|
109
|
+
# Get the samples, divided up per node, for this epoch.
|
|
110
|
+
samples_per_node = dataset.get_samples_per_node(epoch, 0)
|
|
111
|
+
|
|
112
|
+
# Set the samples for each node for this epoch.
|
|
113
|
+
for node_id, node in enumerate(nodes):
|
|
114
|
+
node.samples = samples_per_node[node_id]
|
|
115
|
+
node.initialize_worker_downloads(sample_to_shard)
|
|
116
|
+
|
|
117
|
+
# Track which sample we are currently on, as a worker id. We round-robin over workers.
|
|
118
|
+
worker_sample_index = 0
|
|
119
|
+
|
|
120
|
+
# if first epoch, add time so far to startup time
|
|
121
|
+
if epoch == 0:
|
|
122
|
+
startup_time += time.time() - start_time
|
|
123
|
+
|
|
124
|
+
# Iterate over batches
|
|
125
|
+
for batch in range(batches_per_epoch):
|
|
126
|
+
|
|
127
|
+
step_num = batch + (batches_per_epoch * epoch)
|
|
128
|
+
|
|
129
|
+
# If we are at the last batch, exit.
|
|
130
|
+
if step_num >= total_batches:
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
# Log progress every notification_batches interval.
|
|
134
|
+
if (batch + 1) % notification_batches == 0:
|
|
135
|
+
logger.info(
|
|
136
|
+
f' Epoch: {str(epoch + 1)} | Batch {str(batch + 1)}/{str(batches_per_epoch)}')
|
|
137
|
+
|
|
138
|
+
# We round-robin over workers per device. The current batch's worker is the same
|
|
139
|
+
# across every device.
|
|
140
|
+
curr_worker = batch % workers
|
|
141
|
+
# Track how long each node takes to download the shards that the current batch needs.
|
|
142
|
+
node_batch_download_times = np.array([0] * physical_nodes)
|
|
143
|
+
# Track how many total bytes are downloaded in this batch by all nodes.
|
|
144
|
+
downloaded_bytes = 0
|
|
145
|
+
|
|
146
|
+
# Get current samples and download samples for each node, for this batch.
|
|
147
|
+
for node_id, node in enumerate(nodes):
|
|
148
|
+
|
|
149
|
+
shards_needed, shards_present = node.get_current_batch_shards(
|
|
150
|
+
curr_worker, worker_sample_index, sample_to_shard)
|
|
151
|
+
# Mark all shards present as accessed most recently in this node.
|
|
152
|
+
node.set_shards_used(shards_present, step_num)
|
|
153
|
+
# Push the predownload for the current batch workers ahead by device_batch_size.
|
|
154
|
+
node.update_worker_predownloads(curr_worker, worker_sample_index, sample_to_shard)
|
|
155
|
+
# Track bytes downloaded by this node.
|
|
156
|
+
node_downloaded_bytes = 0
|
|
157
|
+
|
|
158
|
+
# Because we assume downloads also round-robin over workers, we can download shards
|
|
159
|
+
# other than the current batch's shards while looking for current batch's shards.
|
|
160
|
+
while len(shards_needed) > 0:
|
|
161
|
+
download_outcome, download_size = \
|
|
162
|
+
simulate_shard_downloads(node,
|
|
163
|
+
raw_shard_sizes,
|
|
164
|
+
zip_shard_sizes,
|
|
165
|
+
current_batch_downloads=True,
|
|
166
|
+
step_num=step_num,
|
|
167
|
+
cache_limit=cache_limit,
|
|
168
|
+
shards_needed=shards_needed)
|
|
169
|
+
if download_outcome == 'downloaded':
|
|
170
|
+
node_downloaded_bytes += download_size
|
|
171
|
+
downloaded_bytes += download_size
|
|
172
|
+
elif download_outcome == 'present':
|
|
173
|
+
# If the shard was already present in the node, continue downloading.
|
|
174
|
+
pass
|
|
175
|
+
else:
|
|
176
|
+
# If no shard downloads are left in the node, stop downloading.
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
# Calculate how much time this node spent downloading shards
|
|
180
|
+
node_batch_download_times[node_id] = bytes_to_time(node_downloaded_bytes,
|
|
181
|
+
node_network_bandwidth)
|
|
182
|
+
|
|
183
|
+
# The node that took the longest to download shards is the bottleneck. All other nodes
|
|
184
|
+
# use the extra time to continue downloading.
|
|
185
|
+
slowest_download_time = np.max(node_batch_download_times)
|
|
186
|
+
|
|
187
|
+
# If we are on the first step, add slowest_download_time to startup time
|
|
188
|
+
if epoch == 0 and batch == 0:
|
|
189
|
+
startup_time += slowest_download_time
|
|
190
|
+
|
|
191
|
+
# Iterate over nodes again to continue downloading shards.
|
|
192
|
+
for node_id, node in enumerate(nodes):
|
|
193
|
+
# The download time each node has is the avg_batch_process_time plus the extra time
|
|
194
|
+
# the node has from finishing earlier than the slowest node.
|
|
195
|
+
download_time_left = avg_batch_process_time + (slowest_download_time -
|
|
196
|
+
node_batch_download_times[node_id])
|
|
197
|
+
# Get number of bytes we can download in download_time_left.
|
|
198
|
+
# We also include any partially downloaded bytes from previous steps.
|
|
199
|
+
download_bytes_left = time_to_bytes(download_time_left, node_network_bandwidth) + \
|
|
200
|
+
node.partial_shard_bytes
|
|
201
|
+
|
|
202
|
+
while True:
|
|
203
|
+
download_outcome, download_size = \
|
|
204
|
+
simulate_shard_downloads(node,
|
|
205
|
+
raw_shard_sizes,
|
|
206
|
+
zip_shard_sizes,
|
|
207
|
+
current_batch_downloads=False,
|
|
208
|
+
step_num=step_num,
|
|
209
|
+
cache_limit=cache_limit,
|
|
210
|
+
download_bytes_left=download_bytes_left)
|
|
211
|
+
if download_outcome == 'downloaded':
|
|
212
|
+
downloaded_bytes += download_size
|
|
213
|
+
download_bytes_left -= download_size
|
|
214
|
+
elif download_outcome == 'present':
|
|
215
|
+
pass
|
|
216
|
+
else:
|
|
217
|
+
# If no shard downloads are left in the node, or we could only partially
|
|
218
|
+
# download a shard, stop downloading.
|
|
219
|
+
break
|
|
220
|
+
|
|
221
|
+
# Yield or store step number, time and download for this step
|
|
222
|
+
if generator:
|
|
223
|
+
yield step_num, slowest_download_time + avg_batch_process_time, downloaded_bytes
|
|
224
|
+
else:
|
|
225
|
+
step_times.append(slowest_download_time + avg_batch_process_time)
|
|
226
|
+
step_downloads.append(downloaded_bytes)
|
|
227
|
+
|
|
228
|
+
# Increment worker_sample_index by device_batch_size if we are at the last worker.
|
|
229
|
+
# As we round-robin over workers, the sample index per worker is increased as we loop
|
|
230
|
+
# through all workers.
|
|
231
|
+
if curr_worker == workers - 1:
|
|
232
|
+
worker_sample_index += device_batch_size
|
|
233
|
+
|
|
234
|
+
# Simulation is finished. Calculate needed cache limit.
|
|
235
|
+
min_cache_limit = run_cache_limit(nodes, raw_shard_sizes)
|
|
236
|
+
|
|
237
|
+
# Yield results.
|
|
238
|
+
if not generator:
|
|
239
|
+
step_times = np.array(step_times)
|
|
240
|
+
step_downloads = np.array(step_downloads)
|
|
241
|
+
yield step_times, step_downloads, startup_time, min_cache_limit
|
|
242
|
+
else:
|
|
243
|
+
yield startup_time, min_cache_limit
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Copyright 2022-2024 MosaicML Streaming authors
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Class for tracking node information during simulation."""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.typing import NDArray
|
|
10
|
+
from sortedcollections import OrderedSet
|
|
11
|
+
|
|
12
|
+
from simulation.core.last_used_ordered_set import LastUsedOrderedSet
|
|
13
|
+
from simulation.core.utils import remove_padded_samples
|
|
14
|
+
from streaming.base.spanner import Spanner
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NodeTracker():
|
|
18
|
+
"""Tracker for node information during simulation.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
workers (int): The number of workers.
|
|
22
|
+
devices (int): The number of devices.
|
|
23
|
+
predownload (int): The number of samples to predownload.
|
|
24
|
+
device_batch_size (int): The device batch size.
|
|
25
|
+
total_shards (int): Total number of shards in the dataset.
|
|
26
|
+
cache_limit (Optional[int]): The cache limit for the node. Defaults to ``None``.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self,
|
|
30
|
+
workers: int,
|
|
31
|
+
devices: int,
|
|
32
|
+
predownload: int,
|
|
33
|
+
device_batch_size: int,
|
|
34
|
+
total_shards: int,
|
|
35
|
+
cache_limit: Optional[int] = None):
|
|
36
|
+
|
|
37
|
+
self.shards = LastUsedOrderedSet()
|
|
38
|
+
self.all_shards = set()
|
|
39
|
+
self.cache_usage = 0
|
|
40
|
+
self.partial_shard_bytes = 0
|
|
41
|
+
self.worker_download_index = 0
|
|
42
|
+
self.devices = devices
|
|
43
|
+
self.workers = workers
|
|
44
|
+
self.total_workers = workers * devices
|
|
45
|
+
self.device_batch_size = device_batch_size
|
|
46
|
+
self.predownload = predownload
|
|
47
|
+
self.cache_limit = cache_limit
|
|
48
|
+
self.worker_downloads = []
|
|
49
|
+
self.shard_access_starts = np.full(total_shards, -1)
|
|
50
|
+
self.shard_access_ends = np.full(total_shards, -1)
|
|
51
|
+
|
|
52
|
+
# Use the set_epoch_samples method every epoch to set the node's samples.
|
|
53
|
+
self.samples = None
|
|
54
|
+
|
|
55
|
+
def initialize_worker_downloads(self, sample_to_shard: Spanner):
|
|
56
|
+
"""Initialize worker downloads, making shards in the predownload sample range available.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
sample_to_shard (Spanner): The mapping from samples to shards.
|
|
60
|
+
"""
|
|
61
|
+
# For downloads, we round-robin over devices first, then workers.
|
|
62
|
+
if self.samples is not None:
|
|
63
|
+
for worker in range(self.workers):
|
|
64
|
+
for device in range(self.devices):
|
|
65
|
+
download_samples = remove_padded_samples(
|
|
66
|
+
self.samples[device, worker, :self.predownload])
|
|
67
|
+
# Get the shards these samples correspond to, maintaining access order.
|
|
68
|
+
download_shards = OrderedSet(
|
|
69
|
+
[sample_to_shard[sample] for sample in download_samples])
|
|
70
|
+
self.worker_downloads.append(download_shards)
|
|
71
|
+
else:
|
|
72
|
+
raise AttributeError('Must set node samples before accessing them.')
|
|
73
|
+
|
|
74
|
+
def set_shards_used(self, shards: set, step_num: int):
|
|
75
|
+
"""Mark a set of shards as recently used.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
shards (set): The shards to set as used.
|
|
79
|
+
step_num (int): The current step number.
|
|
80
|
+
"""
|
|
81
|
+
for shard in shards:
|
|
82
|
+
self.shards.setuse(shard)
|
|
83
|
+
# For any shard access, we are accessing the shard so we need the shard until
|
|
84
|
+
# at least the next step begins. Adding 0.5 ensures that we evict shards
|
|
85
|
+
# after they are used for the last time, but before they are replaced by
|
|
86
|
+
# new downloads in the next step.
|
|
87
|
+
self.shard_access_ends[shard] = step_num + 0.5
|
|
88
|
+
|
|
89
|
+
def add_shard(self, shard: int, used: bool = True):
|
|
90
|
+
"""Add a shard to the node.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
shard (int): The shard to add.
|
|
94
|
+
used (bool): Whether the shard is used when added.
|
|
95
|
+
"""
|
|
96
|
+
self.shards.setitem(shard, used)
|
|
97
|
+
self.all_shards.add(shard)
|
|
98
|
+
|
|
99
|
+
def get_all_shards(self):
|
|
100
|
+
"""Get all the shards in the node."""
|
|
101
|
+
return self.all_shards
|
|
102
|
+
|
|
103
|
+
def evict_shard(self) -> int:
|
|
104
|
+
"""Evict a shard.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
int: The evicted shard.
|
|
108
|
+
"""
|
|
109
|
+
evicted_shard = self.shards.popLRU()
|
|
110
|
+
return evicted_shard
|
|
111
|
+
|
|
112
|
+
def evict_until_satisfied(self, incoming_shard_size: int, raw_shard_sizes: NDArray[np.int64]):
|
|
113
|
+
"""Evict shards until the node has enough space to download the incoming shard.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
incoming_shard_size (int): The size of the incoming shard.
|
|
117
|
+
raw_shard_sizes (NDArray[np.int64]): The raw shard sizes.
|
|
118
|
+
"""
|
|
119
|
+
# We evict shards until the incoming shard fits into the node's cache.
|
|
120
|
+
if self.cache_limit is not None:
|
|
121
|
+
while self.cache_usage + incoming_shard_size > self.cache_limit:
|
|
122
|
+
evicted_shard = self.evict_shard()
|
|
123
|
+
self.cache_usage -= int(raw_shard_sizes[evicted_shard])
|
|
124
|
+
|
|
125
|
+
def increment_worker_download_index(self):
|
|
126
|
+
"""Increment the worker download index."""
|
|
127
|
+
self.worker_download_index = (self.worker_download_index + 1) % \
|
|
128
|
+
(self.workers * self.devices)
|
|
129
|
+
|
|
130
|
+
def get_worker_download(self,
|
|
131
|
+
worker: Optional[int] = None,
|
|
132
|
+
device: Optional[int] = None,
|
|
133
|
+
index: Optional[int] = None) -> OrderedSet:
|
|
134
|
+
"""Get the shard downloads for a worker on a specific device.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
worker (Optional[int]): The worker index.
|
|
138
|
+
device (Optional[int]): The device index the worker is on.
|
|
139
|
+
index (Optional[int]): The index of the worker download for direct access.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
OrderedSet: The shard downloads, in order, for this worker.
|
|
143
|
+
"""
|
|
144
|
+
if index is not None:
|
|
145
|
+
# Directly access worker_downloads through an index.
|
|
146
|
+
return self.worker_downloads[index]
|
|
147
|
+
elif worker is not None and device is not None:
|
|
148
|
+
# Access worker_downloads through worker and device indices.
|
|
149
|
+
return self.worker_downloads[worker * self.devices + device]
|
|
150
|
+
else:
|
|
151
|
+
raise ValueError('Must specify either index, or worker and device.')
|
|
152
|
+
|
|
153
|
+
def get_current_batch_shards(self, worker: int, worker_sample_index: int,
|
|
154
|
+
sample_to_shard: Spanner) -> tuple[set, set]:
|
|
155
|
+
"""Get this node's shards for the current batch.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
worker (int): The worker.
|
|
159
|
+
worker_sample_index (int): The worker sample index.
|
|
160
|
+
sample_to_shard (Spanner): The mapping from samples to shards.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Tuple[set, set]: shard ids needed by node, shard ids present in node.
|
|
164
|
+
"""
|
|
165
|
+
if self.samples is not None:
|
|
166
|
+
batch_samples = remove_padded_samples(
|
|
167
|
+
self.samples[:, worker, worker_sample_index:worker_sample_index +
|
|
168
|
+
self.device_batch_size].flatten())
|
|
169
|
+
batch_shards = {sample_to_shard[sample] for sample in batch_samples}
|
|
170
|
+
shards_needed = batch_shards.difference(self.shards.keys())
|
|
171
|
+
shards_present = batch_shards.difference(shards_needed)
|
|
172
|
+
return shards_needed, shards_present
|
|
173
|
+
else:
|
|
174
|
+
raise AttributeError('Must set node samples before accessing them.')
|
|
175
|
+
|
|
176
|
+
def get_next_worker_with_downloads(self) -> Optional[OrderedSet]:
|
|
177
|
+
"""Get the next worker with samples to download.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Optional[OrderedSet]: The next worker's shard downloads, or None if no workers have
|
|
181
|
+
samples to download.
|
|
182
|
+
"""
|
|
183
|
+
empty_download_counter = 0
|
|
184
|
+
worker_download = self.get_worker_download(index=self.worker_download_index)
|
|
185
|
+
while len(worker_download) == 0:
|
|
186
|
+
empty_download_counter += 1
|
|
187
|
+
self.increment_worker_download_index()
|
|
188
|
+
worker_download = self.get_worker_download(index=self.worker_download_index)
|
|
189
|
+
|
|
190
|
+
# No workers in the node have samples to download.
|
|
191
|
+
if empty_download_counter >= self.total_workers:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
return worker_download
|
|
195
|
+
|
|
196
|
+
def update_worker_predownloads(self, worker: int, worker_sample_index: int,
|
|
197
|
+
sample_to_shard: Spanner):
|
|
198
|
+
"""Update the worker predownload samples for a worker and device.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
worker (int): The current batch worker index.
|
|
202
|
+
worker_sample_index (int): The worker sample index.
|
|
203
|
+
sample_to_shard (Spanner): The mapping from samples to shards.
|
|
204
|
+
"""
|
|
205
|
+
if self.samples is not None:
|
|
206
|
+
for device in range(self.devices):
|
|
207
|
+
# Retrieve new samples that are now within predownload range of the worker.
|
|
208
|
+
new_download_samples = remove_padded_samples(
|
|
209
|
+
self.samples[device, worker,
|
|
210
|
+
worker_sample_index + self.predownload:worker_sample_index +
|
|
211
|
+
self.device_batch_size + self.predownload])
|
|
212
|
+
|
|
213
|
+
# Want to maintain the shard access order.
|
|
214
|
+
new_download_shards = OrderedSet(
|
|
215
|
+
[sample_to_shard[sample] for sample in new_download_samples])
|
|
216
|
+
|
|
217
|
+
worker_downloads = self.get_worker_download(worker=worker, device=device)
|
|
218
|
+
|
|
219
|
+
# Add in new shards to the worker's shard downloads only if node does not yet have it.
|
|
220
|
+
for shard in new_download_shards:
|
|
221
|
+
if shard not in self.shards:
|
|
222
|
+
worker_downloads.add(shard)
|
|
223
|
+
else:
|
|
224
|
+
raise AttributeError('Must set node samples before accessing them.')
|