Skip to content

multi-node dp support#1603

Open
ganyi1996ppo wants to merge 3 commits into
mainfrom
ganyi/multinode_dp
Open

multi-node dp support#1603
ganyi1996ppo wants to merge 3 commits into
mainfrom
ganyi/multinode_dp

Conversation

@ganyi1996ppo

Copy link
Copy Markdown
Contributor

Summary

Add native multi-node data-parallel serving to ATOM.

Global DP ranks can now be distributed across multiple nodes. Global DP rank 0
acts as the sole serving coordinator, while other nodes launch their local
EngineCore workers and connect back to rank 0.

Multi-node mode is inferred directly from the configured DP topology. No
additional distributed_dp or distributed_dp_serving flags are required.

Motivation

The existing native DP implementation assumes every DP rank is local:

  • global DP rank and local GPU rank are treated as the same value;
  • one CoreManager launches every DP rank;
  • EngineCore communication uses local IPC sockets;
  • distributed initialization can select devices using global rank values.

These assumptions do not hold when DP ranks span multiple nodes. For example,
a node that owns global DP ranks 8–15 still needs to map those workers to local
GPUs 0–7.

Design

DP topology

The topology is configured with:

Argument Description
--data-parallel-size, -dp Total number of DP ranks across all nodes
--data-parallel-size-local Number of DP ranks hosted on this node
--data-parallel-rank First global DP rank hosted on this node
--data-parallel-master-ip Routable IP address of the rank-0 node
--data-parallel-master-port DP rendezvous and control-port base
--data-parallel-base-port Model-runner distributed initialization port

--data-parallel-rank-local is assigned internally to EngineCore workers and
normally should not be set by users.

If --data-parallel-size-local is omitted, it defaults to the global DP size,
preserving existing single-node behavior.

Multi-node mode is inferred when:

data_parallel_size_local < data_parallel_size
or data_parallel_rank > 0

The topology is validated as follows:

data_parallel_size >= 1
data_parallel_size_local >= 1
data_parallel_rank >= 0
data_parallel_rank + data_parallel_size_local <= data_parallel_size

Coordinator/worker model

In multi-node mode:

  • the node whose first global DP rank is 0 is the sole coordinator;
  • only the coordinator serves the OpenAI-compatible HTTP API;
  • the coordinator owns routing sockets for every global EngineCore rank;
  • non-coordinator nodes launch only their local EngineCore workers;
  • requests are routed round-robin unless a request carries an explicit DP-rank
    routing hint.

Single-node deployments continue using the existing local IPC transport.

Global and local ranks

Each EngineCore receives two DP ranks:

  • global DP rank: used for distributed groups and collectives;
  • local DP rank: used for GPU and NUMA placement.

For example, the second node in a DP16 deployment may be configured with:

global DP size = 16
local DP size  = 8
rank offset    = 8

Its rank mapping becomes:

global rank 8  -> local rank 0
global rank 9  -> local rank 1
...
global rank 15 -> local rank 7

This prevents global ranks on later nodes from being interpreted as local GPU
indices.

DP-attention rank mapping

With DP attention enabled, DP and TP ranks are flattened into EngineCore ranks:

global_engine_rank = global_dp_rank * tp_size + tp_rank
local_engine_rank = local_dp_rank * tp_size + tp_rank

The existing PCP + DP-attention incompatibility is unchanged.

EngineCore transport

The rank-0 coordinator binds one request and one output port for every effective
global EngineCore rank.

For rank r:

input_port  = data_parallel_master_port + 100 + 2*r
output_port = data_parallel_master_port + 100 + 2*r + 1

The socket plan stores only these semantic port values. The coordinator derives
wildcard bind addresses, while workers derive connect addresses using
data_parallel_master_ip.

Both single-node IPC and multi-node TCP coordinator sockets are initialized
through the same binding path before workers start.

Backward compatibility

Existing single-node commands continue to work:

python -m atom.entrypoints.openai_server \
  --model "$MODEL" \
  -dp 8

Because the local DP size is omitted, it defaults to 8 and all ranks are
launched locally.

Verification

Completed static verification:

  • Ruff on all modified Python files
  • Python bytecode compilation
  • git diff --check
  • no RCCL/MoE backend implementation changes included

A full multi-node GPU serving and accuracy run is still required before merge.


## Usage examples

### Two nodes with one GPU each

Assume:

