94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
286 lines
12 KiB
Python
286 lines
12 KiB
Python
"""
|
|
LPLBSolver — Linear-Programming Load Balancer for Expert Parallelism.
|
|
|
|
Encapsulates LP matrix construction (offline, at init/rebalance) and
|
|
per-batch solving (online, per MoE layer forward pass).
|
|
|
|
Design for DP-attention:
|
|
Each EP rank counts its local tokens, then all ranks participate in an
|
|
all-reduce to obtain identical global counts. Every rank then solves
|
|
the same LP independently, producing the same log2phy_prob — no
|
|
broadcast is needed. Empty-token ranks contribute zeros in the
|
|
all-reduce so the collective never deadlocks.
|
|
|
|
Usage:
|
|
solver = LPLBSolver(phy2log, log2phy, num_gpus, ep_group)
|
|
log2phy_prob = solver.solve(topk_ids) # per batch
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
import torch
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Global per-layer LPLB solvers
|
|
|
|
|
|
# LP dispatch requires every EP rank to call solver.solve() on every forward
|
|
# pass (including empty-topk ranks under DP-attention) — the all-reduce inside
|
|
# would otherwise hang. Only the DeepSeek-v2 family and its subclasses route
|
|
# empty-rank paths through solver.solve(); other MoE families would deadlock.
|
|
_LPLB_SUPPORTED_MODEL_ARCHS: frozenset[str] = frozenset(
|
|
{
|
|
"DeepseekV2ForCausalLM",
|
|
"DeepseekV3ForCausalLM",
|
|
"DeepseekV32ForCausalLM",
|
|
"MistralLarge3ForCausalLM",
|
|
"MistralLarge3ForCausalLMEagle",
|
|
"Glm4MoeLiteForCausalLM",
|
|
"GlmMoeDsaForCausalLM",
|
|
}
|
|
)
|
|
|
|
|
|
def assert_lplb_supported_model(architecture: str) -> None:
|
|
if architecture not in _LPLB_SUPPORTED_MODEL_ARCHS:
|
|
supported = ", ".join(sorted(_LPLB_SUPPORTED_MODEL_ARCHS))
|
|
raise NotImplementedError(
|
|
f"{architecture} does not support --ep-dispatch-algorithm lp. "
|
|
f"Validated targets: {supported}. Other MoE families have "
|
|
"empty-token early returns that don't participate in the EP "
|
|
"all-reduce inside LPLBSolver.solve(), which would deadlock "
|
|
"under DP-attention."
|
|
)
|
|
|
|
|
|
def get_global_lplb_solver(layer_id: int) -> Optional[LPLBSolver]:
|
|
from sglang.srt.runtime_context import get_resources
|
|
|
|
return get_resources().lplb_solvers.get(layer_id)
|
|
|
|
|
|
def set_global_lplb_solver(layer_id: int, solver: LPLBSolver):
|
|
from sglang.srt.runtime_context import get_resources
|
|
|
|
get_resources().lplb_solvers[layer_id] = solver
|
|
|
|
|
|
def clear_global_lplb_solvers():
|
|
from sglang.srt.runtime_context import get_resources
|
|
|
|
get_resources().lplb_solvers.clear()
|
|
|
|
|
|
class LPLBSolver:
|
|
"""
|
|
Per-layer LPLB solver.
|
|
|
|
At init: pre-computes LP constraint matrices from expert-to-GPU mapping.
|
|
At solve: takes topk_ids, counts tokens, all-reduces, runs LP,
|
|
returns log2phy_prob for probability-based token dispatch.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
phy2log: torch.Tensor,
|
|
log2phy: torch.Tensor,
|
|
num_gpus: int,
|
|
ep_group=None,
|
|
logical_to_all_physical_map_num_valid=None,
|
|
):
|
|
"""
|
|
Args:
|
|
phy2log: (num_physical_experts,) physical-to-logical expert mapping.
|
|
log2phy: (num_logical_experts, max_copies) logical-to-physical mapping (-1 padded).
|
|
num_gpus: Number of GPUs in the EP group.
|
|
ep_group: GroupCoordinator for EP communication (all-reduce).
|
|
logical_to_all_physical_map_num_valid: (num_logical_experts,) number of valid physical copies.
|
|
"""
|
|
device = phy2log.device
|
|
self.num_gpus = num_gpus
|
|
self.ep_group = ep_group
|
|
self._has_redundancy = False
|
|
if logical_to_all_physical_map_num_valid is not None:
|
|
self._has_redundancy = bool(
|
|
(logical_to_all_physical_map_num_valid > 1).any()
|
|
)
|
|
|
|
self.num_logical = log2phy.shape[0]
|
|
self.max_copies = log2phy.shape[1]
|
|
self.num_phy = phy2log.shape[0]
|
|
# B1/B2 GPU-assignment matrices below assume each rank owns a
|
|
# contiguous block of num_phy // num_gpus physical experts.
|
|
if self.num_phy % num_gpus != 0:
|
|
raise ValueError(
|
|
f"LPLBSolver requires num_phy ({self.num_phy}) to be divisible "
|
|
f"by num_gpus ({num_gpus}); per-rank-contiguous ownership is "
|
|
"currently the only supported allocation."
|
|
)
|
|
num_phy_per_gpu = self.num_phy // num_gpus
|
|
|
|
# Count copies per logical expert
|
|
logcnt = torch.bincount(phy2log, minlength=self.num_logical)
|
|
|
|
# Separate single-copy vs replicated experts.
|
|
# Stored as int64 so they can be used directly as index tensors in
|
|
# _solve without per-call .long() casts (Tier 1 optimization).
|
|
self.log_single = torch.nonzero(logcnt == 1).flatten().to(torch.int64)
|
|
self.phy_single = log2phy[self.log_single, 0].to(torch.int64)
|
|
self.log_replicated = torch.nonzero(logcnt > 1).flatten().to(torch.int64)
|
|
self.phy_replicated = (
|
|
torch.nonzero(logcnt[phy2log] > 1).flatten().to(torch.int64)
|
|
)
|
|
|
|
self.num_single = len(self.log_single)
|
|
self.num_red_log = len(self.log_replicated)
|
|
self.num_red_phy = len(self.phy_replicated)
|
|
|
|
# Build GPU assignment matrices
|
|
B_full = torch.zeros(
|
|
(num_gpus, self.num_phy), dtype=torch.float32, device=device
|
|
)
|
|
for i in range(num_gpus):
|
|
B_full[i, i * num_phy_per_gpu : (i + 1) * num_phy_per_gpu] = 1
|
|
self.B1 = B_full[:, self.phy_single].contiguous()
|
|
B2 = B_full[:, self.phy_replicated]
|
|
|
|
# Build C matrix (copy-to-logical mapping)
|
|
C = torch.zeros(
|
|
(self.num_red_log, self.num_red_phy), dtype=torch.float32, device=device
|
|
)
|
|
phy2log_rep = phy2log[self.phy_replicated]
|
|
for i in range(self.num_red_log):
|
|
C[i, phy2log_rep == self.log_replicated[i]] = 1.0
|
|
|
|
# Build A_base = [[C, 0, 0], [B2, I, -1]] (without Big-M column)
|
|
zeros_top_g = torch.zeros(
|
|
(self.num_red_log, num_gpus), dtype=torch.float32, device=device
|
|
)
|
|
zeros_top_1 = torch.zeros(
|
|
(self.num_red_log, 1), dtype=torch.float32, device=device
|
|
)
|
|
I_g = torch.eye(num_gpus, dtype=torch.float32, device=device)
|
|
neg_ones = torch.full((num_gpus, 1), -1.0, dtype=torch.float32, device=device)
|
|
|
|
A_top = torch.hstack([C, zeros_top_g, zeros_top_1])
|
|
A_bottom = torch.hstack([B2, I_g, neg_ones])
|
|
self.A_base = torch.vstack([A_top, A_bottom]).contiguous()
|
|
|
|
# Objective: minimize M (second-to-last var), penalize Big-M auxiliary
|
|
nv = self.A_base.shape[1] + 1 # +1 for Big-M column
|
|
self.c_vec = torch.zeros(nv, dtype=torch.float32, device=device)
|
|
self.c_vec[-2] = 1.0
|
|
self.c_vec[-1] = 1000.0
|
|
|
|
# Store log2phy as int64 so it can be used directly as index tensor
|
|
# without per-call .long() casts (Tier 1 optimization).
|
|
self.log2phy = log2phy.to(torch.int64).contiguous()
|
|
|
|
# Pre-JIT-compile the fused IPM kernel for this (NC, NV) shape so the
|
|
# 20-40s compile cost happens once at startup rather than on the first
|
|
# real request. No-op when the fused backend is unavailable.
|
|
nc = self.A_base.shape[0]
|
|
nv = self.A_base.shape[1] + 1 # +1 for Big-M column added in solve()
|
|
from sglang.jit_kernel.lplb.torch_solver import warmup as _ipm_warmup
|
|
|
|
_ipm_warmup(nc, nv, num_iters=5, device=device)
|
|
|
|
# Pre-compute A_base row sum (used in every prep call).
|
|
self._A_base_row_sum = self.A_base.sum(dim=1).contiguous() # (NC,)
|
|
|
|
# Pre-allocate the buffers the JIT CUDA prep / IPM / post kernels write
|
|
# into. All writes are contiguous full-tensor stores (no strided
|
|
# ``out=`` semantics), so the reuse is safe under high concurrency.
|
|
# Constructed lazily on the first solve() call (we don't know the
|
|
# device-side log2phy_prob shape until then) — see _solve.
|
|
self._A_full = torch.empty(nc, nv, dtype=torch.float32, device=device)
|
|
self._A_full[:, : nv - 1].copy_(self.A_base)
|
|
self._b = torch.empty(nc, dtype=torch.float32, device=device)
|
|
self._t1 = torch.empty(self.num_single, dtype=torch.float32, device=device)
|
|
self._x = torch.empty(nv, dtype=torch.float32, device=device)
|
|
self._log2phy_prob = torch.empty(
|
|
log2phy.shape, dtype=torch.float32, device=device
|
|
)
|
|
|
|
def solve(self, topk_ids: torch.Tensor) -> torch.Tensor:
|
|
"""
|
|
Full LPLB pipeline: count -> all-reduce -> LP solve -> return log2phy_prob.
|
|
|
|
All EP ranks must call this method every MoE layer forward pass,
|
|
including empty-token ranks (which pass an empty topk_ids tensor).
|
|
This ensures the all-reduce collective does not deadlock under
|
|
DP-attention where different ranks may have different token counts.
|
|
|
|
Args:
|
|
topk_ids: (num_tokens, topk) int32 tensor of logical expert IDs.
|
|
Can be empty (shape (0, topk)) for idle ranks.
|
|
|
|
Returns:
|
|
log2phy_prob: (num_logical, max_copies) float32 probability tensor.
|
|
"""
|
|
device = topk_ids.device
|
|
|
|
# Step 1: Count local tokens per logical expert.
|
|
# topk_ids comes from the router and is by construction in
|
|
# [0, num_logical), so we can scatter_add directly without filtering.
|
|
# Boolean masking + numel() (the previous defensive form) forced a
|
|
# GPU->host sync on every forward pass via aten::nonzero and a
|
|
# tensor-shape read; scatter_add on the flattened tensor is async
|
|
# and a no-op when topk_ids is empty (DP-attention idle rank case).
|
|
local_counts = torch.zeros(self.num_logical, dtype=torch.int32, device=device)
|
|
flat_ids = topk_ids.flatten()
|
|
local_counts.scatter_add_(
|
|
0,
|
|
flat_ids.long(),
|
|
torch.ones_like(flat_ids, dtype=torch.int32),
|
|
)
|
|
|
|
# Step 2: All-reduce to get global counts across all EP ranks.
|
|
# All EP ranks must participate — empty-token ranks contribute zeros.
|
|
# After all-reduce, every rank has identical global_counts and solves
|
|
# the same LP independently, so no broadcast is needed.
|
|
# GroupCoordinator.all_reduce may be in-place (pynccl) or out-of-place
|
|
# (ca_comm / pymscclpp / ...) depending on tensor size; small tensors
|
|
# like ours (~num_logical * 4 B) typically take the out-of-place path,
|
|
# so we must capture the return value.
|
|
global_counts = local_counts.float()
|
|
if self.ep_group is not None:
|
|
global_counts = self.ep_group.all_reduce(global_counts)
|
|
|
|
# Step 3: Run LP solver
|
|
return self._solve(global_counts)
|
|
|
|
def _solve(self, global_counts: torch.Tensor) -> torch.Tensor:
|
|
"""Three CUDA kernel launches replace ~14 torch ops.
|
|
|
|
Pipeline (all writes go into pre-allocated buffers from __init__):
|
|
prep_lp_inputs → solve_ipm → extract_log2phy_prob
|
|
Raises if the JIT CUDA backend is unavailable.
|
|
"""
|
|
from sglang.jit_kernel.lplb import cuda_solver
|
|
|
|
cuda_solver.prep_lp_inputs(
|
|
self._A_full,
|
|
self._b,
|
|
self._t1,
|
|
global_counts,
|
|
self.log_single,
|
|
self.log_replicated,
|
|
self.B1,
|
|
self._A_base_row_sum,
|
|
)
|
|
cuda_solver.solve_ipm(self._A_full, self._b, self.c_vec, result=self._x)
|
|
cuda_solver.extract_log2phy_prob(
|
|
self._log2phy_prob,
|
|
self._x,
|
|
self._t1,
|
|
self.phy_single,
|
|
self.phy_replicated,
|
|
self.log2phy,
|
|
)
|
|
return self._log2phy_prob
|