[data] Make checkpoint restore pluggable via CheckpointConfig checkpoint_filter_cls / checkpoint_manager_cls - #65676
Conversation
…nt_filter_cls Add an optional checkpoint_filter_cls field to CheckpointConfig so users can plug in a custom CheckpointFilter subclass for the restore path, mirroring the existing pluggability of the write side. The class is validated at config construction and instantiated once per checkpoint filter actor with (checkpoint_config, checkpointed_ids_ref); default behavior (NumpyArrayBasedCheckpointFilter) is unchanged. CheckpointFilter and NumpyArrayBasedCheckpointFilter are annotated DeveloperAPI and lazily re-exported from ray.data.checkpoint to avoid the import cycle through ray.data.context. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Kit Lee <7000003+wingkitlee0@users.noreply.github.com>
…int_manager_cls Complements checkpoint_filter_cls: a custom filter that consumes a different loaded representation (Arrow-native, sharded) needs control over how checkpoint data is loaded, which lives in CheckpointManager.load_checkpoint. Add checkpoint_manager_cls with the same validation and default-preserving semantics, annotate CheckpointManager and IdColumnCheckpointManager as DeveloperAPI, and document the (ObjectRef, int) contract between manager and filter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Kit Lee <7000003+wingkitlee0@users.noreply.github.com>
…ses, annotate __getattr__ - Deduplicate the checkpoint_filter_cls / checkpoint_manager_cls validation into _validate_checkpoint_cls. - Reject abstract classes at config construction: unlike other pluggable class params in Ray (ExecutionCallback, ShuffleAggregation), these are instantiated inside remote filter actors, so an abstract class would otherwise fail as a confusing actor crash instead of an immediate InvalidCheckpointingConfig. - Add type annotations to the lazy __getattr__ in ray.data.checkpoint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Kit Lee <7000003+wingkitlee0@users.noreply.github.com>
There was a problem hiding this comment.
Code Review
This pull request introduces support for custom checkpoint filters and managers by allowing users to override checkpoint_filter_cls and checkpoint_manager_cls in CheckpointConfig. It includes validation to ensure the provided classes are concrete subclasses of CheckpointFilter and CheckpointManager, exposes these classes via lazy imports to prevent circular dependencies, and adds comprehensive unit tests. The review feedback suggests using a relative import instead of an absolute import in __init__.py to improve robustness, and updating the CheckpointFilter docstring to clarify that the checkpointed IDs reference is passed opaquely rather than always being a sorted NumPy array.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Reviewed by Cursor Bugbot for commit 55b1b63. Configure here.
…_ref - CheckpointFilter.__init__ now accepts (config, checkpoint_ref) matching how the restore path constructs filters, so a subclass that only implements filter_rows_for_block works without defining its own constructor (Cursor Bugbot finding). - Clarify ABC and CheckpointConfig docstrings: the ref is whatever the checkpoint manager's load_checkpoint returned, a sorted NumPy array only by default (Gemini finding). - Use a relative import in the package __getattr__ (Gemini finding). - test_custom_checkpoint_filter_cls now subclasses the ABC directly with no __init__, covering the inherited-constructor path end-to-end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Kit Lee <7000003+wingkitlee0@users.noreply.github.com>
…orts CI's pyrefly check covers ray/data/checkpoint/__init__.py and flags __all__ names that are only resolvable through the runtime __getattr__. Add a TYPE_CHECKING import block so the lazily-exported names are statically visible; runtime resolution is unchanged (still lazy, still cycle-free). Verified with pyrefly==0.51.0: 0 errors on the file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Kit Lee <7000003+wingkitlee0@users.noreply.github.com>

