Skip to content

[DO NOT MERGE] trainer ft - #823

Closed
fzyzcjy wants to merge 2175 commits into
mainfrom
trainer_ft/dev
Closed

[DO NOT MERGE] trainer ft#823
fzyzcjy wants to merge 2175 commits into
mainfrom
trainer_ft/dev

Conversation

@fzyzcjy

@fzyzcjy fzyzcjy commented Mar 30, 2026

Copy link
Copy Markdown
Collaborator

ci-sglang-pr: #28524
ci-megatron-pr: #21

sgl-project/sglang#28524
radixark/Megatron-LM#21

@fzyzcjy
fzyzcjy marked this pull request as draft March 30, 2026 03:56
@fzyzcjy fzyzcjy changed the title Rename DP groups into intra-DP groups [DO NOT MERGE] trainer ft Mar 30, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces support for independent data parallelism (DP) by allowing DP replicas to function as independent Megatron instances. It refactors the Ray training group logic into cells, adds configuration for delayed data splitting, and implements consistency checks for dynamic global batch sizes. Feedback identifies a bug in the data splitting utility where the dynamic batch size was not propagated, suggests avoiding hardcoded configuration values, and recommends using strict zip operations for safer actor-critic connections.

Comment thread miles/utils/data_utils.py Outdated
Comment on lines +6 to +54
def split_train_data_by_dp(args, data: dict[str, Any], *, dp_size: int) -> list[dict[str, Any]]:
"""Split the train data by data parallel size."""
rollout_data = {}

if "prompt" in data:
rollout_data["prompt"] = data["prompt"]

total_lengths = [len(t) for t in data["tokens"]]
data["total_lengths"] = total_lengths

if args.balance_data:
partitions = get_seqlen_balanced_partitions(total_lengths, dp_size, equal_size=True)
else:
partitions = [range(i, len(total_lengths), dp_size) for i in range(dp_size)]

ans = []

for i in range(dp_size):
rollout_data = {}
partition = partitions[i]
rollout_data["partition"] = partition
for key in [
"tokens",
"multimodal_train_inputs",
"response_lengths",
"rewards",
"truncated",
"loss_masks",
"round_number",
"sample_indices",
"rollout_log_probs",
"rollout_routed_experts",
"prompt",
"teacher_log_probs",
]:
if key not in data:
continue
val = [data[key][j] for j in partition]
rollout_data[key] = val
# keys that need to be splited at train side
for key in [
"raw_reward",
"total_lengths",
]:
if key not in data:
continue
rollout_data[key] = data[key]
ans.append(rollout_data)
return ans

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.

high

There are a couple of issues in split_train_data_by_dp:

  1. The rollout_data variable initialized on lines 8-11 is unused because it's redefined inside the loop on line 24. This initial block can be removed for clarity.
  2. More importantly, dynamic_global_batch_size is not being propagated to the split data dictionaries. It is added to the data dict in RolloutManager._convert_samples_to_train_data, but it's not one of the keys that are copied to each rollout_data split. This will cause issues downstream where this value is expected. It should be added to the list of keys that are copied to each split, similar to raw_reward and total_lengths.

Here is a suggested fix that addresses both points:

def split_train_data_by_dp(args, data: dict[str, Any], *, dp_size: int) -> list[dict[str, Any]]:
    """Split the train data by data parallel size."""
    total_lengths = [len(t) for t in data["tokens"]]
    data["total_lengths"] = total_lengths

    if args.balance_data:
        partitions = get_seqlen_balanced_partitions(total_lengths, dp_size, equal_size=True)
    else:
        partitions = [range(i, len(total_lengths), dp_size) for i in range(dp_size)]

    ans = []

    for i in range(dp_size):
        rollout_data = {}
        partition = partitions[i]
        rollout_data["partition"] = partition
        for key in [
            "tokens",
            "multimodal_train_inputs",
            "response_lengths",
            "rewards",
            "truncated",
            "loss_masks",
            "round_number",
            "sample_indices",
            "rollout_log_probs",
            "rollout_routed_experts",
            "prompt",
            "teacher_log_probs",
        ]:
            if key not in data:
                continue
            val = [data[key][j] for j in partition]
            rollout_data[key] = val
        # keys that are not partitioned and should be present in all splits
        for key in [
            "raw_reward",
            "total_lengths",
            "dynamic_global_batch_size",
        ]:
            if key not in data:
                continue
            rollout_data[key] = data[key]
        ans.append(rollout_data)
    return ans

Comment thread miles/backends/megatron_utils/actor.py Outdated
from torchft.process_group import ProcessGroupNCCL

pg = ProcessGroupNCCL(timeout=timedelta(seconds=60))
quorum_id = 0

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.

medium

The quorum_id is hardcoded to 0. According to repository guidelines, model parameters and configuration values should be retrieved from the model configuration rather than being hardcoded. This improves flexibility and ensures consistency across different environments.

References
  1. Model parameters and configuration values should be retrieved from the model configuration rather than being hardcoded.

