Skip to content

[Feat] Support immediate rollout recovery by export hf asynchronously#1966

Open
YanhuiDua wants to merge 1 commit into
InternLM:mainfrom
YanhuiDua:dev-async-hf
Open

[Feat] Support immediate rollout recovery by export hf asynchronously#1966
YanhuiDua wants to merge 1 commit into
InternLM:mainfrom
YanhuiDua:dev-async-hf

Conversation

@YanhuiDua

Copy link
Copy Markdown
Collaborator

No description provided.

@YanhuiDua

Copy link
Copy Markdown
Collaborator Author

@claude review

Comment on lines 1746 to 1748
self._fit()
finally:
self._exp_tracker.close()

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.

Claude: Bug (Critical): close_trace() has been removed from the finally block. The trace runtime (TraceRuntime.close()) flushes pending spans and stops local collector processes. Without it, trace data in-flight at end of training may be lost or delayed until the process exits (relying only on the atexit fallback).

The import of close_trace was also dropped (line 44). This appears to be an accidental deletion during refactoring — the executor shutdown and trace shutdown serve different purposes.

Suggested change
self._fit()
finally:
self._exp_tracker.close()
finally:
self._exp_tracker.close()
if self._hf_export_executor is not None:
self._hf_export_executor.shutdown(wait=True)
close_trace()

(Same fix needed in the DisaggregatedRLTrainer.fit() at line ~1981.)

Comment on lines +895 to +898
previous_ready_hf_path is not None
and str(previous_ready_hf_path) not in self._meta.latest_exp.hf_checkpoint_list
):
rmtree(previous_ready_hf_path, ignore_errors=True)

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.

Claude: Race condition (Warning): There is a timing window between clear_ready_recovery_hf and the rmtree below (line 898) where the health manager's background restart thread may have already captured the old _ready_recovery_hf value in a local variable (ready_recovery_hf = self._ready_recovery_hf in _restart_worker_group) and started a reinit.remote(model_path=old_path). Deleting the path from disk while a Ray worker is loading from it would cause the restart to fail.

This isn't catastrophic (the health check will detect the failure and shut down the group), but it means the immediate-recovery path degrades to a restart failure for that group, which is counterproductive.

A safer approach: defer deletion of the old path until after the next successful weight sync (where all workers are confirmed healthy), or track in-flight restarts with a reference count before removing the directory.

Comment on lines +935 to +938
return None

self._ready_recovery_hf_path = finalized_hf_path
return finalized_hf_path

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.

Claude: Thread safety (Warning): The closure wait_and_publish_recovery_hf writes self._ready_recovery_hf_path (line 938) from the background ThreadPoolExecutor thread. The main thread reads it in the next _maybe_save_recovery_hf call (line 881: previous_ready_hf_path = self._ready_recovery_hf_path).

In CPython this works because Future.result() (called in _wait_or_disable_immediate_recovery) synchronizes through internal condition variables and the GIL, establishing a happens-before relationship. But this relies on an implementation detail and is fragile for anyone maintaining this code. Consider either:

  • Returning the path only through the Future's result value (which you already do) and reading it from pending.result() instead of from self._ready_recovery_hf_path, or
  • Protecting the attribute with an explicit threading.Lock.

Comment on lines +957 to +959
pending = cast(Future[Path], self._pending_hf_export)
return pending.done()

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.

Claude: Defensiveness (Nit): is_hf_export_done and wait_hf_export both use cast(Future[Path], self._pending_hf_export) without guarding against None. If either is ever called out of sequence (before start_hf_export), this will produce a confusing AttributeError: 'NoneType' object has no attribute 'done'.

Consider raising an explicit RuntimeError if self._pending_hf_export is None:

if self._pending_hf_export is None:
    raise RuntimeError("No pending HF export — call start_hf_export first.")
return self._pending_hf_export.done()

Comment on lines +944 to +951
if pending is None:
return

disable_immediate_recovery = not pending.done()
if disable_immediate_recovery:
self._enable_immediate_recovery = False
self.logger.warning(
"Disable immediate recovery because the previous recovery HF "

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.

Claude: Design concern (Warning): When the export doesn't finish before the next weight sync, _enable_immediate_recovery is permanently set to False — it cannot be re-enabled for the remainder of training.

If the slow export was a transient issue (e.g., I/O spike, one large model step), this permanently disables a useful optimization. Consider either:

  • Making this a "cooldown" that re-enables after N successful weight syncs, or
  • At minimum, documenting that this is a permanent disable (the log says "Disable" but doesn't say "for the rest of training").

@@ -2007,12 +2119,16 @@ async def _fit(self):

async def _sync_weights_and_save(self, model_step: int, step_timer_dict: dict):
# producer 已暂停;保持 save -> bind -> update 顺序。

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.

Claude: Behavioral change (Warning): This line adds restart_inactive_workers to the disaggregated trainer's weight-sync path. The old code had a TODO indicating this was intentionally omitted pending design evaluation.

Is this safe for all disaggregated configurations? The colocate trainer restarts inactive workers at the start of _sync_weights_and_save (before offload), but here it's inside the sync_weight timer, right before bind_train_rollout. If the restart takes significant time (with ROLLOUT_RAY_GET_TIMEOUT), it could delay weight updates. Also, since this runs inside timer("sync_weight"), the restart time inflates the sync_weight metric.

Please confirm this ordering is intentional and tested in the disaggregated setup.

@claude

claude Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Claude: ## Summary

This PR adds an "immediate recovery" mode for rollout workers: instead of restarting crashed workers with empty weights and waiting for the next weight-sync, it exports an HF checkpoint asynchronously after each weight update and lets the health manager reload workers from that checkpoint immediately.

The refactoring of _skip_load_weights_during_restart into reinit(**kwargs) is a nice simplification. The overall design (background export → publish to health manager → use on restart) is sound.

Issues

Critical

  • xtuner/v1/train/rl_trainer.py:1746-1748: close_trace() removed from both fit() methods (and its import dropped). The trace runtime won't flush pending spans at end of training. This appears accidental — the executor shutdown replaced it rather than supplementing it.

Warning

  • rl_trainer.py:895-898: Race between rmtree of old recovery path and in-flight health manager restarts that may still be loading from that path.
  • rl_trainer.py:935-938: self._ready_recovery_hf_path written from background thread — correctness depends on CPython GIL + Future synchronization. Fragile for maintainers.
  • rl_trainer.py:944-951: Feature permanently disabled on first slow export with no re-enablement path.
  • rl_trainer.py:2121: New restart_inactive_workers call in disaggregated path — was previously a TODO indicating it needed design evaluation.

Nit

Verdict

REQUEST_CHANGES

ProduceBatchResult impact: not directly affected — the recovery path rejoins the normal flow before batch accounting.

RoutedExperts impact: not affected — no changes to routed-experts plumbing or object-ref ownership.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant