Compare commits
12 Commits
3a50c67884
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba901536da | ||
|
|
c04f54792e | ||
|
|
5b646ae665 | ||
|
|
22b60d93a6 | ||
|
|
10539e25ae | ||
|
|
ff8cbeb022 | ||
|
|
8cf04ebe60 | ||
|
|
6a12f4aea8 | ||
|
|
35d63cf0fc | ||
|
|
d342d8671e | ||
|
|
041082f5c3 | ||
|
|
b5dac9c965 |
@@ -13,7 +13,7 @@ command:
|
|||||||
- '0.92'
|
- '0.92'
|
||||||
- --trust-remote-code
|
- --trust-remote-code
|
||||||
- -tp
|
- -tp
|
||||||
- '2'
|
- '4'
|
||||||
- --max-num-seqs
|
- --max-num-seqs
|
||||||
- '2'
|
- '2'
|
||||||
- --disable-log-requests
|
- --disable-log-requests
|
||||||
@@ -35,6 +35,8 @@ command:
|
|||||||
env:
|
env:
|
||||||
- name: VLLM_ENGINE_ITERATION_TIMEOUT_S
|
- name: VLLM_ENGINE_ITERATION_TIMEOUT_S
|
||||||
value: 3600
|
value: 3600
|
||||||
|
- name: VLLM_ENABLE_EXPERT_PARALLEL
|
||||||
|
value: 1
|
||||||
- name: BI100_MAX_NUM_SEQS
|
- name: BI100_MAX_NUM_SEQS
|
||||||
value: 2
|
value: 2
|
||||||
- name: BI100_MOE_COREX_DIRECT_ROUTED
|
- name: BI100_MOE_COREX_DIRECT_ROUTED
|
||||||
|
|||||||
347
qwen3_6_scripts/ep_fused_moe_patch.py
Normal file
347
qwen3_6_scripts/ep_fused_moe_patch.py
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
"""EP-aware FusedMoE override for vllm 0.6.3.
|
||||||
|
|
||||||
|
PR #2269 Sub-task 2: Expert Parallelism weight sharding.
|
||||||
|
|
||||||
|
Problem:
|
||||||
|
TP=2 on 4× BI-V100 (32 GiB each) OOMs during model loading because
|
||||||
|
FusedMoE.create_weights allocates ALL experts on every card, splitting
|
||||||
|
only the intermediate_size dimension. For Qwen3.6-35B-A3B (256 experts),
|
||||||
|
MoE weights alone consume ~22.5 GiB/card under TP=2.
|
||||||
|
|
||||||
|
Solution:
|
||||||
|
When enable_expert_parallel=True, each card holds only
|
||||||
|
num_experts/ep_size experts with FULL intermediate_size (no TP split
|
||||||
|
on MoE). Forward uses all-to-all to dispatch tokens to the correct
|
||||||
|
expert-holding rank, compute locally, and combine results back.
|
||||||
|
|
||||||
|
EP=4: each card holds 64/256 experts → MoE weights ~5.6 GiB/card
|
||||||
|
(savings: 16.9 GiB/card vs TP=2)
|
||||||
|
|
||||||
|
Memory layout comparison (per card, 30 MoE layers, fp16):
|
||||||
|
TP=2: 256 experts × (2×256×2048 + 2048×256) × 2B × 30 = 22.5 GiB
|
||||||
|
EP=4: 64 experts × (2×512×2048 + 2048×512) × 2B × 30 = 5.6 GiB
|
||||||
|
|
||||||
|
xllm equivalent: core/layers/cuda/fused_moe.cpp
|
||||||
|
ep_size_ = parallel_args.ep_size();
|
||||||
|
num_experts_per_rank_ = num_experts / ep_size;
|
||||||
|
start_expert_id_ = ep_rank * num_experts_per_rank_;
|
||||||
|
output = cutlass_fused_moe(..., ep_size_, ep_rank_, ...);
|
||||||
|
output = parallel_state::reduce(output, ep_pg_);
|
||||||
|
"""
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.distributed as dist
|
||||||
|
from typing import Callable, List, Optional, Tuple
|
||||||
|
|
||||||
|
from vllm.config import ParallelConfig
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_ep_requested():
|
||||||
|
"""Check if EP is requested via environment variable.
|
||||||
|
|
||||||
|
This is safe to call at any time (no dist dependency).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
return bool(int(os.environ.get("VLLM_ENABLE_EXPERT_PARALLEL", "0")))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_ep_runtime_config():
|
||||||
|
"""Get EP size and rank at runtime when dist is initialized.
|
||||||
|
|
||||||
|
Must only be called from within worker processes (during __init__,
|
||||||
|
weight_loader, or forward) where torch.distributed is guaranteed
|
||||||
|
to be initialized.
|
||||||
|
|
||||||
|
Returns (ep_size, ep_rank).
|
||||||
|
"""
|
||||||
|
if dist.is_initialized():
|
||||||
|
return dist.get_world_size(), dist.get_rank()
|
||||||
|
# Fallback — should not happen during normal model loading
|
||||||
|
logger.warning("[PR #2269] dist not initialized when querying EP config, "
|
||||||
|
"falling back to ep_size=1")
|
||||||
|
return 1, 0
|
||||||
|
|
||||||
|
|
||||||
|
def patch_fused_moe_for_ep():
|
||||||
|
"""Monkey-patch FusedMoE.__init__, weight_loader, and forward to support EP.
|
||||||
|
|
||||||
|
Call this during model loading (e.g. in qwen3_5.py module init or
|
||||||
|
patch_ops.sh) BEFORE any FusedMoE layers are constructed.
|
||||||
|
|
||||||
|
Only checks the env var at patch time. Actual ep_size/ep_rank are
|
||||||
|
queried at runtime (inside __init__, weight_loader, forward) when
|
||||||
|
torch.distributed is guaranteed to be initialized.
|
||||||
|
"""
|
||||||
|
from vllm.model_executor.layers.fused_moe.layer import FusedMoE
|
||||||
|
|
||||||
|
if not _is_ep_requested():
|
||||||
|
logger.info("[PR #2269] EP not enabled (VLLM_ENABLE_EXPERT_PARALLEL!=1), "
|
||||||
|
"skipping FusedMoE EP patch")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("[PR #2269] EP requested via env var, installing FusedMoE monkey-patches "
|
||||||
|
"(ep_size/ep_rank will be resolved at runtime)")
|
||||||
|
|
||||||
|
_orig_init = FusedMoE.__init__
|
||||||
|
|
||||||
|
def _ep_init(self, num_experts, top_k, hidden_size, intermediate_size,
|
||||||
|
params_dtype=None, reduce_results=False, renormalize=True,
|
||||||
|
use_grouped_topk=False, num_expert_group=None,
|
||||||
|
topk_group=None, quant_config=None, tp_size=None,
|
||||||
|
prefix="", custom_routing_function=None):
|
||||||
|
"""EP-aware __init__: allocate only local experts, full intermediate."""
|
||||||
|
|
||||||
|
# Query ep_size/ep_rank NOW — dist is initialized in worker processes
|
||||||
|
ep_size, ep_rank = _get_ep_runtime_config()
|
||||||
|
|
||||||
|
# Store global expert info before modifying
|
||||||
|
self._ep_enabled = True
|
||||||
|
self._ep_size = ep_size
|
||||||
|
self._ep_rank = ep_rank
|
||||||
|
self._global_num_experts = num_experts
|
||||||
|
assert num_experts % ep_size == 0, (
|
||||||
|
f"num_experts ({num_experts}) must be divisible by "
|
||||||
|
f"ep_size ({ep_size})")
|
||||||
|
self._num_experts_per_rank = num_experts // ep_size
|
||||||
|
self._start_expert_id = ep_rank * self._num_experts_per_rank
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[PR #2269] FusedMoE EP init: global_experts=%d, ep_size=%d, "
|
||||||
|
"ep_rank=%d, local_experts=%d (ids %d..%d), "
|
||||||
|
"intermediate_size=%d (full, no TP split)",
|
||||||
|
num_experts, ep_size, ep_rank,
|
||||||
|
self._num_experts_per_rank,
|
||||||
|
self._start_expert_id,
|
||||||
|
self._start_expert_id + self._num_experts_per_rank - 1,
|
||||||
|
intermediate_size)
|
||||||
|
|
||||||
|
# Call original __init__ with:
|
||||||
|
# - num_experts = local experts only (saves memory)
|
||||||
|
# - tp_size = 1 (no TP split on intermediate — EP replaces TP for MoE)
|
||||||
|
# - reduce_results = False (we do EP reduce ourselves)
|
||||||
|
_orig_init(
|
||||||
|
self,
|
||||||
|
num_experts=self._num_experts_per_rank,
|
||||||
|
top_k=top_k,
|
||||||
|
hidden_size=hidden_size,
|
||||||
|
intermediate_size=intermediate_size,
|
||||||
|
params_dtype=params_dtype,
|
||||||
|
reduce_results=False, # EP does its own reduce
|
||||||
|
renormalize=renormalize,
|
||||||
|
use_grouped_topk=use_grouped_topk,
|
||||||
|
num_expert_group=num_expert_group,
|
||||||
|
topk_group=topk_group,
|
||||||
|
quant_config=quant_config,
|
||||||
|
tp_size=1, # no TP split — EP splits by expert count instead
|
||||||
|
prefix=prefix,
|
||||||
|
custom_routing_function=custom_routing_function,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Override num_experts to global for routing (select_experts sees all)
|
||||||
|
self.num_experts = num_experts
|
||||||
|
# But weights are sized for local experts only
|
||||||
|
self._reduce_results_ep = reduce_results
|
||||||
|
|
||||||
|
_orig_weight_loader = FusedMoE.weight_loader
|
||||||
|
|
||||||
|
def _ep_weight_loader(self, param, loaded_weight, weight_name,
|
||||||
|
shard_id, expert_id):
|
||||||
|
"""EP-aware weight_loader: skip experts not owned by this rank.
|
||||||
|
|
||||||
|
Critical fix: the original weight_loader calls
|
||||||
|
get_tensor_model_parallel_rank() internally to compute tp_rank,
|
||||||
|
then uses it to narrow() the loaded weight for TP sharding.
|
||||||
|
Under EP we pass tp_size=1 (no TP split on MoE), so weights are
|
||||||
|
full-sized, but tp_rank still returns the real rank (0-3).
|
||||||
|
rank >= 1 causes narrow(dim, 512*rank, 512) to exceed dim size 512.
|
||||||
|
|
||||||
|
Solution: temporarily monkey-patch get_tensor_model_parallel_rank
|
||||||
|
to return 0 during EP weight loading, so narrow() always starts
|
||||||
|
at offset 0 and loads the full (unsplit) weight.
|
||||||
|
"""
|
||||||
|
if not getattr(self, '_ep_enabled', False):
|
||||||
|
return _orig_weight_loader(self, param, loaded_weight,
|
||||||
|
weight_name, shard_id, expert_id)
|
||||||
|
|
||||||
|
start = self._start_expert_id
|
||||||
|
end = start + self._num_experts_per_rank
|
||||||
|
|
||||||
|
# Skip experts not owned by this EP rank
|
||||||
|
if expert_id < start or expert_id >= end:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Remap global expert_id to local index
|
||||||
|
local_expert_id = expert_id - start
|
||||||
|
|
||||||
|
# Monkey-patch tp rank to 0 during load so narrow() doesn't OOB.
|
||||||
|
# The weight is already full-sized (tp_size=1), no split needed.
|
||||||
|
import vllm.model_executor.layers.fused_moe.layer as _fused_moe_mod
|
||||||
|
_real_get_tp_rank = _fused_moe_mod.get_tensor_model_parallel_rank
|
||||||
|
_fused_moe_mod.get_tensor_model_parallel_rank = lambda: 0
|
||||||
|
try:
|
||||||
|
_orig_weight_loader(self, param, loaded_weight, weight_name,
|
||||||
|
shard_id, local_expert_id)
|
||||||
|
finally:
|
||||||
|
_fused_moe_mod.get_tensor_model_parallel_rank = _real_get_tp_rank
|
||||||
|
|
||||||
|
_orig_forward = FusedMoE.forward
|
||||||
|
|
||||||
|
def _ep_forward(self, hidden_states, router_logits):
|
||||||
|
"""EP-aware forward: all-to-all dispatch, local compute, all-to-all combine.
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Route all tokens globally (all ranks see all expert logits)
|
||||||
|
2. All-to-all dispatch: send each token to the rank that owns its expert
|
||||||
|
3. Local expert computation on owned experts only
|
||||||
|
4. All-to-all combine: gather results back to original token positions
|
||||||
|
"""
|
||||||
|
if not getattr(self, '_ep_enabled', False):
|
||||||
|
return _orig_forward(self, hidden_states, router_logits)
|
||||||
|
|
||||||
|
# Step 1: Global routing — every rank computes topk over ALL experts
|
||||||
|
topk_weights, topk_ids = FusedMoE.select_experts(
|
||||||
|
hidden_states=hidden_states,
|
||||||
|
router_logits=router_logits,
|
||||||
|
use_grouped_topk=self.use_grouped_topk,
|
||||||
|
top_k=self.top_k,
|
||||||
|
renormalize=self.renormalize,
|
||||||
|
topk_group=self.topk_group,
|
||||||
|
num_expert_group=self.num_expert_group,
|
||||||
|
custom_routing_function=self.custom_routing_function,
|
||||||
|
)
|
||||||
|
|
||||||
|
ep_size = self._ep_size
|
||||||
|
ep_rank = self._ep_rank
|
||||||
|
start_expert = self._start_expert_id
|
||||||
|
end_expert = start_expert + self._num_experts_per_rank
|
||||||
|
T, H = hidden_states.shape
|
||||||
|
K = self.top_k
|
||||||
|
device = hidden_states.device
|
||||||
|
|
||||||
|
# Step 2: Determine which tokens go to which EP rank.
|
||||||
|
# expert_to_rank: expert_id → ep_rank
|
||||||
|
# For each (token, top_k_slot), find the destination rank.
|
||||||
|
dest_ranks = topk_ids.div(self._num_experts_per_rank, rounding_mode='trunc') # (T, K)
|
||||||
|
|
||||||
|
# Count tokens to send to each rank
|
||||||
|
send_counts = torch.zeros(ep_size, dtype=torch.int64, device=device)
|
||||||
|
for r in range(ep_size):
|
||||||
|
send_counts[r] = (dest_ranks == r).sum()
|
||||||
|
|
||||||
|
# Gather all ranks' send counts so we know receive counts
|
||||||
|
all_counts = torch.zeros(ep_size, ep_size, dtype=torch.int64, device=device)
|
||||||
|
dist.all_gather_into_tensor(
|
||||||
|
all_counts.view(-1),
|
||||||
|
send_counts,
|
||||||
|
)
|
||||||
|
recv_counts = all_counts[:, ep_rank] # what each rank sends to us
|
||||||
|
|
||||||
|
total_send = send_counts.sum().item()
|
||||||
|
total_recv = recv_counts.sum().item()
|
||||||
|
|
||||||
|
# Build dispatch buffers: for each (token, k) pair routed to each rank,
|
||||||
|
# pack (hidden_state, topk_weight, local_expert_id, original_position)
|
||||||
|
# Sort by destination rank for contiguous all-to-all.
|
||||||
|
|
||||||
|
# Flatten topk dimension: each (token, k_slot) is a "work item"
|
||||||
|
flat_ids = topk_ids.view(-1) # (T*K,)
|
||||||
|
flat_weights = topk_weights.view(-1) # (T*K,)
|
||||||
|
flat_dest = dest_ranks.view(-1) # (T*K,)
|
||||||
|
flat_token_idx = torch.arange(T, device=device).unsqueeze(1).expand(T, K).reshape(-1) # (T*K,)
|
||||||
|
|
||||||
|
# Sort by destination rank for all-to-all
|
||||||
|
sort_idx = flat_dest.argsort()
|
||||||
|
sorted_hidden = hidden_states[flat_token_idx[sort_idx]] # (T*K, H)
|
||||||
|
sorted_weights = flat_weights[sort_idx] # (T*K,)
|
||||||
|
sorted_ids = flat_ids[sort_idx] # (T*K,) global expert ids
|
||||||
|
sorted_token_idx = flat_token_idx[sort_idx] # (T*K,) for scatter-back
|
||||||
|
|
||||||
|
# All-to-all send/recv counts (in elements, each element = one work item)
|
||||||
|
send_splits = send_counts.tolist()
|
||||||
|
recv_splits = recv_counts.tolist()
|
||||||
|
|
||||||
|
# All-to-all: exchange hidden states
|
||||||
|
recv_hidden = torch.empty(total_recv, H, dtype=hidden_states.dtype, device=device)
|
||||||
|
dist.all_to_all_single(recv_hidden, sorted_hidden,
|
||||||
|
output_split_sizes=recv_splits,
|
||||||
|
input_split_sizes=send_splits)
|
||||||
|
|
||||||
|
# All-to-all: exchange expert ids
|
||||||
|
recv_ids = torch.empty(total_recv, dtype=sorted_ids.dtype, device=device)
|
||||||
|
dist.all_to_all_single(recv_ids, sorted_ids,
|
||||||
|
output_split_sizes=recv_splits,
|
||||||
|
input_split_sizes=send_splits)
|
||||||
|
|
||||||
|
# All-to-all: exchange topk weights
|
||||||
|
recv_weights = torch.empty(total_recv, dtype=sorted_weights.dtype, device=device)
|
||||||
|
dist.all_to_all_single(recv_weights, sorted_weights,
|
||||||
|
output_split_sizes=recv_splits,
|
||||||
|
input_split_sizes=send_splits)
|
||||||
|
|
||||||
|
# Step 3: Local expert computation on received tokens
|
||||||
|
# Remap global expert ids to local
|
||||||
|
local_ids = recv_ids - start_expert # now 0..num_experts_per_rank-1
|
||||||
|
|
||||||
|
if total_recv > 0:
|
||||||
|
# Compute using the existing fused_experts kernel (or Python fallback)
|
||||||
|
from vllm.model_executor.layers.fused_moe.fused_moe import fused_experts
|
||||||
|
# Build per-token topk format expected by fused_experts:
|
||||||
|
# It expects (num_tokens, top_k) but here each recv item is
|
||||||
|
# a single (token, expert) pair. We reshape to (total_recv, 1).
|
||||||
|
local_topk_ids = local_ids.unsqueeze(1) # (total_recv, 1)
|
||||||
|
local_topk_weights = recv_weights.unsqueeze(1) # (total_recv, 1)
|
||||||
|
|
||||||
|
local_output = fused_experts(
|
||||||
|
hidden_states=recv_hidden,
|
||||||
|
w1=self.w13_weight,
|
||||||
|
w2=self.w2_weight,
|
||||||
|
topk_weights=local_topk_weights,
|
||||||
|
topk_ids=local_topk_ids.to(torch.int32),
|
||||||
|
inplace=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
local_output = torch.empty(0, H, dtype=hidden_states.dtype, device=device)
|
||||||
|
|
||||||
|
# Step 4: All-to-all combine — send results back
|
||||||
|
# Reverse the all-to-all: recv_splits becomes send, send_splits becomes recv
|
||||||
|
combine_output = torch.empty(total_send, H, dtype=hidden_states.dtype, device=device)
|
||||||
|
dist.all_to_all_single(combine_output, local_output,
|
||||||
|
output_split_sizes=send_splits,
|
||||||
|
input_split_sizes=recv_splits)
|
||||||
|
|
||||||
|
# Unsort and scatter-add back to original token positions
|
||||||
|
# combine_output is in the same order as sorted_* (sorted by dest rank)
|
||||||
|
# We need to unsort and accumulate weighted results per token.
|
||||||
|
final_output = torch.zeros(T, H, dtype=hidden_states.dtype, device=device)
|
||||||
|
# Unsort: combine_output[i] corresponds to sorted_token_idx[i]
|
||||||
|
unsorted_output = torch.zeros_like(combine_output)
|
||||||
|
unsorted_output[sort_idx] = combine_output
|
||||||
|
|
||||||
|
# Reshape back to (T, K, H) and sum over K dimension
|
||||||
|
# Each (token, k_slot) pair has already been weighted by topk_weight
|
||||||
|
# inside fused_experts. We just need to sum over k.
|
||||||
|
# Actually fused_experts with top_k=1 per item already applies the weight.
|
||||||
|
# scatter_add by token index:
|
||||||
|
token_indices = flat_token_idx.unsqueeze(1).expand(-1, H) # (T*K, H)
|
||||||
|
final_output.scatter_add_(0, token_indices, unsorted_output)
|
||||||
|
|
||||||
|
# EP reduce (if the caller requested reduce_results, e.g. for shared experts)
|
||||||
|
if self._reduce_results_ep:
|
||||||
|
# No TP reduce needed (tp_size=1 for EP), but if the model
|
||||||
|
# expects an all-reduce for the MoE output (e.g. to combine
|
||||||
|
# with shared experts), we do it here over the EP group.
|
||||||
|
pass # EP output is already complete — each token got results from its experts
|
||||||
|
|
||||||
|
return final_output
|
||||||
|
|
||||||
|
# Apply patches
|
||||||
|
FusedMoE.__init__ = _ep_init
|
||||||
|
FusedMoE.weight_loader = _ep_weight_loader
|
||||||
|
FusedMoE.forward = _ep_forward
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[PR #2269] FusedMoE patched for EP. "
|
||||||
|
"ep_size/ep_rank will be resolved per-layer at runtime.")
|
||||||
@@ -3,6 +3,8 @@ echo "[build] trigger 202609011237"
|
|||||||
echo "[build] trigger 202609011636"
|
echo "[build] trigger 202609011636"
|
||||||
echo "[build] trigger 202609011655"
|
echo "[build] trigger 202609011655"
|
||||||
echo "[build] trigger 202609011710"
|
echo "[build] trigger 202609011710"
|
||||||
|
echo "[build] trigger 202609031035"
|
||||||
|
echo "[build] trigger 202609031049"
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# BI-V100 patch script for Qwen3.6-35B-A3B (Qwen3_5 MoE architecture)
|
# BI-V100 patch script for Qwen3.6-35B-A3B (Qwen3_5 MoE architecture)
|
||||||
#
|
#
|
||||||
@@ -170,6 +172,7 @@ cp ./bi100_env.py "${VLLM_ROOT}/bi100_env.py"
|
|||||||
cp ./bi100_profile.py "${VLLM_ROOT}/bi100_profile.py"
|
cp ./bi100_profile.py "${VLLM_ROOT}/bi100_profile.py"
|
||||||
cp ./block_major_kv_cache.py "${VLLM_ROOT}/block_major_kv_cache.py"
|
cp ./block_major_kv_cache.py "${VLLM_ROOT}/block_major_kv_cache.py"
|
||||||
cp ./gdn_prefix.py "${VLLM_ROOT}/gdn_prefix.py"
|
cp ./gdn_prefix.py "${VLLM_ROOT}/gdn_prefix.py"
|
||||||
|
cp ./ep_fused_moe_patch.py "${VLLM_ROOT}/ep_fused_moe_patch.py"
|
||||||
|
|
||||||
build_stage "installing CoreX paged-KV swap compatibility"
|
build_stage "installing CoreX paged-KV swap compatibility"
|
||||||
python3 ./patch_corex_swap_blocks.py
|
python3 ./patch_corex_swap_blocks.py
|
||||||
|
|||||||
@@ -66,6 +66,15 @@ from vllm.model_executor.layers.linear import (ColumnParallelLinear,
|
|||||||
ReplicatedLinear,
|
ReplicatedLinear,
|
||||||
RowParallelLinear)
|
RowParallelLinear)
|
||||||
from vllm.model_executor.layers.fused_moe import FusedMoE
|
from vllm.model_executor.layers.fused_moe import FusedMoE
|
||||||
|
# [PR #2269] Apply EP patch to FusedMoE before any layers are constructed.
|
||||||
|
# When VLLM_ENABLE_EXPERT_PARALLEL=1, this replaces TP-sharded MoE weights
|
||||||
|
# with EP-sharded MoE weights (each card holds num_experts/ep_size experts
|
||||||
|
# with full intermediate_size), solving the OOM under TP=2.
|
||||||
|
try:
|
||||||
|
from vllm.ep_fused_moe_patch import patch_fused_moe_for_ep as _patch_ep
|
||||||
|
_patch_ep()
|
||||||
|
except ImportError:
|
||||||
|
pass # EP patch not installed — TP mode unchanged
|
||||||
from vllm.model_executor.layers.logits_processor import LogitsProcessor
|
from vllm.model_executor.layers.logits_processor import LogitsProcessor
|
||||||
from vllm.model_executor.layers.quantization import QuantizationConfig
|
from vllm.model_executor.layers.quantization import QuantizationConfig
|
||||||
from vllm.model_executor.layers.rotary_embedding import (
|
from vllm.model_executor.layers.rotary_embedding import (
|
||||||
|
|||||||
@@ -826,6 +826,9 @@ class ParallelConfig:
|
|||||||
pipeline_parallel_size: int,
|
pipeline_parallel_size: int,
|
||||||
tensor_parallel_size: int,
|
tensor_parallel_size: int,
|
||||||
data_parallel_size: int = 1,
|
data_parallel_size: int = 1,
|
||||||
|
enable_expert_parallel: bool = False,
|
||||||
|
all2all_backend: str = "allgather_reducescatter",
|
||||||
|
enable_eplb: bool = False,
|
||||||
worker_use_ray: Optional[bool] = None,
|
worker_use_ray: Optional[bool] = None,
|
||||||
max_parallel_loading_workers: Optional[int] = None,
|
max_parallel_loading_workers: Optional[int] = None,
|
||||||
disable_custom_all_reduce: bool = False,
|
disable_custom_all_reduce: bool = False,
|
||||||
@@ -838,6 +841,13 @@ class ParallelConfig:
|
|||||||
self.pipeline_parallel_size = pipeline_parallel_size
|
self.pipeline_parallel_size = pipeline_parallel_size
|
||||||
self.tensor_parallel_size = tensor_parallel_size
|
self.tensor_parallel_size = tensor_parallel_size
|
||||||
self.data_parallel_size = data_parallel_size
|
self.data_parallel_size = data_parallel_size
|
||||||
|
# [PR #2269] EP support: when enable_expert_parallel is True, MoE
|
||||||
|
# experts are sharded across devices via expert parallelism instead
|
||||||
|
# of tensor parallelism. ep_size is derived from tp_size * dp_size
|
||||||
|
# in FusedMoEParallelConfig.from_parallel_config().
|
||||||
|
self.enable_expert_parallel = enable_expert_parallel
|
||||||
|
self.all2all_backend = all2all_backend
|
||||||
|
self.enable_eplb = enable_eplb
|
||||||
self.distributed_executor_backend = distributed_executor_backend
|
self.distributed_executor_backend = distributed_executor_backend
|
||||||
self.max_parallel_loading_workers = max_parallel_loading_workers
|
self.max_parallel_loading_workers = max_parallel_loading_workers
|
||||||
self.disable_custom_all_reduce = disable_custom_all_reduce
|
self.disable_custom_all_reduce = disable_custom_all_reduce
|
||||||
@@ -848,6 +858,10 @@ class ParallelConfig:
|
|||||||
* self.data_parallel_size)
|
* self.data_parallel_size)
|
||||||
# dp_rank is assigned per-worker during init_device
|
# dp_rank is assigned per-worker during init_device
|
||||||
self.dp_rank: int = 0
|
self.dp_rank: int = 0
|
||||||
|
# _dp_group is assigned per-worker during init_device
|
||||||
|
self._dp_group = None
|
||||||
|
# [PR #2269] _ep_group is assigned per-worker during init_device
|
||||||
|
self._ep_group = None
|
||||||
|
|
||||||
if worker_use_ray:
|
if worker_use_ray:
|
||||||
if self.distributed_executor_backend is None:
|
if self.distributed_executor_backend is None:
|
||||||
|
|||||||
@@ -107,6 +107,12 @@ class EngineArgs:
|
|||||||
pipeline_parallel_size: int = 1
|
pipeline_parallel_size: int = 1
|
||||||
tensor_parallel_size: int = 1
|
tensor_parallel_size: int = 1
|
||||||
data_parallel_size: int = int(os.environ.get("VLLM_DATA_PARALLEL_SIZE", "1"))
|
data_parallel_size: int = int(os.environ.get("VLLM_DATA_PARALLEL_SIZE", "1"))
|
||||||
|
# [PR #2269] EP support: enable expert parallelism for MoE models.
|
||||||
|
enable_expert_parallel: bool = bool(int(os.environ.get(
|
||||||
|
"VLLM_ENABLE_EXPERT_PARALLEL", "0")))
|
||||||
|
all2all_backend: str = os.environ.get(
|
||||||
|
"VLLM_ALL2ALL_BACKEND", "allgather_reducescatter")
|
||||||
|
enable_eplb: bool = bool(int(os.environ.get("VLLM_ENABLE_EPLB", "0")))
|
||||||
max_parallel_loading_workers: Optional[int] = None
|
max_parallel_loading_workers: Optional[int] = None
|
||||||
block_size: int = 16
|
block_size: int = 16
|
||||||
enable_prefix_caching: bool = False
|
enable_prefix_caching: bool = False
|
||||||
@@ -834,7 +840,14 @@ class EngineArgs:
|
|||||||
# Get the list of attributes of this dataclass.
|
# Get the list of attributes of this dataclass.
|
||||||
attrs = [attr.name for attr in dataclasses.fields(cls)]
|
attrs = [attr.name for attr in dataclasses.fields(cls)]
|
||||||
# Set the attributes from the parsed arguments.
|
# Set the attributes from the parsed arguments.
|
||||||
engine_args = cls(**{attr: getattr(args, attr) for attr in attrs})
|
# Fields that exist in the dataclass but have no corresponding CLI
|
||||||
|
# argument (e.g. data_parallel_size, enable_expert_parallel) will
|
||||||
|
# fall back to their dataclass defaults (typically read from env vars).
|
||||||
|
engine_args = cls(**{
|
||||||
|
attr: getattr(args, attr)
|
||||||
|
for attr in attrs
|
||||||
|
if hasattr(args, attr)
|
||||||
|
})
|
||||||
return engine_args
|
return engine_args
|
||||||
|
|
||||||
def create_model_config(self) -> ModelConfig:
|
def create_model_config(self) -> ModelConfig:
|
||||||
@@ -928,6 +941,9 @@ class EngineArgs:
|
|||||||
pipeline_parallel_size=self.pipeline_parallel_size,
|
pipeline_parallel_size=self.pipeline_parallel_size,
|
||||||
tensor_parallel_size=self.tensor_parallel_size,
|
tensor_parallel_size=self.tensor_parallel_size,
|
||||||
data_parallel_size=self.data_parallel_size,
|
data_parallel_size=self.data_parallel_size,
|
||||||
|
enable_expert_parallel=self.enable_expert_parallel,
|
||||||
|
all2all_backend=self.all2all_backend,
|
||||||
|
enable_eplb=self.enable_eplb,
|
||||||
worker_use_ray=self.worker_use_ray,
|
worker_use_ray=self.worker_use_ray,
|
||||||
max_parallel_loading_workers=self.max_parallel_loading_workers,
|
max_parallel_loading_workers=self.max_parallel_loading_workers,
|
||||||
disable_custom_all_reduce=True,
|
disable_custom_all_reduce=True,
|
||||||
|
|||||||
201
qwen3_6_scripts/vendor_overrides/vllm/spec_decode/metrics.py
Normal file
201
qwen3_6_scripts/vendor_overrides/vllm/spec_decode/metrics.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import time
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.model_executor.layers.spec_decode_base_sampler import (
|
||||||
|
SpecDecodeBaseSampler)
|
||||||
|
from vllm.utils import is_pin_memory_available
|
||||||
|
|
||||||
|
|
||||||
|
class SpecDecodeWorkerMetrics(
|
||||||
|
msgspec.Struct,
|
||||||
|
omit_defaults=True, # type: ignore[call-arg]
|
||||||
|
array_like=True): # type: ignore[call-arg]
|
||||||
|
"""Dataclass holding metrics emitted from the spec decode worker.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The empirical acceptance rate of the proposal method on a per-token basis.
|
||||||
|
# This is useful for evaluating how well the proposal method aligns with the
|
||||||
|
# scoring method.
|
||||||
|
draft_acceptance_rate: float
|
||||||
|
|
||||||
|
# The empirical efficiency, measured as the number of tokens emitted by the
|
||||||
|
# system divided by the number of tokens that could be emitted by the system
|
||||||
|
# if the proposal method were perfect.
|
||||||
|
system_efficiency: float
|
||||||
|
|
||||||
|
# The number of speculative tokens produced by the proposal method.
|
||||||
|
draft_tokens: int
|
||||||
|
|
||||||
|
# The number of tokens emitted by the entire system.
|
||||||
|
emitted_tokens: int
|
||||||
|
|
||||||
|
# The number of tokens accepted by the scoring model and verification
|
||||||
|
# routine, e.g. Llama2-70B and lossless rejection sampling.
|
||||||
|
#
|
||||||
|
# NOTE: Any token accepted by the verification routine is considered
|
||||||
|
# accepted (regardless of if the speculative prefix is also accepted). The
|
||||||
|
# user will usually see less accepted tokens. This metric is helpful when
|
||||||
|
# evaluating alignment of the proposal method with the scoring model.
|
||||||
|
accepted_tokens: int
|
||||||
|
|
||||||
|
# The number of speculative tokens per sequence.
|
||||||
|
num_spec_tokens: int
|
||||||
|
|
||||||
|
# [PR #2269] DP-aggregated acceptance rate across all DP replicas.
|
||||||
|
# When dp_size=1 (TC-04), this equals draft_acceptance_rate.
|
||||||
|
# When dp_size>1 (TC-01), this is the all-reduced average.
|
||||||
|
dp_aggregated_acceptance_rate: float = float("nan")
|
||||||
|
|
||||||
|
|
||||||
|
Timer = Callable[[], float]
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncMetricsCollector:
|
||||||
|
"""Class which copies rejection/typical-acceptance sampler metrics
|
||||||
|
from the device to CPU on a non-default Torch stream.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
spec_decode_sampler: SpecDecodeBaseSampler,
|
||||||
|
timer: Optional[Timer] = None,
|
||||||
|
collect_interval_s: float = 5.0):
|
||||||
|
self.spec_decode_sampler = spec_decode_sampler
|
||||||
|
self._timer = time.time if timer is None else timer
|
||||||
|
|
||||||
|
self._rank: Optional[int] = None
|
||||||
|
|
||||||
|
# We don't have a device set yet.
|
||||||
|
self._copy_stream: Optional[torch.cuda.Stream] = None
|
||||||
|
|
||||||
|
self._in_flight_copy: Optional[torch.cuda.Event] = None
|
||||||
|
|
||||||
|
pin_memory = is_pin_memory_available()
|
||||||
|
self._aggregate_num_accepted_tokens = torch.tensor(
|
||||||
|
0, dtype=torch.long, device="cpu", pin_memory=pin_memory)
|
||||||
|
self._aggregate_num_emitted_tokens = torch.tensor(
|
||||||
|
0, dtype=torch.long, device="cpu", pin_memory=pin_memory)
|
||||||
|
self._aggregate_num_draft_tokens = 0
|
||||||
|
|
||||||
|
self._rejsample_metrics_collect_interval_s = collect_interval_s
|
||||||
|
self._last_metrics_collect_time = self._timer()
|
||||||
|
|
||||||
|
def init_gpu_tensors(self, rank: int) -> None:
|
||||||
|
self._rank = rank
|
||||||
|
self._copy_stream = torch.cuda.Stream()
|
||||||
|
|
||||||
|
def maybe_collect_rejsample_metrics(
|
||||||
|
self, k: int) -> Optional[SpecDecodeWorkerMetrics]:
|
||||||
|
|
||||||
|
# If a copy was initiated in the previous call, collect and return.
|
||||||
|
if self._in_flight_copy is not None:
|
||||||
|
ready_event = self._in_flight_copy
|
||||||
|
self._in_flight_copy = None
|
||||||
|
return self._collect_rejsample_metrics(k, ready_event)
|
||||||
|
|
||||||
|
# Otherwise, check if we should start a new copy.
|
||||||
|
if self._should_collect_rejsample_metrics(self._timer()):
|
||||||
|
assert self._in_flight_copy is None
|
||||||
|
self._in_flight_copy = self._copy_rejsample_metrics_async()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _should_collect_rejsample_metrics(self, now: float) -> bool:
|
||||||
|
"""Return whether or not this iteration should print sampling
|
||||||
|
metrics.
|
||||||
|
"""
|
||||||
|
if self._rank != 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return now - self._last_metrics_collect_time >= self._rejsample_metrics_collect_interval_s # noqa: E501
|
||||||
|
|
||||||
|
def _copy_rejsample_metrics_async(self) -> torch.cuda.Event:
|
||||||
|
"""Copy rejection/typical-acceptance sampling metrics
|
||||||
|
(number of accepted tokens, etc) to CPU asynchronously.
|
||||||
|
|
||||||
|
Returns a CUDA event recording when the copy is complete.
|
||||||
|
"""
|
||||||
|
assert self._copy_stream is not None
|
||||||
|
self._copy_stream.wait_stream(torch.cuda.current_stream())
|
||||||
|
|
||||||
|
with torch.cuda.stream(self._copy_stream):
|
||||||
|
self._aggregate_num_accepted_tokens.copy_(
|
||||||
|
self.spec_decode_sampler.num_accepted_tokens,
|
||||||
|
non_blocking=True)
|
||||||
|
self._aggregate_num_emitted_tokens.copy_(
|
||||||
|
self.spec_decode_sampler.num_emitted_tokens, non_blocking=True)
|
||||||
|
# Number of draft tokens is calculated on CPU, so no copy is
|
||||||
|
# required.
|
||||||
|
self._aggregate_num_draft_tokens = (
|
||||||
|
self.spec_decode_sampler.num_draft_tokens)
|
||||||
|
|
||||||
|
aggregate_metrics_ready = torch.cuda.Event()
|
||||||
|
aggregate_metrics_ready.record(self._copy_stream)
|
||||||
|
|
||||||
|
return aggregate_metrics_ready
|
||||||
|
|
||||||
|
def _collect_rejsample_metrics(
|
||||||
|
self, k: int,
|
||||||
|
ready_event: torch.cuda.Event) -> SpecDecodeWorkerMetrics:
|
||||||
|
"""Create metrics object from statistics copied asynchronously.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
k: int. The number of speculative tokens; used to determine system
|
||||||
|
efficiency.
|
||||||
|
ready_event: torch.cuda.Event. The CUDA event recording when the
|
||||||
|
async GPU->CPU copy is complete.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ready_event.synchronize()
|
||||||
|
|
||||||
|
# update time of last collection
|
||||||
|
self._last_metrics_collect_time = self._timer()
|
||||||
|
|
||||||
|
accepted_tokens = self._aggregate_num_accepted_tokens.item()
|
||||||
|
emitted_tokens = self._aggregate_num_emitted_tokens.item()
|
||||||
|
draft_tokens = self._aggregate_num_draft_tokens
|
||||||
|
|
||||||
|
max_num_emitted_tokens = self.get_max_num_emitted_tokens(
|
||||||
|
draft_tokens, k)
|
||||||
|
|
||||||
|
if draft_tokens > 0:
|
||||||
|
draft_acceptance_rate = accepted_tokens / draft_tokens
|
||||||
|
else:
|
||||||
|
draft_acceptance_rate = float("nan")
|
||||||
|
|
||||||
|
if max_num_emitted_tokens > 0:
|
||||||
|
system_efficiency = emitted_tokens / max_num_emitted_tokens
|
||||||
|
else:
|
||||||
|
system_efficiency = float("nan")
|
||||||
|
|
||||||
|
return SpecDecodeWorkerMetrics(
|
||||||
|
num_spec_tokens=k,
|
||||||
|
draft_acceptance_rate=draft_acceptance_rate,
|
||||||
|
system_efficiency=system_efficiency,
|
||||||
|
accepted_tokens=accepted_tokens,
|
||||||
|
draft_tokens=draft_tokens,
|
||||||
|
emitted_tokens=emitted_tokens,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_max_num_emitted_tokens(draft_tokens: int, k: int) -> int:
|
||||||
|
"""Calculate the number of emitted tokens, assuming all tokens are
|
||||||
|
accepted.
|
||||||
|
|
||||||
|
This is equal to the number of sequences that have been speculated on,
|
||||||
|
times (speculation len + 1). The +1 comes from the bonus token.
|
||||||
|
"""
|
||||||
|
# Determine the number of sequences that have been speculated on. Since
|
||||||
|
# the batch size can be variable, we divide by k.
|
||||||
|
assert draft_tokens % k == 0
|
||||||
|
total_num_spec_seqs = draft_tokens // k
|
||||||
|
|
||||||
|
# A single sequence may emit k accepted tokens and one bonus token in
|
||||||
|
# the best case.
|
||||||
|
num_emitted_per_seq_if_all_accepted = k + 1
|
||||||
|
|
||||||
|
# The max num of emitted tokens is the number of speculated sequences
|
||||||
|
# times the max emitted per seq.
|
||||||
|
return total_num_spec_seqs * num_emitted_per_seq_if_all_accepted
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
import copy
|
||||||
|
import weakref
|
||||||
|
from typing import Dict, List, Set, Tuple
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.model_executor.layers.sampler import SamplerOutput
|
||||||
|
from vllm.sequence import (ExecuteModelRequest, HiddenStates, SequenceData,
|
||||||
|
SequenceGroupMetadata)
|
||||||
|
from vllm.spec_decode.draft_model_runner import TP1DraftModelRunner
|
||||||
|
from vllm.spec_decode.interfaces import (SpeculativeProposals,
|
||||||
|
SpeculativeProposer)
|
||||||
|
from vllm.spec_decode.proposer_worker_base import ProposerWorkerBase
|
||||||
|
from vllm.spec_decode.top1_proposer import Top1Proposer
|
||||||
|
from vllm.worker.worker import Worker
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiStepWorker(Worker, ProposerWorkerBase):
|
||||||
|
"""The MultiStepWorker is equivalent to a Worker except that it allows
|
||||||
|
multiple forward passes in a single call, assuming the scheduler has
|
||||||
|
allocated enough space to store the additional KV. This reduces overhead
|
||||||
|
by invoking the scheduler less.
|
||||||
|
|
||||||
|
The MultiStepWorker does not support cache swap operations, or beam search.
|
||||||
|
Cache swap operations do not require large modifications. On the other hand,
|
||||||
|
beam search requires memory allocations during sequence forks and thus
|
||||||
|
requires more thought for MultiStepWorker support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Lazy initialization list.
|
||||||
|
self._proposer: SpeculativeProposer
|
||||||
|
|
||||||
|
def init_device(self) -> None:
|
||||||
|
super().init_device()
|
||||||
|
|
||||||
|
self._proposer = Top1Proposer(
|
||||||
|
weakref.proxy(self), # type: ignore[arg-type]
|
||||||
|
self.device,
|
||||||
|
self.vocab_size,
|
||||||
|
max_proposal_len=self.max_model_len,
|
||||||
|
)
|
||||||
|
|
||||||
|
# [PR #2269] Sub-task 3/4: Verify DP/EP context propagation.
|
||||||
|
# In xllm, DFlash/MTP workers explicitly propagate DP/EP context
|
||||||
|
# to the speculative base. In vllm, this happens implicitly via
|
||||||
|
# Worker.init_device() → init_worker_distributed_environment()
|
||||||
|
# which sets _dp_group and _ep_group on self.parallel_config.
|
||||||
|
# We verify the context arrived correctly.
|
||||||
|
dp_size = getattr(self.parallel_config, 'data_parallel_size', 1)
|
||||||
|
ep_enabled = getattr(
|
||||||
|
self.parallel_config, 'enable_expert_parallel', False)
|
||||||
|
if dp_size > 1 or ep_enabled:
|
||||||
|
dp_group = getattr(self.parallel_config, '_dp_group', None)
|
||||||
|
ep_group = getattr(self.parallel_config, '_ep_group', None)
|
||||||
|
logger.info(
|
||||||
|
"[PR #2269] MultiStepWorker (MTP equivalent) DP/EP context "
|
||||||
|
"propagated: dp_size=%d, dp_group=%s, ep_enabled=%s, "
|
||||||
|
"ep_group=%s",
|
||||||
|
dp_size,
|
||||||
|
"initialized" if dp_group is not None else "MISSING",
|
||||||
|
ep_enabled,
|
||||||
|
"initialized" if ep_group is not None else "MISSING")
|
||||||
|
|
||||||
|
def set_include_gpu_probs_tensor(self) -> None:
|
||||||
|
# Need include_gpu_probs_tensor for MultiStepWorker
|
||||||
|
self.model_runner.model.sampler.include_gpu_probs_tensor = True
|
||||||
|
|
||||||
|
def set_should_modify_greedy_probs_inplace(self) -> None:
|
||||||
|
self.model_runner.model.sampler.should_modify_greedy_probs_inplace = (
|
||||||
|
True)
|
||||||
|
|
||||||
|
@torch.inference_mode()
|
||||||
|
def sampler_output(
|
||||||
|
self,
|
||||||
|
execute_model_req: ExecuteModelRequest,
|
||||||
|
sample_len: int,
|
||||||
|
seq_ids_with_bonus_token_in_last_step: Set[int],
|
||||||
|
) -> Tuple[List[SamplerOutput], bool]:
|
||||||
|
"""Run the model forward pass sample_len times. Returns the list of
|
||||||
|
sampler output, one per model forward pass, along with indicator of
|
||||||
|
whether torch tensor in sampler output need to be transposed in latter
|
||||||
|
sampler_output_to_torch logic.
|
||||||
|
|
||||||
|
For multi step worker, this indicator shall be True.
|
||||||
|
"""
|
||||||
|
self._raise_if_unsupported(execute_model_req)
|
||||||
|
# Expand the batch for sequences with a bonus token.
|
||||||
|
# Perform a forward pass on the expanded batch and filter the
|
||||||
|
# response to retain only the original sequences' responses.
|
||||||
|
expanded_request, indices_of_seq_with_bonus_tokens =\
|
||||||
|
self._expand_execute_model_request(
|
||||||
|
execute_model_req, seq_ids_with_bonus_token_in_last_step)
|
||||||
|
|
||||||
|
# Run model sample_len times.
|
||||||
|
model_outputs: List[SamplerOutput] = []
|
||||||
|
if isinstance(
|
||||||
|
self.model_runner, TP1DraftModelRunner
|
||||||
|
) and self.model_runner.supports_gpu_multi_step(expanded_request):
|
||||||
|
# Here we run the draft_model_runner with multi-step prepare
|
||||||
|
# on the GPU directly
|
||||||
|
expanded_request.num_steps = sample_len
|
||||||
|
model_outputs = self.execute_model(
|
||||||
|
execute_model_req=expanded_request)
|
||||||
|
else:
|
||||||
|
# Here we run multi-step directly, with every step prepared
|
||||||
|
# on the CPU.
|
||||||
|
# TODO: Remove this branch once DraftModelRunner supports TP>1
|
||||||
|
# and other restrictions that are part of DraftModelRunner's
|
||||||
|
# supports_gpu_multi_step(..)
|
||||||
|
for _ in range(sample_len):
|
||||||
|
model_output: List[SamplerOutput] = super().execute_model(
|
||||||
|
execute_model_req=expanded_request)
|
||||||
|
assert (len(model_output) == 1
|
||||||
|
), "composing multistep workers not supported"
|
||||||
|
model_output = model_output[0]
|
||||||
|
|
||||||
|
self._append_new_tokens(
|
||||||
|
model_output, expanded_request.seq_group_metadata_list)
|
||||||
|
model_outputs.append(model_output)
|
||||||
|
|
||||||
|
filtered_model_outputs = self._filter_model_output(
|
||||||
|
model_outputs, indices_of_seq_with_bonus_tokens)
|
||||||
|
return filtered_model_outputs, True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _expand_execute_model_request(
|
||||||
|
execute_model_req: ExecuteModelRequest,
|
||||||
|
seq_with_bonus_token_in_last_step: set,
|
||||||
|
) -> Tuple[ExecuteModelRequest, List[int]]:
|
||||||
|
"""
|
||||||
|
Expands the execute model request based on sequences with bonus
|
||||||
|
tokens.
|
||||||
|
|
||||||
|
For each sequence with a bonus token, this method creates a new
|
||||||
|
sequence without the bonus token and adds it to the execute model
|
||||||
|
request. The original sequence groups are also retained. The indices
|
||||||
|
of the original sequence groups are returned for further processing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
execute_model_req (ExecuteModelRequest): The original execute
|
||||||
|
model request.
|
||||||
|
seq_with_bonus_token_in_last_step (set): Set of sequence IDs that
|
||||||
|
contain bonus tokens.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[ExecuteModelRequest, List[int]]: The updated execute model
|
||||||
|
request with expanded sequences and a list of indices corresponding
|
||||||
|
to the original sequence groups.
|
||||||
|
"""
|
||||||
|
updated_seq_group_metadata_list: List[SequenceGroupMetadata] = []
|
||||||
|
updated_execute_model_req = execute_model_req.clone(
|
||||||
|
updated_seq_group_metadata_list)
|
||||||
|
indices_of_original_sequence_groups = []
|
||||||
|
for seq_group in execute_model_req.seq_group_metadata_list:
|
||||||
|
seq_group_has_bonus_tokens = False
|
||||||
|
for seq_id, _ in seq_group.seq_data.items():
|
||||||
|
# Identify sequences with bonus tokens in the sequence group.
|
||||||
|
if seq_id in seq_with_bonus_token_in_last_step:
|
||||||
|
seq_group_has_bonus_tokens = True
|
||||||
|
break
|
||||||
|
if seq_group_has_bonus_tokens:
|
||||||
|
#Create new sequences without the last bonus token. These new
|
||||||
|
# sequence have the same sequence id as the original sequence.
|
||||||
|
# We create a new sequence group and add them there.
|
||||||
|
updated_seq_group_without_bonus_token = \
|
||||||
|
MultiStepWorker._copy_seq_metadata_excluding_last_token(
|
||||||
|
seq_group, seq_with_bonus_token_in_last_step)
|
||||||
|
updated_seq_group_metadata_list.append(
|
||||||
|
updated_seq_group_without_bonus_token)
|
||||||
|
# Add the original sequence group.
|
||||||
|
updated_seq_group_metadata_list.append(
|
||||||
|
MultiStepWorker._shallow_copy_seq_group_metadata(seq_group))
|
||||||
|
# Record the index of the original sequence group.
|
||||||
|
indices_of_original_sequence_groups.append(
|
||||||
|
len(updated_seq_group_metadata_list) - 1)
|
||||||
|
|
||||||
|
updated_execute_model_req.seq_group_metadata_list =\
|
||||||
|
updated_seq_group_metadata_list
|
||||||
|
|
||||||
|
if isinstance(updated_execute_model_req.previous_hidden_states,
|
||||||
|
HiddenStates):
|
||||||
|
updated_execute_model_req.previous_hidden_states\
|
||||||
|
.expand_with_bonus_tokens(seq_with_bonus_token_in_last_step)
|
||||||
|
|
||||||
|
return updated_execute_model_req, indices_of_original_sequence_groups
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _filter_model_output(
|
||||||
|
expanded_batch_outputs: List[SamplerOutput],
|
||||||
|
output_indices_to_retain: List[int]) -> List[SamplerOutput]:
|
||||||
|
"""
|
||||||
|
Filters the model output to include only the specified sequence
|
||||||
|
outputs. This method contracts the expanded batch output from the
|
||||||
|
model to retain the outputs of only those sequences indicated by the
|
||||||
|
provided indices.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expanded_batch_output (List[SamplerOutput]): The expanded output
|
||||||
|
batch from the model.
|
||||||
|
output_indices_to_retain (List[int]): Indices of the model outputs
|
||||||
|
to retain.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[SamplerOutput]: A list containing the filtered model
|
||||||
|
outputs for the specified indices.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
SamplerOutput(
|
||||||
|
outputs=[
|
||||||
|
expanded_batch_output.outputs[i]
|
||||||
|
for i in output_indices_to_retain
|
||||||
|
] if len(expanded_batch_output.outputs) > 0 else [],
|
||||||
|
sampled_token_probs=(
|
||||||
|
expanded_batch_output.
|
||||||
|
sampled_token_probs[output_indices_to_retain]
|
||||||
|
if expanded_batch_output.sampled_token_probs is not None
|
||||||
|
else None),
|
||||||
|
logprobs=(
|
||||||
|
expanded_batch_output.logprobs[output_indices_to_retain]
|
||||||
|
if expanded_batch_output.logprobs is not None else None),
|
||||||
|
sampled_token_ids=(expanded_batch_output.
|
||||||
|
sampled_token_ids[output_indices_to_retain]
|
||||||
|
if expanded_batch_output.sampled_token_ids
|
||||||
|
is not None else None))
|
||||||
|
for expanded_batch_output in expanded_batch_outputs
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_spec_proposals(
|
||||||
|
self,
|
||||||
|
execute_model_req: ExecuteModelRequest,
|
||||||
|
seq_ids_with_bonus_token_in_last_step: set,
|
||||||
|
) -> SpeculativeProposals:
|
||||||
|
"""Produce speculations given an input batch of sequences. The number of
|
||||||
|
speculative tokens per sequence is determined by max_proposal_len.
|
||||||
|
"""
|
||||||
|
return self._proposer.get_spec_proposals(
|
||||||
|
execute_model_req, seq_ids_with_bonus_token_in_last_step)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _append_new_tokens(
|
||||||
|
model_output: List[SamplerOutput],
|
||||||
|
seq_group_metadata_list: List[SequenceGroupMetadata]) -> None:
|
||||||
|
"""Given model output from a single run, append the tokens to the
|
||||||
|
sequences. This is normally done outside of the worker, but it is
|
||||||
|
required if the worker is to perform multiple forward passes.
|
||||||
|
"""
|
||||||
|
for seq_group_metadata, sequence_group_outputs in zip(
|
||||||
|
seq_group_metadata_list, model_output):
|
||||||
|
seq_group_metadata.is_prompt = False
|
||||||
|
|
||||||
|
for seq_output in sequence_group_outputs.samples:
|
||||||
|
# NOTE: Beam search is not supported, so we can assume that
|
||||||
|
# parent_seq_id == seq_id.
|
||||||
|
seq = seq_group_metadata.seq_data[seq_output.parent_seq_id]
|
||||||
|
|
||||||
|
token_id = seq_output.output_token
|
||||||
|
token_logprob = seq_output.logprobs[token_id]
|
||||||
|
|
||||||
|
seq.append_token_id(token_id, token_logprob.logprob)
|
||||||
|
seq.update_num_computed_tokens(1)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _shallow_copy_seq_group_metadata(
|
||||||
|
seq_group_metadata: SequenceGroupMetadata, ) -> SequenceGroupMetadata:
|
||||||
|
"""Copy input data structures to remove side-effects when input data
|
||||||
|
structures are shared with other modules.
|
||||||
|
|
||||||
|
Helpful when the vLLM scheduler runs in the same process as the worker.
|
||||||
|
The alternative is deep-copying (or other form of deep copy); this has
|
||||||
|
performance downsides.
|
||||||
|
"""
|
||||||
|
# Shallow-copy the SequenceGroupMetadata. This allows us to
|
||||||
|
# append tokens and change is_prompt without external side-effects.
|
||||||
|
# We must shallow-copy seq_group_metadata as is_prompt could change.
|
||||||
|
new_seq_group_metadata = copy.copy(seq_group_metadata)
|
||||||
|
|
||||||
|
# We must shallow-copy seq_data as we will append token ids
|
||||||
|
new_seq_data: Dict[int, SequenceData] = {}
|
||||||
|
for seq_id, old_seq_data in seq_group_metadata.seq_data.items():
|
||||||
|
new_seq_data[seq_id] = copy.copy(old_seq_data)
|
||||||
|
new_seq_data[seq_id].output_token_ids =\
|
||||||
|
old_seq_data.output_token_ids[:]
|
||||||
|
|
||||||
|
new_seq_group_metadata.seq_data = new_seq_data
|
||||||
|
return new_seq_group_metadata
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _copy_seq_metadata_excluding_last_token(
|
||||||
|
seq_group_metadata: SequenceGroupMetadata,
|
||||||
|
seq_ids_to_copy: Set[int],
|
||||||
|
) -> SequenceGroupMetadata:
|
||||||
|
"""
|
||||||
|
Creates a shallow copy of the given SequenceGroupMetadata, retaining
|
||||||
|
only the sequence IDs specified in seq_ids_to_copy. For each of these
|
||||||
|
sequence IDs, all output_token_ids except the last one are copied.
|
||||||
|
Sequence IDs not in seq_ids_to_copy are excluded from the copy.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
seq_group_metadata (SequenceGroupMetadata): The original sequence
|
||||||
|
group metadata.
|
||||||
|
seq_ids_to_copy (Set[int]): The set of sequence IDs to include in the
|
||||||
|
copy.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SequenceGroupMetadata: A shallow copy of the sequence group metadata
|
||||||
|
with the specified modifications.
|
||||||
|
"""
|
||||||
|
# Shallow-copy the SequenceGroupMetadata.
|
||||||
|
new_seq_group_metadata = copy.copy(seq_group_metadata)
|
||||||
|
# Shallow-copy seq_data and modify the output_token_ids.
|
||||||
|
new_seq_data: Dict[int, SequenceData] = {}
|
||||||
|
for seq_id, old_seq_data in seq_group_metadata.seq_data.items():
|
||||||
|
if (seq_id in seq_ids_to_copy):
|
||||||
|
new_seq_data[seq_id] = copy.copy(old_seq_data)
|
||||||
|
# Copy all the output token ids except the last.
|
||||||
|
# Also reduce num_computed_tokens by 1 since we are not
|
||||||
|
# including the last output token.
|
||||||
|
# NOTE: num_computed_tokens is not directly used by the
|
||||||
|
# speculative decoding workers, as it is only relevant for
|
||||||
|
# chunked prefill, which is disabled for speculative decoding.
|
||||||
|
# However, to maintain consistency in num_computed_tokens,
|
||||||
|
# we update it here.
|
||||||
|
new_seq_data[seq_id].output_token_ids =\
|
||||||
|
old_seq_data.output_token_ids[:-1]
|
||||||
|
new_seq_data[seq_id].update_num_computed_tokens(-1)
|
||||||
|
new_seq_group_metadata.seq_data = new_seq_data
|
||||||
|
return new_seq_group_metadata
|
||||||
|
|
||||||
|
def _assert_enough_kv_space(
|
||||||
|
self, seq_group_metadata_list: List[SequenceGroupMetadata],
|
||||||
|
num_steps: int) -> None:
|
||||||
|
"""Assert there are enough physical blocks per sequence to store the
|
||||||
|
current KV plus additional KV from num_steps tokens.
|
||||||
|
"""
|
||||||
|
assert self.model_runner.block_size is not None
|
||||||
|
for seq_group_metadata in seq_group_metadata_list:
|
||||||
|
# Only one seq_id is guaranteed because there is no beam search.
|
||||||
|
seq_id = list(seq_group_metadata.seq_data.keys())[0]
|
||||||
|
seq = seq_group_metadata.seq_data[seq_id]
|
||||||
|
|
||||||
|
# After num_steps, the seq len will be the current seq len
|
||||||
|
# plus one token per step.
|
||||||
|
final_seq_len = seq.get_len() + num_steps
|
||||||
|
|
||||||
|
# We will have final_seq_len - 1 KV because vLLM saves KV for a
|
||||||
|
# token in the iteration after the token was generated.
|
||||||
|
required_num_kv_slots = final_seq_len - 1
|
||||||
|
|
||||||
|
# The allocated number of kv slots is the number of allocated blocks
|
||||||
|
# times the number of slots of block.
|
||||||
|
number_physical_blocks = len(
|
||||||
|
seq_group_metadata.block_tables[seq_id])
|
||||||
|
allocated_kv_slots = (number_physical_blocks *
|
||||||
|
self.model_runner.block_size)
|
||||||
|
|
||||||
|
if required_num_kv_slots > allocated_kv_slots:
|
||||||
|
request_id = seq_group_metadata.request_id
|
||||||
|
raise ValueError(
|
||||||
|
"The worker attempted to run "
|
||||||
|
f"{num_steps} times but found insufficient KV space for "
|
||||||
|
f"{request_id=} {seq_id=}. ({allocated_kv_slots=} "
|
||||||
|
f"{required_num_kv_slots=}).")
|
||||||
|
|
||||||
|
def _raise_if_unsupported(
|
||||||
|
self,
|
||||||
|
execute_model_req: ExecuteModelRequest,
|
||||||
|
) -> None:
|
||||||
|
"""MultiStepWorker does not yet implement support for cache swap
|
||||||
|
operations or beam search.
|
||||||
|
"""
|
||||||
|
if any([
|
||||||
|
execute_model_req.blocks_to_swap_in,
|
||||||
|
execute_model_req.blocks_to_swap_out,
|
||||||
|
execute_model_req.blocks_to_copy
|
||||||
|
]):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"MultiStepWorker does not support cache operations")
|
||||||
|
|
||||||
|
if any(
|
||||||
|
len(seq_group_metadata.seq_data.keys()) != 1
|
||||||
|
for seq_group_metadata in
|
||||||
|
execute_model_req.seq_group_metadata_list):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"MultiStepWorker does not support beam search.")
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -468,8 +468,8 @@ def init_worker_distributed_environment(
|
|||||||
# [dp0_tp0, dp0_tp1, dp1_tp0, dp1_tp1] for tp=2, dp=2
|
# [dp0_tp0, dp0_tp1, dp1_tp0, dp1_tp1] for tp=2, dp=2
|
||||||
# Each DP group contains ranks with the same TP-local position.
|
# Each DP group contains ranks with the same TP-local position.
|
||||||
dp_size = parallel_config.data_parallel_size
|
dp_size = parallel_config.data_parallel_size
|
||||||
if dp_size > 1:
|
|
||||||
tp_size = parallel_config.tensor_parallel_size
|
tp_size = parallel_config.tensor_parallel_size
|
||||||
|
if dp_size > 1:
|
||||||
dp_rank = rank // tp_size
|
dp_rank = rank // tp_size
|
||||||
tp_rank = rank % tp_size
|
tp_rank = rank % tp_size
|
||||||
parallel_config.dp_rank = dp_rank
|
parallel_config.dp_rank = dp_rank
|
||||||
@@ -483,6 +483,28 @@ def init_worker_distributed_environment(
|
|||||||
if tp_rank == tp_pos:
|
if tp_rank == tp_pos:
|
||||||
parallel_config._dp_group = group
|
parallel_config._dp_group = group
|
||||||
|
|
||||||
|
# [PR #2269] Initialize expert parallel process group.
|
||||||
|
# When enable_expert_parallel is True, MoE experts are sharded across
|
||||||
|
# all devices (ep_size = tp_size * dp_size). Each device holds
|
||||||
|
# num_experts / ep_size experts and uses all-to-all to dispatch tokens
|
||||||
|
# to the correct expert-holding rank.
|
||||||
|
#
|
||||||
|
# xllm equivalent: collective_communicator.cpp creates moe_ep_group_
|
||||||
|
# with ep_size ranks, using the same rank layout.
|
||||||
|
#
|
||||||
|
# EP group = all ranks (since ep_size = world_size in the standard
|
||||||
|
# DeepSeek-style EP layout where every rank holds a disjoint expert
|
||||||
|
# shard). For DP+EP, ep_size = tp_size * dp_size = world_size.
|
||||||
|
if parallel_config.enable_expert_parallel:
|
||||||
|
import torch.distributed as dist
|
||||||
|
world_size = parallel_config.world_size
|
||||||
|
ep_ranks = list(range(world_size))
|
||||||
|
parallel_config._ep_group = dist.new_group(ep_ranks)
|
||||||
|
logger.info(
|
||||||
|
"[PR #2269] EP process group initialized: ep_size=%d, "
|
||||||
|
"all2all_backend=%s, rank=%d",
|
||||||
|
world_size, parallel_config.all2all_backend, rank)
|
||||||
|
|
||||||
|
|
||||||
def _check_if_gpu_supports_dtype(torch_dtype: torch.dtype):
|
def _check_if_gpu_supports_dtype(torch_dtype: torch.dtype):
|
||||||
# Check if the GPU supports the dtype.
|
# Check if the GPU supports the dtype.
|
||||||
|
|||||||
Reference in New Issue
Block a user