Skip to content

[Train] Support Out of Band Checkpoints#63161

Closed
pseudo-rnd-thoughts wants to merge 15 commits into
ray-project:masterfrom
pseudo-rnd-thoughts:train-704-reimplement
Closed

[Train] Support Out of Band Checkpoints#63161
pseudo-rnd-thoughts wants to merge 15 commits into
ray-project:masterfrom
pseudo-rnd-thoughts:train-704-reimplement

Conversation

@pseudo-rnd-thoughts

@pseudo-rnd-thoughts pseudo-rnd-thoughts commented May 6, 2026

Copy link
Copy Markdown
Member

Description

Currently ray.train.report expects that all checkpoints are saved within the trainer's storage_path. However, with CheckpointUploadMode.NO_UPLOAD or a custom checkpoint_upload_fn allows users to specify paths outside the storage path.
This causes a problem on resumption for two reasons, we only store the checkpoints path relative to the storage path (which wouldn't be valid for out of band checkpoints) and we resume from the storage path's filesystem (which wouldn't be valid for checkpoints stored on a different filesystem).

Side note: Using relative paths allows user copied a training result's data include the checkpoint data to a new directory and restore it later. For in-band checkpoints, we keep this feature while for out of band checkpoints, we assume that the checkpoints are in a long-term storage that won't be moved.

Checkpoint options

  1. Save in-band on the same file system as storage path
  2. Save out of band on the same file system as the storage path
  3. Save out of band on a different file system as the storage path

The challenge is that for non-local file systems (gcp, s3, etc) then often passwords, env variables, etc are necessary to config a filesystem to read the data. Annoyingly, pyarrow.fs.FileSystem.to_uri() doesn't exist so we can't serialise the filesystem as a string. It is possible to serialising the filesystem with pickle but there are issue with this that I comment on at the end.

Implementation options

We have three options

  1. Only support in-band checkpoints with documentation for out of band checkpoints + callback for checkpoint deletion to handle secret out of band checkpoints
  2. Only support in and out of band checkpoints on the same filesystem, explicitly disallowing checkpoints to be on a different filesystem. Data can still be saved to a different filesystem just as a ray.train.report(checkpoint=...)
  3. Only in and out of band checkpoints on this and other filesystems, however can cause issues for users when restoring from checkpoints on a different non-local filesystem.

Implementing all in and out of band checking

If we want to support all checkpoint options, we need to handle them differently.

  1. In-band checkpoints - we store the relative path to the storage path and use the storage path filesystem on resumptions. (no change for current implementation)
  2. Out of band on the same file system - we store the full path then on resumption use the storage path filesystem
  3. Out of band on a different file system - we store the full path and the file system's type (s3, gcp, etc) that on resumption we create a mock version of the filesystem that will be missing any region, env-vars, etc . Importantly, this will prevent Checkpoint.to_directory(), therefore, we warn users about this on resumption if when we try to confirm the checkpoint still exist, this fails.

For CheckpointConfig(num_to_keep=1), then we will try to delete in and out of band checkpoints, however if this fails for any reason, a warning is now produced.

Alternative implementations

It is feasible to pickle the filesystem and save this information alongside the path allowing the easiest resumptions with Checkpoint(path, pickle.dump(filesystem)).
However, first this is put a security risk as pickle.dump will run whatever it passed allowing RCEs and second that saved some passwords, env vars, might become invalid.

Mark Towers added 4 commits May 5, 2026 16:53
Signed-off-by: Mark Towers <mark@anyscale.com>
Signed-off-by: Mark Towers <mark@anyscale.com>
Signed-off-by: Mark Towers <mark@anyscale.com>
@pseudo-rnd-thoughts
pseudo-rnd-thoughts requested a review from a team as a code owner May 6, 2026 18:19
@pseudo-rnd-thoughts pseudo-rnd-thoughts added the train Ray Train Related Issue label May 6, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for out-of-band checkpoints in Ray Train v2, allowing checkpoints to be stored outside the primary experiment directory. Key changes include updating the _TrainingResultState schema, implementing logic to identify and reconstruct filesystems for external checkpoints, and modifying validation routines to handle out-of-band data gracefully. Review feedback highlights the need for safer deletion policies for out-of-band checkpoints to avoid unintended data loss and suggests more robust error handling when encountering unsupported filesystem types during state restoration.

I am having trouble creating individual review comments. Click here to see my feedback.

python/ray/train/v2/_internal/execution/checkpoint/checkpoint_manager.py (94-102)

high

As noted in the PR description, there's a question of how to handle deletion of out-of-band checkpoints when num_to_keep is used. Automatically deleting data from user-specified, out-of-band locations could be risky and lead to unintended data loss.

A safer approach would be to avoid deleting out-of-band checkpoints automatically. Instead, Ray Train could log a warning indicating that a checkpoint has been marked for deletion but requires manual cleanup because it's out-of-band. This would give users full control over their data in external storage.

This function _is_in_band could be used within _save_state_and_delete_old_checkpoints to implement this safer behavior.

python/ray/train/v2/_internal/execution/checkpoint/checkpoint_manager.py (142-146)

high

The code currently accesses _TYPE_NAME_FS_CLS with a key directly. If state.checkpoint_filesystem_type contains a value that is not in _TYPE_NAME_FS_CLS (e.g., for a less common or custom filesystem), this will raise a KeyError and cause the training resumption to fail. It would be more robust to handle this case gracefully by checking if the key exists and raising a more informative error message if it doesn't.

    fs_factory = _TYPE_NAME_FS_CLS.get(state.checkpoint_filesystem_type)
    if not fs_factory:
        raise CheckpointManagerInitializationError(
            f"Unsupported filesystem type for out-of-band checkpoint: "
            f"'{state.checkpoint_filesystem_type}'. Supported types are: "
            f"{list(_TYPE_NAME_FS_CLS.keys())}"
        )
    return _TrainingResult(
        checkpoint=Checkpoint(state.checkpoint_dir_name, filesystem=fs_factory()),
        metrics=state.metrics,
    )

Comment thread python/ray/train/v2/_internal/execution/checkpoint/checkpoint_manager.py Outdated
Mark Towers added 2 commits May 7, 2026 14:58
Signed-off-by: Mark Towers <mark@anyscale.com>
Signed-off-by: Mark Towers <mark@anyscale.com>
Signed-off-by: Mark Towers <mark@anyscale.com>
"s3": lambda: pyarrow.fs.S3FileSystem(),
"gcs": lambda: pyarrow.fs.GcsFileSystem(),
"hdfs": lambda: pyarrow.fs.HadoopFileSystem("missing-host"),
"abfs": lambda: pyarrow.fs.AzureFileSystem("missing-host"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HDFS/ABFS filesystem reconstruction crashes on construction

Medium Severity

The _TYPE_NAME_FS_CLS entries for "hdfs" and "abfs" use pyarrow.fs.HadoopFileSystem("missing-host") and pyarrow.fs.AzureFileSystem("missing-host"). HadoopFileSystem attempts to connect to the HDFS namenode at construction time, so providing an invalid host will raise an immediate connection error. When _get_training_result_from_state calls fs() at line 135, this crashes the entire state restoration in _load_state, preventing resumption of any run that ever had an HDFS or Azure out-of-band checkpoint—even if other checkpoints in the same run are healthy.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 49d3860. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is intentional

@TimothySeah TimothySeah left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reviewed everything except the unit test, which I will review on my next pass.

Can you clarify what Side note: Using relative paths allows user copied a training result's data include the checkpoint data to a new directory and restore it later. For in-band checkpoints, we keep this feature while for out of band checkpoints, we assume that the checkpoints are in a long-term storage that won't be moved. means?

# Out-of-band: checkpoint_dir_name holds the full path;
# reconstruct the filesystem from type_name.
if state.checkpoint_filesystem_type != "local":
logger.warning(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Will users see this warning even in the "happy" out of band path i.e. user saves checkpoint with recoverable filesystem? I wonder if there's an easy way to only warn when the user actually encounters a problem e.g. they fail to download the checkpoint.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

See below

Comment thread python/ray/train/v2/_internal/execution/checkpoint/checkpoint_manager.py Outdated
continue

if is_out_of_band:
logger.warning(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similar comment here and everywhere else we are warning - I only want users to see warnings when they actually encounter something that fails. Loading an out of band checkpoint into self._checkpoint_results is fine, but actually downloading that with to_directory should surface the real error.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed, I was a tad worried about the console spam that this causes.
One option is to add a keyword only argument to Checkpoint that specifies it was created with an out of band filesystem. Then apply a try / except to to_directory that can add this debug information.
This could reduce the number of warnings that we're creating.

Thoughts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need to add a new argument to Checkpoint? Ideally we should avoid adding new arguments - even if they are backwards compatible by default - and whether a checkpoint is in band or out of band should be invisible to users. I think if to_directory fails the error message can just mention the "out of band different filesystem" case as a potential cause.

@pseudo-rnd-thoughts

Copy link
Copy Markdown
Member Author

Can you clarify what Side note: Using relative paths allows user copied a training result's data include the checkpoint data to a new directory and restore it later. For in-band checkpoints, we keep this feature while for out of band checkpoints, we assume that the checkpoints are in a long-term storage that won't be moved. means?

One of the features of the checkpoint manager that I didn't understand intuitively is that it saves the checkpoint dir names as the relative path to the storage path rather than the absolute path. This was an unexpected design decision that I post-hoc rationalised as allowing users to copy the experiment directory to a new directory, users can resume or load the results with the new storage path. If we used absolute paths then this wouldn't be possible.

If this is an incorrect understand of why this decision was taken then I would be fascinated to understand its true intentions

Signed-off-by: Mark Towers <mark@anyscale.com>
Comment thread python/ray/train/v2/_internal/execution/checkpoint/checkpoint_manager.py Outdated

@TimothySeah TimothySeah left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One of the features of the checkpoint manager that I didn't understand intuitively is that it saves the checkpoint dir names as the relative path to the storage path rather than the absolute path. This was an unexpected design decision that I post-hoc rationalised as allowing users to copy the experiment directory to a new directory, users can resume or load the results with the new storage path. If we used absolute paths then this wouldn't be possible.

If this is an incorrect understand of why this decision was taken then I would be fascinated to understand its true intentions

Ah gotcha thanks for clarifying. Tbh I'm not sure what the original motivation was, but my mental model is that in the past the CheckpointManager only managed in band (i.e. all with the same filesystem and directory prefix) checkpoints so storing any information that included the filesystem and directory prefix was unnecessary.

continue

if is_out_of_band:
logger.warning(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need to add a new argument to Checkpoint? Ideally we should avoid adding new arguments - even if they are backwards compatible by default - and whether a checkpoint is in band or out of band should be invisible to users. I think if to_directory fails the error message can just mention the "out of band different filesystem" case as a potential cause.

@pseudo-rnd-thoughts

Copy link
Copy Markdown
Member Author

Do we need to add a new argument to Checkpoint? Ideally we should avoid adding new arguments - even if they are backwards compatible by default - and whether a checkpoint is in band or out of band should be invisible to users. I think if to_directory fails the error message can just mention the "out of band different filesystem" case as a potential cause.

Possibly, the place it would make most sense to insert the error as an extra exception case in to_directory

This is just a preference of more logging or better error message.
Personally, I'm 60% error message, 40% logging. Happy to change @TimothySeah if you think one is better.

@pseudo-rnd-thoughts pseudo-rnd-thoughts added the go add ONLY when ready to merge, run all tests label May 11, 2026
@TimothySeah

Copy link
Copy Markdown
Contributor

Do we need to add a new argument to Checkpoint? Ideally we should avoid adding new arguments - even if they are backwards compatible by default - and whether a checkpoint is in band or out of band should be invisible to users. I think if to_directory fails the error message can just mention the "out of band different filesystem" case as a potential cause.

Possibly, the place it would make most sense to insert the error as an extra exception case in to_directory

This is just a preference of more logging or better error message. Personally, I'm 60% error message, 40% logging. Happy to change @TimothySeah if you think one is better.

I think minimizing API changes and proactive logging in favor of raising informative errors when something actually goes wrong would be the least intrusive/confusing to users

Signed-off-by: Mark Towers <mark@anyscale.com>
@pseudo-rnd-thoughts

Copy link
Copy Markdown
Member Author

I think minimizing API changes and proactive logging in favor of raising informative errors when something actually goes wrong would be the least intrusive/confusing to users

I think we are agreed on the current implementation then?

Signed-off-by: Mark Towers <mark@anyscale.com>
Signed-off-by: Mark Towers <mark@anyscale.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Reviewed by Cursor Bugbot for commit d3d633e. Configure here.

ckpt_path = Path(checkpoint_path)

if ckpt_path.is_relative_to(exp_path):
return str(ckpt_path.relative_to(exp_path))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Path uses OS-specific separators instead of POSIX format

Medium Severity

extract_checkpoint_dir_name_from_path returns str(ckpt_path.relative_to(exp_path)) which uses OS-specific path separators. On Windows this produces backslashes (e.g., "trial\\checkpoint"), but pyarrow filesystem paths always use forward slashes. The old code used string slicing that preserved the original forward-slash format. The result is stored in JSON state and later joined with Path(...).as_posix() on restore — if the state file is created on Windows, the backslash becomes part of the filename literal on Linux, corrupting the path. Using .as_posix() instead of str() would maintain consistency with all other path construction in this file.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d3d633e. Configure here.

@TimothySeah TimothySeah left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think minimizing API changes and proactive logging in favor of raising informative errors when something actually goes wrong would be the least intrusive/confusing to users

I think we are agreed on the current implementation then?

Can you do a train run with an out of band checkpoint and show me the logs? Basically I think it should be something like:

  • report it with NO_UPLOAD: no logs from uploading (no upload performed) or serializing into checkpoint manager state
  • recover it from new run + get_checkpoint. No logs from deserializing from checkpointmanager.
  • try to download it with to_directory or with contextmanager. Doesn't work and only raise ValueError then.

Iiuc, right now this might still produce logs for the first 2 steps.

I also looked at the unit tests and gave some feedback - I think this should be good to go after 1 final pass.

Comment thread python/ray/train/v2/_internal/execution/checkpoint/checkpoint_manager.py Outdated
Comment thread python/ray/train/v2/tests/test_data_parallel_trainer.py Outdated
CreateBucketConfiguration={"LocationConstraint": region},
)
# S3 with default upload
s3.put_object(Bucket=bucket_name, Key="epoch-6/results.txt", Body="6")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wait what does this test? Iiuc, report will upload a checkpoint to the local directory, and this s3.put_object isn't tracked at all?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The checkpoint is originally stored on S3 but copied into the local experiment directory.
I don't test that all checkpoint files exist, just the checkpoint exists. I've added this testing that the local checkpoint contents exists.

Comment thread python/ray/train/v2/tests/test_data_parallel_trainer.py Outdated
assert isinstance(checkpoint.filesystem, pyarrow.fs.S3FileSystem)

reported_checkpoints = ray.train.get_all_reported_checkpoints()
assert len(reported_checkpoints) == 8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we also test that to_directory or with works from some checkpoints but raises a helpful error from others?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, added to the tests now

# Verify the snapshot left on S3 is loadable and points to the
# correct (in-band/out-of-band) checkpoint locations.
restored_result = Result.from_path(str(in_band_uri / "test-s3-in-out-of-band"))
assert len(restored_result.best_checkpoints) == 7

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same here: consider calling to_directory/with from these checkpoints and verifying that some work and some error as expected

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh and we can test that the pattern we are recommending to users in the error message (making a new checkpoint like Checkpoint(path=<old checkpoint's path>, filesystem=<new filesystem that they just made>) works.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, added to the tests to confirm the checkpoint's contents

Mark Towers added 2 commits May 13, 2026 16:38
Signed-off-by: Mark Towers <mark@anyscale.com>
Signed-off-by: Mark Towers <mark@anyscale.com>
@pseudo-rnd-thoughts

pseudo-rnd-thoughts commented May 13, 2026

Copy link
Copy Markdown
Member Author

@TimothySeah Here the full logs from test_s3_in_out_of_band_checkpointing which I think is a fair comparison as it contain in and out of band checkpoints

2026-05-13 17:05:52,306	INFO worker.py:2018 -- Started a local Ray instance. View the dashboard at http://127.0.0.1:8265 
(TrainController pid=94721) Requesting resources: {'CPU': 1} * 1
(TrainController pid=94721) Attempting to start training worker group of size 1 with the following resources: [{'CPU': 1}] * 1
(TrainController pid=94721) Started training worker group of size 1: 
(TrainController pid=94721) - (ip=127.0.0.1, pid=94730) world_rank=0, local_rank=0, node_rank=0
(RayTrainWorker pid=94730) Checkpoint successfully created at: Checkpoint(filesystem=local, path=/private/var/folders/vl/ys8w0ghs7r1dy30rlzn389_r0000gp/T/tmpq7k74aue/storage-dir/test-local-in-out-of-band/epoch-1)
(RayTrainWorker pid=94730) Reporting training result 1: TrainingReport(checkpoint=Checkpoint(filesystem=local, path=/private/var/folders/vl/ys8w0ghs7r1dy30rlzn389_r0000gp/T/tmpq7k74aue/storage-dir/test-local-in-out-of-band/epoch-1), metrics={'score': 1}, validation=False) from rank 0
(RayTrainWorker pid=94730) Reporting training result 2: TrainingReport(checkpoint=Checkpoint(filesystem=local, path=/private/var/folders/vl/ys8w0ghs7r1dy30rlzn389_r0000gp/T/tmpq7k74aue/storage-dir/epoch-2), metrics={'score': 2}, validation=False) from rank 0
(RayTrainWorker pid=94730) Reporting training result 3: TrainingReport(checkpoint=Checkpoint(filesystem=local, path=/private/var/folders/vl/ys8w0ghs7r1dy30rlzn389_r0000gp/T/tmpq7k74aue/storage-dir/epoch-3), metrics={'score': 3}, validation=False) from rank 0
(RayTrainWorker pid=94730) Reporting training result 4: TrainingReport(checkpoint=Checkpoint(filesystem=local, path=/private/var/folders/vl/ys8w0ghs7r1dy30rlzn389_r0000gp/T/tmpq7k74aue/oob-dir/epoch-4), metrics={'score': 4}, validation=False) from rank 0
(RayTrainWorker pid=94730) Reporting training result 5: TrainingReport(checkpoint=Checkpoint(filesystem=local, path=/private/var/folders/vl/ys8w0ghs7r1dy30rlzn389_r0000gp/T/tmpq7k74aue/oob-dir/epoch-5), metrics={'score': 5}, validation=False) from rank 0
(RayTrainWorker pid=94730) Found credentials in environment variables.
(RayTrainWorker pid=94730) Checkpoint successfully created at: Checkpoint(filesystem=local, path=/private/var/folders/vl/ys8w0ghs7r1dy30rlzn389_r0000gp/T/tmpq7k74aue/storage-dir/test-local-in-out-of-band/epoch-6)
(RayTrainWorker pid=94730) Reporting training result 6: TrainingReport(checkpoint=Checkpoint(filesystem=local, path=/private/var/folders/vl/ys8w0ghs7r1dy30rlzn389_r0000gp/T/tmpq7k74aue/storage-dir/test-local-in-out-of-band/epoch-6), metrics={'score': 6}, validation=False) from rank 0
(RayTrainWorker pid=94730) Reporting training result 7: TrainingReport(checkpoint=Checkpoint(filesystem=s3, path=7444c405b9ba4da78c6897207b9b1d72/epoch-7), metrics={'score': 7}, validation=False) from rank 0
(RayTrainWorker pid=94730) Reporting training result 8: TrainingReport(checkpoint=Checkpoint(filesystem=s3, path=7444c405b9ba4da78c6897207b9b1d72/epoch-8), metrics={'score': 8}, validation=False) from rank 0
(RayTrainWorker pid=94730) Error in training function:
(RayTrainWorker pid=94730) Traceback (most recent call last):
(RayTrainWorker pid=94730)   File "/Users/mark/PycharmProjects/ray/scratch/train-704.py", line 136, in train_fn
(RayTrainWorker pid=94730)     raise RuntimeError("intentional failure after checkpoint reports")
(RayTrainWorker pid=94730) RuntimeError: intentional failure after checkpoint reports
(RayTrainWorker pid=94730) 
(TrainController pid=94721) [FailurePolicy] RAISE
(TrainController pid=94721)   Source: worker group
(TrainController pid=94721)   Error count: 1 (max allowed: 0)
(TrainController pid=94721) Error: Training failed due to worker errors:
(TrainController pid=94721) [Rank 0 Error Snippet]:
(TrainController pid=94721) ray.train.WorkerGroupError: Training failed due to worker errors:
(TrainController pid=94721) [Rank 0 Error Snippet]:
(TrainController pid=94721) 
(TrainController pid=94775) A run snapshot was found in storage folder at: '/private/var/folders/vl/ys8w0ghs7r1dy30rlzn389_r0000gp/T/tmpq7k74aue/storage-dir/test-local-in-out-of-band'
(TrainController pid=94775) This snapshot contains a list of checkpoints reported via `ray.train.report` and will be loaded. This allows the latest checkpoint found in the snapshot to be accessible within your training function via `ray.train.get_checkpoint`.
(TrainController pid=94775) If you meant to start a brand new training job without any information about previous checkpoints found in this directory, please configure a new, unique `RunConfig(name)` or delete the existing folder at '/private/var/folders/vl/ys8w0ghs7r1dy30rlzn389_r0000gp/T/tmpq7k74aue/storage-dir/test-local-in-out-of-band'.
(TrainController pid=94775) Could not verify out-of-band checkpoint exists at Checkpoint(filesystem=s3, path=7444c405b9ba4da78c6897207b9b1d72/epoch-7). Error was When getting information for key 'epoch-7' in bucket '7444c405b9ba4da78c6897207b9b1d72': AWS Error ACCESS_DENIED during HeadObject operation: No response body.. Reason is probably as the checkpoint's filesystem was reconstructed without credentials. Update the checkpoint's filesystem with a configured one.
(TrainController pid=94775) Could not verify out-of-band checkpoint exists at Checkpoint(filesystem=s3, path=7444c405b9ba4da78c6897207b9b1d72/epoch-8). Error was When getting information for key 'epoch-8' in bucket '7444c405b9ba4da78c6897207b9b1d72': AWS Error ACCESS_DENIED during HeadObject operation: No response body.. Reason is probably as the checkpoint's filesystem was reconstructed without credentials. Update the checkpoint's filesystem with a configured one.
(TrainController pid=94775) Requesting resources: {'CPU': 1} * 1
(TrainController pid=94775) Attempting to start training worker group of size 1 with the following resources: [{'CPU': 1}] * 1
(TrainController pid=94775) Started training worker group of size 1: 
(TrainController pid=94775) - (ip=127.0.0.1, pid=94780) world_rank=0, local_rank=0, node_rank=0
2026-05-13 17:06:18,296	INFO checkpoint_manager.py:493 -- A run snapshot was found in storage folder at: '/private/var/folders/vl/ys8w0ghs7r1dy30rlzn389_r0000gp/T/tmpq7k74aue/storage-dir/test-local-in-out-of-band'
This snapshot contains a list of checkpoints reported via `ray.train.report` and will be loaded. This allows the latest checkpoint found in the snapshot to be accessible within your training function via `ray.train.get_checkpoint`.
If you meant to start a brand new training job without any information about previous checkpoints found in this directory, please configure a new, unique `RunConfig(name)` or delete the existing folder at '/private/var/folders/vl/ys8w0ghs7r1dy30rlzn389_r0000gp/T/tmpq7k74aue/storage-dir/test-local-in-out-of-band'.
2026-05-13 17:06:18,440	WARNING checkpoint_manager.py:542 -- Could not verify out-of-band checkpoint exists at Checkpoint(filesystem=s3, path=7444c405b9ba4da78c6897207b9b1d72/epoch-7). Error was When getting information for key 'epoch-7' in bucket '7444c405b9ba4da78c6897207b9b1d72': AWS Error ACCESS_DENIED during HeadObject operation: No response body.. Reason is probably as the checkpoint's filesystem was reconstructed without credentials. Update the checkpoint's filesystem with a configured one.
2026-05-13 17:06:18,920	WARNING checkpoint_manager.py:542 -- Could not verify out-of-band checkpoint exists at Checkpoint(filesystem=s3, path=7444c405b9ba4da78c6897207b9b1d72/epoch-8). Error was When getting information for key 'epoch-8' in bucket '7444c405b9ba4da78c6897207b9b1d72': AWS Error ACCESS_DENIED during HeadObject operation: No response body.. Reason is probably as the checkpoint's filesystem was reconstructed without credentials. Update the checkpoint's filesystem with a configured one.
(TrainController pid=94721) Traceback (most recent call last): [repeated 2x across cluster] (Ray deduplicates logs by default. Set RAY_DEDUP_LOGS=0 to disable log deduplication, or see https://docs.ray.io/en/master/ray-observability/user-guides/configure-logging.html#log-deduplication for more options.)
(TrainController pid=94721)   File "/Users/mark/PycharmProjects/ray/scratch/train-704.py", line 136, in train_fn [repeated 2x across cluster]
(TrainController pid=94721)     raise RuntimeError("intentional failure after checkpoint reports") [repeated 2x across cluster]
(TrainController pid=94721) RuntimeError: intentional failure after checkpoint reports [repeated 2x across cluster]

You can see at the end, the checkpoint resumption that for the two out of band s3 checkpoints we fail to confirm that it exists.

Note: The RuntimeError is shown twice for some reason which isn't expected. Not sure why

@justinvyu justinvyu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we just force the user to report a "pointer checkpoint" instead? Then, we still keep track of the "in-band" checkpoint and the user just needs to manage loading the path to the out of band checkpoint themselves.

# Instead of "NO_UPLOAD", force the user to report an in-band pointer checkpoint to the out of band checkpoint
torch.distributed.checkpoint.save(..., "s3://a/b/c")
with TemporaryDirectory() as temp_ckpt_dir:
    pointer_dict = {"uri": "s3://a/b/c"}
    json.dump(pointer_dict, temp_ckpt_dir)
    ray.train.report(..., checkpoint=pointer_dict, checkpoint_upload_mode=SYNC)

# Force custom_upload_fn to return in-band checkpoint.
# The contract of custom_upload_fn is that it must return an in-band checkpoint.
def custom_upload_fn(checkpoint_source, checkpoint_destination):
    do_out_of_band_checkpoint()
    pointer_dict = {"uri": "s3://a/b/c"}
    write_pointer_checkpoint_to_destination(pointer_dict)

If users need us to clean up checkpoints ("top k checkpoints"), we can add a callback hook that fires when a checkpoint gets evicted from the top k checkpoints and they can add the out of band cleanup themselves.

I'm thinking we can tighten the API contract a bit to avoid adding complexity handling out of band vs in band, different filesystem vs same filesystem. Also would like to avoid the reverse lookup of filesystem string name -> filesystem.

@pseudo-rnd-thoughts

pseudo-rnd-thoughts commented May 15, 2026

Copy link
Copy Markdown
Member Author

I'm thinking we can tighten the API contract a bit to avoid adding complexity handling out of band vs in band, different filesystem vs same filesystem. Also would like to avoid the reverse lookup of filesystem string name -> filesystem.

I agree we have a choice with this PR, either open the API to fully support in and out of band checkpoints, or restrict the API to only support in-band checkpoints with a documentation like your solution for users that need out of band checkpoint. Out of band checkpointing does feel like an edge by users to support, however callback being necessary for in-band only support is off putting to me.

To me, I think we should support out of band checkpoint as the API needs to expand minimally to support them with the biggest difficulty coming from different out of band filesystems which we could disallow. This would restrict the API to only support in and out of band checkpoints on the same filesystem.

We have three options

  1. Only support in-band checkpoints with documentation for out of band checkpoints + callback for checkpoint deletion to handle secret out of band checkpoints
  2. Only support in and out of band checkpoints on the same filesystem, explicitly disallowing checkpoints to be on a different filesystem. Data can still be saved to a different filesystem just as a ray.train.report(checkpoint=...)
  3. Only in and out of band checkpoints on this and other filesystems, however can cause issues for users when restoring from checkpoints on a different non-local filesystem.

@justinvyu Any other considerations? If we want to support out of band checkpointing, this is the best solution IMO, if we want to disallow out of band checkpoint all together, then we can do this in a separate PR.

@justinvyu

Copy link
Copy Markdown
Contributor

callback being necessary for in-band only support is off putting to me.

I was proposing callback only being needed for out of band support. We would trigger the hook right before deleting the "in-band pointer checkpoint" and then the user can clean up the out of band checkpoint themselves.

I like option 1 the most. I think detecting the same filesystem is a bit tricky due to the wide variety of s3 clients people could be using. Then, our "same filesystem out of band" download with to_directory() might not even work. I'd rather have the user control both upload and download themselves if they're doing custom writing to storage.

@pseudo-rnd-thoughts

Copy link
Copy Markdown
Member Author

Closing, in favor of an implementation that restricts the API to only support in-band checkpoints and raise an error when out of band checkpoints are used

edoakes pushed a commit that referenced this pull request Jul 15, 2026
…nd checkpoints (#63645)

## Description
Currently `ray.train.report` expects that all checkpoints are saved
within the trainer's `storage_path`. However, with
`CheckpointUploadMode.NO_UPLOAD` or a custom `checkpoint_upload_fn`
allows users to specify paths outside the storage path.
This causes a problem on resumption for two reasons, we only store the
checkpoints path relative to the storage path (which wouldn't be valid
for out of band checkpoints) and we resume from the storage path's
filesystem (which wouldn't be valid for checkpoints stored on a
different filesystem).

Therefore, we have three options to either restrict the API to
1. Only save in-band checkpoints and raise errors on all others
2. Support in and out of band checkpoints on the same filesystems but
not support checkpoints on a different filesystem
3. Support all in and out of band checkpoints on any filesystem. (This
was implemented in #63161)

The challenge is that for non-local file systems (gcp, s3, etc) often
passwords, env variables, etc are necessary to config a filesystem to
read the data. As pyarrow doesn't have `pyarrow.fs.FileSystem.to_uri()`
we can't serialise the filesystem as a string. It is possible to
serialising the filesystem with pickle but there are obvious issue with
this.

As a result, this PR restricts the API to only support in-band
checkpoint to avoid several problems with out of band checkpoints
(option 1).

To access the storage context, its possible for users to do
`ray.train.get_context().get_storage().experiment_fs_path` and
`ray.train.get_context().get_storage().storage_filesystem`

---------

Signed-off-by: Mark Towers <mark@anyscale.com>
Co-authored-by: Mark Towers <mark@anyscale.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

go add ONLY when ready to merge, run all tests train Ray Train Related Issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants