Compare commits
14 Commits
8813233ef2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba901536da | ||
|
|
c04f54792e | ||
|
|
5b646ae665 | ||
|
|
22b60d93a6 | ||
|
|
10539e25ae | ||
|
|
ff8cbeb022 | ||
|
|
8cf04ebe60 | ||
|
|
6a12f4aea8 | ||
|
|
35d63cf0fc | ||
|
|
d342d8671e | ||
|
|
041082f5c3 | ||
|
|
b5dac9c965 | ||
|
|
3a50c67884 | ||
|
|
84f6b833c0 |
@@ -13,13 +13,9 @@ command:
|
||||
- '0.92'
|
||||
- --trust-remote-code
|
||||
- -tp
|
||||
- '2'
|
||||
- -dp
|
||||
- '2'
|
||||
- '4'
|
||||
- --max-num-seqs
|
||||
- '2'
|
||||
- -dp
|
||||
- '2'
|
||||
- --disable-log-requests
|
||||
- --disable-frontend-multiprocessing
|
||||
- --max-num-batched-tokens
|
||||
@@ -39,6 +35,8 @@ command:
|
||||
env:
|
||||
- name: VLLM_ENGINE_ITERATION_TIMEOUT_S
|
||||
value: 3600
|
||||
- name: VLLM_ENABLE_EXPERT_PARALLEL
|
||||
value: 1
|
||||
- name: BI100_MAX_NUM_SEQS
|
||||
value: 2
|
||||
- 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 202609011655"
|
||||
echo "[build] trigger 202609011710"
|
||||
echo "[build] trigger 202609031035"
|
||||
echo "[build] trigger 202609031049"
|
||||
#!/usr/bin/env bash
|
||||
# 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 ./block_major_kv_cache.py "${VLLM_ROOT}/block_major_kv_cache.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"
|
||||
python3 ./patch_corex_swap_blocks.py
|
||||
|
||||
@@ -66,6 +66,15 @@ from vllm.model_executor.layers.linear import (ColumnParallelLinear,
|
||||
ReplicatedLinear,
|
||||
RowParallelLinear)
|
||||
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.quantization import QuantizationConfig
|
||||
from vllm.model_executor.layers.rotary_embedding import (
|
||||
|
||||
@@ -243,6 +243,7 @@ class OpenAIServingChat(OpenAIServing):
|
||||
|
||||
# set up reasoning parser
|
||||
self.reasoning_parser_cls = None
|
||||
self._template_patched = False # [BI100] one-shot chat template patch
|
||||
if reasoning_parser:
|
||||
try:
|
||||
from vllm.reasoning import ReasoningParserManager
|
||||
@@ -309,6 +310,19 @@ class OpenAIServingChat(OpenAIServing):
|
||||
model_config = self.model_config
|
||||
tokenizer = await self.engine_client.get_tokenizer(lora_request)
|
||||
|
||||
# [BI100] Runtime equivalent of patch_chat_template.py.
|
||||
# Qwen3.5/3.6-MoE templates inject '<think>\n\n</think>\n\n'
|
||||
# when enable_thinking=false, which causes the model to
|
||||
# degenerate (outputs '!!!!!'). Patch it out once.
|
||||
if not self._template_patched and hasattr(tokenizer, 'chat_template'):
|
||||
_tgt = r"{{- '<think>\n\n</think>\n\n' }}"
|
||||
if isinstance(tokenizer.chat_template, str) and _tgt in tokenizer.chat_template:
|
||||
tokenizer.chat_template = tokenizer.chat_template.replace(
|
||||
_tgt, "{{- '' }}", 1)
|
||||
logger.info("[BI100] Patched chat_template: removed empty "
|
||||
"<think></think> block for non-thinking mode")
|
||||
self._template_patched = True
|
||||
|
||||
conversation, mm_data_future = parse_chat_messages_futures(
|
||||
request.messages, model_config, tokenizer)
|
||||
|
||||
|
||||
@@ -826,6 +826,9 @@ class ParallelConfig:
|
||||
pipeline_parallel_size: int,
|
||||
tensor_parallel_size: int,
|
||||
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,
|
||||
max_parallel_loading_workers: Optional[int] = None,
|
||||
disable_custom_all_reduce: bool = False,
|
||||
@@ -838,6 +841,13 @@ class ParallelConfig:
|
||||
self.pipeline_parallel_size = pipeline_parallel_size
|
||||
self.tensor_parallel_size = tensor_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.max_parallel_loading_workers = max_parallel_loading_workers
|
||||
self.disable_custom_all_reduce = disable_custom_all_reduce
|
||||
@@ -848,6 +858,10 @@ class ParallelConfig:
|
||||
* self.data_parallel_size)
|
||||
# dp_rank is assigned per-worker during init_device
|
||||
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 self.distributed_executor_backend is None:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import argparse
|
||||
import dataclasses
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import (TYPE_CHECKING, Any, Dict, List, Literal, Mapping, Optional,
|
||||
Tuple, Type, Union)
|
||||
@@ -105,7 +106,13 @@ class EngineArgs:
|
||||
Type[ExecutorBase]]] = None
|
||||
pipeline_parallel_size: int = 1
|
||||
tensor_parallel_size: int = 1
|
||||
data_parallel_size: int = 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
|
||||
block_size: int = 16
|
||||
enable_prefix_caching: bool = False
|
||||
@@ -349,12 +356,6 @@ class EngineArgs:
|
||||
type=int,
|
||||
default=EngineArgs.tensor_parallel_size,
|
||||
help='Number of tensor parallel replicas.')
|
||||
parser.add_argument('--data-parallel-size',
|
||||
'-dp',
|
||||
type=int,
|
||||
default=EngineArgs.data_parallel_size,
|
||||
help='Number of data parallel replicas. '
|
||||
'Total GPUs = tp * pp * dp.')
|
||||
parser.add_argument(
|
||||
'--max-parallel-loading-workers',
|
||||
type=int,
|
||||
@@ -839,7 +840,14 @@ class EngineArgs:
|
||||
# Get the list of attributes of this dataclass.
|
||||
attrs = [attr.name for attr in dataclasses.fields(cls)]
|
||||
# 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
|
||||
|
||||
def create_model_config(self) -> ModelConfig:
|
||||
@@ -933,6 +941,9 @@ class EngineArgs:
|
||||
pipeline_parallel_size=self.pipeline_parallel_size,
|
||||
tensor_parallel_size=self.tensor_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,
|
||||
max_parallel_loading_workers=self.max_parallel_loading_workers,
|
||||
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
|
||||
# Each DP group contains ranks with the same TP-local position.
|
||||
dp_size = parallel_config.data_parallel_size
|
||||
tp_size = parallel_config.tensor_parallel_size
|
||||
if dp_size > 1:
|
||||
tp_size = parallel_config.tensor_parallel_size
|
||||
dp_rank = rank // tp_size
|
||||
tp_rank = rank % tp_size
|
||||
parallel_config.dp_rank = dp_rank
|
||||
@@ -483,6 +483,28 @@ def init_worker_distributed_environment(
|
||||
if tp_rank == tp_pos:
|
||||
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):
|
||||
# Check if the GPU supports the dtype.
|
||||
|
||||
Reference in New Issue
Block a user