```bash
MODEL=/path/to/model
COORDINATOR_IP=10.0.0.10

The model path must be available on both nodes.

Node 0: coordinator, global DP rank 0

export AITER_LOG_LEVEL=WARNING

python -m atom.entrypoints.openai_server \
  --model "$MODEL" \
  -tp 1 \
  -dp 2 \
  --data-parallel-size-local 1 \
  --data-parallel-rank 0 \
  --data-parallel-master-ip "$COORDINATOR_IP" \
  --data-parallel-master-port 29500 \
  --data-parallel-base-port 29550 \
  --host 0.0.0.0 \
  --server-port 8000

Node 1: worker, global DP rank 1

export AITER_LOG_LEVEL=WARNING

python -m atom.entrypoints.openai_server \
  --model "$MODEL" \
  -tp 1 \
  -dp 2 \
  --data-parallel-size-local 1 \
  --data-parallel-rank 1 \
  --data-parallel-master-ip "$COORDINATOR_IP" \
  --data-parallel-master-port 29500 \
  --data-parallel-base-port 29550

Send requests only to node 0:

curl "http://${COORDINATOR_IP}:8000/v1/completions" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "'"$MODEL"'",
    "prompt": "Explain data-parallel inference in one sentence.",
    "max_tokens": 64
  }'

Two nodes with TP4 on each node

This configuration creates two global DP replicas, each using four GPUs:

global DP size = 2
local DP size per node = 1
TP size = 4
total GPUs = DP × TP = 8

Node 0

export AITER_LOG_LEVEL=WARNING

python -m atom.entrypoints.openai_server \
  --model "$MODEL" \
  -tp 4 \
  -dp 2 \
  --data-parallel-size-local 1 \
  --data-parallel-rank 0 \
  --data-parallel-master-ip "$COORDINATOR_IP" \
  --data-parallel-master-port 29500 \
  --data-parallel-base-port 29550 \
  --host 0.0.0.0 \
  --server-port 8000

Node 1

export AITER_LOG_LEVEL=WARNING

python -m atom.entrypoints.openai_server \
  --model "$MODEL" \
  -tp 4 \
  -dp 2 \
  --data-parallel-size-local 1 \
  --data-parallel-rank 1 \
  --data-parallel-master-ip "$COORDINATOR_IP" \
  --data-parallel-master-port 29500 \
  --data-parallel-base-port 29550

Four nodes with two DP ranks per node

For global DP8, TP1, with two GPUs per node:

Node Local size Rank offset Global ranks
Node 0 2 0 0–1
Node 1 2 2 2–3
Node 2 2 4 4–5
Node 3 2 6 6–7

Signed-off-by: ganyi <ygan@amd.com>
Copilot AI review requested due to automatic review settings July 15, 2026 06:08
@github-actions

Copy link
Copy Markdown
Contributor

🏷️ CI Guide

Runs automatically on every eligible PR before approval:

  • ✅ Pre Checkin: Black, Ruff, catalog schema validation, non-GPU unit tests

Heavy model tests:

  • ✅ Run after the PR is approved and Pre Checkin passes
  • ✅ Run immediately when an approval review is submitted
  • ✅ Can be requested before approval with labels
Label Tests
ci:full Run all heavy PR model tests: native ATOM, vLLM, and SGLang
ci:atom Run native ATOM model accuracy tests
ci:vllm Run ATOM vLLM OOT model accuracy tests
ci:sglang Run ATOM SGLang model accuracy tests

Heavy jobs are skipped when the PR is not approved and no matching ci:* label is present.
Add labels via the sidebar or gh pr edit 1603 --add-label <label>

Signed-off-by: ganyi <ygan@amd.com>

Copilot AI 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.

Pull request overview

This PR adds native multi-node data-parallel (DP) serving support to ATOM by separating global DP ranks (for distributed groups/collectives) from local DP ranks (for GPU/NUMA placement), and introducing a coordinator/worker model where only global DP rank 0 serves the HTTP API while other nodes run EngineCore workers and connect back to rank 0.

Changes:

  • Extends DP topology/config to support multi-node deployments (global vs local DP size/rank, master IP/ports) and validates topology in ParallelConfig.
  • Updates EngineCore/CoreManager launch and routing to support multi-node socket addressing and rank mapping, including DP-attention rank flattening behavior.
  • Adds CLI/kwargs backward-compat plumbing so callers can configure DP topology consistently via ParallelConfig (with fallback for older loose kwargs).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
atom/utils/envs.py Adds ATOM_DP_SIZE_LOCAL env var for local DP topology override.
atom/model_engine/model_runner.py Passes local_rank into distributed init to ensure correct device selection under multi-node DP.
atom/model_engine/llm_engine.py Maintains backward compatibility for older callers passing DP topology via loose kwargs instead of ParallelConfig.
atom/model_engine/engine_core.py Updates DP init assertions to allow global/local rank decoupling.
atom/model_engine/engine_core_mgr.py Implements multi-node DP socket planning, global/local rank assignment, and coordinator vs worker behavior.
atom/model_engine/async_proc.py Fixes NUMA/GPU mapping to use local DP rank when available.
atom/model_engine/arg_utils.py Adds CLI flags and builds a ParallelConfig encapsulating multi-node DP topology.
atom/config.py Makes data_parallel_size_local optional with default-to-global behavior, adds multinode detection, hashing, env overrides, and topology validation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +135 to +139
master_ip = config.parallel_config.data_parallel_master_ip
addresses = {
"input_address": f"tcp://{master_ip}:{plan.input_port}",
"output_address": f"tcp://{master_ip}:{plan.output_port}",
}
Comment on lines +164 to +168
coordinator_addresses = [
{
"input_address": f"tcp://0.0.0.0:{plan.input_port}",
"output_address": f"tcp://0.0.0.0:{plan.output_port}",
}
Comment on lines +208 to +212
if not self._distributed_coordinator:
for proc in self.engine_core_processes:
proc.join()
raise SystemExit(0)

Copilot AI review requested due to automatic review settings July 15, 2026 06:15

Copilot AI 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.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Comment on lines +53 to +59
if "parallel_config" not in config_kwargs:
if "data_parallel_size" in kwargs:
config.parallel_config.data_parallel_size = kwargs["data_parallel_size"]
if "data_parallel_size_local" in kwargs:
config.parallel_config.data_parallel_size_local = kwargs[
"data_parallel_size_local"
]
Comment thread atom/config.py
Comment on lines +769 to +773
def is_multinode_dp(self) -> bool:
return (
self.data_parallel_size_local < self.data_parallel_size
or self.data_parallel_rank > 0
)
Comment thread atom/utils/envs.py
"ATOM_DP_RANK": lambda: int(os.getenv("ATOM_DP_RANK", "0")),
"ATOM_DP_RANK_LOCAL": lambda: int(os.getenv("ATOM_DP_RANK_LOCAL", "0")),
"ATOM_DP_SIZE": lambda: int(os.getenv("ATOM_DP_SIZE", "1")),
"ATOM_DP_SIZE_LOCAL": lambda: int(os.getenv("ATOM_DP_SIZE_LOCAL", "1")),
Comment on lines +431 to +435
"data_parallel_master_port": data_parallel_master_port,
}
if data_parallel_base_port is not None:
parallel_config_kwargs["data_parallel_base_port"] = data_parallel_base_port
kwargs["parallel_config"] = ParallelConfig(**parallel_config_kwargs)
@zufayu
zufayu requested a review from yitingw1 July 16, 2026 02:05
Signed-off-by: ganyi <ygan@amd.com>
Copilot AI review requested due to automatic review settings July 16, 2026 02:16

Copilot AI 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.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Comment on lines +51 to +74
# Backward compatibility for callers that still pass DP topology as
# loose LLMEngine kwargs instead of a ParallelConfig.
if "parallel_config" not in config_kwargs:
if "data_parallel_size" in kwargs:
config.parallel_config.data_parallel_size = kwargs["data_parallel_size"]
if "data_parallel_size_local" in kwargs:
config.parallel_config.data_parallel_size_local = kwargs[
"data_parallel_size_local"
]
if "data_parallel_rank" in kwargs:
config.parallel_config.data_parallel_rank = kwargs["data_parallel_rank"]
if "data_parallel_rank_local" in kwargs:
config.parallel_config.data_parallel_rank_local = kwargs[
"data_parallel_rank_local"
]
if "data_parallel_master_ip" in kwargs:
config.parallel_config.data_parallel_master_ip = kwargs[
"data_parallel_master_ip"
]
if "data_parallel_master_port" in kwargs:
config.parallel_config.data_parallel_master_port = kwargs[
"data_parallel_master_port"
]
self.data_parallel_size = config.parallel_config.data_parallel_size
Comment on lines +53 to +55
def _iter_dp_rank_assignments(config: Config):
"""Yield ``(global_dp_rank, local_dp_rank)`` for ranks owned by this node."""
pc = config.parallel_config
Comment on lines 122 to 124
logger.info(
f"{self.label}: Creating EngineCore for DP rank {dp_rank}/{self.local_engine_count}"
)
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