Why are these changes needed?
The checkpoint restore path currently hardcodes its two concrete classes —
IdColumnCheckpointManager(loads checkpointed IDs) andNumpyArrayBasedCheckpointFilter(filters blocks against them) — even though both ABCs (CheckpointManager,CheckpointFilter) already define the interfaces. This PR makes the restore path pluggable via two optionalCheckpointConfigfields,checkpoint_manager_clsandcheckpoint_filter_cls, mirroring the existing pluggability pattern on the write side (CheckpointWriter.createdispatches on config).Motivation: the restore path is the main scalability pressure point for large datasets (see #60200, #61509). Concretely, to checkpoint a 10B+ row partitioned Parquet dataset, the current design doesn't scale — all checkpointed IDs are coalesced and sorted into a single in-memory array (~80 GB at 10B uint64 IDs). Supporting that scale requires checkpointing that takes advantage of the dataset's partitioning: a custom manager loads checkpoint IDs as per-partition shards instead of one array, and a matching custom filter checks each block only against its shard. This PR exposes the interfaces for that customization without changing default behavior; today it requires forking or monkeypatching internals.
Changes:
CheckpointConfig(checkpoint_filter_cls=..., checkpoint_manager_cls=...): both optional, validated to be subclasses of their respective ABCs, defaultNone(existing behavior unchanged).create_checkpoint_filter_opresolves the manager class and_CheckpointFilterFn.init_checkpoint_filterresolves the filter class from the config; covers both the V1Readand V2ReadFilespaths since both go throughcreate_checkpoint_filter_op.CheckpointFilter,NumpyArrayBasedCheckpointFilter,CheckpointManager, andIdColumnCheckpointManagerare annotated@DeveloperAPIand lazily re-exported fromray.data.checkpoint(lazy to avoid thecheckpoint_filter -> ray.data.context -> ray.data.checkpointimport cycle).CheckpointConfigdocstring: the filter is constructed per actor with(checkpoint_config, checkpointed_ids_ref); the manager is constructed on the driver with(checkpoint_config=..., data_context=...)and itsload_checkpointreturns(ObjectRef, int)— the ref is passed opaquely to the filter, the size feeds the per-actor memory reservation, and(None, 0)skips the filter operator entirely.Not duplicating existing work: no open PR touches checkpoint filter pluggability (searched
gh pr listfor CheckpointFilter/checkpoint filter). Related-but-different open issues: #60704 asks for custom placement of the filter in the plan, #54520 for per-op config — this PR is complementary to both (it changes which filter runs, not where).Related issue number
Related: #60200, #61509, #60704
Checks
pre-commit run --files ...) — all hooks pass (semgrep skipped: unsupported on Linux ARM64 locally).pytest python/ray/data/tests/test_checkpoint.pyrun locally: 68 passed, 3 failed — the 3 (test_partial_failure_no_duplicates{,_partitioned,_row_based}) fail identically on unmodified master in this environment (ModuleNotFoundError: No module named 'test_checkpoint'from the mid-testray.init()when pytest runs from the repo root) and all 3 pass on both master and this branch when run frompython/ray/data/tests/. Not related to this change.TestCheckpointConfig::test_{invalid,valid}_checkpoint_filter_clsandtest_{invalid,valid}_checkpoint_manager_cls(config validation),test_custom_checkpoint_filter_cls(end-to-end: a no-op filter subclass keeps already-checkpointed rows, proving the custom class is instantiated in the filter actor), andtest_custom_checkpoint_manager_cls(end-to-end: a manager subclass returning(None, 0)skips filtering despite checkpoint data being present, proving the custom manager drives the restore).Notes for reviewers:
inspect.isabstract) in addition to the usualissubclasscheck. This is stricter than Ray's other pluggable-class params (ExecutionCallback,ShuffleAggregation), which are instantiated on the driver right after validation and so fail loudly on their own. Here the filter class is serialized to and instantiated inside remote filter actors, so without early validation an abstract class only fails deep in the actor pool as a confusing crash/retry loop — validating atCheckpointConfigconstruction surfaces the mistake immediately on the driver.DeveloperAPIclasses aren't required by the API-discrepancy check, so nodoc/source/data/api/checkpoint.rstentry is included; happy to add one if preferred.DataContext.checkpoint_configpath (CheckpointConfig(**value)) picks up the new kwargs automatically.🤖 Generated with Claude Code