Skip to content

[Core] Let normal task retries prefer a different node after worker failure #65862

Description

@wangzhigang1999

Description

Should a normal task retry after WORKER_DIED prefer a different feasible node from the immediately preceding attempt?

Today, when two equivalent nodes are available, a task whose worker exits can be retried on the same still-alive node. This is correct for a deterministic application crash, but it can repeat a node-local failure such as a broken device, corrupted local environment, or unhealthy worker runtime even when another eligible node is idle.

The desired behavior would be a soft preference, not a hard exclusion:

  1. Preserve the task's resources, label selectors, affinity, and fallback order.
  2. Within each existing scheduling tier, prefer a node other than the previous failed node.
  3. Fall back immediately to the original tier if no alternative is currently available.
  4. Apply this only to the next attempt and remember at most one node.

I am intentionally limiting the question to WORKER_DIED. For NODE_DIED, the old NodeID is already unavailable to the scheduler, so an additional exclusion is generally redundant.

The main ambiguity is that WORKER_DIED is broad: it can represent node-local trouble, but it can also be caused by deterministic user native code. Before preparing a PR, I would like feedback on whether Ray Core should:

  • keep the current behavior;
  • apply this soft preference by default; or
  • expose an opt-in retry placement policy.

Use case

Long-running task workloads often rely on max_retries to tolerate transient worker or hardware failures. If a node remains registered but has a local fault, immediately retrying there can consume another attempt for the same reason. A one-attempt soft preference can use an already-idle peer without reducing schedulability in single-node, hard-affinity, or temporarily busy-cluster cases.

Minimal reproduction:

import os

import ray
from ray.cluster_utils import Cluster
from ray.util.scheduling_strategies import NodeAffinitySchedulingStrategy

cluster = Cluster()
cluster.add_node(num_cpus=0, resources={"control": 1})
cluster.add_node(num_cpus=1, resources={"first": 1})
cluster.add_node(num_cpus=1, resources={"second": 1})
ray.init(address=cluster.address)
cluster.wait_for_nodes()

nodes = ray.nodes()
first = next(n["NodeID"] for n in nodes if n["Resources"].get("first") == 1)
second = next(n["NodeID"] for n in nodes if n["Resources"].get("second") == 1)

@ray.remote(num_cpus=0, resources={"control": 0.01})
class Recorder:
    def __init__(self):
        self.nodes = []

    def record(self, node_id):
        self.nodes.append(node_id)
        return len(self.nodes)

    def get(self):
        return self.nodes

@ray.remote(max_retries=1)
def task(recorder):
    node_id = ray.get_runtime_context().get_node_id()
    if ray.get(recorder.record.remote(node_id)) == 1:
        os._exit(1)
    return node_id

recorder = Recorder.remote()
ray.get(task.options(
    scheduling_strategy=NodeAffinitySchedulingStrategy(first, soft=True)
).remote(recorder))
print(ray.get(recorder.get.remote()))

On a master build at 2b30448d6b, I observed [first, first] in 5/5 fresh three-raylet runs. The relevant scheduling and retry files are unchanged at the current master tip, c76067e02c. A prototype implementing the soft preference produced [first, second] in 10/10 runs on one host and 10/10 runs across three physical Kubernetes hosts.

The prototype also preserved these boundaries in repeated tests:

  • hard node affinity: retry remained on the pinned node;
  • no free capacity on the alternative node: retry fell back to the original node immediately;
  • application exceptions: scheduling behavior was unchanged;
  • the retry hint did not create a new process-global scheduling class.

Related but distinct issues: #39861 asks to change node-pool configuration after machine failure; #59522 concerns Ray Data/LLM actor resiliency; #48996 concerns scheduling stalls while detecting node removal.

AI assistance was used to explore the code path and validate the prototype. I will review and defend every changed line before proposing any implementation PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions