Skip to content

Commit 64f4f5a

Browse files
authored
Only the main process writes the sample cache under data-parallel (#1271)
Under an accelerate data-parallel launch every rank holds the full gathered results and wrote the same parquet cache file concurrently, corrupting it and making subsequent loads fail. Write the cache only on the main process and add a barrier so the other ranks wait for that write before reading. Add a regression test. Fixes #1102
1 parent 95ad3bc commit 64f4f5a

2 files changed

Lines changed: 53 additions & 7 deletions

File tree

src/lighteval/utils/cache_management.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -414,13 +414,21 @@ def wrapper(self, docs: Union[Doc, List[Doc]], *args, **kwargs): # noqa C901
414414
)
415415
new_results = func(self, docs_not_cached, *args, **kwargs)
416416

417-
# Store new results in file cache
418-
cache.cache_samples(
419-
docs=docs_not_cached,
420-
results=new_results,
421-
task_ids=task_ids,
422-
sampling_method=sampling_method,
423-
)
417+
# Store new results in file cache. Under a data-parallel launch (e.g. accelerate with
418+
# several processes), every rank holds the full, gathered results, so only the main
419+
# process writes the cache file. Letting every rank write the same parquet concurrently
420+
# corrupts it and makes subsequent loads fail. Other ranks wait at the barrier below
421+
# before reading. See https://github.com/huggingface/lighteval/issues/1102.
422+
accelerator = getattr(self, "accelerator", None)
423+
if accelerator is None or accelerator.is_main_process:
424+
cache.cache_samples(
425+
docs=docs_not_cached,
426+
results=new_results,
427+
task_ids=task_ids,
428+
sampling_method=sampling_method,
429+
)
430+
if accelerator is not None:
431+
accelerator.wait_for_everyone()
424432

425433
# 3) Create final results by pulling from newly saved file cache
426434
final_cached_results = cache.get_samples_from_cache(docs, task_ids, sampling_method)

tests/unit/utils/test_caching.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,44 @@ def test_cache_transformers(self, mock_create_model, mock_accelerator, mock_gree
214214
],
215215
)
216216

217+
@patch("lighteval.models.transformers.transformers_model.TransformersModel._padded_greedy_until")
218+
@patch("lighteval.models.transformers.transformers_model.Accelerator")
219+
@patch("lighteval.models.transformers.transformers_model.TransformersModel._create_auto_model")
220+
def test_cache_only_main_process_writes(self, mock_create_model, mock_accelerator, mock_greedy_until):
221+
"""Regression test for #1102. Under a data-parallel (accelerate) launch every rank holds the same
222+
gathered results and would write the same parquet concurrently, corrupting the cache. Only the main
223+
process must write; other ranks must wait at a barrier (so the file exists before they read it)."""
224+
from lighteval.models.transformers.transformers_model import TransformersModel, TransformersModelConfig
225+
226+
mock_create_model = Mock() # noqa F841
227+
mock_accelerator_instance = Mock()
228+
mock_accelerator_instance.device = torch.device("cpu")
229+
mock_accelerator.return_value = mock_accelerator_instance
230+
mock_greedy_until.return_value = self.model_responses
231+
232+
with tempfile.TemporaryDirectory() as temp_dir:
233+
config = TransformersModelConfig(model_name="Qwen/Qwen3-0.6B", cache_dir=temp_dir)
234+
model = TransformersModel(config)
235+
cache: SampleCache = model._cache
236+
task_id = cache.get_task_id(self.task_name, SamplingMethod.GENERATIVE)
237+
cache_file = cache.get_cache_path(task_id)
238+
239+
# Non-main process: must NOT write the cache file, but must hit the barrier and still return
240+
# results (in a real run the main process has written the file by the time the barrier clears,
241+
# which we emulate by patching the cache read).
242+
mock_accelerator_instance.is_main_process = False
243+
mock_accelerator_instance.wait_for_everyone.reset_mock()
244+
with patch.object(cache, "get_samples_from_cache", return_value=self.model_responses):
245+
results = model.greedy_until(self.docs)
246+
self.assertFalse(cache_file.exists(), "Non-main process must not write the cache file (#1102)")
247+
mock_accelerator_instance.wait_for_everyone.assert_called()
248+
self.assertEqual(len(results), len(self.docs))
249+
250+
# Main process: must write the cache file.
251+
mock_accelerator_instance.is_main_process = True
252+
model.greedy_until(self.docs)
253+
self.assertTrue(cache_file.exists(), "Main process must write the cache file")
254+
217255
@patch("lighteval.models.vllm.vllm_model.VLLMModel._loglikelihood_tokens")
218256
@patch("lighteval.models.vllm.vllm_model.VLLMModel._greedy_until")
219257
@patch("lighteval.models.vllm.vllm_model.VLLMModel._create_auto_model")

0 commit comments

Comments
 (0)