speedy-utils 1.1.40__py3-none-any.whl → 1.1.43__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.
- llm_utils/__init__.py +2 -0
- llm_utils/llm_ray.py +370 -0
- llm_utils/lm/llm.py +36 -29
- speedy_utils/__init__.py +10 -0
- speedy_utils/common/utils_io.py +3 -1
- speedy_utils/multi_worker/__init__.py +12 -0
- speedy_utils/multi_worker/dataset_ray.py +303 -0
- speedy_utils/multi_worker/parallel_gpu_pool.py +178 -0
- speedy_utils/multi_worker/process.py +989 -86
- speedy_utils/multi_worker/progress.py +140 -0
- speedy_utils/multi_worker/thread.py +202 -42
- speedy_utils/scripts/mpython.py +49 -4
- {speedy_utils-1.1.40.dist-info → speedy_utils-1.1.43.dist-info}/METADATA +5 -3
- {speedy_utils-1.1.40.dist-info → speedy_utils-1.1.43.dist-info}/RECORD +16 -12
- {speedy_utils-1.1.40.dist-info → speedy_utils-1.1.43.dist-info}/WHEEL +0 -0
- {speedy_utils-1.1.40.dist-info → speedy_utils-1.1.43.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Efficient Ray-based parallel processing for HuggingFace datasets.
|
|
3
|
+
|
|
4
|
+
This module provides a simple, dataset.map-like API that leverages Ray for
|
|
5
|
+
distributed processing while handling per-worker resource initialization
|
|
6
|
+
(like tokenizers) efficiently.
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
import threading
|
|
11
|
+
from typing import Callable, Any, TypeVar, Generic
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
__all__ = ['multi_process_dataset_ray', 'WorkerResources']
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WorkerResources:
|
|
18
|
+
"""
|
|
19
|
+
Container for per-worker resources that should be initialized once per worker.
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
def init_worker():
|
|
23
|
+
return WorkerResources(
|
|
24
|
+
tokenizer=AutoTokenizer.from_pretrained("..."),
|
|
25
|
+
model=load_model(),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def process_item(item, resources):
|
|
29
|
+
tokens = resources.tokenizer.encode(item['text'])
|
|
30
|
+
return {'tokens': tokens}
|
|
31
|
+
|
|
32
|
+
results = multi_process_dataset_ray(process_item, dataset, worker_init=init_worker)
|
|
33
|
+
"""
|
|
34
|
+
def __init__(self, **kwargs):
|
|
35
|
+
for k, v in kwargs.items():
|
|
36
|
+
setattr(self, k, v)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def multi_process_dataset_ray(
|
|
40
|
+
func: Callable[[Any, Any], Any],
|
|
41
|
+
dataset,
|
|
42
|
+
*,
|
|
43
|
+
workers: int | None = None,
|
|
44
|
+
worker_init: Callable[[], WorkerResources] | None = None,
|
|
45
|
+
batch_size: int = 1,
|
|
46
|
+
desc: str = "Processing",
|
|
47
|
+
progress: bool = True,
|
|
48
|
+
return_results: bool = True,
|
|
49
|
+
output_path: str | Path | None = None,
|
|
50
|
+
**func_kwargs,
|
|
51
|
+
) -> list[Any] | None:
|
|
52
|
+
"""
|
|
53
|
+
Process a HuggingFace dataset in parallel using Ray.
|
|
54
|
+
|
|
55
|
+
Simple API similar to dataset.map() but with Ray parallelism and efficient
|
|
56
|
+
per-worker resource initialization.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
func: Function to apply to each item. Signature: func(item, resources=None, **kwargs)
|
|
60
|
+
where resources is the WorkerResources from worker_init (if provided).
|
|
61
|
+
dataset: HuggingFace dataset (or path to dataset on disk).
|
|
62
|
+
workers: Number of workers. None = use all available Ray CPUs.
|
|
63
|
+
worker_init: Optional function that returns WorkerResources, called ONCE per worker.
|
|
64
|
+
Use this for expensive initialization like loading tokenizers/models.
|
|
65
|
+
batch_size: Process items in batches for efficiency (default: 1 = per-item).
|
|
66
|
+
desc: Description for progress bar.
|
|
67
|
+
progress: Show progress bar.
|
|
68
|
+
return_results: If True, collect and return all results. If False, return None
|
|
69
|
+
(useful when func writes to disk).
|
|
70
|
+
output_path: If provided, save results to this path as they complete.
|
|
71
|
+
**func_kwargs: Additional kwargs passed to func.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of results from func(item) for each item, or None if return_results=False.
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
# Simple usage
|
|
78
|
+
results = multi_process_dataset_ray(
|
|
79
|
+
lambda item, **_: item['text'].upper(),
|
|
80
|
+
dataset
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# With per-worker tokenizer
|
|
84
|
+
def init_worker():
|
|
85
|
+
from transformers import AutoTokenizer
|
|
86
|
+
return WorkerResources(tokenizer=AutoTokenizer.from_pretrained("gpt2"))
|
|
87
|
+
|
|
88
|
+
def tokenize(item, resources, max_length=512):
|
|
89
|
+
return resources.tokenizer.encode(item['text'], max_length=max_length)
|
|
90
|
+
|
|
91
|
+
results = multi_process_dataset_ray(
|
|
92
|
+
tokenize,
|
|
93
|
+
dataset,
|
|
94
|
+
worker_init=init_worker,
|
|
95
|
+
max_length=1024,
|
|
96
|
+
)
|
|
97
|
+
"""
|
|
98
|
+
import ray
|
|
99
|
+
from tqdm import tqdm
|
|
100
|
+
import numpy as np
|
|
101
|
+
|
|
102
|
+
# Handle dataset path vs object
|
|
103
|
+
dataset_path = None
|
|
104
|
+
if isinstance(dataset, (str, Path)):
|
|
105
|
+
dataset_path = str(dataset)
|
|
106
|
+
import datasets
|
|
107
|
+
dataset = datasets.load_from_disk(dataset_path)
|
|
108
|
+
elif hasattr(dataset, '_data_files') or hasattr(dataset, '_indices'):
|
|
109
|
+
# It's a HF dataset object - try to get its path
|
|
110
|
+
# Workers will reload from disk for memory efficiency
|
|
111
|
+
try:
|
|
112
|
+
if hasattr(dataset, 'cache_files') and dataset.cache_files:
|
|
113
|
+
dataset_path = str(Path(dataset.cache_files[0]['filename']).parent)
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
total_items = len(dataset)
|
|
118
|
+
|
|
119
|
+
# Initialize Ray and get available workers
|
|
120
|
+
if not ray.is_initialized():
|
|
121
|
+
ray.init(address="auto", ignore_reinit_error=True)
|
|
122
|
+
|
|
123
|
+
if workers is None:
|
|
124
|
+
workers = int(ray.cluster_resources().get("CPU", os.cpu_count() or 4))
|
|
125
|
+
|
|
126
|
+
# Ensure we don't have more workers than items
|
|
127
|
+
workers = min(workers, total_items)
|
|
128
|
+
|
|
129
|
+
# Pre-compute shard boundaries (avoid per-worker shuffle!)
|
|
130
|
+
shard_size = total_items // workers
|
|
131
|
+
shard_ranges = []
|
|
132
|
+
for i in range(workers):
|
|
133
|
+
start = i * shard_size
|
|
134
|
+
end = total_items if i == workers - 1 else (i + 1) * shard_size
|
|
135
|
+
shard_ranges.append((start, end))
|
|
136
|
+
|
|
137
|
+
# Progress tracking actor
|
|
138
|
+
@ray.remote
|
|
139
|
+
class ProgressActor:
|
|
140
|
+
def __init__(self, total):
|
|
141
|
+
self.count = 0
|
|
142
|
+
self.total = total
|
|
143
|
+
|
|
144
|
+
def update(self, n=1):
|
|
145
|
+
self.count += n
|
|
146
|
+
return self.count
|
|
147
|
+
|
|
148
|
+
def get_count(self):
|
|
149
|
+
return self.count
|
|
150
|
+
|
|
151
|
+
progress_actor = ProgressActor.remote(total_items) if progress else None
|
|
152
|
+
|
|
153
|
+
# Define the worker task
|
|
154
|
+
@ray.remote
|
|
155
|
+
def process_shard(
|
|
156
|
+
shard_id: int,
|
|
157
|
+
start_idx: int,
|
|
158
|
+
end_idx: int,
|
|
159
|
+
dataset_path_or_ref,
|
|
160
|
+
worker_init_fn,
|
|
161
|
+
func_to_apply,
|
|
162
|
+
func_kw,
|
|
163
|
+
batch_sz,
|
|
164
|
+
progress_ref,
|
|
165
|
+
do_return,
|
|
166
|
+
):
|
|
167
|
+
import datasets
|
|
168
|
+
|
|
169
|
+
# Load dataset (memory-mapped = fast)
|
|
170
|
+
if isinstance(dataset_path_or_ref, str):
|
|
171
|
+
ds = datasets.load_from_disk(dataset_path_or_ref)
|
|
172
|
+
else:
|
|
173
|
+
ds = ray.get(dataset_path_or_ref)
|
|
174
|
+
|
|
175
|
+
# Select this worker's slice
|
|
176
|
+
shard = ds.select(range(start_idx, end_idx))
|
|
177
|
+
del ds # Free reference
|
|
178
|
+
|
|
179
|
+
# Initialize per-worker resources ONCE
|
|
180
|
+
resources = worker_init_fn() if worker_init_fn else None
|
|
181
|
+
|
|
182
|
+
results = [] if do_return else None
|
|
183
|
+
count = 0
|
|
184
|
+
|
|
185
|
+
if batch_sz == 1:
|
|
186
|
+
# Per-item processing
|
|
187
|
+
for item in shard:
|
|
188
|
+
result = func_to_apply(item, resources=resources, **func_kw)
|
|
189
|
+
if do_return:
|
|
190
|
+
results.append(result)
|
|
191
|
+
count += 1
|
|
192
|
+
if progress_ref and count % 100 == 0:
|
|
193
|
+
ray.get(progress_ref.update.remote(100))
|
|
194
|
+
else:
|
|
195
|
+
# Batch processing
|
|
196
|
+
batch = []
|
|
197
|
+
for item in shard:
|
|
198
|
+
batch.append(item)
|
|
199
|
+
if len(batch) >= batch_sz:
|
|
200
|
+
batch_results = func_to_apply(batch, resources=resources, **func_kw)
|
|
201
|
+
if do_return:
|
|
202
|
+
if isinstance(batch_results, list):
|
|
203
|
+
results.extend(batch_results)
|
|
204
|
+
else:
|
|
205
|
+
results.append(batch_results)
|
|
206
|
+
count += len(batch)
|
|
207
|
+
if progress_ref and count % 100 < batch_sz:
|
|
208
|
+
ray.get(progress_ref.update.remote(min(100, len(batch))))
|
|
209
|
+
batch = []
|
|
210
|
+
|
|
211
|
+
# Process remaining items
|
|
212
|
+
if batch:
|
|
213
|
+
batch_results = func_to_apply(batch, resources=resources, **func_kw)
|
|
214
|
+
if do_return:
|
|
215
|
+
if isinstance(batch_results, list):
|
|
216
|
+
results.extend(batch_results)
|
|
217
|
+
else:
|
|
218
|
+
results.append(batch_results)
|
|
219
|
+
count += len(batch)
|
|
220
|
+
|
|
221
|
+
# Report remaining progress
|
|
222
|
+
if progress_ref:
|
|
223
|
+
remaining = count % 100
|
|
224
|
+
if remaining > 0:
|
|
225
|
+
ray.get(progress_ref.update.remote(remaining))
|
|
226
|
+
|
|
227
|
+
return results
|
|
228
|
+
|
|
229
|
+
# Put dataset in object store if no path available
|
|
230
|
+
dataset_ref = dataset_path if dataset_path else ray.put(dataset)
|
|
231
|
+
|
|
232
|
+
# Submit all shard tasks
|
|
233
|
+
futures = []
|
|
234
|
+
for i, (start, end) in enumerate(shard_ranges):
|
|
235
|
+
future = process_shard.remote(
|
|
236
|
+
i, start, end,
|
|
237
|
+
dataset_ref,
|
|
238
|
+
worker_init,
|
|
239
|
+
func,
|
|
240
|
+
func_kwargs,
|
|
241
|
+
batch_size,
|
|
242
|
+
progress_actor,
|
|
243
|
+
return_results,
|
|
244
|
+
)
|
|
245
|
+
futures.append(future)
|
|
246
|
+
|
|
247
|
+
# Progress bar polling thread
|
|
248
|
+
pbar = None
|
|
249
|
+
stop_polling = threading.Event()
|
|
250
|
+
|
|
251
|
+
def poll_progress():
|
|
252
|
+
nonlocal pbar
|
|
253
|
+
pbar = tqdm(total=total_items, desc=desc, disable=not progress)
|
|
254
|
+
while not stop_polling.is_set():
|
|
255
|
+
try:
|
|
256
|
+
count = ray.get(progress_actor.get_count.remote())
|
|
257
|
+
pbar.n = count
|
|
258
|
+
pbar.refresh()
|
|
259
|
+
except Exception:
|
|
260
|
+
pass
|
|
261
|
+
stop_polling.wait(0.3)
|
|
262
|
+
# Final update
|
|
263
|
+
try:
|
|
264
|
+
pbar.n = total_items
|
|
265
|
+
pbar.refresh()
|
|
266
|
+
pbar.close()
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
if progress and progress_actor:
|
|
271
|
+
poll_thread = threading.Thread(target=poll_progress, daemon=True)
|
|
272
|
+
poll_thread.start()
|
|
273
|
+
|
|
274
|
+
# Collect results
|
|
275
|
+
t0 = time.time()
|
|
276
|
+
try:
|
|
277
|
+
shard_results = ray.get(futures)
|
|
278
|
+
finally:
|
|
279
|
+
stop_polling.set()
|
|
280
|
+
if progress and progress_actor:
|
|
281
|
+
poll_thread.join(timeout=2)
|
|
282
|
+
|
|
283
|
+
elapsed = time.time() - t0
|
|
284
|
+
rate = total_items / elapsed if elapsed > 0 else 0
|
|
285
|
+
print(f"✅ Processed {total_items:,} items in {elapsed:.1f}s ({rate:.1f} items/s)")
|
|
286
|
+
|
|
287
|
+
if not return_results:
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
# Flatten results from all shards
|
|
291
|
+
all_results = []
|
|
292
|
+
for shard_result in shard_results:
|
|
293
|
+
if shard_result:
|
|
294
|
+
all_results.extend(shard_result)
|
|
295
|
+
|
|
296
|
+
# Optionally save
|
|
297
|
+
if output_path:
|
|
298
|
+
import pickle
|
|
299
|
+
with open(output_path, 'wb') as f:
|
|
300
|
+
pickle.dump(all_results, f)
|
|
301
|
+
print(f"💾 Saved results to {output_path}")
|
|
302
|
+
|
|
303
|
+
return all_results
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import ray
|
|
4
|
+
import time
|
|
5
|
+
import datetime
|
|
6
|
+
import numpy as np
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from tqdm.auto import tqdm
|
|
9
|
+
|
|
10
|
+
# --- 1. Shared Global Counter (Ray Actor) ---
|
|
11
|
+
@ray.remote
|
|
12
|
+
class ProgressTracker:
|
|
13
|
+
def __init__(self, total_items):
|
|
14
|
+
self.total_items = total_items
|
|
15
|
+
self.processed_count = 0
|
|
16
|
+
self.start_time = time.time()
|
|
17
|
+
|
|
18
|
+
def increment(self):
|
|
19
|
+
self.processed_count += 1
|
|
20
|
+
|
|
21
|
+
def get_stats(self):
|
|
22
|
+
elapsed = time.time() - self.start_time
|
|
23
|
+
speed = self.processed_count / elapsed if elapsed > 0 else 0
|
|
24
|
+
return self.processed_count, self.total_items, speed, elapsed
|
|
25
|
+
|
|
26
|
+
# --- 2. Cluster Manager ---
|
|
27
|
+
class RayRunner:
|
|
28
|
+
def __init__(self, gpus_per_worker=1, test_mode=False):
|
|
29
|
+
self.gpus_per_worker = gpus_per_worker
|
|
30
|
+
self.test_mode = test_mode
|
|
31
|
+
|
|
32
|
+
# Logging Setup
|
|
33
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
34
|
+
self.log_base = f"/tmp/raylog/runs_{timestamp}"
|
|
35
|
+
|
|
36
|
+
# Initialize Ray if not in test mode
|
|
37
|
+
if self.test_mode:
|
|
38
|
+
print(f">>> [TEST MODE] Running locally (CPU).")
|
|
39
|
+
else:
|
|
40
|
+
if not ray.is_initialized():
|
|
41
|
+
ray.init(address="auto", ignore_reinit_error=True)
|
|
42
|
+
|
|
43
|
+
resources = ray.cluster_resources()
|
|
44
|
+
self.total_gpus = int(resources.get("GPU", 0))
|
|
45
|
+
if self.total_gpus == 0:
|
|
46
|
+
raise RuntimeError("No GPUs found in cluster!")
|
|
47
|
+
|
|
48
|
+
print(f">>> Connected. Available GPUs: {self.total_gpus}")
|
|
49
|
+
print(f">>> Logs redirected to: {self.log_base}")
|
|
50
|
+
os.makedirs(self.log_base, exist_ok=True)
|
|
51
|
+
|
|
52
|
+
def run(self, worker_class, all_data, **kwargs):
|
|
53
|
+
# --- TEST MODE ---
|
|
54
|
+
if self.test_mode:
|
|
55
|
+
# Run local simple version
|
|
56
|
+
worker = worker_class(worker_id=0, log_dir=None, tracker=None, **kwargs)
|
|
57
|
+
worker.setup()
|
|
58
|
+
return [worker.process_one_item(x) for x in all_data[:3]]
|
|
59
|
+
|
|
60
|
+
# --- CLUSTER MODE ---
|
|
61
|
+
num_workers = self.total_gpus // self.gpus_per_worker
|
|
62
|
+
print(f">>> Spawning {num_workers} workers for {len(all_data)} items.")
|
|
63
|
+
|
|
64
|
+
# 1. Start the Global Tracker
|
|
65
|
+
tracker = ProgressTracker.remote(len(all_data))
|
|
66
|
+
|
|
67
|
+
# 2. Prepare Shards
|
|
68
|
+
shards = np.array_split(all_data, num_workers)
|
|
69
|
+
|
|
70
|
+
# 3. Create Remote Worker Class
|
|
71
|
+
RemoteWorker = ray.remote(num_gpus=self.gpus_per_worker)(worker_class)
|
|
72
|
+
|
|
73
|
+
actors = []
|
|
74
|
+
futures = []
|
|
75
|
+
|
|
76
|
+
for i, shard in enumerate(shards):
|
|
77
|
+
if len(shard) == 0: continue
|
|
78
|
+
|
|
79
|
+
# Initialize Actor
|
|
80
|
+
actor = RemoteWorker.remote(
|
|
81
|
+
worker_id=i,
|
|
82
|
+
log_dir=self.log_base,
|
|
83
|
+
tracker=tracker, # Pass the tracker handle
|
|
84
|
+
**kwargs
|
|
85
|
+
)
|
|
86
|
+
actors.append(actor)
|
|
87
|
+
|
|
88
|
+
# Launch Task
|
|
89
|
+
futures.append(actor._run_shard.remote(shard.tolist()))
|
|
90
|
+
|
|
91
|
+
results = ray.get(futures)
|
|
92
|
+
return [item for sublist in results for item in sublist]
|
|
93
|
+
|
|
94
|
+
# --- 3. The Base Worker ---
|
|
95
|
+
class RayWorkerBase(ABC):
|
|
96
|
+
def __init__(self, worker_id, log_dir, tracker, **kwargs):
|
|
97
|
+
self.worker_id = worker_id
|
|
98
|
+
self.log_dir = log_dir
|
|
99
|
+
self.tracker = tracker
|
|
100
|
+
self.kwargs = kwargs
|
|
101
|
+
self._log_file_handle = None
|
|
102
|
+
self._last_print_time = 0
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def setup(self):
|
|
106
|
+
"""User must override to initialize models/resources"""
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def process_one_item(self, item):
|
|
111
|
+
"""User must override to process a single item"""
|
|
112
|
+
raise NotImplementedError
|
|
113
|
+
|
|
114
|
+
def _redirect_output(self):
|
|
115
|
+
"""Workers > 0 write to disk. Worker 0 writes to Notebook."""
|
|
116
|
+
if self.worker_id == 0 or self.log_dir is None:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
log_path = os.path.join(self.log_dir, f"worker_{self.worker_id}.log")
|
|
120
|
+
self._log_file_handle = open(log_path, "w", buffering=1)
|
|
121
|
+
sys.stdout = self._log_file_handle
|
|
122
|
+
sys.stderr = self._log_file_handle
|
|
123
|
+
|
|
124
|
+
def _print_global_stats(self):
|
|
125
|
+
"""Only used by Worker 0 to print pretty global stats"""
|
|
126
|
+
if self.tracker is None: return
|
|
127
|
+
|
|
128
|
+
# Limit print frequency to every 5 seconds to avoid spamming Jupyter
|
|
129
|
+
if time.time() - self._last_print_time < 5:
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# Fetch stats from the Actor
|
|
133
|
+
count, total, speed, elapsed = ray.get(self.tracker.get_stats.remote())
|
|
134
|
+
|
|
135
|
+
if speed > 0:
|
|
136
|
+
eta = (total - count) / speed
|
|
137
|
+
eta_str = str(datetime.timedelta(seconds=int(eta)))
|
|
138
|
+
else:
|
|
139
|
+
eta_str = "?"
|
|
140
|
+
|
|
141
|
+
# \r allows overwriting the line (basic animation)
|
|
142
|
+
msg = (f"[Global] {count}/{total} | {count/total:.1%} | "
|
|
143
|
+
f"Speed: {speed:.2f} it/s | ETA: {eta_str}")
|
|
144
|
+
print(msg)
|
|
145
|
+
self._last_print_time = time.time()
|
|
146
|
+
|
|
147
|
+
def _run_shard(self, shard):
|
|
148
|
+
self._redirect_output()
|
|
149
|
+
try:
|
|
150
|
+
self.setup()
|
|
151
|
+
results = []
|
|
152
|
+
|
|
153
|
+
# Simple loop, no tqdm needed for Worker 0 as it prints Global Stats
|
|
154
|
+
# Worker > 0 can use tqdm if they want, but it goes to log file
|
|
155
|
+
iterator = shard
|
|
156
|
+
if self.worker_id > 0:
|
|
157
|
+
iterator = tqdm(shard, desc=f"Worker {self.worker_id}")
|
|
158
|
+
|
|
159
|
+
for item in iterator:
|
|
160
|
+
try:
|
|
161
|
+
res = self.process_one_item(item)
|
|
162
|
+
results.append(res)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
print(f"Error {item}: {e}")
|
|
165
|
+
results.append(None)
|
|
166
|
+
|
|
167
|
+
# Update Global Counter
|
|
168
|
+
if self.tracker:
|
|
169
|
+
self.tracker.increment.remote()
|
|
170
|
+
|
|
171
|
+
# Worker 0: Print Global Stats
|
|
172
|
+
if self.worker_id == 0:
|
|
173
|
+
self._print_global_stats()
|
|
174
|
+
|
|
175
|
+
return results
|
|
176
|
+
finally:
|
|
177
|
+
if self._log_file_handle:
|
|
178
|
+
self._log_file_handle.close()
|