Comment thread miles/ray/actor_group.py
def async_connect(self, critic_group):
return [
actor.connect_actor_critic.remote(critic)
for actor, critic in zip(self._actor_handles, critic_group._actor_handles, strict=False)

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.

medium

The zip function is used with strict=False. However, in RayTrainGroup.connect, zip is used with strict=True when iterating over cells. For consistency and safety, it's better to use strict=True here as well. This will ensure that an error is raised if the number of actor and critic handles within a cell do not match, which would indicate a configuration problem.

Suggested change
for actor, critic in zip(self._actor_handles, critic_group._actor_handles, strict=False)
for actor, critic in zip(self._actor_handles, critic_group._actor_handles, strict=True)

Comment thread tests/fast/dist_utils.py Fixed
fzyzcjy added a commit that referenced this pull request Apr 3, 2026
PR #850 added test_eager_create_task in test_logging_utils.py which
imports eager_create_task from miles.utils.async_utils, but the function
was only defined in the trainer_ft/dev branch (PR #823) and never merged
to main, breaking CI fast tests.

Cherry-pick the async_utils changes from PR #823.
Shi-Dong pushed a commit that referenced this pull request Apr 5, 2026
PR #850 added test_eager_create_task in test_logging_utils.py which
imports eager_create_task from miles.utils.async_utils, but the function
was only defined in the trainer_ft/dev branch (PR #823) and never merged
to main, breaking CI fast tests.

Cherry-pick the async_utils changes from PR #823.
GuanxingLu pushed a commit to GuanxingLu/miles that referenced this pull request Apr 21, 2026
PR radixark#850 added test_eager_create_task in test_logging_utils.py which
imports eager_create_task from miles.utils.async_utils, but the function
was only defined in the trainer_ft/dev branch (PR radixark#823) and never merged
to main, breaking CI fast tests.

Cherry-pick the async_utils changes from PR radixark#823.
Comment thread tests/fast/dist_utils.py

def find_free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", 0))

Check warning

Code scanning / CodeQL

Binding a socket to all network interfaces Medium test

Binding a socket to all interfaces (using
''
) is a security risk.

Copilot Autofix

AI 4 months ago

To fix this, bind the temporary probe socket to loopback instead of all interfaces.
General approach: replace wildcard bind addresses ("" / "0.0.0.0") with a dedicated local interface, here 127.0.0.1, since this utility is used with MASTER_ADDR=localhost.

Best single change (without changing behavior): in tests/fast/dist_utils.py, update find_free_port() line 11 from:

  • s.bind(("", 0))
    to:
  • s.bind(("127.0.0.1", 0))

No new imports, methods, or dependencies are required.

Suggested changeset 1
tests/fast/dist_utils.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tests/fast/dist_utils.py b/tests/fast/dist_utils.py
--- a/tests/fast/dist_utils.py
+++ b/tests/fast/dist_utils.py
@@ -8,7 +8,7 @@
 
 def find_free_port() -> int:
     with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
-        s.bind(("", 0))
+        s.bind(("127.0.0.1", 0))
         return s.getsockname()[1]
 
 
EOF
@@ -8,7 +8,7 @@

def find_free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", 0))
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]


Copilot is powered by AI and may make mistakes. Always verify output.
fzyzcjy added 17 commits June 13, 2026 12:10
…1 injection

Rename ExpectedReconfigure -> ReconfigureInfo and fold _shape_of into a
ReconfigureInfo.from_event staticmethod. Soak now asserts num_successful_injections
> 0: a soak that injected nothing exercised no fault tolerance and proved nothing.
RayTrainGroup records the active rollout_id at train() entry and the
control server exposes it through a new read-only GET /api/v1/progress
endpoint, so an external fault injector can observe training progress.

The progress field defaults to None before the first train() call; the
endpoint only reads it, so existing behavior is unchanged.
Thread a mock RayTrainGroup through the app fixture and assert the
progress endpoint reflects group.current_rollout_id (including the
pre-train null case).
The fault injector now polls GET /api/v1/progress and stops once
training reaches num_steps - COOLDOWN_ROLLOUTS (=3), so the final
rollouts run fault-free. poll+inject latency (~1s) is far below a
rollout (~tens of s), so a fault lands in the observed rollout or at
most the next one; COOLDOWN=3 absorbs that <=2-rollout slippage, putting
the last heal no later than the final rollout. The soak therefore
provably ends at full cell membership, with no trailing shrink.
…-shrink tolerance

With the injector cooldown guaranteeing a fault-free tail, the soak now
provably ends fully healed. Remove _is_tolerated_trailing_shrink and the
final_rollout_id parameter, and assert unconditionally that the last
reconfigure event restores full cell membership. Any sequence ending in
a shrink now fails, even at the final rollout id.
…e tail

Move training progress out of RayTrainGroup into a TrainingProgress holder
owned by the control server, which runs in the training process. The
inject-fault endpoint now reads the authoritative current rollout and rejects
(409) any injection within the final COOLDOWN_ROLLOUTS, making the fault-free
tail race-free instead of relying on injector-side poll timing.
…rces cooldown

The fault injector and its scenario callers no longer need stop_at_rollout_id
or progress polling; the server rejects tail injections directly, so the
injector returns to its rollout-unaware form and its existing except path
swallows the 409 without counting it.
…ace-free guarantee

Replace the removed GET /api/v1/progress tests with inject-fault cooldown tests
(forwarded when progress is None or before the tail, rejected with 409 inside
the tail) and rewrite the README soak cooldown argument as a server-side
rejection that does not depend on rollout duration.
…cument race-free guarantee"

This reverts commit 7d76c8d.
fzyzcjy added 20 commits July 8, 2026 10:05
Review comment on #1399: use pydantic NonNegativeInt for cell_index and
rank_within_cell.
Review comment on #1401: decorating a coroutine would exit the
with_context scope before the coroutine runs, silently dropping the
context. Assert at decoration time until async support is needed.
Review comment on #1403: a scalar torch.Tensor in metrics fails
MetricEvent validation/JSON serialization; unwrap via .item().
Review comments on #1404: reject non-positive buffer_size (would hit a
ZeroDivisionError later) and negative num_ids (would silently decrement
the monotonic counter). Raise ValueError per reviewer preference.
…port (PR #1405)

Review comments on #1405: match the getattr(args, "enable_witness",
False) access used elsewhere so minimal args objects don't crash, and
move the WitnessSnapshotParamEvent import to the top of the test.
…(PR #1407)

Review comments on #1407: wid_tokens is built via torch.full with one
id per sample, so enforce that invariant before trusting wid_tokens[0];
also switch the running-union loop to |=.
Review comment on #1409: 'rank' must always be skipped when grouping
dump bundles (absolute rank IDs differ between FT and non-FT runs).
The only caller passing custom keys already includes it; enforce the
invariant instead of silently dropping it.
#1410)

Review comments on #1410: the per-rollout max-attempt scan was
O(events x rollouts); compute it in one pass. Also move the polars and
comparator display imports to the top of the module.
Review comments on #1411: a missing event_dir made
load_reconfigure_events return [] and let an empty expectation pass
silently; fail fast with a clear message instead.
… (PR #1412)

Review comment on #1412: a CPU tensor crashes all_reduce on NCCL
groups. Derive the device from the process group itself (cpu for gloo,
cuda for nccl).
Review comment on #1413: raw data_ptr values can collide across
devices, which would alias unrelated storages during checkpoint
transfer. Include the device in the dedup key and cover mixed-device
encoding with tests.
…1415)

Review comment on #1415: calling _create_transport without torchft
installed previously failed with a confusing NoneType call.
…1424)

Review comment on #1424: actor, ref and teacher forward-only passes all
dumped into the same phase/rollout directory, wiping each other's
output. Segment the exp_name by the caller's store_prefix (e.g.
ref_/teacher_).
Review comment on #1395: if destroying the local process group raised,
the ray.get on the engines' already-in-flight destroy calls was
skipped, so a caller on the FT reconnect path could recreate the group
while engines were still tearing down.
Review comment on #1387: reload previously recreated groups from only
(ranks, backend), silently dropping timeout, pg_options, group_desc and
any other arguments. Store the original new_group args/kwargs verbatim
and replay them on reload so the rebuilt group matches the original
exactly.
fzyzcjy added 2 commits July 8, 2026 10:46
Review comments on #1397: the FT delayed-split copy in
miles/utils/data_utils.py had drifted from the rollout-side original -
it was missing rollout_indexer_topk and opd_reverse_kl (added on main
after the fork), so the delayed-split path silently dropped them.
Delete data_utils.py: the split logic lives only in
train_data_conversion.py as split_train_data_by_dp_raw (key list is the
union; seq_witness_ids is injected only on the delayed path), with
split_train_data_by_dp a thin wrapper that ray.puts each partition.
…raw (PR #1397)

Review comment on #1397: the pre-loop rollout_data dict (and its prompt
entry) was discarded when the loop reassigned rollout_data, and prompt
is already split inside the loop.
fzyzcjy added 3 commits July 8, 2026 11:04
Review comment on #1418: init() marked the cell StateAllocatedAlive
before dispatching the remote init. Flip the order so alive means init
completed; a failed init now transitions
Uninitialized -> Errored (indep_dp_info=None) -> Stopped via the
existing kill_on_failure path instead of relying on the premature alive
state.
# Conflicts:
#	miles/backends/megatron_utils/model_provider.py
#	miles/ray/actor_group.py
#	miles/utils/arguments.py
The single ft label ran all 14 FT e2e entries at once. Split it by
scenario type so PRs can trigger a quick FT signal separately from the
long soak runs (measured runtimes: comparison scenarios take 4-14 min
each, random-crash soak 26-56 min, realistic-gsm8k 2-3 h):

- ft-fast: comparison scenarios (no_failure, deterministic,
  with_failure entries, including real-rollout modes)
- ft-slow: soak scenarios (random-crash survival, realistic-gsm8k
  convergence)

The matching run-ci-ft-fast / run-ci-ft-slow GitHub labels must be
created in repo settings; run-ci-ft is obsolete.
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.

2 participants