Files
lightseekorg--tokenspeed/python/tokenspeed/runtime/utils/server_args.py
T
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

1991 lines
77 KiB
Python
Executable File

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""The arguments of the server."""
import argparse
import dataclasses
import json
import os
import random
from typing import Literal
from tokenspeed_kernel.ops.attention.triton.linear.chunk_delta_h import (
CHUNK_SIZE as FLA_CHUNK_SIZE,
)
from tokenspeed_kernel.platform import current_platform
from tokenspeed.runtime.distributed.mapping import Mapping, _resolve_parallelism_sizes
from tokenspeed.runtime.utils import (
get_amdgpu_memory_capacity,
get_colorful_logger,
get_nvgpu_memory_capacity,
is_valid_ipv6_address,
maybe_model_redirect,
nullable_str,
)
from tokenspeed.runtime.utils.network import is_port_available
logger = get_colorful_logger(__name__)
ENABLE_CP = os.environ.get("ENABLE_CP", "false").lower() in ("true", "1")
def str_to_bool(value: str | bool) -> bool:
if isinstance(value, bool):
return value
normalized = value.lower()
if normalized in {"1", "true", "yes", "on"}:
return True
if normalized in {"0", "false", "no", "off"}:
return False
raise argparse.ArgumentTypeError(f"invalid boolean value: {value!r}")
@dataclasses.dataclass
class ServerArgs:
# Model and tokenizer
model: str
tokenizer: str | None = None
tokenizer_mode: str = "auto"
skip_tokenizer_init: bool = False
load_format: str = "auto"
trust_remote_code: bool = True
dtype: str = "auto"
kv_cache_dtype: str = "auto"
kv_cache_quant_method: str = "none"
quantization: str | None = None
quantization_param_path: nullable_str = None
max_model_len: int | None = None
device: str = "cuda"
served_model_name: str | None = None
revision: str | None = None
language_model_only: bool = False
# Port for the HTTP server
host: str = "127.0.0.1"
port: int = 8000
# Memory and scheduling
gpu_memory_utilization: float | None = None
max_num_seqs: int | None = None
max_total_tokens: int | None = None
chunked_prefill_size: int | None = None
max_prefill_tokens: int = 8192
enable_mixed_batch: bool = False
block_size: int = 64
# special kv cache
mamba_ssm_dtype: str = "float32"
mamba_track_interval: int = 256
max_mamba_cache_size: int | None = None
mamba_full_memory_ratio: float = 0.9
enable_mamba_l2: bool = False
mamba_l2_host_slots: int = 0
mamba_l2_ratio: float = 2.0
mamba_l2_layout: str = "layer_first"
mamba_l2_io_backend: str = "kernel"
mamba_l2_host_gb: int = 0
# Other runtime options
stream_interval: int = 1
stream_output: bool = False
# Inline detokenization is the only supported path and is intentionally
# not configurable from the CLI.
enable_inline_detokenizer: bool = True
seed: int | None = None
distributed_timeout_seconds: int | None = None
download_dir: str | None = None
# Used for customizing extensible models
ext_yaml: str | None = None
base_gpu_id: int = 0
gpu_id_step: int = 1
# Logging
log_level: str = "info"
log_level_http: str | None = None
enable_log_requests: bool = False
log_requests_level: int = 0
enable_log_request_stats: bool = False
enable_metrics: bool = False
decode_log_interval: int = 40
metrics_reporters: list[str] | None = None
app_key: str | None = None
# API related
api_key: str | None = None
enable_cache_report: bool = False
kv_events_config: str | None = None
# Data parallelism
data_parallel_size: int | None = None
load_balance_method: str = "shortest_queue"
load_watch_interval: float = 0.02
# Expert parallelism
ep_size: int = 1
init_expert_location: str = "trivial"
ep_num_redundant_experts: int = 0
ep_dispatch_algorithm: (
Literal[
"static",
"dynamic",
"fake",
"static_with_zero_expert",
"dynamic_with_zero_expert",
]
| None
) = None
eplb_algorithm: str = "auto"
expert_distribution_recorder_mode: (
Literal["stat", "stat_approx", "per_pass", "per_token"] | None
) = None
expert_distribution_recorder_buffer_size: int | None = None
enable_expert_distribution_metrics: bool = False
enable_eplb: bool = False
# MoE backend
moe_backend: str = "auto"
draft_moe_backend: str | None = None
all2all_backend: str = "none"
deepep_mode: Literal["auto", "normal", "low_latency"] = "auto"
disable_flashinfer_cutlass_moe_fp4_allgather: bool = False
# KVStore
enable_kvstore: bool = False
kvstore_ratio: float = 2.0
kvstore_size: int = 0
kvstore_io_backend: str = "kernel"
kvstore_mem_layout: str = "layer_first"
kvstore_storage_backend: str | None = None
kvstore_storage_backend_extra_config: str | None = None
enable_mla_l1_5_cache: bool = False
# Multi-node distributed serving
dist_init_addr: str | None = None
nnodes: int = 1
node_rank: int = 0
# Hugging Face model config overrides in JSON
hf_overrides: str = "{}"
preferred_sampling_params: str | None = None
# Kernel backend
attention_backend: str | None = None
drafter_attention_backend: str | None = None
sampling_backend: str | None = None
dp_sampling: bool = False
dp_sampling_min_bs: int | None = None
attention_use_fp4_indexer_cache: bool | None = None
use_trtllm_ragged_deepseek_prefill: bool | None = None
# DeepSeek V4
deepseek_v4_mega_moe_max_num_tokens: int = 0
deepseek_v4_indexer_prefill_max_logits_mb: int = 512
deepseek_v4_prefill_chunk_size: int = 4
# Grammar backend
grammar_backend: str = "none"
# Used by ``input_processor`` to defer json_schema grammars past the
# model's reasoning channel.
reasoning_parser: str | None = None
grammar_compile_timeout_secs: float = 30.0
grammar_compile_max_retries: int = 2
disable_any_whitespace: bool = False
# Force the synchronous eager grammar fallback even on CUDA. Useful
# for parity-testing against the captured-grammar path (output should
# match; throughput will be lower since the sync stalls every step).
disable_capturable_grammar: bool = False
# Speculative decoding
draft_model_path_use_base: bool | None = False
speculative_config: str | None = None
speculative_algorithm: str | None = None
speculative_draft_model_path: str | None = None
speculative_draft_model_quantization: str | None = "unquant"
speculative_num_steps: int = 3
speculative_eagle_topk: int = 1
speculative_num_draft_tokens: int | None = None
eagle3_layers_to_capture: str | None = None
# Logprob support flags — all OFF by default. Enabling extends the
# captured CUDA-graph footprint; requests asking for logprobs on a
# server started without the matching flag will receive empty logprobs.
enable_output_logprobs: bool = False
# Runtime options
disable_pdl: bool = False
enable_prefix_caching: bool = True
disable_kvstore: bool = False
enforce_eager: bool = False
disable_cuda_graph_padding: bool = False
enable_cudagraph_gc: bool = False
enable_nccl_nvls: bool = False
enable_symm_mem: bool = False
disable_custom_all_reduce: bool = False
disable_overlap_schedule: bool = False
disable_tf32: bool = False
force_deterministic_rsag: bool = False
disable_sampling_tp_sync: bool = False
low_latency_max_num_tokens_per_gpu: int = 256
max_cudagraph_capture_size: int | None = None
disable_prefill_graph: bool | None = False
# Breakable prefill graph bucket cap: None = auto min(2048, chunk); 0 disables.
prefill_graph_max_tokens: int | None = None
# Explicit prefill bucket list; unset = the relative-stride ladder (see get_prefill_token_buckets).
prefill_graph_capture_sizes: list[int] | None = None
cudagraph_capture_sizes: list[int] | None = None
enable_nan_detection: bool = False
enable_nvtx: bool = False
enable_p2p_check: bool = False
triton_attention_reduce_in_fp32: bool = False
delete_ckpt_after_loading: bool = False
weight_loader_prefetch_checkpoints: bool = False
weight_loader_prefetch_num_threads: int = 4
enable_memory_saver: bool = False
enable_custom_logit_processor: bool = False
mla_disable_ragged: bool = False
warmups: str | None = None
# parallel strategy
nprocs_per_node: int | None = None
world_size: int | None = None
attn_tp_size: int | None = None
dense_tp_size: int | None = None
moe_tp_size: int | None = None
mapping: Mapping | None = None
mla_chunk_multiplier: int = 4
mm_attention_backend: str | None = None
# For PD/EPD disaggregation: "null", "prefill", "decode", or "encode" (vision-tower-only).
disaggregation_mode: str = "null"
disaggregation_bootstrap_port: int = 8998
disaggregation_transfer_backend: str = "mooncake"
disaggregation_ib_device: str | None = None
disaggregation_layerwise_interval: int = 1
pdlb_url: str | None = None
skip_server_warmup: bool = False
# For communication + norm fusion
comm_fusion_max_num_tokens: int = 2048
enable_allreduce_fusion: bool = False
enable_expert_parallel: bool = False
@property
def mamba_cache_chunk_size(self) -> int:
return max(FLA_CHUNK_SIZE, self.block_size)
def __post_init__(self):
self.resolve_basic_defaults()
self.resolve_parallelism()
self.resolve_memory_and_scheduling()
self.resolve_kernel_backends()
self.resolve_cache()
self.resolve_speculative_decoding()
self.resolve_communication()
self.resolve_disaggregation()
self.validate()
def resolve_basic_defaults(self):
self.model = maybe_model_redirect(self.model)
if self.kv_cache_dtype == "fp8":
self.kv_cache_dtype = "fp8_e4m3"
self.resolve_config_aliases()
# Set missing default values
if self.tokenizer is None:
self.tokenizer = self.model
if self.served_model_name is None:
self.served_model_name = self.model
if self.seed is None:
self.seed = random.randint(0, 1 << 30)
def resolve_config_aliases(self):
if self.use_trtllm_ragged_deepseek_prefill is not None:
self.mla_disable_ragged = not self.use_trtllm_ragged_deepseek_prefill
if self.speculative_config is not None:
try:
config = json.loads(self.speculative_config)
except json.JSONDecodeError as exc:
raise ValueError("--speculative-config must be valid JSON") from exc
if not isinstance(config, dict):
raise ValueError("--speculative-config must be a JSON object")
method = config.get("method")
if method is not None and self.speculative_algorithm is None:
self.speculative_algorithm = str(method).upper()
draft_model = config.get("model")
if draft_model is not None and self.speculative_draft_model_path is None:
self.speculative_draft_model_path = str(draft_model)
num_speculative_tokens = config.get("num_speculative_tokens")
if num_speculative_tokens is not None:
num_speculative_tokens = int(num_speculative_tokens)
if self.speculative_algorithm == "DFLASH":
if self.speculative_num_draft_tokens is None:
self.speculative_num_draft_tokens = num_speculative_tokens
self.speculative_num_steps = max(num_speculative_tokens - 1, 0)
else:
self.speculative_num_steps = num_speculative_tokens
if self.speculative_num_draft_tokens is None:
self.speculative_num_draft_tokens = self.speculative_num_steps + 1
def resolve_memory_and_scheduling(self):
if current_platform().is_amd:
gpu_mem = get_amdgpu_memory_capacity()
elif current_platform().is_nvidia:
gpu_mem = get_nvgpu_memory_capacity()
else:
# GPU memory is not known yet or no GPU is available.
gpu_mem = None
# Set GPU memory utilization, which depends on the tensor parallelism size.
self._gpu_memory_utilization_defaulted = False
if self.gpu_memory_utilization is None:
if self.mapping.world_size >= 16:
self.gpu_memory_utilization = 0.79
elif self.mapping.world_size >= 8:
self.gpu_memory_utilization = 0.81
elif self.mapping.world_size >= 4:
self.gpu_memory_utilization = 0.95
elif self.mapping.world_size >= 2:
self.gpu_memory_utilization = 0.87
else:
self.gpu_memory_utilization = 0.88
self._gpu_memory_utilization_defaulted = True
# Set the chunked prefill token budget.
if self.chunked_prefill_size is None:
self.chunked_prefill_size = 8192
# Set CUDA graph max capture size.
if self.max_cudagraph_capture_size is None:
# Based on detailed statistics, when serving TP1/TP2 models on lower-end GPUs with HBM<25G, you can either disable CUDA graph or set max_cudagraph_capture_size to a very small value to reduce graph memory overhead, with almost no impact on performance. TP4/TP8 serving still needs CUDA graph for high performance, and 80 is enough for lower-end GPUs.
if gpu_mem is not None and gpu_mem < 25_000:
if self.mapping.world_size < 4:
self.max_cudagraph_capture_size = 8
else:
self.max_cudagraph_capture_size = 80
elif self.speculative_algorithm:
self.max_cudagraph_capture_size = 80
else:
self.max_cudagraph_capture_size = 160
# Set max number of sequences.
if self.max_num_seqs is None:
if self.speculative_algorithm:
self.max_num_seqs = 80
else:
self.max_num_seqs = 160
def resolve_kernel_backends(self):
# Choose kernel backends
# attention_backend default is NOT set here — deferred to
# AttnInitializer.modify_args where both hardware and model arch are known.
if self.sampling_backend is None:
# ``flashinfer`` is the only built-in backend that respects per-request
# ``temperature`` / ``top_p`` / ``top_k``. ``greedy`` is argmax-only
# (see ``GreedySamplingBackend.sample``: *"sampling_info is ignored
# for single-step (always argmax)"*) — fast for hand-tuned greedy
# decoding but silently wrong for any serving deployment where
# requests carry sampling params, since the model collapses into
# repetition-mode loops within a few hundred steps. Default to the
# sampling-respecting backend on NVIDIA where flashinfer is
# available, fall back to greedy elsewhere; users can still opt
# into greedy explicitly via ``--sampling-backend greedy``.
if current_platform().is_nvidia:
self.sampling_backend = "flashinfer"
else:
self.sampling_backend = "greedy"
def resolve_parallelism(self):
world_size = self.world_size
nprocs_per_node = self.nprocs_per_node
nnodes = 1 if self.nnodes is None else self.nnodes
attn_tp_size = self.attn_tp_size
attn_dp_size = self.data_parallel_size
# ``ENABLE_CP`` interprets attention TP size as CP size.
attn_cp_size = 1
if ENABLE_CP:
attn_cp_size, attn_tp_size = attn_tp_size, 1
if world_size is None:
world_size = 1
if attn_tp_size is not None:
world_size *= attn_tp_size
if attn_cp_size is not None:
world_size *= attn_cp_size
if attn_dp_size is not None:
world_size *= attn_dp_size
logger.info(
"Inferred world_size (%s) from attn_tp_size (%s) x attn_cp_size (%s) x attn_dp_size (%s)",
world_size,
attn_tp_size,
attn_cp_size,
attn_dp_size,
)
else:
logger.info("Specified world_size (%s)", world_size)
attn_tp_size, attn_cp_size, attn_dp_size = _resolve_parallelism_sizes(
world_size, attn_tp_size, attn_cp_size, attn_dp_size
)
# Dense layers still default to full TP participation when no
# dedicated dense_tp_size is provided.
dense_tp_size = self.dense_tp_size
if self.dense_tp_size is None:
# dense always do tp now.
dense_tp_size = world_size
dense_dp_size = None
# --enable-expert-parallel auto-sets ep_size = world_size
if self.enable_expert_parallel and self.ep_size == 1:
self.ep_size = world_size
logger.info("--enable-expert-parallel: auto-setting ep_size=%s", world_size)
# MoE parallel sizes default to consuming the full world size unless
# the user overrides them explicitly.
moe_ep_size = 1 if self.ep_size is None else self.ep_size
moe_tp_size = (
world_size // moe_ep_size if self.moe_tp_size is None else self.moe_tp_size
)
moe_dp_size = None
self.mapping = Mapping(
world_size=world_size,
attn_tp_size=attn_tp_size,
attn_cp_size=attn_cp_size,
attn_dp_size=attn_dp_size,
dense_tp_size=dense_tp_size,
dense_dp_size=dense_dp_size,
moe_tp_size=moe_tp_size,
moe_ep_size=moe_ep_size,
moe_dp_size=moe_dp_size,
nprocs_per_node=nprocs_per_node,
nnodes=nnodes,
base_gpu_id=self.base_gpu_id,
gpu_id_step=self.gpu_id_step,
)
# Impl constraints:
if self.mapping.moe.has_tp and self.mapping.moe.has_ep:
raise ValueError("MoE TP and EP cannot be both > 1")
logger.info("Parallelism configuration:\n%s", self.mapping)
def resolve_cache(self):
# Handle KVStore settings.
self._handle_kvstore()
self.validate_cache_options()
def resolve_speculative_decoding(self):
# Keep drafter backend consistent with the main model unless explicitly set.
if (
self.speculative_algorithm is not None
and self.drafter_attention_backend is None
):
self.drafter_attention_backend = self.attention_backend
if (
self.speculative_algorithm == "MTP"
and self.speculative_draft_model_path is None
):
self.draft_model_path_use_base = True
if self.draft_model_path_use_base:
self.speculative_draft_model_path = self.model
if self.speculative_draft_model_path == self.model:
self.draft_model_path_use_base = True
if self.speculative_draft_model_quantization == "unquant":
self.speculative_draft_model_quantization = None
if self.speculative_algorithm == "DFLASH":
expected_steps = max(int(self.speculative_num_draft_tokens) - 1, 0)
if self.speculative_num_steps == ServerArgs.speculative_num_steps:
self.speculative_num_steps = expected_steps
elif self.speculative_num_steps != expected_steps:
raise ValueError(
"DFLASH requires speculative_num_steps to equal "
"speculative_num_draft_tokens - 1. "
f"Got {self.speculative_num_steps=} and "
f"{self.speculative_num_draft_tokens=}."
)
if self.eagle3_layers_to_capture is not None:
self.eagle3_layers_to_capture = [
int(x) for x in self.eagle3_layers_to_capture.split(",")
]
# Hoist the PD-decode topk == 1 check to startup.
if self.speculative_algorithm is not None and self.speculative_eagle_topk != 1:
raise ValueError(
"speculative_eagle_topk > 1 (tree spec) is not currently "
f"supported: {self.speculative_eagle_topk=}. Only chain spec "
"(topk=1) is wired end-to-end."
)
def resolve_communication(self):
# Auto-enable allreduce fusion on supported single-node TP configurations.
platform = current_platform()
if (
not self.enable_allreduce_fusion
and (current_platform().is_hopper_plus or platform.is_amd)
and self.mapping.nnodes == 1
and self.mapping.has_attn_tp
and not self.mapping.has_attn_dp
):
self.enable_allreduce_fusion = True
logger.info("Auto-enabled allreduce fusion")
if self.mapping.attn.tp_size != self.mapping.dense.tp_size:
self.comm_fusion_max_num_tokens = -1
self.enable_allreduce_fusion = False
logger.info(
"allreduce is forbidden due to different attn_tp_size: %s and dense_tp_size: %s!",
self.mapping.attn.tp_size,
self.mapping.dense.tp_size,
)
def resolve_disaggregation(self):
# PD disaggregation
if self.disaggregation_mode == "prefill":
self.enforce_eager = True
logger.warning("CUDA graph is disabled for prefill server")
elif self.disaggregation_mode == "decode":
# Prefix caching stays configurable for decode servers.
logger.info(
"enable_prefix_caching=%r for decode server",
self.enable_prefix_caching,
)
elif self.disaggregation_mode == "encode":
# Encode server: vision tower only, no LM / KV pool / prefix cache.
# enforce_eager left as-is (the vision tower keeps its own CUDA graph).
if self.mapping.has_attn_dp:
raise ValueError(
"disaggregation_mode=encode currently supports "
"data_parallel_size == 1 inside one encode server; run "
"multiple independent encode servers for horizontal scale."
)
self.enable_prefix_caching = False
# Prefill graph disable logic is handled by AttnInitializer.modify_args
# after the attention backend is resolved.
if (
self.disaggregation_mode == "prefill"
and self.load_balance_method != "round_robin"
):
if self.mapping.has_attn_dp:
raise ValueError(
"Not supported when "
f"{self.disaggregation_mode=} {self.load_balance_method=} "
f"{self.mapping.attn.dp_size=}"
)
def _handle_kvstore(self):
if self.disaggregation_mode in ("decode", "encode"):
self.enable_kvstore = False
logger.info(
"%s instance has set enable_kvstore to False!",
self.disaggregation_mode,
)
elif not self.disable_kvstore:
self.enable_kvstore = True
if self.kvstore_storage_backend == "mooncake":
if self.kvstore_mem_layout == "layer_first":
self.kvstore_mem_layout = "page_first"
logger.warning(
"Mooncake storage backend does not support layer_first layout, switching to %s layout",
self.kvstore_mem_layout,
)
if self.kvstore_io_backend == "direct":
self.kvstore_io_backend = "kernel"
logger.warning(
"Mooncake storage backend uses page_first layout, which requires kernel io backend"
)
def validate_cache_options(self):
if self.enable_kvstore and not self.enable_prefix_caching:
raise ValueError(
"KVStore and disabled prefix caching are mutually exclusive "
"and cannot be used at the same time. Please use only one of them."
)
def validate(self):
if (
self.max_num_seqs is not None
and self.max_num_seqs < self.mapping.attn.dp_size
):
raise ValueError(
f"max_num_seqs must be >= attn_dp_size: {self.max_num_seqs=} < {self.mapping.attn.dp_size=}"
)
if self.mapping.has_attn_cp and self.max_num_seqs > 1:
raise ValueError("CP attention is enabled but max_num_seqs > 1")
if self.mapping.has_attn_dp:
if self.chunked_prefill_size > self.max_prefill_tokens:
raise ValueError(
f"chunked_prefill_size must be <= max_prefill_tokens: {self.chunked_prefill_size=} > {self.max_prefill_tokens=}"
)
if self.deepseek_v4_prefill_chunk_size <= 0:
raise ValueError("deepseek_v4_prefill_chunk_size must be positive")
if self.enable_eplb and (self.expert_distribution_recorder_mode is None):
self.expert_distribution_recorder_mode = "stat"
logger.info(
"EPLB is enabled. The expert_distribution_recorder_mode is automatically set."
)
if (self.enable_eplb or (self.init_expert_location is not None)) and (
self.ep_dispatch_algorithm is None
):
self.ep_dispatch_algorithm = "static"
logger.info(
"EPLB is enabled or init_expert_location is provided. ep_dispatch_algorithm is configured."
)
from tokenspeed.runtime.utils.env import envs
envs.TOKENSPEED_MAMBA_SSM_DTYPE.set(self.mamba_ssm_dtype)
if not self.disable_pdl:
os.environ.setdefault("TORCHINDUCTOR_ENABLE_PDL", "1")
# Enable PDL for fused attention kernels.
os.environ.setdefault("TRTLLM_ENABLE_PDL", "1")
os.environ.setdefault("TLLM_LOG_LEVEL", "INFO")
@staticmethod
def add_cli_args(parser: argparse.ArgumentParser):
parser.allow_abbrev = False
# Model and port args
parser.add_argument(
"model_path",
nargs="?",
metavar="model",
default=None,
help="The model name or path (positional argument). "
"Equivalent to --model.",
)
parser.add_argument(
"--model",
"--model-path",
metavar="MODEL",
type=str,
default=None,
help="The path of the model weights. This can be a local folder or a Hugging Face repo ID.",
)
parser.add_argument(
"--tokenizer",
metavar="TOKENIZER",
type=str,
default=ServerArgs.tokenizer,
help="The path of the tokenizer.",
)
parser.add_argument(
"--host", type=str, default=ServerArgs.host, help="The host of the server."
)
parser.add_argument(
"--port", type=int, default=ServerArgs.port, help="The port of the server."
)
parser.add_argument(
"--tokenizer-mode",
type=str,
default=ServerArgs.tokenizer_mode,
choices=["auto", "slow", "deepseek_v4"],
help="Tokenizer mode. 'auto' will use the fast "
"tokenizer and model-specific tokenizer hooks if available, "
"'slow' will always use the slow tokenizer.",
)
parser.add_argument(
"--skip-tokenizer-init",
action=argparse.BooleanOptionalAction,
default=ServerArgs.skip_tokenizer_init,
help="If set, skip init tokenizer and pass input_ids in generate request",
)
parser.add_argument(
"--language-model-only",
action="store_true",
default=ServerArgs.language_model_only,
help="Skip vision/audio encoders on a multimodal checkpoint and "
"run text-only. Multimodal requests are rejected.",
)
parser.add_argument("--ext-yaml", type=str, default=None)
parser.add_argument(
"--load-format",
type=str,
default=ServerArgs.load_format,
choices=[
"auto",
"pt",
"safetensors",
"npcache",
"dummy",
"extensible",
],
help="The format of the model weights to load. "
'"auto" will try to load the weights in the safetensors format '
"and fall back to the pytorch bin format if safetensors format "
"is not available. "
'"pt" will load the weights in the pytorch bin format. '
'"safetensors" will load the weights in the safetensors format. '
'"npcache" will load the weights in pytorch format and store '
"a numpy cache to speed up the loading. "
'"dummy" will initialize the weights with random values.',
)
parser.add_argument(
"--trust-remote-code",
action=argparse.BooleanOptionalAction,
default=False,
help="Whether or not to allow for custom models defined on the Hub in their own modeling files.",
)
parser.add_argument(
"--dtype",
type=str,
default=ServerArgs.dtype,
choices=["auto", "half", "float16", "bfloat16", "float", "float32"],
help="Data type for model weights and activations.\n\n"
'* "auto" will use FP16 precision for FP32 and FP16 models, and '
"BF16 precision for BF16 models.\n"
'* "half" for FP16. Recommended for AWQ quantization.\n'
'* "float16" is the same as "half".\n'
'* "bfloat16" for a balance between precision and range.\n'
'* "float" is shorthand for FP32 precision.\n'
'* "float32" for FP32 precision.',
)
parser.add_argument(
"--kv-cache-dtype",
type=str,
default=ServerArgs.kv_cache_dtype,
choices=["auto", "fp8", "fp8_e4m3"],
help='Data type for kv cache storage. "auto" will use model data type. "fp8" is an alias for "fp8_e4m3".',
)
parser.add_argument(
"--kv-cache-quant-method",
type=str,
default=ServerArgs.kv_cache_quant_method,
choices=["none", "per_token_head"],
help="kv cache quant method",
)
parser.add_argument(
"--quantization",
type=str,
default=ServerArgs.quantization,
choices=[
"fp8",
"mxfp4",
"nvfp4",
"w8a8_fp8",
"compressed-tensors",
],
help="The quantization method.",
)
parser.add_argument(
"--quantization-param-path",
type=nullable_str,
default=None,
help="Path to the JSON file containing the KV cache "
"scaling factors. This should generally be supplied, when "
"KV cache dtype is FP8. Otherwise, KV cache scaling factors "
"default to 1.0, which may cause accuracy issues. ",
)
parser.add_argument(
"--max-model-len",
metavar="MAX_MODEL_LEN",
type=int,
default=ServerArgs.max_model_len,
help="The model's maximum context length. Defaults to None (will use the value from the model's config.json instead).",
)
parser.add_argument(
"--device",
type=str,
default="cuda",
choices=["cuda"],
help="The device type.",
)
parser.add_argument(
"--served-model-name",
type=str,
default=ServerArgs.served_model_name,
help="Override the model name returned by the v1/models endpoint in OpenAI API server.",
)
parser.add_argument(
"--revision",
type=str,
default=None,
help="The specific model version to use. It can be a branch "
"name, a tag name, or a commit id. If unspecified, will use "
"the default version.",
)
# Memory and scheduling
parser.add_argument(
"--gpu-memory-utilization",
metavar="GPU_MEMORY_UTILIZATION",
type=float,
default=ServerArgs.gpu_memory_utilization,
help="The fraction of GPU memory to use for model weights and KV cache. Use a smaller value if you see out-of-memory errors.",
)
parser.add_argument(
"--max-num-seqs",
metavar="MAX_NUM_SEQS",
type=int,
default=ServerArgs.max_num_seqs,
help="Maximum number of sequences to process concurrently.",
)
parser.add_argument(
"--max-total-tokens",
type=int,
default=ServerArgs.max_total_tokens,
help="The maximum number of tokens in the memory pool. If not specified, it will be automatically calculated based on the memory usage fraction. "
"This overrides the automatically calculated token pool size.",
)
parser.add_argument(
"--chunked-prefill-size",
metavar="CHUNKED_PREFILL_SIZE",
type=int,
default=ServerArgs.chunked_prefill_size,
help="Maximum number of tokens the scheduler may issue in a single iteration. Setting this to -1 disables chunked prefill.",
)
parser.add_argument(
"--enable-mixed-batch",
action="store_true",
dest="enable_mixed_batch",
default=ServerArgs.enable_mixed_batch,
help="Allow the scheduler to issue prefill and decode requests in the same iteration.",
)
parser.add_argument(
"--block-size",
metavar="BLOCK_SIZE",
type=int,
default=ServerArgs.block_size,
)
# KVStore
parser.add_argument(
"--disable-kvstore",
action="store_true",
help="Disable KVStore",
)
parser.add_argument(
"--kvstore-ratio",
type=float,
default=ServerArgs.kvstore_ratio,
help="The ratio of the size of the KVStore host memory pool to the size of the device pool.",
)
parser.add_argument(
"--kvstore-size",
type=int,
default=ServerArgs.kvstore_size,
help="The size of the KVStore host memory pool in gigabytes, which will override kvstore_ratio if set.",
)
parser.add_argument(
"--kvstore-io-backend",
type=str,
choices=["direct", "kernel"],
default=ServerArgs.kvstore_io_backend,
help="The IO backend for KVStore transfer between CPU and GPU.",
)
parser.add_argument(
"--kvstore-mem-layout",
type=str,
choices=[
"layer_first",
"page_first",
"page_head",
],
default=ServerArgs.kvstore_mem_layout,
help="The layout of the KVStore host memory pool.",
)
parser.add_argument(
"--kvstore-storage-backend",
type=str,
choices=["mooncake"],
default=ServerArgs.kvstore_storage_backend,
help="The storage backend for KVStore. "
"Built-in backends: mooncake. "
"For dynamic backend, use --kvstore-storage-backend-extra-config to specify: "
"backend_name (custom name), module_path (Python module path), class_name (backend class name).",
)
parser.add_argument(
"--kvstore-storage-backend-extra-config",
type=str,
default=ServerArgs.kvstore_storage_backend_extra_config,
help="A dictionary in JSON string format containing extra configuration for the storage backend.",
)
parser.add_argument(
"--enable-mla-l1-5-cache",
action="store_true",
help="Enable MLA L1.5 cache in disaggregation paths.",
)
# Mamba Cache
parser.add_argument(
"--mamba-ssm-dtype",
type=str,
default=ServerArgs.mamba_ssm_dtype,
choices=["float32", "bfloat16"],
help="It is used to tune mamba ssm dtype",
)
parser.add_argument(
"--mamba-track-interval",
type=int,
default=ServerArgs.mamba_track_interval,
help="The interval to track the mamba state during decode.",
)
parser.add_argument(
"--max-mamba-cache-size",
type=int,
default=ServerArgs.max_mamba_cache_size,
help="The maximum number of Mamba cache chunks. If unset, the pool size is profiled from available memory.",
)
parser.add_argument(
"--mamba-full-memory-ratio",
type=float,
default=ServerArgs.mamba_full_memory_ratio,
help="Memory ratio used to split cache budget between Mamba state chunks and full-attention KV cache.",
)
parser.add_argument(
"--enable-mamba-l2",
action="store_true",
help="Enable host-memory L2 cache for Mamba state slots.",
)
parser.add_argument(
"--mamba-l2-host-slots",
type=int,
default=ServerArgs.mamba_l2_host_slots,
help="Number of host Mamba L2 slots. If 0, derive from --mamba-l2-host-gb or --mamba-l2-ratio.",
)
parser.add_argument(
"--mamba-l2-ratio",
type=float,
default=ServerArgs.mamba_l2_ratio,
help="Mamba host L2 slot ratio relative to device Mamba slots when host slots are not explicit.",
)
parser.add_argument(
"--mamba-l2-layout",
type=str,
choices=["layer_first"],
default=ServerArgs.mamba_l2_layout,
help="Mamba host L2 memory layout.",
)
parser.add_argument(
"--mamba-l2-io-backend",
type=str,
choices=["direct", "kernel"],
default=ServerArgs.mamba_l2_io_backend,
help="IO backend for Mamba L2 host/device transfers.",
)
parser.add_argument(
"--mamba-l2-host-gb",
type=int,
default=ServerArgs.mamba_l2_host_gb,
help="Mamba L2 host memory budget in GiB. Overrides --mamba-l2-ratio when host slots are not explicit.",
)
parser.add_argument(
"--max-prefill-tokens",
metavar="MAX_PREFILL_TOKENS",
type=int,
default=ServerArgs.max_prefill_tokens,
help=(
"Maximum prefill-token budget used when chunked prefill is "
"disabled. Per-iteration scheduling is controlled by "
"--chunked-prefill-size."
),
)
# Other runtime options
parser.add_argument(
"--stream-interval",
type=int,
default=ServerArgs.stream_interval,
help="The interval (or buffer size) for streaming in terms of the token length. A smaller value makes streaming smoother, while a larger value makes the throughput higher",
)
parser.add_argument(
"--stream-output",
action="store_true",
help="Whether to output as a sequence of disjoint segments.",
)
parser.add_argument(
"--seed",
metavar="SEED",
type=int,
default=ServerArgs.seed,
help="The random seed.",
)
parser.add_argument(
"--distributed-timeout-seconds",
metavar="DISTRIBUTED_TIMEOUT_SECONDS",
type=int,
default=ServerArgs.distributed_timeout_seconds,
help="Set timeout for torch.distributed initialization.",
)
parser.add_argument(
"--download-dir",
type=str,
default=ServerArgs.download_dir,
help="Model download directory for huggingface.",
)
parser.add_argument(
"--base-gpu-id",
type=int,
default=ServerArgs.base_gpu_id,
help="The base GPU ID to start allocating GPUs from. Useful when running multiple instances on the same machine.",
)
parser.add_argument(
"--gpu-id-step",
type=int,
default=ServerArgs.gpu_id_step,
help="The delta between consecutive GPU IDs that are used. For example, setting it to 2 will use GPU 0,2,4,...",
)
# Logging
parser.add_argument(
"--log-level",
type=str,
default=ServerArgs.log_level,
help="The logging level of all loggers.",
)
parser.add_argument(
"--log-level-http",
type=str,
default=ServerArgs.log_level_http,
help="The logging level of HTTP server. If not set, reuse --log-level by default.",
)
parser.add_argument(
"--enable-log-requests",
action=argparse.BooleanOptionalAction,
default=ServerArgs.enable_log_requests,
help="Log metadata, inputs, outputs of all requests. The verbosity is decided by --log-requests-level",
)
parser.add_argument(
"--log-requests-level",
type=int,
default=0,
help="0: Log metadata. 1. Log metadata and partial input/output. 2. Log every input/output.",
choices=[0, 1, 2],
)
parser.add_argument(
"--enable-log-request-stats",
action=argparse.BooleanOptionalAction,
default=ServerArgs.enable_log_request_stats,
help=(
"Log a one-line per-request performance summary when each request "
"finishes or aborts: timings (queue/prefill/ttft/total/preemption), "
"token counts (prompt/cache/output), cache-hit rate, decode "
"throughput, and spec-decode acceptance. Measured entirely on the "
"host (no GPU sync), so it adds no engine slowdown."
),
)
parser.add_argument(
"--enable-metrics",
action="store_true",
help="Enable log metrics.",
)
parser.add_argument(
"--metrics-reporters",
action="append",
choices=["prometheus"],
default=["prometheus"],
help="Select metrics reporter(can be specified multiple times)",
)
parser.add_argument(
"--app-key",
type=str,
default=ServerArgs.app_key,
help="Set app key of the server",
)
parser.add_argument(
"--decode-log-interval",
type=int,
default=ServerArgs.decode_log_interval,
help="The log interval of decode batch.",
)
# API related
parser.add_argument(
"--api-key",
type=str,
default=ServerArgs.api_key,
help="Set API key of the server. It is also used in the OpenAI API compatible server.",
)
parser.add_argument(
"--enable-cache-report",
action="store_true",
help="Return number of cached tokens in usage.prompt_tokens_details for each openai request.",
)
parser.add_argument(
"--kv-events-config",
type=str,
default=ServerArgs.kv_events_config,
help=(
"JSON KV cache event publisher config. Set "
"'enable_kv_cache_events': true and publisher 'zmq' to "
"publish device prefix-cache mutations."
),
)
# Data parallelism
parser.add_argument(
"--data-parallel-size",
metavar="DATA_PARALLEL_SIZE",
type=int,
default=ServerArgs.data_parallel_size,
help="The data parallelism size. If not set, inferred from world_size and attn_tp_size.",
)
parser.add_argument(
"--load-balance-method",
type=str,
default=ServerArgs.load_balance_method,
help="The load balancing strategy for data parallelism.",
choices=[
"round_robin",
"shortest_queue",
"minimum_cache_usage",
],
)
parser.add_argument(
"--load-watch-interval",
type=float,
default=ServerArgs.load_watch_interval,
help="The interval of load watching in seconds.",
)
# Expert parallelism
parser.add_argument(
"--expert-parallel-size",
"--ep-size",
type=int,
default=ServerArgs.ep_size,
help="The expert parallelism size.",
)
parser.add_argument(
"--init-expert-location",
type=str,
default=ServerArgs.init_expert_location,
help="Initial location of EP experts.",
)
parser.add_argument(
"--ep-num-redundant-experts",
type=int,
default=ServerArgs.ep_num_redundant_experts,
help="Allocate this number of redundant experts in expert parallel.",
)
parser.add_argument(
"--ep-dispatch-algorithm",
type=str,
default=ServerArgs.ep_dispatch_algorithm,
help="The algorithm to choose ranks for redundant experts in expert parallel.",
)
parser.add_argument(
"--eplb-algorithm",
type=str,
default=ServerArgs.eplb_algorithm,
help="Chosen EPLB algorithm",
)
parser.add_argument(
"--expert-distribution-recorder-mode",
type=str,
default=ServerArgs.expert_distribution_recorder_mode,
help="Mode of expert distribution recorder.",
)
parser.add_argument(
"--expert-distribution-recorder-buffer-size",
type=int,
default=ServerArgs.expert_distribution_recorder_buffer_size,
help="Circular buffer size of expert distribution recorder. Set to -1 to denote infinite buffer.",
)
parser.add_argument(
"--enable-expert-distribution-metrics",
action="store_true",
help="Enable logging metrics for expert balancedness",
)
parser.add_argument(
"--enable-eplb",
action="store_true",
help="Enable EPLB algorithm",
)
parser.add_argument(
"--moe-backend",
type=str,
default=ServerArgs.moe_backend,
help="MoE runner backend: auto, triton, gluon, flashinfer_trtllm",
)
parser.add_argument(
"--draft-moe-backend",
type=str,
default=ServerArgs.draft_moe_backend,
help="MoE runner backend for the draft model in speculative decoding. "
"If not set, defaults to --moe-backend.",
)
parser.add_argument(
"--all2all-backend",
metavar="ALL2ALL_BACKEND",
type=str,
default=ServerArgs.all2all_backend,
help="MoE all-to-all backend: none, deepep, etc.",
)
parser.add_argument(
"--deepep-mode",
type=str,
choices=["normal", "low_latency", "auto"],
default=ServerArgs.deepep_mode,
help="Select the mode when enable DeepEP MoE, could be `normal`, `low_latency` or `auto`. Default is `auto`, which means `low_latency` for decode batch and `normal` for prefill batch.",
)
parser.add_argument(
"--disable-flashinfer-cutlass-moe-fp4-allgather",
action="store_true",
help="Disable flashinfer cutlass MoE FP4 allgather.",
)
# Multi-node distributed serving
parser.add_argument(
"--dist-init-addr",
type=str,
help="The host address for initializing distributed backend (e.g., `192.168.0.2:25000`).",
)
parser.add_argument(
"--nnodes", type=int, default=ServerArgs.nnodes, help="The number of nodes."
)
parser.add_argument(
"--node-rank", type=int, default=ServerArgs.node_rank, help="The node rank."
)
# Model override args
parser.add_argument(
"--hf-overrides",
metavar="HF_OVERRIDES",
type=str,
help="A dictionary in JSON string format used to override default model configurations.",
default=ServerArgs.hf_overrides,
)
parser.add_argument(
"--preferred-sampling-params",
type=str,
help="json-formatted sampling settings that will be returned in /get_model_info",
)
# Kernel backend
attention_backend_choices = [
"mha",
"mla",
"fa3",
"fa4",
"triton",
"flashinfer",
"trtllm",
"trtllm_mla",
"flashmla",
"tokenspeed_mla",
"hybrid_linear_attn",
]
parser.add_argument(
"--attention-backend",
type=str,
choices=attention_backend_choices,
default=ServerArgs.attention_backend,
help="Choose the kernels for attention layers.",
)
parser.add_argument(
"--drafter-attention-backend",
type=str,
choices=attention_backend_choices,
help="Attention backend for drafter model in speculative decoding. "
"If not specified, uses the same backend as the main model (attention_backend).",
)
parser.add_argument(
"--sampling-backend",
type=str,
choices=[
"greedy",
"flashinfer",
"flashinfer_full",
"triton",
"triton_full",
],
default=ServerArgs.sampling_backend,
help="Sampling backend. "
"'greedy': argmax + verify_chain_greedy, zero sampling-param plumbing. "
"'flashinfer': temperature/top_k/top_p via fused softmax + top_k_top_p_sampling_from_probs; "
"min_p and penalties silently ignored. "
"'triton': temperature/top_k/top_p via MRV2-style logits-to-Gumbel-Max; "
"min_p and penalties silently ignored. "
"'flashinfer_full': adds min_p plus frequency/presence/repetition penalties and logit_bias "
"via the softmax+renorm+min_p kernel sequence. "
"'triton_full': adds min_p plus frequency/presence/repetition penalties and logit_bias "
"with Triton Gumbel-Max for single-step sampling. "
"Allocates a counts[max_req_pool_size, vocab_size] int32 buffer (substantial memory). "
"Finite top_k values must be < 128 or -1.",
)
parser.add_argument(
"--dp-sampling",
action="store_true",
default=ServerArgs.dp_sampling,
help=(
"Enable Batch-DP spec-verify sampling. Backend selection defaults "
"to auto; override with TOKENSPEED_DP_SAMPLING_BACKEND."
),
)
parser.add_argument(
"--dp-sampling-min-bs",
type=int,
default=ServerArgs.dp_sampling_min_bs,
help="Minimum effective decode batch for Batch-DP spec-verify. "
"Defaults to 2 * TP size.",
)
parser.add_argument(
"--attention-use-fp4-indexer-cache",
"--attention-config.use-fp4-indexer-cache",
"--attention_config.use_fp4_indexer_cache",
type=str_to_bool,
nargs="?",
const=True,
default=ServerArgs.attention_use_fp4_indexer_cache,
help="Use the MXFP4 sparse attention indexer cache layout.",
)
parser.add_argument(
"--attention-config.use-trtllm-ragged-deepseek-prefill",
"--attention-config.use_trtllm_ragged_deepseek_prefill",
"--attention_config.use_trtllm_ragged_deepseek_prefill",
dest="use_trtllm_ragged_deepseek_prefill",
type=str_to_bool,
nargs="?",
const=True,
default=ServerArgs.use_trtllm_ragged_deepseek_prefill,
help="Use ragged prefill for DeepSeek MLA attention.",
)
parser.add_argument(
"--deepseek-v4-mega-moe-max-num-tokens",
type=int,
default=ServerArgs.deepseek_v4_mega_moe_max_num_tokens,
help=(
"DeepSeek V4 MegaMoE staging-buffer cap on tokens per forward "
"(0 = derive from chunked-prefill / cuda-graph budgets)."
),
)
parser.add_argument(
"--deepseek-v4-indexer-prefill-max-logits-mb",
type=int,
default=ServerArgs.deepseek_v4_indexer_prefill_max_logits_mb,
help=(
"DeepSeek V4 sparse indexer prefill workspace cap (MiB) for the "
"softplus_sqrt logits buffer."
),
)
parser.add_argument(
"--deepseek-v4-prefill-chunk-size",
type=int,
default=ServerArgs.deepseek_v4_prefill_chunk_size,
help=(
"Maximum number of requests per DeepSeek V4 FlashMLA prefill " "chunk."
),
)
parser.add_argument(
"--grammar-backend",
type=str,
choices=["xgrammar", "none"],
default=ServerArgs.grammar_backend,
help="Grammar backend. 'none' disables grammar-guided decoding entirely ",
)
parser.add_argument(
"--reasoning-parser",
type=str,
default=ServerArgs.reasoning_parser,
help=(
"Reasoning parser name (e.g. 'minimax', 'kimi_k25'). "
"Used to defer json_schema grammars past the model's "
"reasoning channel."
),
)
parser.add_argument(
"--grammar-compile-timeout-secs",
type=float,
default=ServerArgs.grammar_compile_timeout_secs,
help="Per-compile wallclock budget before the request is aborted.",
)
parser.add_argument(
"--grammar-compile-max-retries",
type=int,
default=ServerArgs.grammar_compile_max_retries,
help="Compile timeouts allowed before a grammar key is permanently rejected.",
)
parser.add_argument(
"--disable-any-whitespace",
action="store_true",
default=ServerArgs.disable_any_whitespace,
help="Compile xgrammar JSON grammars in tight mode (no arbitrary "
"whitespace between tokens). Mitigates models that wedge into "
"endless whitespace until length cutoff. xgrammar only.",
)
parser.add_argument(
"--disable-capturable-grammar",
action="store_true",
default=ServerArgs.disable_capturable_grammar,
help="Force the synchronous eager grammar fallback even on CUDA. "
"For parity-testing the captured-grammar path: output should "
"match; throughput will be lower (sync stall every step).",
)
parser.add_argument(
"--mla-disable-ragged",
action="store_true",
help="Disable the ragged prefill wrapper on MLA kernel backends during EXTEND.",
)
# Speculative decoding
parser.add_argument(
"--draft-model-path-use-base",
action="store_true",
help="The path of the draft model weights use the path of the base model",
)
parser.add_argument(
"--speculative-config",
"--speculative_config",
type=str,
default=ServerArgs.speculative_config,
help="JSON speculative decoding configuration. Supported keys are method, model, and num_speculative_tokens.",
)
parser.add_argument(
"--speculative-algorithm",
type=str,
choices=["EAGLE3", "MTP", "DFLASH"],
help="Speculative algorithm.",
)
parser.add_argument(
"--speculative-draft-model-path",
type=str,
help="The path of the draft model weights. This can be a local folder or a Hugging Face repo ID.",
)
parser.add_argument(
"--speculative-draft-model-quantization",
type=str,
default=ServerArgs.speculative_draft_model_quantization,
help="Quantization method for the draft model. Defaults to 'unquant'.",
)
parser.add_argument(
"--speculative-num-steps",
type=int,
help="The number of steps sampled from draft model in Speculative Decoding.",
default=ServerArgs.speculative_num_steps,
)
parser.add_argument(
"--speculative-eagle-topk",
type=int,
help="The number of tokens sampled from the draft model in each speculative step.",
choices=[1],
default=ServerArgs.speculative_eagle_topk,
)
parser.add_argument(
"--speculative-num-draft-tokens",
type=int,
help="The number of tokens sampled from the draft model in Speculative Decoding.",
default=ServerArgs.speculative_num_draft_tokens,
)
parser.add_argument(
"--enable-output-logprobs",
action="store_true",
default=ServerArgs.enable_output_logprobs,
help="Enable per-token sampled-token logprobs. OFF by default; enabling extends the captured CUDA-graph footprint. Requests asking for logprobs on a server without this flag receive empty logprobs.",
)
parser.add_argument(
"--eagle3-layers-to-capture",
type=str,
help="The layers of Eagle3 to capture.",
default=ServerArgs.eagle3_layers_to_capture,
)
# Runtime options
parser.add_argument(
"--disable-pdl",
action="store_true",
help="Disable PDL launch.",
)
prefix_cache_group = parser.add_mutually_exclusive_group()
prefix_cache_group.add_argument(
"--enable-prefix-caching",
action="store_true",
default=ServerArgs.enable_prefix_caching,
help="Enable prefix caching.",
)
prefix_cache_group.add_argument(
"--no-enable-prefix-caching",
dest="enable_prefix_caching",
action="store_false",
help="Disable prefix caching.",
)
parser.add_argument(
"--enforce-eager",
action="store_true",
help="Disable CUDA graph.",
)
parser.add_argument(
"--disable-cuda-graph-padding",
action="store_true",
help="Disable cuda graph when padding is needed. Still uses cuda graph when padding is not needed.",
)
parser.add_argument(
"--enable-cudagraph-gc",
action="store_true",
help="Enable garbage collection during CUDA graph capture. If disabled (default), GC is frozen during capture to speed up the process.",
)
parser.add_argument(
"--enable-nccl-nvls",
action="store_true",
help="Enable NCCL NVLS for prefill heavy requests when available.",
)
parser.add_argument(
"--enable-symm-mem",
action="store_true",
help="Enable NCCL symmetric memory for fast collectives.",
)
parser.add_argument(
"--disable-custom-all-reduce",
action="store_true",
help="Disable the custom all-reduce kernel and fall back to NCCL.",
)
parser.add_argument(
"--disable-overlap-schedule",
action="store_true",
help="Disable the overlap scheduler, which overlaps the CPU scheduler with GPU model worker.",
)
parser.add_argument(
"--disable-tf32",
action="store_true",
help="Disable forcing TF32 on for cuBLAS/cuDNN. By default the server sets "
"NVIDIA_TF32_OVERRIDE=1 and TORCH_ALLOW_TF32_CUBLAS_OVERRIDE=1.",
)
parser.add_argument(
"--max-cudagraph-capture-size",
metavar="MAX_CUDAGRAPH_CAPTURE_SIZE",
type=int,
default=ServerArgs.max_cudagraph_capture_size,
help="Set the maximum batch size for CUDA graph capture.",
)
parser.add_argument(
"--cudagraph-capture-sizes",
metavar="CUDAGRAPH_CAPTURE_SIZE",
type=int,
nargs="+",
help="Set the list of batch sizes for CUDA graph capture.",
)
parser.add_argument(
"--disable-prefill-graph",
action="store_true",
help="Disable cuda graph for prefill.",
)
parser.add_argument(
"--prefill-graph-max-tokens",
type=int,
default=ServerArgs.prefill_graph_max_tokens,
help="Largest token bucket captured by the breakable prefill CUDA "
"graph. Default (unset) = min(2048, chunked-prefill size); "
"0 disables.",
)
parser.add_argument(
"--prefill-graph-capture-sizes",
metavar="PREFILL_GRAPH_CAPTURE_SIZE",
type=int,
nargs="+",
help="Explicit list of token-bucket sizes to capture for the "
"breakable prefill graph (like --cudagraph-capture-sizes for "
"decode). Unset: a relative-stride ladder bounding padded compute "
"at ~12.5%% of any size.",
)
parser.add_argument(
"--enable-nan-detection",
action="store_true",
help="Enable the NaN guard: sanitize non-finite logits before "
"sampling, detect requests whose logits contained NaN (or whose "
"sampled token id escaped the vocab range), and terminate only "
"those requests with a numerical error so corruption cannot "
"spread to the rest of the batch.",
)
parser.add_argument(
"--enable-nvtx",
action="store_true",
help="Emit NVTX ranges around input_prep / target_forward / "
"sampling / drafter stages for nsys profiling. Off by default "
"(true no-op — no NVTX calls are made). Also enabled by "
"TOKENSPEED_NVTX=1.",
)
parser.add_argument(
"--enable-p2p-check",
action="store_true",
help="Enable the full GPU P2P access check, otherwise trust the driver's P2P report.",
)
parser.add_argument(
"--triton-attention-reduce-in-fp32",
action="store_true",
help="Cast the intermediate attention results to fp32 to avoid possible crashes related to fp16."
"This only affects Triton attention kernels.",
)
parser.add_argument(
"--delete-ckpt-after-loading",
action="store_true",
help="Delete the model checkpoint after loading the model.",
)
parser.add_argument(
"--weight-loader-prefetch-checkpoints",
action="store_true",
help=(
"Prefetch safetensors checkpoint shards into OS page cache before "
"loading. Local ranks split the shard list to reduce repeated reads "
"from shared filesystems."
),
)
parser.add_argument(
"--weight-loader-prefetch-num-threads",
type=int,
default=ServerArgs.weight_loader_prefetch_num_threads,
help="Number of background threads per rank for checkpoint prefetching.",
)
parser.add_argument(
"--enable-memory-saver",
action="store_true",
help="Allow saving memory using release_memory_occupation and resume_memory_occupation",
)
parser.add_argument(
"--enable-custom-logit-processor",
action="store_true",
help="Enable users to pass custom logit processors to the server (disabled by default for security)",
)
# Server warmups
parser.add_argument(
"--skip-server-warmup",
action="store_true",
help="If set, skip warmup.",
)
parser.add_argument(
"--warmups",
type=str,
required=False,
help="Specify custom warmup functions (csv) to run before server starts eg. --warmups=warmup_name1,warmup_name2 "
"will run the functions `warmup_name1` and `warmup_name2` specified in warmup.py before the server starts listening for requests",
)
parser.add_argument(
"--tensor-parallel-size",
"--tp",
type=int,
default=None,
help="Sets tensor parallelism size uniformly (equivalent to --attn-tp-size). "
"Cannot be used together with --attn-tp-size.",
)
parser.add_argument(
"--enable-expert-parallel",
action="store_true",
help="Enable expert parallelism by automatically setting ep_size to world_size.",
)
# Specify different parallel strategies, different combinations correspond to different communication groups and weight partitioning, as well as different communication methods
parser.add_argument(
"--attn-tp-size",
type=int,
default=ServerArgs.attn_tp_size,
help="Specify tp size for attn part",
)
parser.add_argument(
"--dense-tp-size",
type=int,
default=ServerArgs.dense_tp_size,
help="Specify tp size for dense part, default equals nprocs-per-node, if non dp_attn && combine_dense mode, this parameter will be overridden by attn_tp_size",
)
parser.add_argument(
"--moe-tp-size",
type=int,
default=ServerArgs.moe_tp_size,
help="Specify tp size for MoE part, default equals nprocs-per-node, if non dp_attn && combine_dense mode, this parameter will be overridden by attn_tp_size",
)
parser.add_argument(
"--nprocs-per-node",
type=int,
default=ServerArgs.nprocs_per_node,
help="Number of processes to start per node",
)
parser.add_argument(
"--world-size",
type=int,
default=ServerArgs.world_size,
help="Total number of processes across all nodes.",
)
parser.add_argument(
"--force-deterministic-rsag",
action="store_true",
help="Enable force deterministic rsag.",
)
parser.add_argument(
"--disable-sampling-tp-sync",
action="store_true",
help="Skip broadcasting sampler outputs across the attention TP "
"group. Only safe when the sampling kernels are deterministic.",
)
parser.add_argument(
"--low-latency-max-num-tokens-per-gpu",
type=int,
default=ServerArgs.low_latency_max_num_tokens_per_gpu,
help="Low latency max num tokens per gpu",
)
parser.add_argument(
"--mla-chunk-multiplier",
type=int,
default=ServerArgs.mla_chunk_multiplier,
help=(
"Per-iter MLA chunked-prefill chunk capacity multiplier; "
"the actual capacity is chunked_prefill_size * mla_chunk_multiplier."
),
)
# Multimodal
mm_attention_backend_choices = [
"fa3",
"fa4",
"triton_attn",
"flashinfer_cudnn",
]
parser.add_argument(
"--mm-attention-backend",
type=str,
choices=mm_attention_backend_choices,
default=ServerArgs.mm_attention_backend,
help="Set multimodal attention backend.",
)
# Disaggregation
parser.add_argument(
"--disaggregation-mode",
type=str,
default="null",
choices=["null", "prefill", "decode", "encode"],
help='Used for PD/EPD disaggregation. "prefill" for prefill-only server, "decode" for decode-only server, and "encode" for a vision-tower-only server that ships image embeddings to a prefill server. If not specified, it is not disaggregated',
)
parser.add_argument(
"--comm-fusion-max-num-tokens",
type=int,
default=ServerArgs.comm_fusion_max_num_tokens,
help="Max num tokens for communication fusion workspace",
)
parser.add_argument(
"--enable-allreduce-fusion",
action="store_true",
help="Enable allreduce fusion for improved decode performance. Auto-enabled on supported single-node TP configurations.",
)
parser.add_argument(
"--disaggregation-bootstrap-port",
type=int,
default=ServerArgs.disaggregation_bootstrap_port,
help="Bootstrap server port on the prefill server. Default is 8998.",
)
parser.add_argument(
"--disaggregation-transfer-backend",
type=str,
default=ServerArgs.disaggregation_transfer_backend,
choices=["mooncake", "mooncake_async"],
help="The backend for disaggregation transfer. Default is mooncake.",
)
parser.add_argument(
"--disaggregation-ib-device",
type=str,
default=ServerArgs.disaggregation_ib_device,
help="The InfiniBand devices for disaggregation transfer, accepts single device (e.g., --disaggregation-ib-device mlx5_0) "
"or multiple comma-separated devices (e.g., --disaggregation-ib-device mlx5_0,mlx5_1). "
"Default is None, which triggers automatic device detection when mooncake backend is enabled.",
)
parser.add_argument(
"--disaggregation-layerwise-interval",
type=int,
default=ServerArgs.disaggregation_layerwise_interval,
help="The interval of layerwise transfer for disaggregation. Default is 1.",
)
parser.add_argument(
"--pdlb-url",
type=str,
default=None,
help="The URL of the PD disaggregation load balancer. If set, the prefill/decode server will register with the load balancer.",
)
@classmethod
def from_cli_args(cls, args: argparse.Namespace):
args.ep_size = args.expert_parallel_size
# Resolve model (positional model arg vs --model)
positional_model = getattr(args, "model_path", None)
if positional_model is not None and args.model is not None:
raise ValueError(
"Cannot specify model both as a positional argument and --model. "
"Use one or the other."
)
if positional_model is not None:
args.model = positional_model
if args.model is None:
raise ValueError(
"Model is required. Provide it as a positional argument "
"(e.g., `tokenspeed serve <model>`) or via --model/--model-path."
)
# --tensor-parallel-size → --attn-tp-size
tensor_parallel_size = getattr(args, "tensor_parallel_size", None)
if tensor_parallel_size is not None:
if args.attn_tp_size is not None:
raise ValueError(
"Cannot specify both --tensor-parallel-size and --attn-tp-size. "
"--tensor-parallel-size is an alias for --attn-tp-size."
)
args.attn_tp_size = tensor_parallel_size
# Only pass fields that argparse actually produced. Falling back to
# ``None`` for missing attrs would silently clobber dataclass defaults
# for non-CLI-exposed fields (e.g. ``enable_inline_detokenizer``).
attrs = [attr.name for attr in dataclasses.fields(cls)]
return cls(
**{attr: getattr(args, attr) for attr in attrs if hasattr(args, attr)}
)
def url(self):
if is_valid_ipv6_address(self.host):
return f"http://[{self.host}]:{self.port}"
return f"http://{self.host}:{self.port}"
def prepare_server_args(argv: list[str]) -> ServerArgs:
"""
Prepare the server arguments from the command line arguments.
Args:
args: The command line arguments. Typically, it should be `sys.argv[1:]`.
Returns:
The server arguments.
"""
parser = argparse.ArgumentParser(allow_abbrev=False)
ServerArgs.add_cli_args(parser)
raw_args = parser.parse_args(argv)
server_args = ServerArgs.from_cli_args(raw_args)
return server_args
ZMQ_TCP_PORT_DELTA = 233
@dataclasses.dataclass
class PortArgs:
# The ipc filename for AsyncLLM to receive BatchTokenIDOut directly
# from the scheduler (zmq).
tokenizer_ipc_name: str
# The ipc filename for scheduler (rank 0) to receive inputs from tokenizer (zmq)
scheduler_input_ipc_name: str
# The port for nccl initialization (torch.dist)
nccl_port: int
# The ipc filename for rpc call between Engine and Scheduler
rpc_ipc_name: str
# The ipc filename for Scheduler to send metrics
metrics_ipc_name: str
# The ipc filename for Tokenizer and worker tokenizer
tokenizer_worker_ipc_name: str | None
@staticmethod
def init_new(server_args: ServerArgs, dp_rank: int | None = None) -> "PortArgs":
port = server_args.port + random.randint(100, 1000)
while True:
if is_port_available(port):
break
if port < 60000:
port += 42
else:
port -= 43
# DP attention. Use TCP + port to handle both single-node and multi-node.
if server_args.mapping.nnodes == 1 and server_args.dist_init_addr is None:
# Only use default port fallback when dp_size == 1
# For dp_size > 1, we need explicit dist_init_addr to avoid port conflicts
if server_args.mapping.has_attn_dp:
raise ValueError(
f"When dp_size > 1 (dp_size={server_args.mapping.attn.dp_size}), you must provide --dist-init-addr. "
f"Example: --dist-init-addr 127.0.0.1:4000"
)
dist_init_addr = ("127.0.0.1", server_args.port + ZMQ_TCP_PORT_DELTA)
else:
dist_init_addr = server_args.dist_init_addr.split(":")
if len(dist_init_addr) != 2:
raise ValueError(
"please provide --dist-init-addr as host:port of head node"
)
dist_init_host, dist_init_port = dist_init_addr
dist_init_port = int(dist_init_port)
# Scan forward until we find a port cluster where all derived ports are free.
# This handles the case where a previous engine instance left ports in
# TIME_WAIT or its child processes haven't fully terminated yet.
# Note: the port at offset +1 (formerly detokenizer_port) is intentionally
# skipped so the rest of the port layout stays stable for any external
# tooling that indexed off the historical port cluster.
while True:
port_base = dist_init_port + 1
rpc_port = port_base + 2
metrics_ipc_port = port_base + 3
if dp_rank is None:
# TokenizerManager to DataParallelController
scheduler_input_port = port_base + 4
else:
scheduler_input_port = port_base + 2 + 1 + dp_rank
rpc_ipc_port = scheduler_input_port + 1
if all(
is_port_available(p)
for p in [
dist_init_port,
port_base,
rpc_port,
metrics_ipc_port,
scheduler_input_port,
rpc_ipc_port,
]
):
break
dist_init_port += 10
return PortArgs(
tokenizer_ipc_name=f"tcp://{dist_init_host}:{port_base}",
scheduler_input_ipc_name=f"tcp://{dist_init_host}:{scheduler_input_port}",
nccl_port=port,
rpc_ipc_name=f"tcp://{dist_init_host}:{rpc_port}",
metrics_ipc_name=f"tcp://{dist_init_host}:{metrics_ipc_port}",
tokenizer_worker_ipc_name=None,
)