[bugfix] restore ODPS sessions by partition index on resume - #681
Conversation
| # Restore sessions for each input_path | ||
| for input_path, session_ids in session_ids_by_input.items(): | ||
| input_path, sep, sess_idx = input_and_idx.rpartition("#") | ||
| if not sep or not sess_idx.isdigit(): |
There was a problem hiding this comment.
Low-med: this raise fires for any key shaped ...#non-digit:..., including keys this reader never produced, which pre-diff code warned-and-skipped. Two concrete paths:
main.py:839-840also loadsdataloader_state.jsonfrom an externalfine_tune_checkpointsource. An older-tzrec source checkpoint whose ODPS keys reference unrelated tables (fine-tuning onto different inputs) previously fell into the unknown-input_pathwarning below and was skipped; now it aborts startup on every rank with a misleading "restart from scratch".- Parquet source ids are
{file_path}:{start}(parquet_dataset.py:276) and#is legal in POSIX/OSS paths, so e.g./data/train#v2/part-0.parquet:1024in a mixed-history state file hard-fails an ODPS job.
Suggestion: scope the raise to keys that claim one of this job's own inputs — for an old-format key, input_and_idx is the old input path, so raise only when it (or the parsed input_path) is in self._input_to_sess, otherwise fall through to the existing unknown-path warning. That keeps the intended hard failure for genuinely old checkpoints of the resumed table while restoring the tolerant skip for foreign keys.
| sessions are still active and puts each one back at its own position, so | ||
| partitions that never produced a key (e.g. empty ones) keep the freshly | ||
| created session at their own index. Raises RuntimeError if any session is | ||
| expired/invalid. |
There was a problem hiding this comment.
Nit: the paragraph was rewritten but the Raises sentence still only covers the expired/invalid case. The function now also raises for a key without a session index (line 571) and for an index beyond the current session list (line 591), and warns-and-skips unknown input paths (line 580). Since this docstring was touched anyway, worth completing, e.g.: "Raises RuntimeError if a key carries no session index (written by an older version), if an index exceeds the current number of partition sessions, or if any session is expired/invalid."
| and os.environ.get("ALIBABA_CLOUD_ECS_METADATA", "") == "", | ||
| "odps config not found", | ||
| ) | ||
| def test_odps_dataset_checkpoint_resume_empty_partition(self): |
There was a problem hiding this comment.
The two new RuntimeError branches — old-format key (odps_dataset.py:571) and out-of-range index (:591) — are the user-facing guardrails of this fix but have no committed coverage; every test that exercises _restore_sessions is gated behind ODPS credentials, and even those never hit the error paths.
Both are cheap to test offline in the style of OdpsStorageErrorLogTest below. The old-format raise fires during key parsing before any attribute access, so it works with a bare stub:
with self.assertRaisesRegex(RuntimeError, "older TorchEasyRec"):
odps_dataset.OdpsReader._restore_sessions(
SimpleNamespace(), {"odps://p/tables/t/dt=x#sessid:10": 0}
)The bounds check only needs _input_to_sess / _table_to_cli stubs (a SimpleNamespace with a 1-element session list and a key claiming index 5). Worth adding so the guards don't silently regress in non-credentialed CI lanes.
| self.assertIsInstance(batch.checkpoint_info, dict) | ||
|
|
||
| # Checkpoint keys should be in format "{input_path}#{session_id}:{start}" | ||
| # Checkpoint keys are "{input_path}#{sess_idx}#{session_id}:{start}" |
There was a problem hiding this comment.
The comment now claims the three-field format, but none of the assertions below actually distinguish it — they all pass identically under the old {input_path}#{session_id}:{start} layout, so a regression that drops sess_idx from to_batches would only be caught by the credential-gated resume tests. Cheap pin, since input paths contain no #: segments = prefix.split("#") → assertEqual(len(segments), 3) and assertTrue(segments[1].isdigit()).
Multi-area review summary (code quality, performance, test coverage, documentation, security — all five completed)The core fix is correct. Independently traced and confirmed:
Inline comments posted (none blocking):
Non-blocking observations (pre-existing, not introduced by this PR):
|
On resume, OdpsReader rebuilt the per-input session list as the sessions
found in the checkpoint followed by the freshly created sessions from that
count onwards. This assumed the checkpointed sessions were exactly the leading
partitions in order, but a partition that produced no rows before the
checkpoint (e.g. an empty one) never gets a key, and the key order follows the
rank-major merge rather than partition order. A restored session could then
land at the wrong position, leaving its partition's fresh session in the list
and re-reading that partition from row 0.
The checkpoint source key now carries the session's position
("{input_path}#{sess_idx}#{session_id}:{start}") and restore puts each
validated session back at that index, so unconsumed partitions keep their own
fresh session. Keys without an index come from an older version and raise a
clear error.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgZZz8Baa365GMyVuRFvZb
b2fe091 to
13fa120
Compare
|
Addressed the four inline comments in 13fa120:
The restored sessions not being registered with |
Summary
OdpsReader._restore_sessionsrebuilt the per-input session list on resume asrestored_sess_reqs + current_sessions[n_restored:], which assumes the sessions found in the checkpoint are exactly the leading partitions in order. That does not hold:is_orderby_partition) never gets a checkpoint key, so the consumed set skips it.dataloader_state.jsonfollows the rank-major merge insave_dataloader_state, not partition order.With partitions
[P0, P1(empty), P2]consumed intoP2, restore produced[s0, s2, s2_new]:P2's remaining rows were read via the old session and thenP2was read again in full via the fresh session, with no error.The
get_read_sessionresponse carries no partition spec, so the reader cannot map a session back to its partition on its own. The checkpoint source key now carries the session position,{input_path}#{sess_idx}#{session_id}:{start}, and restore puts each validated session back at that index. Unconsumed partitions keep their freshly created session at their own position.Error handling on resume:
RuntimeErrorasking to restart from scratch (those sessions expire within a day of the writer dying anyway).fine_tune_checkpointstate referencing other tables, or parquet paths containing#, still do not abort startup.is_orderby_partitiontoggled off) raises.Alternative considered: putting the partition spec in the key instead of the index. The index is simpler to parse and is what positional restore needs.
Test Plan
test_odps_dataset_checkpoint_resume_empty_partition(ODPS-credentialed): table withdt=20240319(10000 rows), an emptydt=20240318, anddt=20240320(10000 rows) underis_orderby_partition=True. Consumes into the third partition, restores into a fresh dataset, asserts sessions 0 and 2 are the checkpoint sessions and session 1 is the dataset's own fresh one, then reads to exhaustion and asserts consumed plus remaining rows equal 20000. This fails on master with the duplicate read.OdpsRestoreSessionsTestwith a stub client: positional placement with reserved keys mixed in, unknown-path warn-and-skip, old-format key raise, out-of-range index raise. Runs in every CI lane.test_odps_dataset_checkpoint_resume_orderby_partitionadjusted for the new key layout;test_odps_dataset_checkpoint_metadatanow pins the three-segment key.pre-commit run --files ...andpyrefly checkclean. ODPS tests run on the CPU CI for this PR.🤖 Generated with Claude Code
https://claude.ai/code/session_01MgZZz8Baa365GMyVuRFvZb