Compare commits

..
Author SHA1 Message Date
Alexander MatveevandClaude Opus 4.6 6d32045b96 Address review: use registered envs.VLLM_DISABLE_PUSH_ALLREDUCE
Replace direct os.environ.get(_DISABLE_ENV_VAR) == "1" check with
envs.VLLM_DISABLE_PUSH_ALLREDUCE to use the centrally registered
env var from envs.py, which provides validation and caching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-15 16:23:12 -04:00
Alexander MatveevandClaude Opus 4.6 41f6a7664a Address review: register VLLM_DISABLE_PUSH_ALLREDUCE in envs.py
Register the push allreduce feature toggle env var in the central
envs.py registry so it is validated on startup and follows the
standard vllm env var pattern. Default is False (push allreduce
enabled); set to 1 to disable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-15 16:23:12 -04:00
Alexander MatveevandClaude Opus 4.6 d35dfbb2e3 Address review: add architecture-specific threshold selection
The push threshold map was labeled as sm100-specific but applied
unconditionally to all architectures. Now:
- PUSH_THRESHOLD_SM100 is only used on Blackwell (compute capability 10.x)
- PUSH_THRESHOLD_DEFAULT provides conservative 512 KB thresholds for
  architectures without tuned values
- _THRESHOLD_BY_ARCH maps GPU major compute capability to threshold tables
- A log message is emitted when falling back to conservative defaults

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-15 16:23:12 -04:00
Alexander MatveevandClaude Opus 4.6 66eb0286ed Address review: add type annotation and cleanup for push_ar_comm
- Add PushAllReduce | None type annotation on push_ar_comm to be
  consistent with other communicator fields (ca_comm, qr_comm, etc.)
- Add push_ar_comm.close() + None assignment in destroy() method
  to match the cleanup pattern for other communicators
- Add lazy import of PushAllReduce alongside other communicator imports

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-15 16:23:12 -04:00
Alexander MatveevandClaude Opus 4.6 7b879d0a98 Address review: bind test sockets to localhost instead of all interfaces
Fix CodeQL security warning by binding test helper sockets to
"localhost" instead of "" (all interfaces). These sockets are only
used for finding a free port for torch distributed init in tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-15 16:23:12 -04:00
Alexander MatveevandClaude Opus 4.6 4640e0d269 Address review: add CUDA error checking and runtime buffer overflow guard
- Add PUSH_AR_CUDACHECK macro wrapping all CUDA API calls (cudaGetDevice,
  cudaDeviceGetAttribute, cudaMalloc, cudaMemset, cudaIpcGetMemHandle,
  cudaIpcOpenMemHandle) to match the CUDACHECK pattern in custom_all_reduce.cuh
- Replace assert(input_bytes <= push_buffer_bytes_) with a runtime
  std::runtime_error check that is not compiled out under -DNDEBUG
- Add #include <stdexcept> and #include <string> for the runtime check

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-15 16:23:12 -04:00
Alexander MatveevandAlexander Matveev e3b4fdaf5d perf: add push-based allreduce for small tensor reductions
Port SGLang's push-based 2-buffer allreduce protocol into vLLM as a new
communicator backend for small-message reductions. The push protocol
eliminates the two explicit cross-GPU NVLink barrier round-trips used by
the existing barrier-based CustomAllreduce, replacing them with a
sentinel-based data arrival detection mechanism and double-buffered epoch
alternation.

Key advantages over the barrier-based approach:
- Zero barriers: data arrival IS the synchronization (positive-zero sentinel)
- Single NVLink round-trip instead of two barrier exchanges + remote reads
- All SMs active (SM_count CTAs vs 2 CTAs) for higher NVLink bandwidth
- No cudaMemcpy to IPC staging buffer in eager mode
- PDL (griddepcontrol) support for kernel overlap on sm_90+

The new PushAllReduce is inserted in the CudaCommunicator dispatch chain
above the existing CustomAllreduce for messages below a size threshold
(~720 KB at TP=8). Larger messages continue to use the barrier-based
path. The existing CustomAllreduce code is not modified.

Measured results on DeepSeek-V4-Pro (61 layers, TP=8, 8x NVIDIA B200,
BS=1, decode with ISL=4, OSL=33024):
- Throughput: +2.14% (84.06 vs 82.30 tokens/s)
- TPOT: -2.09% (11.90 vs 12.15 ms/token)

Correctness verified via lm_eval gsm8k 5-shot with no regression
(exact_match delta within statistical noise).

The feature can be disabled at runtime via VLLM_DISABLE_PUSH_ALLREDUCE=1
to fall back to the barrier-based path.

