Conversation
Signed-off-by: luozijian <luozijian0924@gmail.com>
There was a problem hiding this comment.
Code Review
This pull request moves the model existence check inside the checkpoint status context block in both process_save_weights and process_save_weights_for_sampler, raising a ValueError if the model is not found. It also introduces a new test suite to verify checkpoint status behaviors and retries. The feedback suggests using a custom exception instead of a generic ValueError to avoid generating noisy tracebacks in the logs for expected stale requests.
| if not self.backend.has_model(model_id): | ||
| raise ValueError(_model_not_found_error(model_id).error) |
There was a problem hiding this comment.
Raising a generic ValueError here will propagate through _checkpoint_status_context and process_single_requests. Both of these catch Exception and log it using logger.exception, which will produce two noisy, full tracebacks in the logs for what is an expected and common scenario (a stale request for an unloaded model after a server restart).
To keep the logs clean, consider introducing a custom exception (e.g., ModelNotLoadedError) and updating _checkpoint_status_context and process_single_requests to handle it gracefully (e.g., logging a simple warning or info message without a traceback) while still marking the checkpoint and future as failed.
| if not self.backend.has_model(model_id): | |
| raise ValueError(_model_not_found_error(model_id).error) | |
| if not self.backend.has_model(model_id): | |
| raise ModelNotLoadedError(_model_not_found_error(model_id).error) |
There was a problem hiding this comment.
Thanks for catching the noisy stale-request logs. Addressed in e0f30b5d8676e3a69e1321ac71cc3c2444478574 with a dedicated internal _ModelNotLoadedError in both save paths. The two exception handlers omit tracebacks only for that subtype; the existing _model_not_found_error helper still emits one info message, without an additional warning. Checkpoint/future failure finalization and error text are preserved.
The regression matrix covers training, sampler and ephemeral sampler saves: unloaded requests produce no traceback and one info message, while ordinary backend ValueError and OSError retain both existing exception logs. Successful saves remain covered. On the exact signed commit, the checkpoint suite plus test_engine.py reran with 33 passed in 28.61s in the pinned local CPU environment. This does not claim GPU validation or resolve the previously reported upstream Anyscale credentials issue.
The contributor personally reviewed/signed the increment; implementation and verification used Codex assistance. The signed file blobs match the reviewed and previously hook-checked candidate; no history rewrite was performed.
| if not self.backend.has_model(model_id): | ||
| raise ValueError(_model_not_found_error(model_id).error) |
There was a problem hiding this comment.
Raising a generic ValueError here will propagate through _checkpoint_status_context and process_single_requests. Both of these catch Exception and log it using logger.exception, which will produce two noisy, full tracebacks in the logs for what is an expected and common scenario (a stale request for an unloaded model after a server restart).
To keep the logs clean, consider introducing a custom exception (e.g., ModelNotLoadedError) and updating _checkpoint_status_context and process_single_requests to handle it gracefully (e.g., logging a simple warning or info message without a traceback) while still marking the checkpoint and future as failed.
| if not self.backend.has_model(model_id): | |
| raise ValueError(_model_not_found_error(model_id).error) | |
| if not self.backend.has_model(model_id): | |
| raise ModelNotLoadedError(_model_not_found_error(model_id).error) |
There was a problem hiding this comment.
Addressed in the same signed e0f30b5 increment as the training-save path. The sampler and persist=False ephemeral sampler cases are included in the logging/status regression matrix; full details and validation limits are in my reply on the first thread.
|
Thanks for taking a look! A brief CI note: the GPU Actions job stopped while submitting its Anyscale job, with Could a maintainer please advise the normal way to run this check for a fork PR when convenient? I have left the workflow and permissions unchanged; I am not asking for secrets to be shared or checks to be bypassed. Local CPU evidence and its limitations are in the PR description. Thank you! This CI investigation and follow-up were prepared with AI assistance. |
Signed-off-by: luozijian <luozijian0924@gmail.com>
|
Thanks for reviewing. I investigated the CPU failure at The MoE code/tests, dependency files, and CPU workflow are unchanged by this PR. I ran the three MoE LoRA configurations on both the pre-patch baseline Could a maintainer please rerun the failed CPU job when convenient? My rerun attempt was denied for insufficient repository permissions. I have left the test tolerances and workflow unchanged. If it fails again, I can continue investigating with the runner environment and a reproducible input. Thank you! This investigation and message were prepared with AI assistance. |
ktanishqk
left a comment
There was a problem hiding this comment.
fix is right, move it in the with statement. however, testing is a bit too extensive and adds failure points that might hurt more than help. going to ask for some consolidation and reduction there - check reviews!
| @pytest.mark.parametrize("outcome", ["success", "backend_error", "backend_value_error", "unloaded"]) | ||
| @pytest.mark.parametrize("mode", ["training", "sampler", "ephemeral_sampler"]) | ||
| def test_checkpoint_and_future_reach_matching_terminal_status(checkpoint_engine, mode, outcome): |
There was a problem hiding this comment.
the grid is easy to grow and hard to read. imo: unloaded × training/sampler + one ValueError (for the _ModelNotLoadedError check) is enough
| if outcome == "unloaded": | ||
| captured_log.exception.assert_not_called() | ||
| captured_log.info.assert_called_once() | ||
| assert "model not loaded" in captured_log.info.call_args.args[0] | ||
| captured_log.warning.assert_not_called() | ||
| elif outcome in ("backend_error", "backend_value_error"): | ||
| assert captured_log.exception.call_count == 2 | ||
| else: | ||
| captured_log.exception.assert_not_called() |
There was a problem hiding this comment.
seems brittle. i wouldn't test logs here.
if someone later adds a logger.info on this path, this test fails even though the checkpoint status is still right.
i’d drop the counts and just assert the DB rows.
| @pytest.mark.asyncio | ||
| @pytest.mark.parametrize("mode", ["training", "sampler", "ephemeral_sampler"]) | ||
| async def test_failed_save_can_be_deleted_and_retried(checkpoint_engine, monkeypatch, mode): | ||
| engine = checkpoint_engine | ||
| engine.backend.has_model.return_value = False | ||
| async_db = create_async_engine(get_async_database_url(str(engine.db_engine.url))) | ||
| monkeypatch.setattr(api.app.state, "db_engine", async_db, raising=False) | ||
| monkeypatch.setattr(api.app.state, "engine_config", engine.config, raising=False) | ||
| monkeypatch.setattr(api.app.state, "sampler_checkpoint_validation_lock", asyncio.Lock(), raising=False) | ||
| monkeypatch.setattr(api.app.state, "validated_sampler_checkpoints", set(), raising=False) |
There was a problem hiding this comment.
can we not just check for checkpoint PENDING -> FAILED and then delete returns 204? the rest aren't this bug and give the test extra ways to fail.
|
hey @LOGO127 - i had started working on this issue after creating it because i was waiting on scope confirmation before opening a pr. |
|
Thanks @ktanishqk for coordinating and taking the reviewer role. The test-only follow-up has now been consolidated locally into three cases: unloaded training save, unloaded sampler save, and one ordinary backend ValueError. It checks the PENDING -> FAILED database transition and DELETE 204, without logger-count, duplicate/retry, or successful-save matrices. The reduced tests still reproduce the original bug: unmodified pre-fix source gives 2 failed / 1 passed; the candidate plus test_engine.py gives 21 passed. Ruff, Black and secret-detection hooks pass. These are local CPU results in the previously documented independent pinned environment, not a resolution of the existing MoE/Anyscale CI failures. The follow-up is local pending contributor review/sign-off; this PR's published head remains e0f30b5. Thanks for helping keep the regression focused. |
Signed-off-by: luozijian <luozijian0924@gmail.com>
|
Addressed the review in e9940cf by reducing the checkpoint regression coverage to the three requested cases: unloaded training save, unloaded sampler save, and one ordinary backend ValueError. The test now only checks PENDING -> FAILED plus DELETE 204; the logger-count, retry/duplicate, ephemeral, and success matrices were removed. Validation: old production code + the reduced tests gives 2 failed / 1 passed (the two unloaded cases stay PENDING); current PR head + the reduced tests and test_engine.py gives 21 passed. Ruff, Black, and hardcoded-secret hooks pass as well. Thanks for the direction — the production fix is unchanged. |
Summary
Move the unloaded-model checks into the existing checkpoint-status context for
training and sampler saves. The request's future and checkpoint then both reach
FAILED with the same error instead of leaving the checkpoint PENDING.
When a queued save is dispatched after its model is no longer loaded, the current
early ErrorResponse completes the future as FAILED but bypasses checkpoint
finalization. Deleting that checkpoint returns 425, and reusing its name returns
409. This change allows the failed checkpoint record to be deleted and retried.
The two-file patch retains successful-save behavior and ephemeral sampler
persist=Falsebehavior. It does not repair historical orphan records, changeretention policy (#2116), or implement Megatron checkpoint publication (#2145).
Validation
model across training, persistent sampler and ephemeral sampler saves, plus
actual ASGI/SQLite delete-and-retry paths. Only the backend is mocked in these
committed tests.
0b286bac: 6 failed / 6 passed. Failures are threePENDING-vs-FAILED statuses and three delete responses of 425 instead of 204.
tests passed 94 tests (not an additional 94).
and 3 passing controls on baseline. This deliberately stages unload before
dispatch; it does not claim that ordinary FIFO requests reorder themselves.
Tinker SDK 0.24.0 creates a tiny-Qwen LoRA model and successfully saves a training
checkpoint. One test passed in 44.90s. This is a successful-save control, not a
cross-process reproduction of the unload condition.
signed contents match that patch, and
git diff --checkpasses.Local validation used WSL Ubuntu, Python 3.12 and an independent locked CPU
environment (JAX 0.11.0, Flax 0.12.8, CPU Torch 2.11.0, Tinker 0.24.0), not the
full production dependency installation. Supplementary probes and environment
files are not included in this scoped patch. No GPU training, PostgreSQL,
SDK >=0.25 protobuf compatibility, model quality or performance is claimed.
To run the committed regression suite in a configured JAX development environment:
Attribution
AI-assisted investigation, implementation and test automation with Codex. The
contributor reviewed the patch and personally signed commit
8671853.Note
Medium Risk
Changes checkpoint failure finalization and error handling on save paths; behavior for successful saves is intended unchanged but affects DB state and delete/retry semantics for failed saves.
Overview
Fixes stale save requests when a model is no longer loaded: training and sampler weight saves now validate
has_modelinside_checkpoint_status_contextinstead of returning early, so checkpoint rows move from PENDING to FAILED alongside the request future.Introduces
_ModelNotLoadedErrorfor that case; checkpoint and batch request handlers still return client ErrorResponses but skip full exception logging for this expected stale-request path. Save-weight processors no longer declareErrorResponsein their return types because failures are raised through the context manager.Adds ASGI/SQLite regression tests covering unloaded-model and backend-failure saves (training and sampler) and that a failed checkpoint can be deleted (204) instead of blocking on PENDING/425.
Reviewed by Cursor Bugbot for commit e9940cf. Bugbot is set up for automated code reviews on this repo. Configure here.