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
585 lines
21 KiB
Python
585 lines
21 KiB
Python
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from typing import Iterable, List, Optional, Set, Tuple, Union
|
|
|
|
import torch
|
|
|
|
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
|
from sglang.srt.utils.hf_transformers_utils import AutoConfig
|
|
|
|
|
|
@dataclass
|
|
class MoELoRABatchInfo:
|
|
# Per-request segment indptrs used by MoE LoRA routing, shape (bs + 1,).
|
|
seg_indptr: torch.Tensor
|
|
|
|
# Per-request adapter index used by MoE LoRA routing, shape (bs,).
|
|
req_to_lora: torch.Tensor
|
|
|
|
# A mask indicating if lora adapter is enabled. Shape (num_loras,)
|
|
adapter_enabled: torch.Tensor
|
|
|
|
# A mapping of which lora adapter is used for each token. Shape (num_tokens,)
|
|
# If a token has no lora adapter, the value is -1.
|
|
token_lora_mapping: torch.Tensor
|
|
|
|
|
|
@dataclass
|
|
class LoRABatchInfo:
|
|
# The forward mode is using CUDA Graph.
|
|
use_cuda_graph: bool
|
|
|
|
# Batch size
|
|
bs: int
|
|
|
|
# Number of segments. For triton backend, it is equal to batch size.
|
|
num_segments: int
|
|
|
|
# Indice pointers of each segment in shape (num_segments + 1, )
|
|
seg_indptr: torch.Tensor
|
|
|
|
# The index of lora adapter used by each segment, in shape (num_segments,)
|
|
weight_indices: torch.Tensor
|
|
|
|
# ranks of each lora adapter, in shape (lora_num,)
|
|
lora_ranks: torch.Tensor
|
|
|
|
# scaling of each lora adapter, in shape (lora_num,)
|
|
scalings: torch.Tensor
|
|
|
|
# Maximum segment length of current batch
|
|
max_len: Optional[int]
|
|
|
|
# Lengths of each segments in shape (num_segments,)
|
|
seg_lens: Optional[torch.Tensor]
|
|
|
|
# The logical (re)ordering of input rows (tokens), in shape (num_tokens,)
|
|
permutation: Optional[torch.Tensor]
|
|
|
|
# Total number of tokens this batch info expects (host-side int).
|
|
# Used by lm_head LoRA to validate input shape without GPU sync.
|
|
expected_tokens: Optional[int] = None
|
|
|
|
# CPU-side flag: True when at least one request uses a LoRA adapter.
|
|
# Computed from Python lists in prepare_lora_batch to avoid GPU sync.
|
|
has_active_lora: bool = False
|
|
|
|
# Per-request segment indptrs, shape (bs + 1,). Required by MoE virtual
|
|
# experts which map tokens to requests regardless of the dense-LoRA
|
|
# backend's internal segmentation. For the triton backend these are
|
|
# identical to seg_indptr/weight_indices; for csgmv they differ because
|
|
# its segments are chunked across adapters.
|
|
req_seg_indptr: Optional[torch.Tensor] = None
|
|
|
|
# Per-request adapter index, shape (bs,).
|
|
req_weight_indices: Optional[torch.Tensor] = None
|
|
|
|
# MoE LoRA batch info
|
|
moe_lora_info: Optional[MoELoRABatchInfo] = None
|
|
|
|
|
|
class LoRAType(Enum):
|
|
LORA_A = 0
|
|
LORA_B = 1
|
|
|
|
|
|
def copy_weight_into_buffer(
|
|
buffer_view: torch.Tensor,
|
|
weight: torch.Tensor,
|
|
) -> None:
|
|
"""
|
|
Copy a LoRA weight tensor into a destination buffer.
|
|
|
|
When a pinned CPU source has a dtype mismatch with a device destination,
|
|
cast on the destination device instead of doing the conversion on CPU.
|
|
"""
|
|
if weight.dtype == buffer_view.dtype:
|
|
buffer_view.copy_(weight, non_blocking=True)
|
|
return
|
|
|
|
if weight.device.type == "cpu" and buffer_view.device.type != "cpu":
|
|
weight = weight.to(device=buffer_view.device, non_blocking=True)
|
|
|
|
buffer_view.copy_(weight.to(dtype=buffer_view.dtype), non_blocking=True)
|
|
|
|
|
|
def get_hidden_dim(
|
|
module_name: str,
|
|
config: AutoConfig,
|
|
base_model: torch.nn.Module,
|
|
layer_idx: int,
|
|
lora_added_vocab_size: int = 0,
|
|
) -> Tuple[int]:
|
|
"""
|
|
Given a module_name (might be a stacked name), return the hidden dims of modules' input and output.
|
|
"""
|
|
|
|
if hasattr(base_model, "get_hidden_dim"):
|
|
return base_model.get_hidden_dim(module_name, layer_idx)
|
|
else:
|
|
"""
|
|
WARNING: get_hidden_dim() is not defined,
|
|
which is used to get the hidden dim for different lora modules
|
|
Use the default one, but please check if it is correct for your model.
|
|
Please implement the function in the model class if it is not.
|
|
You can reference this function in llama.py.
|
|
"""
|
|
head_dim = getattr(
|
|
config, "head_dim", config.hidden_size // config.num_attention_heads
|
|
)
|
|
if module_name == "qkv_proj":
|
|
return config.hidden_size, head_dim * (
|
|
config.num_attention_heads + config.num_key_value_heads * 2
|
|
)
|
|
elif module_name == "o_proj":
|
|
o_head_dim = getattr(config, "v_head_dim", None) or head_dim
|
|
return (
|
|
o_head_dim * config.num_attention_heads,
|
|
config.hidden_size,
|
|
)
|
|
elif module_name == "gate_up_proj":
|
|
inter = config.intermediate_size
|
|
first_k = getattr(config, "first_k_dense_replace", None)
|
|
moe_freq = getattr(config, "moe_layer_freq", 1)
|
|
if (
|
|
first_k is not None
|
|
and layer_idx >= first_k
|
|
and layer_idx % moe_freq == 0
|
|
):
|
|
moe_inter = getattr(config, "moe_intermediate_size", None)
|
|
n_shared = getattr(config, "n_shared_experts", None)
|
|
if moe_inter is not None and n_shared is not None:
|
|
inter = moe_inter * n_shared
|
|
return config.hidden_size, inter * 2
|
|
elif module_name == "down_proj":
|
|
inter = config.intermediate_size
|
|
first_k = getattr(config, "first_k_dense_replace", None)
|
|
moe_freq = getattr(config, "moe_layer_freq", 1)
|
|
if (
|
|
first_k is not None
|
|
and layer_idx >= first_k
|
|
and layer_idx % moe_freq == 0
|
|
):
|
|
moe_inter = getattr(config, "moe_intermediate_size", None)
|
|
n_shared = getattr(config, "n_shared_experts", None)
|
|
if moe_inter is not None and n_shared is not None:
|
|
inter = moe_inter * n_shared
|
|
return inter, config.hidden_size
|
|
elif module_name == "fused_qkv_a_proj_with_mqa":
|
|
q_lora_rank = getattr(config, "q_lora_rank", None) or 0
|
|
kv_lora_rank = config.kv_lora_rank
|
|
qk_rope_head_dim = config.qk_rope_head_dim
|
|
return (
|
|
config.hidden_size,
|
|
q_lora_rank + kv_lora_rank + qk_rope_head_dim,
|
|
)
|
|
elif module_name == "q_b_proj":
|
|
return (
|
|
config.q_lora_rank,
|
|
config.num_attention_heads
|
|
* (config.qk_nope_head_dim + config.qk_rope_head_dim),
|
|
)
|
|
elif module_name == "kv_b_proj":
|
|
return (
|
|
config.kv_lora_rank,
|
|
config.num_attention_heads
|
|
* (config.qk_nope_head_dim + config.v_head_dim),
|
|
)
|
|
elif module_name in DSA_INDEXER_LORA_NAMES:
|
|
from sglang.srt.configs.model_config import (
|
|
get_dsa_index_head_dim,
|
|
get_dsa_index_n_heads,
|
|
)
|
|
|
|
if module_name == "indexer.wq_b":
|
|
return (
|
|
config.q_lora_rank,
|
|
get_dsa_index_n_heads(config) * get_dsa_index_head_dim(config),
|
|
)
|
|
elif module_name == "indexer.wk":
|
|
return config.hidden_size, get_dsa_index_head_dim(config)
|
|
else: # indexer.weights_proj
|
|
return config.hidden_size, get_dsa_index_n_heads(config)
|
|
elif module_name == "gate_up_proj_moe":
|
|
moe_inter = (
|
|
getattr(config, "moe_intermediate_size", None)
|
|
or config.intermediate_size
|
|
)
|
|
return config.hidden_size, moe_inter * 2
|
|
elif module_name == "down_proj_moe":
|
|
moe_inter = (
|
|
getattr(config, "moe_intermediate_size", None)
|
|
or config.intermediate_size
|
|
)
|
|
return moe_inter, config.hidden_size
|
|
elif module_name == "embed_tokens":
|
|
# For embedding: input is vocab_size (as embedding lookup), output is hidden_size
|
|
# if contain extra tokens will be added; otherwise is 0.
|
|
return config.vocab_size + lora_added_vocab_size, config.hidden_size
|
|
elif module_name == "lm_head":
|
|
# For lm_head: input is hidden_size, output is vocab_size
|
|
# if contain extra tokens will be added; otherwise is 0.
|
|
return config.hidden_size, config.vocab_size + lora_added_vocab_size
|
|
else:
|
|
raise NotImplementedError(
|
|
"get_hidden_dim not implemented for " + module_name
|
|
)
|
|
|
|
|
|
def get_normalized_target_modules(
|
|
target_modules: Union[str, Iterable[str]],
|
|
) -> set[str]:
|
|
"""
|
|
Mapping a list of target module name to names of the normalized LoRA weights.
|
|
Handles both base module names (e.g., "gate_proj") and prefixed module names (e.g., "feed_forward.gate_proj").
|
|
|
|
Also handles PEFT shorthand strings like "all-linear" or "all" by returning
|
|
{"all"} as a sentinel value. Callers that need a concrete module set
|
|
should use :func:`auto_detect_lora_target_modules` to resolve the shorthand
|
|
against the loaded base model.
|
|
"""
|
|
# Handle PEFT shorthand strings — return {"all"} as sentinel.
|
|
# Callers can resolve to concrete names via auto_detect_lora_target_modules().
|
|
if isinstance(target_modules, str):
|
|
if target_modules not in ["all", "all-linear"]:
|
|
raise ValueError(
|
|
"Only 'all' or 'all-linear' can be used as the string for target module"
|
|
)
|
|
return {"all"}
|
|
|
|
params_mapping = {
|
|
"q_proj": "qkv_proj",
|
|
"k_proj": "qkv_proj",
|
|
"v_proj": "qkv_proj",
|
|
"gate_proj": "gate_up_proj",
|
|
"up_proj": "gate_up_proj",
|
|
"out_proj": "out_proj",
|
|
"embed_tokens": "embed_tokens",
|
|
"vocab_emb": "embed_tokens",
|
|
"embeddings": "embed_tokens",
|
|
"word_embeddings": "embed_tokens",
|
|
"lm_head": "lm_head",
|
|
"output": "lm_head",
|
|
"unembed_tokens": "lm_head",
|
|
"q_a_proj": "fused_qkv_a_proj_with_mqa",
|
|
"kv_a_proj_with_mqa": "fused_qkv_a_proj_with_mqa",
|
|
# DSA indexer projections are qualified with their parent module name
|
|
# because the bare leaf names collide with unrelated modules in other
|
|
# models (e.g. DeepSeek-V4 attention `wq_b`, Pixtral vision `wk`).
|
|
"wq_b": "indexer.wq_b",
|
|
"wk": "indexer.wk",
|
|
"weights_proj": "indexer.weights_proj",
|
|
}
|
|
|
|
result = set()
|
|
for name in target_modules:
|
|
base_name = name.split(".")[-1]
|
|
normalized_name = params_mapping.get(base_name, base_name)
|
|
result.add(normalized_name)
|
|
return result
|
|
|
|
|
|
def get_stacked_multiply(
|
|
module_name: str, base_model: Optional[torch.nn.Module] = None
|
|
) -> int:
|
|
"""
|
|
Mapping a lora module name to its magnification at output dimension.
|
|
Models can override via a get_stacked_multiply(module_name) method.
|
|
"""
|
|
if base_model is not None and hasattr(base_model, "get_stacked_multiply"):
|
|
return base_model.get_stacked_multiply(module_name)
|
|
stacked_rank = {
|
|
"qkv_proj": 3,
|
|
"in_proj_qkvz": 4, # GDN packed input projection
|
|
"gate_up_proj": 2,
|
|
"gate_up_proj_moe": 2,
|
|
"in_proj": 2,
|
|
"fused_qkv_a_proj_with_mqa": 2,
|
|
}
|
|
return stacked_rank[module_name] if module_name in stacked_rank else 1
|
|
|
|
|
|
def get_target_module_name(full_module_name: str, target_modules: Set[str]) -> str:
|
|
"""
|
|
Get the target module name in target_modules that can match full_module_name.
|
|
|
|
If there is a target module name in target_modules that can match full_module_name, return this name
|
|
Else raise ValueError.
|
|
|
|
When multiple target modules match (e.g. both "up_proj" and "gate_up_proj"
|
|
are substrings), the longest match wins to avoid ambiguity.
|
|
"""
|
|
best = None
|
|
for target_module in target_modules:
|
|
if target_module in full_module_name:
|
|
if best is None or len(target_module) > len(best):
|
|
best = target_module
|
|
if best is not None:
|
|
return best
|
|
raise ValueError(
|
|
f"Cannot find target module name for {full_module_name} in {target_modules}"
|
|
)
|
|
|
|
|
|
EMBEDDING_NAMES = ["embed_tokens", "lm_head"]
|
|
ROW_PARALLELISM_LINEAR_LORA_NAMES = ["o_proj", "out_proj", "down_proj", "down_proj_moe"]
|
|
DSA_INDEXER_LORA_NAMES = frozenset(
|
|
{"indexer.wq_b", "indexer.wk", "indexer.weights_proj"}
|
|
)
|
|
REPLICATED_LINEAR_LORA_NAMES = [
|
|
"fused_qkv_a_proj_with_mqa",
|
|
"fc1_latent_proj",
|
|
"fc2_latent_proj",
|
|
*DSA_INDEXER_LORA_NAMES,
|
|
]
|
|
|
|
# Normalized module names that the LoRA system fully supports
|
|
# (i.e. get_hidden_dim, init_buffers, and init_lora_modules can handle them).
|
|
_KNOWN_LORA_TARGET_MODULES = frozenset(
|
|
{
|
|
"qkv_proj",
|
|
"o_proj",
|
|
"out_proj",
|
|
"in_proj",
|
|
"in_proj_qkvz",
|
|
"up_proj",
|
|
"gate_up_proj",
|
|
"down_proj",
|
|
"fc1_latent_proj",
|
|
"fc2_latent_proj",
|
|
"embed_tokens",
|
|
"lm_head",
|
|
"fused_qkv_a_proj_with_mqa",
|
|
"q_b_proj",
|
|
"kv_b_proj",
|
|
}
|
|
| DSA_INDEXER_LORA_NAMES
|
|
)
|
|
|
|
|
|
def auto_detect_lora_target_modules(model: "torch.nn.Module") -> set:
|
|
"""Discover LoRA-compatible modules by inspecting the base model.
|
|
|
|
Walks the model graph and returns the set of *normalized* target-module
|
|
names that (a) actually exist in the model and (b) the LoRA memory pool
|
|
can handle. This is used to resolve PEFT shorthands like ``"all-linear"``
|
|
without requiring the user to enumerate modules on the CLI.
|
|
"""
|
|
from sglang.srt.layers.linear import LinearBase
|
|
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
|
|
from sglang.srt.layers.vocab_parallel_embedding import (
|
|
ParallelLMHead,
|
|
VocabParallelEmbedding,
|
|
)
|
|
|
|
raw_names: set = set()
|
|
dsa_indexer_leaf_names = {
|
|
target_name.split(".")[-1] for target_name in DSA_INDEXER_LORA_NAMES
|
|
}
|
|
for name, module in model.named_modules():
|
|
if isinstance(module, FusedMoE):
|
|
raw_names.add("gate_up_proj")
|
|
raw_names.add("down_proj")
|
|
elif isinstance(module, ParallelLMHead):
|
|
raw_names.add("lm_head")
|
|
elif isinstance(module, VocabParallelEmbedding):
|
|
raw_names.add("embed_tokens")
|
|
elif isinstance(module, LinearBase):
|
|
parts = name.split(".")
|
|
leaf_name = parts[-1]
|
|
parent_qualified_name = ".".join(parts[-2:])
|
|
if parent_qualified_name in DSA_INDEXER_LORA_NAMES:
|
|
raw_names.add(parent_qualified_name)
|
|
elif leaf_name in dsa_indexer_leaf_names:
|
|
# Bare DSA indexer leaf names are ambiguous across model
|
|
# families. Only auto-detect them when the actual module path
|
|
# proves they are under an `indexer` parent.
|
|
continue
|
|
else:
|
|
raw_names.add(leaf_name)
|
|
|
|
normalized = get_normalized_target_modules(raw_names)
|
|
result = normalized & _KNOWN_LORA_TARGET_MODULES
|
|
|
|
# Allow models to declare additional LoRA-compatible modules that
|
|
# cannot be auto-discovered or need to bypass normalization
|
|
# (e.g. Mamba in_proj, non-gated up_proj).
|
|
if hasattr(model, "supported_lora_modules"):
|
|
result.update(set(model.supported_lora_modules) & _KNOWN_LORA_TARGET_MODULES)
|
|
|
|
return result
|
|
|
|
|
|
def get_lm_head_lora_b_shard_size(output_dim: int, shard_indices=None) -> int:
|
|
"""Get the LoRA B output dimension for lm_head, accounting for TP.
|
|
|
|
lm_head is column-parallel, so its LoRA B must be sharded along the
|
|
vocab dimension to match the base output. When shard_indices is
|
|
provided, the returned size reflects the base model's actual per-rank
|
|
vocab partition.
|
|
|
|
Args:
|
|
output_dim: Full (unsharded) output dimension (vocab_size).
|
|
shard_indices: VocabParallelEmbeddingShardIndices from the base
|
|
ParallelLMHead layer. When provided, returns the per-rank
|
|
org vocab size from the base model's actual sharding.
|
|
"""
|
|
if shard_indices is not None:
|
|
return shard_indices.num_org_elements
|
|
return output_dim
|
|
|
|
|
|
def generate_sequence_lengths(
|
|
forward_batch: ForwardBatch, device: Optional[torch.device] = None
|
|
) -> torch.Tensor:
|
|
|
|
device = torch.get_default_device() if device is None else device
|
|
with torch.device(device):
|
|
if forward_batch.forward_mode.is_decode():
|
|
seg_lens = torch.ones(forward_batch.batch_size, dtype=torch.int32)
|
|
elif forward_batch.forward_mode.is_target_verify():
|
|
seg_lens = torch.full(
|
|
size=(forward_batch.batch_size,),
|
|
fill_value=forward_batch.spec_info.draft_token_num,
|
|
dtype=torch.int32,
|
|
)
|
|
elif forward_batch.forward_mode.is_extend():
|
|
seg_lens = (
|
|
forward_batch.extend_seq_lens
|
|
if forward_batch.extend_seq_lens.device == device
|
|
else torch.tensor(
|
|
forward_batch.extend_seq_lens_cpu,
|
|
dtype=torch.int32,
|
|
)
|
|
)
|
|
else:
|
|
raise ValueError(f"Unsupported forward mode: {forward_batch.forward_mode}")
|
|
return seg_lens
|
|
|
|
|
|
def get_lm_head_pruned_lens(
|
|
forward_batch: ForwardBatch,
|
|
) -> Optional[List[int]]:
|
|
"""
|
|
Compute per-sequence pruned lengths for lm_head LoRA.
|
|
|
|
Returns a list of pruned lengths (one per sequence) if pruning applies,
|
|
or None if lm_head pruning is not applicable for this batch.
|
|
|
|
Pruning rules:
|
|
- Extend without logprobs: 1 token per sequence
|
|
- Extend with logprobs: max(extend_len - logprob_start_len, 1) per sequence
|
|
- Decode / target_verify / draft_extend_v2: no pruning
|
|
|
|
IMPORTANT: This must stay in sync with LogitsProcessor._get_pruned_states()
|
|
in sglang/srt/layers/logits_processor.py, which determines how many tokens
|
|
per sequence are passed to lm_head. If the pruning conditions or lengths
|
|
there change, this function must be updated to match, otherwise the
|
|
lm_head LoRA will operate on incorrectly shaped inputs.
|
|
"""
|
|
lm_head_pruning = (
|
|
forward_batch.forward_mode.is_extend()
|
|
and not forward_batch.forward_mode.is_target_verify()
|
|
and not forward_batch.forward_mode.is_draft_extend_v2()
|
|
)
|
|
|
|
if not lm_head_pruning:
|
|
return None
|
|
|
|
if forward_batch.return_logprob:
|
|
pruned_lens = []
|
|
for ext_len, start_len in zip(
|
|
forward_batch.extend_seq_lens_cpu,
|
|
forward_batch.extend_logprob_start_lens_cpu,
|
|
):
|
|
pruned_lens.append(1 if ext_len == start_len else ext_len - start_len)
|
|
else:
|
|
pruned_lens = [1] * forward_batch.batch_size
|
|
|
|
return pruned_lens
|
|
|
|
|
|
def merge_and_chunk_segments(
|
|
weight_indices: list[int],
|
|
pruned_lens: List[int],
|
|
chunk_size: int,
|
|
) -> Tuple[List[int], List[int]]:
|
|
"""
|
|
Merge consecutive same-adapter sequences and chunk at chunk_size boundaries.
|
|
|
|
Merges consecutive sequences that use the same adapter into single
|
|
segments, splitting any segment that exceeds chunk_size.
|
|
|
|
Args:
|
|
weight_indices: Per-sequence adapter indices.
|
|
pruned_lens: Per-sequence pruned token counts.
|
|
chunk_size: Maximum segment length before splitting.
|
|
|
|
Returns:
|
|
(seg_weight_indices, seg_lens): Merged and chunked segments.
|
|
"""
|
|
seg_weight_indices: List[int] = []
|
|
seg_lens: List[int] = []
|
|
for wi, pl in zip(weight_indices, pruned_lens):
|
|
if seg_weight_indices and seg_weight_indices[-1] == wi:
|
|
seg_lens[-1] += pl
|
|
else:
|
|
seg_weight_indices.append(wi)
|
|
seg_lens.append(pl)
|
|
# Split the last segment if it exceeds chunk_size
|
|
while seg_lens[-1] > chunk_size:
|
|
remainder = seg_lens[-1] - chunk_size
|
|
seg_lens[-1] = chunk_size
|
|
seg_weight_indices.append(wi)
|
|
seg_lens.append(remainder)
|
|
|
|
return seg_weight_indices, seg_lens
|
|
|
|
|
|
def build_lm_head_pass_segments(
|
|
weight_indices: List[int],
|
|
pruned_lens: List[int],
|
|
logprobs_chunk_size: int,
|
|
) -> List[Tuple[List[int], List[int]]]:
|
|
"""
|
|
Precompute per-pass segment info for lm_head LoRA logprobs processing.
|
|
|
|
When LogitsProcessor uses chunked logprobs processing
|
|
(process_input_logprobs_by_chunk), pruned hidden states are split into
|
|
fixed-size passes. Each pass needs its own segmentation
|
|
(weight_indices, seg_lens) so that lm_head LoRA operates on the
|
|
correct adapter assignments per pass.
|
|
|
|
Args:
|
|
weight_indices: Per-sequence adapter indices.
|
|
pruned_lens: Per-sequence pruned token counts.
|
|
logprobs_chunk_size: Fixed pass size used by LogitsProcessor.
|
|
|
|
Returns:
|
|
List of (seg_weight_indices, seg_lens) tuples, one per pass.
|
|
"""
|
|
# Expand to per-token weight index
|
|
token_wi: List[int] = []
|
|
for wi, pl in zip(weight_indices, pruned_lens):
|
|
token_wi.extend([wi] * pl)
|
|
total = len(token_wi)
|
|
num_passes = (total + logprobs_chunk_size - 1) // logprobs_chunk_size
|
|
|
|
result: List[Tuple[List[int], List[int]]] = []
|
|
for i in range(num_passes):
|
|
start = i * logprobs_chunk_size
|
|
end = min((i + 1) * logprobs_chunk_size, total)
|
|
|
|
# Run-length encode the pass's adapter indices
|
|
seg_wi: List[int] = []
|
|
seg_lens: List[int] = []
|
|
for t in range(start, end):
|
|
if seg_wi and seg_wi[-1] == token_wi[t]:
|
|
seg_lens[-1] += 1
|
|
else:
|
|
seg_wi.append(token_wi[t])
|
|
seg_lens.append(1)
|
|
result.append((seg_wi, seg_lens))
|
|
|
|
return result
|