Tianye/miles 2gpu overlap#12
Conversation
…pipelines In any full-overlap topology (every train GPU is also a peer pipeline's infer GPU) a pipeline can land in a state where the scheduler still records it as owning actor_infer (cluster_id ∈ active_allocations) but the gap-ratio planner sees neither progress nor a pending GENERATION request to derive demand from. Most commonly hit on 2-GPU 2-pipeline (P1 train=[0]/infer=[0,1], P2 train=[1]/infer=[0,1]) when a peer's INITIALIZATION grab donor-shrinks the holder's DP workers below the threshold needed to produce a first rollout — the holder then drops out of pipeline_states entirely and never gets workers back. Add a third branch in plan_generation_gap_ratio: if step_target == 0, step_target_estimate is None, AND the cluster_id is still in active_allocations, assign floor demand (_STARVATION_FLOOR_STEP_DEMAND = 1.0 step-target units) instead of `continue`. This keeps the pipeline visible to the gap-ratio loop both as a receiver (so it can reclaim ≥1 TP bundle to break the deadlock) and as a donor (so a peer with real demand can shrink it down to the floor). The active_allocations guard distinguishes this from a legitimate idle — notify_release_gpus clears active_allocations on the release path. Invariant being enforced: a pipeline that holds an actor_infer cluster must stay visible to the gap-ratio loop, regardless of whether it has demand signals. Floor demand factored out as _STARVATION_FLOOR_STEP_DEMAND alongside the existing iteration-bound constants.
…ap smoke
Adapts scripts/run_smoke_dual.sh (4-GPU disjoint pools) to a 2-GPU
full-overlap topology requested by the user:
P1: train=[0] infer=[0,1]
P2: train=[1] infer=[0,1]
Both pipelines share both physical GPUs for inference, so every train
GPU is also the sibling pipeline's infer GPU — this exercises the
donor-shrink-before-receiver-expand path in
rlix/scheduler/scheduler.py:1027-1073 and the new planner starvation
guard added in the previous commit.
Key config tuned for 2-GPU memory pressure:
--sglang-mem-fraction-static 0.3
--sglang-disable-cuda-graph --sglang-disable-piecewise-cuda-graph
--sglang-max-prefill-tokens 1024 --sglang-chunked-prefill-size 1024
--use-precision-aware-optimizer --optimizer-cpu-offload
--optimizer-offload-fraction 1.0
--max-tokens-per-gpu 256
PYTORCH_CUDA_ALLOC_CONF=garbage_collection_threshold:0.6,max_split_size_mb:64
MILES_DUAL_P1_TRAIN=0 P1_INFER=0,1 P2_TRAIN=1 P2_INFER=0,1
Force-added past .gitignore (scripts/ is ignored by default).
| if step_target <= 0.0: | ||
| step_target_estimate = get_pending_generation_step_target_estimate(pending_bucket_gen, cluster_id) | ||
| if step_target_estimate is None: | ||
| if step_target_estimate is not None: |
There was a problem hiding this comment.
I think the root issue is that pending_bucket_gen is only a transient signal queue, not durable rollout intent. Original approach of rely on it is too hacky.
Once a pending GEN request is consumed, absence from the bucket no longer proves the pipeline has no rollout GPU demand. It may simply mean the signal was consumed before rollout data was actually produced.
A more reliable signal may be the pipeline-side batch state. If the pipeline has opened a rollout batch and is blocked in get_batch(), then rollout GPU demand is still live until that batch is produced/consumed or explicitly closed by release/cancel/failure.
So instead of using active_allocations plus a synthetic floor demand, can we recover the intended step target from the open rollout-batch / waiting-for-get_batch() state?
PR: 2-GPU full-overlap planner starvation recovery
Background
User-requested 2ppl partial-overlap 拓扑,2 块 GPU 都被两条 pipeline 同时当 infer GPU 用:
和
scripts/run_smoke_dual.sh(4-GPU disjoint pool)相比,这里每个 train GPU 同时也是对面 pipeline 的 infer GPU,触发scheduler.py:1027-1073的 donor-shrink-before-receiver-expand 路径。在 11 次 attempt 的 debug(见debug_log.md)里发现一个新死锁,本 PR 修掉它。Root cause
死锁在
rlix/scheduler/planner.py:222-232的 gap-ratio planner:step_target == 0pending_bucket_gen里没 P1 的 GEN 请求step_target_estimate is None: continue,把 P1 完全踢出pipeline_statesP1 starve 后没有任何外部触发能把它救出来:F40 Runtime 的
_expand_workers只在自己的after_training里 fire,而after_training必须先有一次成功 rollout,rollout 又需要 ≥1 个 active engine — 闭环。The fix
在
plan_generation_gap_ratio里把step_target_estimate is None: continue改成三分支 if/elif/else:active_allocations守卫的语义关键——它区分两种情况:notify_release_gpus会清掉active_allocations)→ 走 continue,保持原行为Verification
Smoke:
scripts/run_smoke_dual_2gpu.sh(本 PR 新增,从run_smoke_dual.sh改的 2-GPU 变体)完整时间线(attempt 11 v7b):
18:16:13P1 init complete + activate_routing=[0,1]18:16:34 ~ 18:19:19P1 被 P2 INIT 抢占多轮,饥饿守卫数次救场自动恢复18:19:46P2 拿到 engine 0(gap-ratio 把 P1 一个 engine donate 给 P2)18:20:59 / 18:21:01两条 rollout 完成18:22:26Megatron train end elapsed=23.5s, step 0 metrics 全有18:22:27cpu_bucket_cache published + 两条 training loop complete18:22:28shutdown_hard complete,EXIT_CODE=0nvidia-smi: GPU 0 = 1 MiB, GPU 1 = 1 MiB(清理干净)回归保护:
tests/test_gap_ratio.py7 个用例,新分支不影响任何现有 case。无 race / 无震荡:
if step_target_estimate is not None仍然先判,pending 路径优先级最高active_allocations是 scheduler 内部状态,每个 plan cycle 在锁内读取_compute_shrink_budget_by_pipeline_id会保护对方 ≥ target_gpu_countprogress_totals_fn返回真实step_target > 0,本分支不再触发Files changed
rlix/scheduler/planner.py(+37 / -4) — 饥饿恢复分支 +_STARVATION_FLOOR_STEP_DEMAND常量scripts/run_smoke_dual_2gpu.sh(+133) — 新增,2-GPU 全 overlap smokeNotes