Download tests/test_checkpoint_eval_paths.py from Jack04810/agentic-rl-main: direct link, hf CLI and curl.
- Browser
- Download file 17.4 kB
-
https://huggingface.co/Jack04810/agentic-rl-main/resolve/main/tests/test_checkpoint_eval_paths.py
- Command line
-
hf download hf://Jack04810/agentic-rl-main/tests/test_checkpoint_eval_paths.py
-
curl -L -o test_checkpoint_eval_paths.py https://huggingface.co/Jack04810/agentic-rl-main/resolve/main/tests/test_checkpoint_eval_paths.py
17.4 kB
| from __future__ import annotations | |
| import json | |
| import os | |
| import shutil | |
| from pathlib import Path | |
| from typing import Any | |
| import pytest | |
| from opsd_utils.checkpoint_eval_paths import ( | |
| find_best_checkpoint_path, | |
| find_checkpoint_evaluation_policy, | |
| recover_interrupted_checkpoint_eval_save, | |
| update_final_checkpoint_link, | |
| validate_checkpoint_eval_output_dir, | |
| ) | |
| from opsd_utils.checkpoint_eval import CheckpointEvaluationTriggerCallback | |
| class _State: | |
| best_model_checkpoint = None | |
| class _Trainer: | |
| state = _State() | |
| def _checkpoint_eval_policy_state( | |
| *, | |
| step: int, | |
| score: float, | |
| evaluation_count: int, | |
| lower_score_streak: int = 0, | |
| stop_requested: bool = False, | |
| ) -> dict[str, Any]: | |
| """Build the callback payload HF serializes into trainer_state.json.""" | |
| return { | |
| "version": 1, | |
| "patience": 3, | |
| "tie_policy": "reset", | |
| "state": { | |
| "best_score": score, | |
| "best_step": step, | |
| "lower_score_streak": lower_score_streak, | |
| "evaluation_count": evaluation_count, | |
| "stop_requested": stop_requested, | |
| }, | |
| } | |
| def _write_native_checkpoint_state( | |
| checkpoint: Path, | |
| *, | |
| step: int, | |
| policy: dict[str, Any], | |
| best_global_step: int | None = None, | |
| ) -> None: | |
| """Write only the durable state needed to prove a native HF save.""" | |
| checkpoint.mkdir(exist_ok=True) | |
| state = { | |
| "global_step": step, | |
| "best_global_step": step if best_global_step is None else best_global_step, | |
| "stateful_callbacks": { | |
| "CheckpointEvaluationTriggerCallback": { | |
| "args": {"enabled": True, "patience": 3, "tie_policy": "reset"}, | |
| "attributes": { | |
| "pending_checkpoint_evaluation": False, | |
| "trigger_count": policy["state"]["evaluation_count"], | |
| "checkpoint_eval_state": policy, | |
| }, | |
| } | |
| }, | |
| } | |
| (checkpoint / "trainer_state.json").write_text( | |
| json.dumps(state, indent=2, sort_keys=True), encoding="utf-8" | |
| ) | |
| def _interrupted_native_save_layout( | |
| tmp_path: Path, | |
| ) -> tuple[Path, Path, dict[str, Any], dict[str, Any]]: | |
| """Model HF's write-complete / rotation-not-yet-started crash window.""" | |
| old_checkpoint = tmp_path / "checkpoint-100" | |
| new_checkpoint = tmp_path / "checkpoint-200" | |
| old_policy = _checkpoint_eval_policy_state( | |
| step=100, | |
| score=0.60, | |
| evaluation_count=1, | |
| ) | |
| new_policy = _checkpoint_eval_policy_state( | |
| step=200, | |
| score=0.75, | |
| evaluation_count=2, | |
| ) | |
| _write_native_checkpoint_state(old_checkpoint, step=100, policy=old_policy) | |
| # The callback's prior successful save is the durable pointer evidence | |
| # that checkpoint-100 was the retained best before native rotation. | |
| os.symlink(old_checkpoint.name, tmp_path / "final_checkpoint") | |
| _write_native_checkpoint_state(new_checkpoint, step=200, policy=new_policy) | |
| return old_checkpoint, new_checkpoint, old_policy, new_policy | |
| def _rotated_before_callback_publication_layout( | |
| tmp_path: Path, | |
| *, | |
| with_stale_sidecar: bool = True, | |
| ) -> tuple[Path, Path, dict[str, Any], dict[str, Any]]: | |
| """Model HF rotation completing immediately before callback ``on_save``. | |
| This is the narrow single-directory crash state: HF has already deleted | |
| the old best but the callback has not yet repointed ``final_checkpoint`` | |
| or published the new policy sidecar. | |
| """ | |
| old_checkpoint, new_checkpoint, old_policy, new_policy = _interrupted_native_save_layout( | |
| tmp_path | |
| ) | |
| if with_stale_sidecar: | |
| (tmp_path / "checkpoint_eval_state.json").write_text( | |
| json.dumps({"version": 1, "checkpoint_eval_state": old_policy}), | |
| encoding="utf-8", | |
| ) | |
| shutil.rmtree(old_checkpoint) | |
| assert (tmp_path / "final_checkpoint").is_symlink() | |
| assert not (tmp_path / "final_checkpoint").exists() | |
| return old_checkpoint, new_checkpoint, old_policy, new_policy | |
| def test_update_final_checkpoint_link_uses_relative_atomic_symlink(tmp_path: Path) -> None: | |
| checkpoint = tmp_path / "checkpoint-12" | |
| checkpoint.mkdir() | |
| (checkpoint / "config.json").write_text("{}", encoding="utf-8") | |
| final = update_final_checkpoint_link(tmp_path, checkpoint) | |
| assert final.is_symlink() | |
| assert not Path(os.readlink(final)).is_absolute() | |
| assert final.resolve() == checkpoint.resolve() | |
| assert list(tmp_path.glob(".final_checkpoint.tmp-*")) == [] | |
| def test_find_best_checkpoint_path_uses_trainer_state(tmp_path: Path) -> None: | |
| checkpoint = tmp_path / "checkpoint-7" | |
| checkpoint.mkdir() | |
| trainer = _Trainer() | |
| trainer.state.best_model_checkpoint = str(checkpoint) | |
| assert find_best_checkpoint_path(trainer, tmp_path) == checkpoint.resolve() | |
| def test_find_best_checkpoint_path_uses_resumed_callback_policy(tmp_path: Path) -> None: | |
| checkpoint = tmp_path / "checkpoint-9" | |
| checkpoint.mkdir() | |
| trainer = _Trainer() | |
| callback = CheckpointEvaluationTriggerCallback(output_dir=tmp_path) | |
| callback.policy.observe(0.8, step=9) | |
| class _Handler: | |
| callbacks = [callback] | |
| trainer.callback_handler = _Handler() | |
| assert find_best_checkpoint_path(trainer, tmp_path) == checkpoint.resolve() | |
| def test_existing_ambiguous_layout_is_rejected(tmp_path: Path) -> None: | |
| (tmp_path / "checkpoint-1").mkdir() | |
| (tmp_path / "checkpoint-2").mkdir() | |
| with pytest.raises(ValueError, match="multiple checkpoint"): | |
| validate_checkpoint_eval_output_dir(tmp_path) | |
| (tmp_path / "checkpoint-2").rmdir() | |
| (tmp_path / "final_checkpoint").mkdir() | |
| with pytest.raises(ValueError, match="real final_checkpoint"): | |
| validate_checkpoint_eval_output_dir(tmp_path) | |
| def test_dangling_internal_best_link_can_be_replaced_after_checkpoint_rotation(tmp_path: Path) -> None: | |
| old_checkpoint = tmp_path / "checkpoint-1" | |
| old_checkpoint.mkdir() | |
| update_final_checkpoint_link(tmp_path, old_checkpoint) | |
| # With save_total_limit=1, native Trainer rotation can remove the old | |
| # checkpoint before the callback's on_save gets a chance to point the | |
| # compatibility link at the new one. | |
| old_checkpoint.rmdir() | |
| new_checkpoint = tmp_path / "checkpoint-2" | |
| new_checkpoint.mkdir() | |
| assert (tmp_path / "final_checkpoint").is_symlink() | |
| assert not (tmp_path / "final_checkpoint").exists() | |
| validate_checkpoint_eval_output_dir(tmp_path) | |
| final = update_final_checkpoint_link(tmp_path, new_checkpoint) | |
| assert final.resolve() == new_checkpoint.resolve() | |
| def test_recover_post_rotation_dangling_link_uses_retained_native_policy( | |
| tmp_path: Path, | |
| ) -> None: | |
| old_checkpoint, new_checkpoint, old_policy, new_policy = ( | |
| _rotated_before_callback_publication_layout(tmp_path) | |
| ) | |
| retained = recover_interrupted_checkpoint_eval_save( | |
| tmp_path, patience=3, tie_policy="reset" | |
| ) | |
| assert retained == new_checkpoint.resolve() | |
| assert not old_checkpoint.exists() | |
| final = tmp_path / "final_checkpoint" | |
| assert final.is_symlink() | |
| assert final.resolve() == new_checkpoint.resolve() | |
| assert os.readlink(final) == new_checkpoint.name | |
| sidecar = json.loads((tmp_path / "checkpoint_eval_state.json").read_text(encoding="utf-8")) | |
| assert sidecar == {"version": 1, "checkpoint_eval_state": new_policy} | |
| assert sidecar["checkpoint_eval_state"] != old_policy | |
| validate_checkpoint_eval_output_dir(tmp_path) | |
| def test_recover_post_rotation_rejects_missing_sidecar_without_old_policy_proof( | |
| tmp_path: Path, | |
| ) -> None: | |
| _, new_checkpoint, _, new_policy = _rotated_before_callback_publication_layout( | |
| tmp_path, with_stale_sidecar=False | |
| ) | |
| assert recover_interrupted_checkpoint_eval_save(tmp_path) is None | |
| final = tmp_path / "final_checkpoint" | |
| assert final.is_symlink() | |
| assert not final.exists() | |
| assert new_checkpoint.is_dir() | |
| assert not (tmp_path / "checkpoint_eval_state.json").exists() | |
| def test_post_rotation_recovery_rejects_unproven_or_dangerous_artifacts(tmp_path: Path) -> None: | |
| old_checkpoint, new_checkpoint, _, new_policy = _rotated_before_callback_publication_layout( | |
| tmp_path | |
| ) | |
| # A retained directory name is never enough: its policy must nominate and | |
| # prove this exact saved step. | |
| new_policy["state"]["best_step"] = 100 | |
| _write_native_checkpoint_state(new_checkpoint, step=200, policy=new_policy) | |
| assert recover_interrupted_checkpoint_eval_save(tmp_path) is None | |
| final = tmp_path / "final_checkpoint" | |
| assert final.is_symlink() | |
| assert os.readlink(final) == old_checkpoint.name | |
| assert not final.exists() | |
| # Restore the valid native proof, then turn the sidecar into an external | |
| # symlink. Recovery must neither follow nor replace it. | |
| new_policy["state"]["best_step"] = 200 | |
| _write_native_checkpoint_state(new_checkpoint, step=200, policy=new_policy) | |
| external_sidecar = tmp_path / "external-sidecar.json" | |
| external_sidecar.write_text("do not replace", encoding="utf-8") | |
| sidecar = tmp_path / "checkpoint_eval_state.json" | |
| sidecar.unlink() | |
| sidecar.symlink_to(external_sidecar) | |
| assert recover_interrupted_checkpoint_eval_save(tmp_path) is None | |
| assert sidecar.is_symlink() | |
| assert external_sidecar.read_text(encoding="utf-8") == "do not replace" | |
| assert os.readlink(final) == old_checkpoint.name | |
| def test_post_rotation_recovery_never_replaces_noncanonical_final_link( | |
| tmp_path: Path, target: str | |
| ) -> None: | |
| _, new_checkpoint, _, _ = _rotated_before_callback_publication_layout(tmp_path) | |
| final = tmp_path / "final_checkpoint" | |
| final.unlink() | |
| final.symlink_to(target) | |
| assert recover_interrupted_checkpoint_eval_save(tmp_path) is None | |
| assert final.is_symlink() | |
| assert os.readlink(final) == target | |
| assert new_checkpoint.is_dir() | |
| def test_post_rotation_recovery_requires_second_evaluation( | |
| tmp_path: Path, | |
| ) -> None: | |
| _, new_checkpoint, _, new_policy = _rotated_before_callback_publication_layout(tmp_path) | |
| new_policy["state"]["evaluation_count"] = 1 | |
| _write_native_checkpoint_state(new_checkpoint, step=200, policy=new_policy) | |
| assert recover_interrupted_checkpoint_eval_save(tmp_path) is None | |
| final = tmp_path / "final_checkpoint" | |
| assert final.is_symlink() | |
| assert not final.exists() | |
| sidecar = json.loads((tmp_path / "checkpoint_eval_state.json").read_text(encoding="utf-8")) | |
| assert sidecar["checkpoint_eval_state"]["state"]["best_step"] == 100 | |
| def test_recover_interrupted_native_save_uses_trainer_state_proof_before_pruning( | |
| tmp_path: Path, | |
| ) -> None: | |
| old_checkpoint, new_checkpoint, _, new_policy = _interrupted_native_save_layout(tmp_path) | |
| retained = recover_interrupted_checkpoint_eval_save(tmp_path) | |
| assert retained == new_checkpoint.resolve() | |
| assert not old_checkpoint.exists() | |
| assert new_checkpoint.is_dir() | |
| final = tmp_path / "final_checkpoint" | |
| assert final.is_symlink() | |
| assert final.resolve() == new_checkpoint.resolve() | |
| sidecar = json.loads((tmp_path / "checkpoint_eval_state.json").read_text(encoding="utf-8")) | |
| assert sidecar == {"version": 1, "checkpoint_eval_state": new_policy} | |
| # Validation is intentionally the recovery entrypoint used by main, and | |
| # after the proof-driven repair it sees the normal one-checkpoint layout. | |
| validate_checkpoint_eval_output_dir(tmp_path) | |
| assert sorted(path.name for path in tmp_path.glob("checkpoint-*")) == ["checkpoint-200"] | |
| def test_validation_is_read_only_until_rank_zero_recovery_has_proven_the_save( | |
| tmp_path: Path, | |
| ) -> None: | |
| old_checkpoint, new_checkpoint, _, _ = _interrupted_native_save_layout(tmp_path) | |
| # main invokes recovery on rank zero and synchronizes before validation. | |
| # Validation itself must remain non-destructive: it may reject the two-dir | |
| # window, but it must never decide which model to delete on every rank. | |
| with pytest.raises(ValueError, match="multiple checkpoint"): | |
| validate_checkpoint_eval_output_dir(tmp_path) | |
| assert old_checkpoint.is_dir() | |
| assert new_checkpoint.is_dir() | |
| assert (tmp_path / "final_checkpoint").resolve() == old_checkpoint.resolve() | |
| assert recover_interrupted_checkpoint_eval_save(tmp_path) == new_checkpoint.resolve() | |
| validate_checkpoint_eval_output_dir(tmp_path) | |
| assert not old_checkpoint.exists() | |
| assert new_checkpoint.is_dir() | |
| def test_interrupted_save_with_mismatched_new_policy_is_rejected_without_deletion( | |
| tmp_path: Path, | |
| ) -> None: | |
| old_checkpoint, new_checkpoint, _, new_policy = _interrupted_native_save_layout(tmp_path) | |
| # Directory names alone are never enough evidence: make the new callback | |
| # policy nominate the old step despite global_step/best_global_step=200. | |
| new_policy["state"]["best_step"] = 100 | |
| _write_native_checkpoint_state(new_checkpoint, step=200, policy=new_policy) | |
| assert recover_interrupted_checkpoint_eval_save(tmp_path) is None | |
| assert old_checkpoint.is_dir() | |
| assert new_checkpoint.is_dir() | |
| assert (tmp_path / "final_checkpoint").resolve() == old_checkpoint.resolve() | |
| assert not (tmp_path / "checkpoint_eval_state.json").exists() | |
| with pytest.raises(ValueError, match="multiple checkpoint"): | |
| validate_checkpoint_eval_output_dir(tmp_path) | |
| # validate must not turn ambiguous evidence into a destructive repair. | |
| assert old_checkpoint.is_dir() | |
| assert new_checkpoint.is_dir() | |
| assert (tmp_path / "final_checkpoint").resolve() == old_checkpoint.resolve() | |
| def test_interrupted_save_rejects_checkpoint_directory_symlinks(tmp_path: Path) -> None: | |
| old_checkpoint, new_checkpoint, _, _ = _interrupted_native_save_layout(tmp_path) | |
| external_root = tmp_path / "external" | |
| external_root.mkdir() | |
| external_new = external_root / "checkpoint-200" | |
| new_checkpoint.rename(external_new) | |
| new_checkpoint.symlink_to(external_new, target_is_directory=True) | |
| # A directory symlink must not become an escape hatch for recovery's | |
| # destructive prune or for a final_checkpoint link outside output_dir. | |
| assert recover_interrupted_checkpoint_eval_save(tmp_path) is None | |
| assert old_checkpoint.is_dir() | |
| assert new_checkpoint.is_symlink() | |
| assert external_new.is_dir() | |
| assert (tmp_path / "final_checkpoint").resolve() == old_checkpoint.resolve() | |
| def test_interrupted_save_rejects_incompatible_serialized_policy(tmp_path: Path) -> None: | |
| old_checkpoint, new_checkpoint, _, new_policy = _interrupted_native_save_layout(tmp_path) | |
| new_policy["patience"] = 0 | |
| _write_native_checkpoint_state(new_checkpoint, step=200, policy=new_policy) | |
| assert recover_interrupted_checkpoint_eval_save(tmp_path) is None | |
| assert old_checkpoint.is_dir() | |
| assert new_checkpoint.is_dir() | |
| assert (tmp_path / "final_checkpoint").resolve() == old_checkpoint.resolve() | |
| def test_interrupted_save_requires_strictly_better_compatible_new_policy( | |
| tmp_path: Path, | |
| field: str, | |
| value: Any, | |
| ) -> None: | |
| old_checkpoint, new_checkpoint, _, new_policy = _interrupted_native_save_layout(tmp_path) | |
| if field in {"best_score"}: | |
| new_policy["state"][field] = value | |
| else: | |
| new_policy[field] = value | |
| _write_native_checkpoint_state(new_checkpoint, step=200, policy=new_policy) | |
| assert recover_interrupted_checkpoint_eval_save( | |
| tmp_path, patience=3, tie_policy="reset" | |
| ) is None | |
| assert old_checkpoint.is_dir() | |
| assert new_checkpoint.is_dir() | |
| assert (tmp_path / "final_checkpoint").resolve() == old_checkpoint.resolve() | |
| def test_interrupted_save_prefers_newer_sidecar_baseline_before_pruning(tmp_path: Path) -> None: | |
| old_checkpoint, new_checkpoint, _, _ = _interrupted_native_save_layout(tmp_path) | |
| # A lower evaluation after checkpoint-100 has no native checkpoint, but | |
| # its sidecar is the current committed policy baseline. The prospective | |
| # score must still beat that score, not merely the old trainer-state score. | |
| sidecar_policy = _checkpoint_eval_policy_state( | |
| step=100, | |
| score=0.80, | |
| evaluation_count=2, | |
| lower_score_streak=1, | |
| ) | |
| (tmp_path / "checkpoint_eval_state.json").write_text( | |
| json.dumps({"version": 1, "checkpoint_eval_state": sidecar_policy}), | |
| encoding="utf-8", | |
| ) | |
| assert recover_interrupted_checkpoint_eval_save(tmp_path) is None | |
| assert old_checkpoint.is_dir() | |
| assert new_checkpoint.is_dir() | |
| assert (tmp_path / "final_checkpoint").resolve() == old_checkpoint.resolve() | |
| def test_find_checkpoint_evaluation_policy_prefers_live_callback_after_restore() -> None: | |
| trainer = _Trainer() | |
| stale_policy = CheckpointEvaluationTriggerCallback().policy | |
| active_callback = CheckpointEvaluationTriggerCallback() | |
| active_callback.policy.observe(0.9, step=4) | |
| trainer.checkpoint_eval_policy = stale_policy | |
| class _Handler: | |
| callbacks = [active_callback] | |
| trainer.callback_handler = _Handler() | |
| assert find_checkpoint_evaluation_policy(trainer) is active_callback.policy | |