Signed-off-by: Alexander Matveev <amatveev@redhat.com>
2026-06-15 16:23:12 -04:00
11 changed files with 176 additions and 146 deletions
@@ -33,7 +33,6 @@ from vllm.distributed.device_communicators.custom_all_reduce import CustomAllred
from vllm.distributed.device_communicators.flashinfer_all_reduce import (
FlashInferAllReduce,
)
from vllm.distributed.device_communicators.push_all_reduce import PushAllReduce
from vllm.distributed.device_communicators.pynccl import (
PyNcclCommunicator,
register_nccl_symmetric_ops,
@@ -81,7 +80,6 @@ class CommunicatorBenchmark:
# Initialize communicators
self.custom_allreduce = None
self.push_ar_comm = None
self.pynccl_comm = None
self.symm_mem_comm = None
self.symm_mem_comm_multimem = None
@@ -108,23 +106,6 @@ class CommunicatorBenchmark:
)
self.custom_allreduce = None
try:
self.push_ar_comm = PushAllReduce(
group=self.cpu_group,
device=self.device,
max_size=self.max_size_override,
)
if not self.push_ar_comm.disabled:
logger.info("Rank %s: PushAllReduce initialized", self.rank)
else:
logger.info("Rank %s: PushAllReduce disabled", self.rank)
self.push_ar_comm = None
except Exception as e:
logger.warning(
"Rank %s: Failed to initialize PushAllReduce: %s", self.rank, e
)
self.push_ar_comm = None
try:
self.pynccl_comm = PyNcclCommunicator(
group=self.cpu_group, device=self.device
@@ -235,19 +216,6 @@ class CommunicatorBenchmark:
)
)
if self.push_ar_comm is not None:
comm = self.push_ar_comm
communicators.append(
(
"push_ar",
lambda t, c=comm: c.all_reduce(t),
lambda t, c=comm: c.should_use(t),
comm.capture(),
{},
None,
)
)
if self.pynccl_comm is not None:
comm = self.pynccl_comm
communicators.append(
+17 -11
View File
@@ -14,11 +14,12 @@ using fptr_t = int64_t;
using namespace vllm::push_ar;
// Initialize the manager; returns opaque pointer as int64_t
fptr_t init_push_ar(int64_t rank, int64_t world_size, int64_t push_buffer_bytes,
int64_t max_num_cta) {
auto* mgr = new PushAllReduceManager(
static_cast<int>(rank), static_cast<int>(world_size), push_buffer_bytes,
static_cast<int>(max_num_cta));
fptr_t init_push_ar(int64_t rank, int64_t world_size,
int64_t push_buffer_bytes, int64_t max_num_cta) {
auto* mgr = new PushAllReduceManager(static_cast<int>(rank),
static_cast<int>(world_size),
push_buffer_bytes,
static_cast<int>(max_num_cta));
return reinterpret_cast<fptr_t>(mgr);
}
@@ -26,7 +27,8 @@ fptr_t init_push_ar(int64_t rank, int64_t world_size, int64_t push_buffer_bytes,
torch::Tensor get_push_ar_ipc_handle(fptr_t _mgr) {
auto* mgr = reinterpret_cast<PushAllReduceManager*>(_mgr);
cudaIpcMemHandle_t handle = mgr->get_ipc_handle();
auto t = torch::from_blob(&handle, {static_cast<int64_t>(sizeof(handle))},
auto t = torch::from_blob(&handle,
{static_cast<int64_t>(sizeof(handle))},
torch::kUInt8)
.clone();
return t;
@@ -38,7 +40,8 @@ void post_init_push_ar(fptr_t _mgr, torch::Tensor all_handles) {
int world_size = all_handles.size(0);
std::vector<cudaIpcMemHandle_t> handles(world_size);
for (int i = 0; i < world_size; i++) {
memcpy(&handles[i], all_handles[i].data_ptr(), sizeof(cudaIpcMemHandle_t));
memcpy(&handles[i], all_handles[i].data_ptr(),
sizeof(cudaIpcMemHandle_t));
}
mgr->post_init(handles);
}
@@ -51,7 +54,8 @@ static bool _is_weak_contiguous(const torch::Tensor& t) {
}
// Perform allreduce
void push_ar_all_reduce(fptr_t _mgr, torch::Tensor& inp, torch::Tensor& out) {
void push_ar_all_reduce(fptr_t _mgr, torch::Tensor& inp,
torch::Tensor& out) {
auto* mgr = reinterpret_cast<PushAllReduceManager*>(_mgr);
const at::cuda::OptionalCUDAGuard device_guard(device_of(inp));
auto stream = c10::cuda::getCurrentCUDAStream().stream();
@@ -73,13 +77,15 @@ void push_ar_all_reduce(fptr_t _mgr, torch::Tensor& inp, torch::Tensor& out) {
out.numel());
break;
case at::ScalarType::Float:
mgr->allreduce<float>(stream, reinterpret_cast<float*>(inp.data_ptr()),
mgr->allreduce<float>(stream,
reinterpret_cast<float*>(inp.data_ptr()),
reinterpret_cast<float*>(out.data_ptr()),
out.numel());
break;
default:
TORCH_CHECK(false,
"push allreduce: unsupported dtype (need bf16/fp16/fp32)");
TORCH_CHECK(
false,
"push allreduce: unsupported dtype (need bf16/fp16/fp32)");
}
}
+20 -16
View File
@@ -21,14 +21,15 @@
namespace vllm {
namespace push_ar {
#define PUSH_AR_CUDACHECK(cmd) \
do { \
cudaError_t e = cmd; \
if (e != cudaSuccess) { \
throw std::runtime_error(std::string("push_all_reduce CUDA error at ") + \
__FILE__ + ":" + std::to_string(__LINE__) + \
" '" + cudaGetErrorString(e) + "'"); \
} \
#define PUSH_AR_CUDACHECK(cmd) \
do { \
cudaError_t e = cmd; \
if (e != cudaSuccess) { \
throw std::runtime_error( \
std::string("push_all_reduce CUDA error at ") + __FILE__ + \
":" + std::to_string(__LINE__) + " '" + \
cudaGetErrorString(e) + "'"); \
} \
} while (0)
class PushAllReduceManager {
@@ -88,8 +89,8 @@ class PushAllReduceManager {
peer_storage_[i] = storage_;
} else {
PUSH_AR_CUDACHECK(cudaIpcOpenMemHandle(&peer_storage_[i],
peer_handles[i],
cudaIpcMemLazyEnablePeerAccess));
peer_handles[i],
cudaIpcMemLazyEnablePeerAccess));
}
}
// Create PushController pointing to local signal region
@@ -107,12 +108,13 @@ class PushAllReduceManager {
const int num_threads = select_num_threads<T>(num_items);
// Verify input fits in push buffer (runtime check, not compiled out)
const int64_t input_bytes = static_cast<int64_t>(sizeof(T)) * num_elements;
const int64_t input_bytes =
static_cast<int64_t>(sizeof(T)) * num_elements;
if (input_bytes > push_buffer_bytes_) {
throw std::runtime_error("push_all_reduce: input (" +
std::to_string(input_bytes) +
" bytes) exceeds push buffer capacity (" +
std::to_string(push_buffer_bytes_) + " bytes)");
throw std::runtime_error(
"push_all_reduce: input (" + std::to_string(input_bytes) +
" bytes) exceeds push buffer capacity (" +
std::to_string(push_buffer_bytes_) + " bytes)");
}
// Build kernel params
@@ -223,7 +225,9 @@ class PushAllReduceManager {
return static_cast<char*>(base) + push_signal_bytes();
}
static int64_t align128(int64_t size) { return ((size + 127) / 128) * 128; }
static int64_t align128(int64_t size) {
return ((size + 127) / 128) * 128;
}
// Members
int rank_;
+5 -3
View File
@@ -235,8 +235,9 @@ struct alignas(sizeof(T) * N) AlignedStorage {
template <typename T, std::size_t N>
struct AlignedVector {
private:
static_assert((N > 0 && (N & (N - 1)) == 0) && sizeof(T) * N <= kMaxVecBytes,
"CUDA vector size exceeds arch limit (max 16 bytes)");
static_assert(
(N > 0 && (N & (N - 1)) == 0) && sizeof(T) * N <= kMaxVecBytes,
"CUDA vector size exceeds arch limit (max 16 bytes)");
using element_t = typename detail::sized_int<T>;
using storage_t = AlignedStorage<element_t, N>;
@@ -393,7 +394,8 @@ __device__ __forceinline__ void st_global_volatile_16B(const T& x, void* addr,
static_assert(alignof(T) == 16 && sizeof(T) == 16);
const uint4 val = *reinterpret_cast<const uint4*>(&x);
addr = ptr_typed_offset<T>(addr, offset);
asm volatile("st.volatile.global.v4.b32 [%4], {%0, %1, %2, %3};" ::"r"(val.x),
asm volatile("st.volatile.global.v4.b32 [%4], {%0, %1, %2, %3};" ::"r"(
val.x),
"r"(val.y), "r"(val.z), "r"(val.w), "l"(addr));
}
+4 -3
View File
@@ -105,8 +105,8 @@ TORCH_LIBRARY_EXPAND(CONCAT(TORCH_EXTENSION_NAME, _cuda_utils), cuda_utils) {
#endif
// Push-based allreduce (ported from SGLang)
fptr_t init_push_ar(int64_t rank, int64_t world_size, int64_t push_buffer_bytes,
int64_t max_num_cta);
fptr_t init_push_ar(int64_t rank, int64_t world_size,
int64_t push_buffer_bytes, int64_t max_num_cta);
torch::Tensor get_push_ar_ipc_handle(fptr_t _mgr);
void post_init_push_ar(fptr_t _mgr, torch::Tensor all_handles);
void push_ar_all_reduce(fptr_t _mgr, torch::Tensor& inp, torch::Tensor& out);
@@ -119,7 +119,8 @@ TORCH_LIBRARY_EXPAND(CONCAT(TORCH_EXTENSION_NAME, _push_ar), push_ar) {
push_ar.def("post_init_push_ar", &post_init_push_ar);
push_ar.def("push_ar_all_reduce(int mgr, Tensor inp, Tensor! out) -> ()");
push_ar.def(
"push_ar_all_reduce(int mgr, Tensor inp, Tensor! out) -> ()");
push_ar.impl("push_ar_all_reduce", torch::kCUDA, &push_ar_all_reduce);
push_ar.def("dispose_push_ar", &dispose_push_ar);
+4 -2
View File
@@ -39,9 +39,11 @@ def init_groups(rank: int, world_size: int, port: int):
os.environ["MASTER_ADDR"] = "localhost"
os.environ["MASTER_PORT"] = str(port)
torch.accelerator.set_device_index(rank)
torch.cuda.set_device(rank)
dist.init_process_group(backend="gloo", rank=rank, world_size=world_size)
dist.init_process_group(
backend="gloo", rank=rank, world_size=world_size
)
_cpu_group = dist.group.WORLD
# Create a separate NCCL group for reference allreduce
+93 -54
View File
@@ -38,7 +38,7 @@ def _init_groups(rank: int, world_size: int, port: int):
"""Initialize gloo (CPU) and nccl process groups."""
os.environ["MASTER_ADDR"] = "localhost"
os.environ["MASTER_PORT"] = str(port)
torch.accelerator.set_device_index(rank)
torch.cuda.set_device(rank)
dist.init_process_group(backend="gloo", rank=rank, world_size=world_size)
return dist.group.WORLD, dist.new_group(backend="nccl")
@@ -94,7 +94,7 @@ def _push_ar_init_worker(rank, world_size, port):
@pytest.mark.parametrize("world_size", [2])
def test_push_ar_initialization(world_size):
if torch.accelerator.device_count() < world_size:
if torch.cuda.device_count() < world_size:
pytest.skip(f"Need {world_size} GPUs")
mp.spawn(
_push_ar_init_worker,
@@ -159,7 +159,7 @@ def _push_ar_should_use_worker(rank, world_size, port):
def test_push_ar_should_use():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_push_ar_should_use_worker,
@@ -204,7 +204,7 @@ def _push_ar_correctness_int_worker(rank, world_size, port):
@pytest.mark.parametrize("world_size", [2])
def test_push_ar_correctness_integer(world_size):
if torch.accelerator.device_count() < world_size:
if torch.cuda.device_count() < world_size:
pytest.skip(f"Need {world_size} GPUs")
mp.spawn(
_push_ar_correctness_int_worker,
@@ -253,7 +253,7 @@ def _push_ar_correctness_float_worker(rank, world_size, port):
@pytest.mark.parametrize("world_size", [2])
def test_push_ar_correctness_float(world_size):
if torch.accelerator.device_count() < world_size:
if torch.cuda.device_count() < world_size:
pytest.skip(f"Need {world_size} GPUs")
mp.spawn(
_push_ar_correctness_float_worker,
@@ -302,7 +302,9 @@ def _push_ar_zero_handling_worker(rank, world_size, port):
assert torch.all(out_pz == 0.0), "Positive zero handling failed"
# Case 4: Negative zeros
inp_nz = torch.tensor([-0.0] * 1024, dtype=torch.bfloat16, device=device)
inp_nz = torch.tensor(
[-0.0] * 1024, dtype=torch.bfloat16, device=device
)
out_nz = push_ar.all_reduce(inp_nz)
assert torch.all(out_nz == 0.0)
@@ -317,7 +319,7 @@ def _push_ar_zero_handling_worker(rank, world_size, port):
@pytest.mark.timeout(120)
def test_push_ar_zero_handling():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_push_ar_zero_handling_worker,
@@ -343,20 +345,24 @@ def _push_ar_epoch_worker(rank, world_size, port):
NUM_ITERATIONS = 1000
for i in range(NUM_ITERATIONS):
inp = torch.randint(0, 16, (7168,), dtype=torch.bfloat16, device=device)
inp = torch.randint(
0, 16, (7168,), dtype=torch.bfloat16, device=device
)
out_push = push_ar.all_reduce(inp)
out_nccl = inp.clone()
dist.all_reduce(out_nccl, group=nccl_group)
assert torch.all(out_push == out_nccl), f"Epoch mismatch at iteration {i}"
assert torch.all(out_push == out_nccl), (
f"Epoch mismatch at iteration {i}"
)
push_ar.close()
_teardown()
def test_push_ar_epoch_alternation():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_push_ar_epoch_worker,
@@ -397,18 +403,22 @@ def _push_ar_thread_count_worker(rank, world_size, port):
for size in test_sizes_tc:
if size * 2 > push_ar.max_message_bytes:
continue
inp = torch.randint(0, 8, (size,), dtype=torch.bfloat16, device=device)
inp = torch.randint(
0, 8, (size,), dtype=torch.bfloat16, device=device
)
out = push_ar.all_reduce(inp)
ref = inp.clone()
dist.all_reduce(ref, group=nccl_group)
assert torch.all(out == ref), f"Failed at size={size} (sm_count={sm_count})"
assert torch.all(out == ref), (
f"Failed at size={size} (sm_count={sm_count})"
)
push_ar.close()
_teardown()
def test_push_ar_thread_count():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_push_ar_thread_count_worker,
@@ -436,7 +446,9 @@ def _push_ar_threshold_worker(rank, world_size, port):
# Case 1: Exactly at threshold (BF16)
max_elems = max_bytes // 2
max_elems = (max_elems // 8) * 8 # align to 16 bytes
inp_exact = torch.randint(0, 8, (max_elems,), dtype=torch.bfloat16, device=device)
inp_exact = torch.randint(
0, 8, (max_elems,), dtype=torch.bfloat16, device=device
)
assert push_ar.should_use(inp_exact) is True
out_exact = push_ar.all_reduce(inp_exact)
ref_exact = inp_exact.clone()
@@ -464,7 +476,7 @@ def _push_ar_threshold_worker(rank, world_size, port):
def test_push_ar_threshold():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_push_ar_threshold_worker,
@@ -487,7 +499,9 @@ def _push_ar_outofplace_worker(rank, world_size, port):
push_ar = PushAllReduce(group=cpu_group, device=device)
inp = torch.randint(1, 16, (7168,), dtype=torch.bfloat16, device=device)
inp = torch.randint(
1, 16, (7168,), dtype=torch.bfloat16, device=device
)
inp_original = inp.clone()
out = push_ar.all_reduce(inp)
@@ -508,7 +522,7 @@ def _push_ar_outofplace_worker(rank, world_size, port):
def test_push_ar_outofplace():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_push_ar_outofplace_worker,
@@ -548,7 +562,7 @@ def _push_ar_dtype_worker(rank, world_size, port):
def test_push_ar_dtype():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_push_ar_dtype_worker,
@@ -576,19 +590,23 @@ def _push_ar_multilayer_worker(rank, world_size, port):
for step in range(NUM_STEPS):
for layer in range(NUM_LAYERS):
inp = torch.randint(0, 16, (7168,), dtype=torch.bfloat16, device=device)
inp = torch.randint(
0, 16, (7168,), dtype=torch.bfloat16, device=device
)
out = push_ar.all_reduce(inp)
ref = inp.clone()
dist.all_reduce(ref, group=nccl_group)
if not torch.all(out == ref):
raise RuntimeError(f"Mismatch at step={step}, layer={layer}")
raise RuntimeError(
f"Mismatch at step={step}, layer={layer}"
)
push_ar.close()
_teardown()
def test_push_ar_multilayer():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_push_ar_multilayer_worker,
@@ -612,7 +630,9 @@ def _push_ar_lifecycle_worker(rank, world_size, port):
push_ar = PushAllReduce(group=cpu_group, device=device)
# Normal allreduce works
inp = torch.randint(0, 16, (1024,), dtype=torch.bfloat16, device=device)
inp = torch.randint(
0, 16, (1024,), dtype=torch.bfloat16, device=device
)
out = push_ar.all_reduce(inp)
assert out is not None
@@ -630,7 +650,7 @@ def _push_ar_lifecycle_worker(rank, world_size, port):
def test_push_ar_lifecycle():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_push_ar_lifecycle_worker,
@@ -676,7 +696,7 @@ def _push_ar_warmup_worker(rank, world_size, port):
def test_push_ar_warmup():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_push_ar_warmup_worker,
@@ -720,14 +740,16 @@ def _push_ar_asymmetric_worker(rank, world_size, port):
all_inputs = [torch.empty_like(inp) for _ in range(world_size)]
dist.all_gather(all_inputs, inp, group=nccl_group)
expected_sum = torch.stack(all_inputs).float().sum(dim=0).to(inp.dtype)
torch.testing.assert_close(out_push, expected_sum, atol=5e-2, rtol=5e-2)
torch.testing.assert_close(
out_push, expected_sum, atol=5e-2, rtol=5e-2
)
push_ar.close()
_teardown()
def test_push_ar_asymmetric():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_push_ar_asymmetric_worker,
@@ -760,7 +782,7 @@ def _push_ar_identical_worker(rank, world_size, port):
def test_push_ar_identical_values():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_push_ar_identical_worker,
@@ -801,14 +823,16 @@ def _dispatch_priority_worker(rank, world_size, port):
out_small = push_ar.all_reduce(small_inp)
ref_small = small_inp.clone()
dist.all_reduce(ref_small, group=nccl_group)
torch.testing.assert_close(out_small, ref_small, atol=1e-2, rtol=1e-2)
torch.testing.assert_close(
out_small, ref_small, atol=1e-2, rtol=1e-2
)
push_ar.close()
_teardown()
def test_dispatch_priority():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_dispatch_priority_worker,
@@ -833,7 +857,9 @@ def _coexistence_worker(rank, world_size, port):
for _ in range(50):
# Small message -> push allreduce
small = torch.randint(0, 16, (7168,), dtype=torch.bfloat16, device=device)
small = torch.randint(
0, 16, (7168,), dtype=torch.bfloat16, device=device
)
out_small = push_ar.all_reduce(small)
ref_small = small.clone()
dist.all_reduce(ref_small, group=nccl_group)
@@ -844,7 +870,7 @@ def _coexistence_worker(rank, world_size, port):
def test_coexistence():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_coexistence_worker,
@@ -869,8 +895,12 @@ def _interleaved_ar_worker(rank, world_size, port):
for sz in [7168, 1024, 4096]:
for dtype in [torch.bfloat16, torch.float16]:
inp1 = torch.randint(1, 16, (sz,), dtype=dtype, device=device)
inp2 = torch.randint(1, 16, (sz,), dtype=dtype, device=device)
inp1 = torch.randint(
1, 16, (sz,), dtype=dtype, device=device
)
inp2 = torch.randint(
1, 16, (sz,), dtype=dtype, device=device
)
out1 = push_ar.all_reduce(inp1)
ref1 = inp1.clone()
@@ -880,15 +910,19 @@ def _interleaved_ar_worker(rank, world_size, port):
ref2 = inp2.clone()
dist.all_reduce(ref2, group=nccl_group)
torch.testing.assert_close(out1, ref1, atol=1e-2, rtol=1e-2)
torch.testing.assert_close(out2, ref2, atol=1e-2, rtol=1e-2)
torch.testing.assert_close(
out1, ref1, atol=1e-2, rtol=1e-2
)
torch.testing.assert_close(
out2, ref2, atol=1e-2, rtol=1e-2
)
push_ar.close()
_teardown()
def test_interleaved_ar():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_interleaved_ar_worker,
@@ -915,13 +949,15 @@ def _graph_capture_worker(rank, world_size, port):
sz = 7168
# Allocate graph input in graph memory pool
graph_inp = torch.randint(1, 16, (sz,), dtype=torch.bfloat16, device=device)
graph_inp = torch.randint(
1, 16, (sz,), dtype=torch.bfloat16, device=device
)
# Warmup
with push_ar.capture():
for _ in range(NUM_AR):
push_ar.all_reduce(graph_inp)
torch.accelerator.synchronize()
torch.cuda.synchronize()
# Capture graph
graph = torch.cuda.CUDAGraph()
@@ -937,7 +973,7 @@ def _graph_capture_worker(rank, world_size, port):
torch.randint(1, 16, (sz,), dtype=torch.bfloat16, device=device)
)
graph.replay()
torch.accelerator.synchronize()
torch.cuda.synchronize()
# Verify last output is correct
ref = graph_inp.clone()
@@ -946,11 +982,8 @@ def _graph_capture_worker(rank, world_size, port):
# of graph_inp.
dist.all_reduce(ref, group=nccl_group)
torch.testing.assert_close(
outs[-1],
ref,
atol=1e-2,
rtol=1e-2,
msg=f"Graph replay {replay_iter} failed",
outs[-1], ref, atol=1e-2, rtol=1e-2,
msg=f"Graph replay {replay_iter} failed"
)
push_ar.close()
@@ -958,7 +991,7 @@ def _graph_capture_worker(rank, world_size, port):
def test_graph_capture():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_graph_capture_worker,
@@ -988,7 +1021,9 @@ def _push_ar_transformer_sim_worker(rank, world_size, port):
for step in range(NUM_STEPS):
for block in range(NUM_BLOCKS):
# AR 1: attention wo_b output
attn_out = torch.randn(1, hidden_size, dtype=torch.bfloat16, device=device)
attn_out = torch.randn(
1, hidden_size, dtype=torch.bfloat16, device=device
)
attn_reduced = push_ar.all_reduce(attn_out)
attn_ref = attn_out.clone()
dist.all_reduce(attn_ref, group=nccl_group)
@@ -1001,7 +1036,9 @@ def _push_ar_transformer_sim_worker(rank, world_size, port):
)
# AR 2: MoE output
moe_out = torch.randn(1, hidden_size, dtype=torch.bfloat16, device=device)
moe_out = torch.randn(
1, hidden_size, dtype=torch.bfloat16, device=device
)
moe_reduced = push_ar.all_reduce(moe_out)
moe_ref = moe_out.clone()
dist.all_reduce(moe_ref, group=nccl_group)
@@ -1018,7 +1055,7 @@ def _push_ar_transformer_sim_worker(rank, world_size, port):
def test_push_ar_transformer_sim():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_push_ar_transformer_sim_worker,
@@ -1039,9 +1076,9 @@ def _feature_toggle_enabled_worker(rank, world_size, port):
os.environ.pop("VLLM_DISABLE_PUSH_ALLREDUCE", None)
from vllm.distributed.device_communicators.push_all_reduce import (
_DISABLE_ENV_VAR,
_FEATURE_DESCRIPTION,
PushAllReduce,
_FEATURE_DESCRIPTION,
_DISABLE_ENV_VAR,
)
push_ar = PushAllReduce(group=cpu_group, device=device)
@@ -1062,7 +1099,7 @@ def _feature_toggle_enabled_worker(rank, world_size, port):
def test_feature_toggle_enabled():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_feature_toggle_enabled_worker,
@@ -1102,7 +1139,7 @@ def _feature_toggle_disabled_worker(rank, world_size, port):
def test_feature_toggle_disabled():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_feature_toggle_disabled_worker,
@@ -1129,7 +1166,9 @@ def _feature_toggle_not_disabled_worker(rank, world_size, port):
push_ar = PushAllReduce(group=cpu_group, device=device)
# Feature should still be enabled (only "1" disables)
assert not push_ar.disabled, "PushAllReduce should be enabled when env var != '1'"
assert not push_ar.disabled, (
"PushAllReduce should be enabled when env var != '1'"
)
# Clean up
os.environ.pop("VLLM_DISABLE_PUSH_ALLREDUCE", None)
@@ -1138,7 +1177,7 @@ def _feature_toggle_not_disabled_worker(rank, world_size, port):
def test_feature_toggle_not_disabled():
if torch.accelerator.device_count() < 2:
if torch.cuda.device_count() < 2:
pytest.skip("Need 2 GPUs")
mp.spawn(
_feature_toggle_not_disabled_worker,
+9 -3
View File
@@ -3024,8 +3024,12 @@ def qr_max_size() -> int:
# push allreduce (ported from SGLang)
def init_push_ar(rank: int, world_size: int, buffer_bytes: int, max_cta: int) -> int:
return torch.ops._C_push_ar.init_push_ar(rank, world_size, buffer_bytes, max_cta)
def init_push_ar(
rank: int, world_size: int, buffer_bytes: int, max_cta: int
) -> int:
return torch.ops._C_push_ar.init_push_ar(
rank, world_size, buffer_bytes, max_cta
)
def get_push_ar_ipc_handle(mgr: int) -> torch.Tensor:
@@ -3036,7 +3040,9 @@ def post_init_push_ar(mgr: int, handles: torch.Tensor) -> None:
torch.ops._C_push_ar.post_init_push_ar(mgr, handles)
def push_ar_all_reduce(mgr: int, inp: torch.Tensor, out: torch.Tensor) -> None:
def push_ar_all_reduce(
mgr: int, inp: torch.Tensor, out: torch.Tensor
) -> None:
torch.ops._C_push_ar.push_ar_all_reduce(mgr, inp, out)
@@ -113,11 +113,9 @@ class CudaCommunicator(DeviceCommunicatorBase):
# Initialize push-based allreduce (faster for small messages)
# Only available on NVIDIA CUDA GPUs with NVLink
if (
current_platform.is_cuda()
and self.ca_comm is not None
and not self.ca_comm.disabled
):
if (current_platform.is_cuda()
and self.ca_comm is not None
and not self.ca_comm.disabled):
try:
from vllm.distributed.device_communicators.push_all_reduce import (
PushAllReduce,
@@ -226,7 +224,6 @@ class CudaCommunicator(DeviceCommunicatorBase):
"NCCL_SYMM_MEM",
"QUICK_REDUCE",
"FLASHINFER",
"PUSH_AR",
"CUSTOM",
"SYMM_MEM",
"PYNCCL",
@@ -260,8 +257,6 @@ class CudaCommunicator(DeviceCommunicatorBase):
enabled_ar_backends.append("QUICK_REDUCE")
if self.fi_ar_comm is not None and not self.fi_ar_comm.disabled:
enabled_ar_backends.append("FLASHINFER")
if self.push_ar_comm is not None:
enabled_ar_backends.append("PUSH_AR")
if self.ca_comm is not None and not self.ca_comm.disabled:
enabled_ar_backends.append("CUSTOM")
if self.symm_mem_comm is not None and not self.symm_mem_comm.disabled:
@@ -308,7 +303,10 @@ class CudaCommunicator(DeviceCommunicatorBase):
assert out is not None
return out
push_ar_comm = self.push_ar_comm
if push_ar_comm is not None and push_ar_comm.should_use(input_):
if (
push_ar_comm is not None
and push_ar_comm.should_use(input_)
):
out = push_ar_comm.all_reduce(input_)
if out is not None:
return out
@@ -16,6 +16,7 @@ Protocol:
import logging
import os
from contextlib import contextmanager
from typing import Optional
import torch
import torch.distributed as dist
@@ -70,7 +71,7 @@ class PushAllReduce:
self,
group: dist.ProcessGroup,
device: torch.device,
max_size: int | None = None,
max_size: Optional[int] = None,
):
self.group = group
self.device = device
@@ -110,7 +111,9 @@ class PushAllReduce:
self.push_buffer_bytes = max_size
else:
arch_major = props.major
threshold_map = _THRESHOLD_BY_ARCH.get(arch_major, PUSH_THRESHOLD_DEFAULT)
threshold_map = _THRESHOLD_BY_ARCH.get(
arch_major, PUSH_THRESHOLD_DEFAULT
)
self.push_buffer_bytes = threshold_map.get(
self.world_size, DEFAULT_PUSH_BUFFER
)
@@ -128,7 +131,9 @@ class PushAllReduce:
if env_override:
val = int(env_override)
if val == 0:
logger.info("PushAllReduce disabled via VLLM_PUSH_AR_BUFFER_BYTES=0")
logger.info(
"PushAllReduce disabled via VLLM_PUSH_AR_BUFFER_BYTES=0"
)
self.disabled = True
return
# Round up to 128-byte alignment for kernel volatile stores
@@ -149,7 +154,8 @@ class PushAllReduce:
self._exchange_ipc_handles()
logger.info(
"PushAllReduce initialized: rank=%d, ws=%d, buffer=%d KB, sm=%d",
"PushAllReduce initialized: rank=%d, ws=%d, "
"buffer=%d KB, sm=%d",
self.rank,
self.world_size,
self.push_buffer_bytes // 1024,
@@ -180,7 +186,9 @@ class PushAllReduce:
local_handle = ops.get_push_ar_ipc_handle(self._ptr) # shape (64,)
# All-gather handles: each rank broadcasts its handle
handle_list = [torch.empty_like(local_handle) for _ in range(self.world_size)]
handle_list = [
torch.empty_like(local_handle) for _ in range(self.world_size)
]
dist.all_gather(handle_list, local_handle, group=self.group)
all_handles = torch.stack(handle_list) # shape (world_size, 64)
@@ -200,7 +208,7 @@ class PushAllReduce:
return False
return inp_size <= self.max_message_bytes
def all_reduce(self, input_: torch.Tensor) -> torch.Tensor | None:
def all_reduce(self, input_: torch.Tensor) -> Optional[torch.Tensor]:
"""Perform push-based allreduce. Returns new output tensor."""
if self._IS_CAPTURING:
if torch.cuda.is_current_stream_capturing():
+3 -7
View File
@@ -590,7 +590,8 @@ class GroupCoordinator:
maybe_ca_context = ca_comm.capture() # type: ignore
# Enter push allreduce capture context
push_ar_comm = getattr(self.device_communicator, "push_ar_comm", None)
push_ar_comm = getattr(
self.device_communicator, 'push_ar_comm', None)
if push_ar_comm is not None:
maybe_push_ar_context = push_ar_comm.capture() # type: ignore
@@ -607,12 +608,7 @@ class GroupCoordinator:
if curr_stream != stream:
stream.wait_stream(curr_stream)
with (
torch.cuda.stream(stream),
maybe_ca_context,
maybe_push_ar_context,
maybe_aiter_context,
):
with torch.cuda.stream(stream), maybe_ca_context, maybe_push_ar_context, maybe_aiter_context:
yield graph_capture_context
def all_reduce(self, input_: torch.Tensor) -> torch.Tensor: