643 lines
23 KiB
Rust
643 lines
23 KiB
Rust
//! Runtime configuration: the rust-server boot knobs
|
|
//! ([`RustServerServerArgs`]), the scheduler's typed `server_args` handoff
|
|
//! ([`ServerArgs`] / [`ModelConfig`]), the [`RuntimeConfig`] pairing them for
|
|
//! `runtime::start`, and the Rust MM pipeline handoff ([`MmSpec`]).
|
|
//!
|
|
//! [`ServerArgs`] / [`ModelConfig`] / [`DefaultSamplingParams`] /
|
|
//! [`DisaggregationMode`] / [`MmSpec`] / [`MmFamily`] / [`MmResample`] are
|
|
//! also `#[pyclass]`es: the Python scheduler (`RustServer._build_server_args`
|
|
//! / `_build_mm_spec`) constructs them directly by keyword and hands them to
|
|
//! `Server`. There is one schema — this file — and
|
|
//! pyo3 enforces it at construction: every field is a required, typed
|
|
//! constructor argument, so a drifted caller fails at boot rather than running
|
|
//! on a silently-defaulted knob. The `#[pyo3::pymethods]` constructors below
|
|
//! each struct — plus the one hand-written extraction,
|
|
//! [`PreferredSamplingParams`] — are the only Python-facing code in this file;
|
|
//! the rest is pure Rust.
|
|
|
|
use std::net::SocketAddr;
|
|
use std::sync::Arc;
|
|
|
|
use serde::Serialize;
|
|
|
|
/// Boot knobs specific to the embedded rust server — none of these exist in
|
|
/// the Python-built [`ServerArgs`]; they arrive as explicit
|
|
/// `Server::start` parameters.
|
|
#[derive(Clone, Debug)]
|
|
pub struct RustServerServerArgs {
|
|
pub http_addr: SocketAddr,
|
|
pub http_api_worker_num: usize,
|
|
pub to_scheduler_cap: usize,
|
|
pub from_scheduler_cap: usize,
|
|
pub stage_channel_cap: usize,
|
|
/// CPU core ids the pools pin to (e.g. this rank's NUMA-local cores minus
|
|
/// the scheduler's reserved launch cores). `None` → run unpinned.
|
|
pub cores: Option<Vec<usize>>,
|
|
}
|
|
|
|
impl Default for RustServerServerArgs {
|
|
fn default() -> Self {
|
|
Self {
|
|
http_addr: "127.0.0.1:30000".parse().unwrap(),
|
|
http_api_worker_num: 2,
|
|
to_scheduler_cap: 8192,
|
|
from_scheduler_cap: 8192,
|
|
stage_channel_cap: 8192,
|
|
cores: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct RuntimeConfig {
|
|
/// Rust-server-only boot knobs (listen address, pool/ring sizes, pinning).
|
|
pub rust_server_args: RustServerServerArgs,
|
|
/// The scheduler's [`ServerArgs`] (worker counts, tokenizer source,
|
|
/// config-endpoint metadata). `Arc` so cloning the config (and, downstream,
|
|
/// each `AppState`) is cheap; immutable after construction.
|
|
pub server_args: Arc<ServerArgs>,
|
|
}
|
|
|
|
impl Default for RuntimeConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
rust_server_args: RustServerServerArgs::default(),
|
|
server_args: Arc::new(ServerArgs::default()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The scheduler's launch-time handoff (`RustServer._build_server_args`):
|
|
/// the `server_args` fields the rust server reads, the resolved
|
|
/// [`ModelConfig`], and launch-time stamps. Values are post-`__post_init__`
|
|
/// (all paths and names resolved). Constructed from Python via the `#[new]` in
|
|
/// `lib.rs`, whose keyword parameters are exactly these fields.
|
|
#[pyo3::pyclass(frozen, from_py_object, module = "sglang.srt.rust_extensions._server")]
|
|
#[derive(Clone, Debug)]
|
|
pub struct ServerArgs {
|
|
/// HF repo id / local dir of the model, reported by `/get_model_info`.
|
|
pub model_path: String,
|
|
/// Model name reported by `/v1/models` and `/server_info`.
|
|
pub served_model_name: String,
|
|
/// Tokenizer source (model dir / `tokenizer.json` / HF repo id). Empty only
|
|
/// in standalone (test) configs — then boot requires `skip_tokenizer_init`.
|
|
pub tokenizer_path: String,
|
|
/// HF revision, used only when `tokenizer_path` is a repo id. `None` → main.
|
|
pub revision: Option<String>,
|
|
/// Weight format selected by `--load-format`, reported by `/get_model_info`.
|
|
/// The blob carries the post-`__post_init__` value (`auto` is already
|
|
/// narrowed to `gguf` / `mistral` / `runai_streamer` / `remote` where the
|
|
/// checkpoint demands it). Not consumed for loading -- the scheduler owns
|
|
/// that; `None` only when the blob omits the key.
|
|
pub load_format: Option<String>,
|
|
/// Operator-supplied weight version, reported by `/model_info`. Defaults to
|
|
/// `"default"` on the Python side, so it is present in every blob; `None`
|
|
/// only when the blob omits the key.
|
|
pub weight_version: Option<String>,
|
|
/// HTTP bind address (see [`Self::bind`]).
|
|
pub host: String,
|
|
pub port: u16,
|
|
/// Log levels driving the access log — uvicorn runs at
|
|
/// `log_level_http or log_level` (see [`Self::http_access_log_enabled`]).
|
|
pub log_level: String,
|
|
pub log_level_http: Option<String>,
|
|
/// Optional built-in chat-template name or path to a Jinja/legacy JSON
|
|
/// template file. Without an override, uses the tokenizer config template.
|
|
pub chat_template: Option<String>,
|
|
/// Parser selected by `--tool-call-parser`.
|
|
pub tool_call_parser: Option<String>,
|
|
/// Reasoning splitter selected by `--reasoning-parser` (e.g. deepseek-r1).
|
|
/// When set, chat completions strip the model's reasoning markers out of
|
|
/// `content` into `reasoning_content` — both unary and streaming.
|
|
pub reasoning_parser: Option<String>,
|
|
/// Python's global default for whether an SSE stream ends with a usage chunk.
|
|
pub stream_response_default_include_usage: bool,
|
|
/// Pinned tokenizer threads / detok shards (Python asserts both ≥ 1).
|
|
pub tokenizer_worker_num: usize,
|
|
pub detokenizer_worker_num: usize,
|
|
/// Token-ids-in / token-ids-out mode: no tokenizer load, raw `output_ids`
|
|
/// frames.
|
|
pub skip_tokenizer_init: bool,
|
|
/// Streamed `/generate` frames carry per-step deltas instead of cumulative
|
|
/// text. Matches the Python `TokenizerManager`.
|
|
pub incremental_streaming_output: bool,
|
|
/// PD-disaggregation role. (On prefill, the KV bootstrap registry is mounted
|
|
/// on the api router — see [`Self::enable_pd_bootstrap`].)
|
|
pub disaggregation_mode: DisaggregationMode,
|
|
/// The resolved Python `ModelConfig`, attached at handoff time.
|
|
pub model_config: ModelConfig,
|
|
/// Launch-time sampling defaults merged beneath per-request values and
|
|
/// advertised by `/get_model_info`.
|
|
pub preferred_sampling_params: Option<PreferredSamplingParams>,
|
|
/// Over-long inputs are truncated to fit the context instead of 400ing, and
|
|
/// `max_new_tokens` is clamped rather than rejected (Python
|
|
/// `TokenizerManager._validate_one_request`).
|
|
pub allow_auto_truncate: bool,
|
|
/// `return_hidden_states` is refused unless the server was launched with it:
|
|
/// the scheduler simply won't produce them, so the request would 200 with the
|
|
/// field silently missing.
|
|
pub enable_return_hidden_states: bool,
|
|
/// Output slots reserved per request on top of its input (eagle stores draft
|
|
/// tokens there). Not a `server_args` field — `TokenizerManager` derives it and
|
|
/// `RustServer._build_server_args` stamps it in, so both sides count alike.
|
|
pub num_reserved_tokens: u64,
|
|
/// Launch-time stamps (not `server_args` fields): sglang package version
|
|
/// and the scheduler-derived KV token capacity, reported by `/server_info`.
|
|
pub version: String,
|
|
pub max_total_num_tokens: u64,
|
|
}
|
|
|
|
#[pyo3::pymethods]
|
|
impl ServerArgs {
|
|
#[new]
|
|
#[pyo3(signature = (*,
|
|
model_path,
|
|
served_model_name,
|
|
tokenizer_path,
|
|
revision,
|
|
load_format,
|
|
weight_version,
|
|
host,
|
|
port,
|
|
log_level,
|
|
log_level_http,
|
|
chat_template,
|
|
tool_call_parser,
|
|
reasoning_parser,
|
|
stream_response_default_include_usage,
|
|
tokenizer_worker_num,
|
|
detokenizer_worker_num,
|
|
skip_tokenizer_init,
|
|
incremental_streaming_output,
|
|
disaggregation_mode,
|
|
model_config,
|
|
preferred_sampling_params,
|
|
allow_auto_truncate,
|
|
enable_return_hidden_states,
|
|
num_reserved_tokens,
|
|
version,
|
|
max_total_num_tokens,
|
|
))]
|
|
// The parameter list IS the schema; one keyword per field, all required.
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn py_new(
|
|
model_path: String,
|
|
served_model_name: String,
|
|
tokenizer_path: String,
|
|
revision: Option<String>,
|
|
load_format: Option<String>,
|
|
weight_version: Option<String>,
|
|
host: String,
|
|
port: u16,
|
|
log_level: String,
|
|
log_level_http: Option<String>,
|
|
chat_template: Option<String>,
|
|
tool_call_parser: Option<String>,
|
|
reasoning_parser: Option<String>,
|
|
stream_response_default_include_usage: bool,
|
|
tokenizer_worker_num: usize,
|
|
detokenizer_worker_num: usize,
|
|
skip_tokenizer_init: bool,
|
|
incremental_streaming_output: bool,
|
|
disaggregation_mode: DisaggregationMode,
|
|
model_config: ModelConfig,
|
|
preferred_sampling_params: Option<PreferredSamplingParams>,
|
|
allow_auto_truncate: bool,
|
|
enable_return_hidden_states: bool,
|
|
num_reserved_tokens: u64,
|
|
version: String,
|
|
max_total_num_tokens: u64,
|
|
) -> Self {
|
|
Self {
|
|
model_path,
|
|
served_model_name,
|
|
tokenizer_path,
|
|
revision,
|
|
load_format,
|
|
weight_version,
|
|
host,
|
|
port,
|
|
log_level,
|
|
log_level_http,
|
|
chat_template,
|
|
tool_call_parser,
|
|
reasoning_parser,
|
|
stream_response_default_include_usage,
|
|
tokenizer_worker_num,
|
|
detokenizer_worker_num,
|
|
skip_tokenizer_init,
|
|
incremental_streaming_output,
|
|
disaggregation_mode,
|
|
model_config,
|
|
preferred_sampling_params,
|
|
allow_auto_truncate,
|
|
enable_return_hidden_states,
|
|
num_reserved_tokens,
|
|
version,
|
|
max_total_num_tokens,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for ServerArgs {
|
|
/// A standalone (test) config: no model, no tokenizer, unified role, but a
|
|
/// complete `model_config` so the runtime boots. Real launches never use
|
|
/// this — Python supplies every field.
|
|
fn default() -> Self {
|
|
Self {
|
|
model_path: String::new(),
|
|
served_model_name: String::new(),
|
|
tokenizer_path: String::new(),
|
|
revision: None,
|
|
load_format: None,
|
|
weight_version: None,
|
|
host: "127.0.0.1".into(),
|
|
port: 30000,
|
|
log_level: "info".into(),
|
|
log_level_http: None,
|
|
chat_template: None,
|
|
tool_call_parser: None,
|
|
reasoning_parser: None,
|
|
stream_response_default_include_usage: false,
|
|
tokenizer_worker_num: 1,
|
|
detokenizer_worker_num: 1,
|
|
skip_tokenizer_init: false,
|
|
incremental_streaming_output: false,
|
|
disaggregation_mode: DisaggregationMode::Null,
|
|
model_config: ModelConfig::default(),
|
|
preferred_sampling_params: None,
|
|
allow_auto_truncate: false,
|
|
enable_return_hidden_states: false,
|
|
num_reserved_tokens: 0,
|
|
version: String::new(),
|
|
max_total_num_tokens: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `--preferred-sampling-params`, carried verbatim: `/get_model_info` echoes
|
|
/// whatever Python advertises, and the keys are whatever `SamplingParams`
|
|
/// accepts, so there is no fixed field list to model as a `#[pyclass]`.
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[serde(transparent)]
|
|
pub struct PreferredSamplingParams(pub serde_json::Value);
|
|
|
|
impl<'py> pyo3::FromPyObject<'_, 'py> for PreferredSamplingParams {
|
|
type Error = pyo3::PyErr;
|
|
|
|
fn extract(obj: pyo3::Borrowed<'_, 'py, pyo3::PyAny>) -> pyo3::PyResult<Self> {
|
|
let text = obj.extract::<String>()?;
|
|
serde_json::from_str(&text).map(Self).map_err(|e| {
|
|
pyo3::exceptions::PyValueError::new_err(format!(
|
|
"preferred_sampling_params is not valid JSON: {e}"
|
|
))
|
|
})
|
|
}
|
|
}
|
|
|
|
/// PD-disaggregation role, the values of `--disaggregation-mode`. Exposed to
|
|
/// Python as an enum (`DisaggregationMode.Null` / `.Prefill` / `.Decode`).
|
|
#[pyo3::pyclass(
|
|
eq,
|
|
frozen,
|
|
from_py_object,
|
|
module = "sglang.srt.rust_extensions._server"
|
|
)]
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum DisaggregationMode {
|
|
/// Unified prefill + decode.
|
|
Null,
|
|
Prefill,
|
|
Decode,
|
|
}
|
|
|
|
/// The slice of the resolved Python `ModelConfig` the rust server reads.
|
|
#[pyo3::pyclass(frozen, from_py_object, module = "sglang.srt.rust_extensions._server")]
|
|
#[derive(Clone, Debug)]
|
|
pub struct ModelConfig {
|
|
/// Resolved context length (`max_model_len` in `/v1/models`); the ceiling
|
|
/// for input + `max_new_tokens`.
|
|
pub context_len: u64,
|
|
/// Bounds client-supplied token ids — return 400s out-of-vocab ids before
|
|
/// they crash the scheduler's embedding lookup.
|
|
pub vocab_size: u64,
|
|
/// Whether the model accepts multimodal inputs. Gates the MM Encoding branch
|
|
/// in to-scheduler; `false` silently ignores mm fields, as the Python
|
|
/// `TokenizerManager` does with `mm_processor is None`.
|
|
pub is_multimodal: bool,
|
|
/// Resolved default sampling parameters, from Python's
|
|
/// `ModelConfig.get_default_sampling_params()`. Already gated on
|
|
/// `--sampling-defaults`: holds the model's generation_config.json values
|
|
/// in "model" mode, and is all-`None` in "openai" mode. Consumed when a chat
|
|
/// request omits `temperature`/`top_p` — the conversion must not skip
|
|
/// straight to the OpenAI terminal defaults.
|
|
pub default_sampling_params: DefaultSamplingParams,
|
|
}
|
|
|
|
#[pyo3::pymethods]
|
|
impl ModelConfig {
|
|
#[new]
|
|
#[pyo3(signature = (*, context_len, vocab_size, is_multimodal, default_sampling_params))]
|
|
fn py_new(
|
|
context_len: u64,
|
|
vocab_size: u64,
|
|
is_multimodal: bool,
|
|
default_sampling_params: DefaultSamplingParams,
|
|
) -> Self {
|
|
Self {
|
|
context_len,
|
|
vocab_size,
|
|
is_multimodal,
|
|
default_sampling_params,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for ModelConfig {
|
|
/// Test-only: a small but complete model so the runtime boots.
|
|
fn default() -> Self {
|
|
Self {
|
|
context_len: 2048,
|
|
vocab_size: 1000,
|
|
is_multimodal: false,
|
|
default_sampling_params: DefaultSamplingParams::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One `SamplingParams` field per key `get_default_sampling_params()` may emit
|
|
/// (`repetition_penalty`, `temperature`, `top_k`, `top_p`, `min_p`); `None`
|
|
/// where the generation config does not set it.
|
|
///
|
|
/// `top_k` / `min_p` / `repetition_penalty` are carried for parity with the
|
|
/// Python dict but not yet consumed: the Dynamo chat request type only carries
|
|
/// `temperature` and `top_p`, so the conversion resolves just those two.
|
|
#[pyo3::pyclass(frozen, from_py_object, module = "sglang.srt.rust_extensions._server")]
|
|
#[derive(Clone, Debug, Default)]
|
|
#[allow(dead_code)]
|
|
pub struct DefaultSamplingParams {
|
|
pub temperature: Option<f64>,
|
|
pub top_p: Option<f64>,
|
|
pub top_k: Option<i64>,
|
|
pub min_p: Option<f64>,
|
|
pub repetition_penalty: Option<f64>,
|
|
}
|
|
|
|
#[pyo3::pymethods]
|
|
impl DefaultSamplingParams {
|
|
#[new]
|
|
#[pyo3(signature = (*, temperature = None, top_p = None, top_k = None, min_p = None, repetition_penalty = None))]
|
|
fn py_new(
|
|
temperature: Option<f64>,
|
|
top_p: Option<f64>,
|
|
top_k: Option<i64>,
|
|
min_p: Option<f64>,
|
|
repetition_penalty: Option<f64>,
|
|
) -> Self {
|
|
Self {
|
|
temperature,
|
|
top_p,
|
|
top_k,
|
|
min_p,
|
|
repetition_penalty,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The Rust MM pipeline handoff, built by `RustServer._build_mm_spec` from
|
|
/// the resolved `RustMmSpec` and passed to `Server.start_mm_workers`. Same
|
|
/// contract as [`ServerArgs`]: every field is a required, typed constructor
|
|
/// keyword, so a drifted Python caller fails at boot.
|
|
#[pyo3::pyclass(frozen, from_py_object, module = "sglang.srt.rust_extensions._server")]
|
|
#[derive(Clone, Debug)]
|
|
pub struct MmSpec {
|
|
/// Park feature buffers in POSIX shm rather than inline. Set by the Python
|
|
/// launcher (`RustMmProcessor._use_feature_shm`) exactly when the scheduler
|
|
/// broadcasts across TP ranks and will unwrap `ShmPointerMMData`.
|
|
pub feature_shm: bool,
|
|
/// The family pipeline and its resolved processor parameters.
|
|
pub pipeline: sglang_mm::registry::PipelineSpec,
|
|
}
|
|
|
|
#[pyo3::pymethods]
|
|
impl MmSpec {
|
|
/// The parameter list is flat because every family so far shares the
|
|
/// Qwen-VL processor geometry; a family with different knobs adds its own
|
|
/// keywords and match arm here.
|
|
#[new]
|
|
#[pyo3(signature = (*,
|
|
family,
|
|
feature_shm,
|
|
image_token_id,
|
|
patch_size,
|
|
merge_size,
|
|
temporal_patch_size,
|
|
min_pixels,
|
|
max_pixels,
|
|
image_mean,
|
|
image_std,
|
|
resample,
|
|
))]
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn py_new(
|
|
family: MmFamily,
|
|
feature_shm: bool,
|
|
image_token_id: i32,
|
|
patch_size: usize,
|
|
merge_size: usize,
|
|
temporal_patch_size: usize,
|
|
min_pixels: usize,
|
|
max_pixels: usize,
|
|
image_mean: [f32; 3],
|
|
image_std: [f32; 3],
|
|
resample: MmResample,
|
|
) -> Self {
|
|
use sglang_mm::registry::PipelineSpec;
|
|
let pipeline = match family {
|
|
MmFamily::QwenVl => PipelineSpec::QwenVl(sglang_mm::qwen_vl::QwenVlSpec {
|
|
image_token_id,
|
|
patch_size,
|
|
merge_size,
|
|
temporal_patch_size,
|
|
min_pixels,
|
|
max_pixels,
|
|
image_mean,
|
|
image_std,
|
|
resample: resample.into(),
|
|
}),
|
|
};
|
|
Self {
|
|
feature_shm,
|
|
pipeline,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Which `sglang_mm` family pipeline serves the model — one variant per
|
|
/// [`sglang_mm::registry::PipelineSpec`] arm. Exposed to Python as an enum
|
|
/// (`MmFamily.QwenVl`); `RustMmFamily.name` maps onto it at handoff.
|
|
#[pyo3::pyclass(
|
|
eq,
|
|
frozen,
|
|
from_py_object,
|
|
module = "sglang.srt.rust_extensions._server"
|
|
)]
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum MmFamily {
|
|
QwenVl,
|
|
}
|
|
|
|
/// The HF image processor the Rust resize must reproduce bit-exactly (see
|
|
/// [`sglang_mm::qwen_vl::Resampler`]). Exposed to Python as an enum
|
|
/// (`MmResample.AtenU8` / `.Pil`); `RustMmFamily.image_processors` maps each
|
|
/// processor class onto it.
|
|
#[pyo3::pyclass(
|
|
eq,
|
|
frozen,
|
|
from_py_object,
|
|
module = "sglang.srt.rust_extensions._server"
|
|
)]
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum MmResample {
|
|
/// `Qwen2VLImageProcessor` / `…Fast` — torchvision on a uint8 tensor.
|
|
AtenU8,
|
|
/// `Qwen2VLImageProcessorPil`, behind `--disable-fast-image-processor`.
|
|
Pil,
|
|
}
|
|
|
|
impl From<MmResample> for sglang_mm::qwen_vl::Resampler {
|
|
fn from(r: MmResample) -> Self {
|
|
match r {
|
|
MmResample::AtenU8 => Self::AtenU8,
|
|
MmResample::Pil => Self::Pil,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn join_host_port(host: &str, port: u16) -> String {
|
|
if host.contains(':') && !host.starts_with('[') {
|
|
format!("[{host}]:{port}") // bare IPv6 (`::`) needs brackets to bind
|
|
} else {
|
|
format!("{host}:{port}")
|
|
}
|
|
}
|
|
|
|
impl ServerArgs {
|
|
/// Fail fast at startup on values the types cannot express.
|
|
pub fn validate(&self) -> Result<(), String> {
|
|
if self.served_model_name.is_empty() {
|
|
return Err("empty 'served_model_name' in server_args".into());
|
|
}
|
|
if let Some(preferred) = &self.preferred_sampling_params {
|
|
super::sampling::SamplingParamsInput::from_preferred(&preferred.0)
|
|
.map_err(|e| format!("invalid preferred_sampling_params: {e}"))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// True on a prefill or decode node — requests need bootstrap routing.
|
|
pub fn is_disaggregation(&self) -> bool {
|
|
self.disaggregation_mode != DisaggregationMode::Null
|
|
}
|
|
|
|
/// Serve the PD KV bootstrap registry on the api listener: every prefill
|
|
/// rust server hosts it, unconditionally — no extra topology gating. KV
|
|
/// managers and decode nodes reach the registry at the resolved
|
|
/// `disaggregation_bootstrap_port`, which rust-server mode aliases to the
|
|
/// api port, so whichever prefill server that port names is the one that
|
|
/// receives the registrations.
|
|
pub fn enable_pd_bootstrap(&self) -> bool {
|
|
self.disaggregation_mode == DisaggregationMode::Prefill
|
|
}
|
|
|
|
/// Whether the served model is multimodal, from the scheduler's config. See
|
|
/// [`ModelConfig::is_multimodal`].
|
|
pub fn model_is_multimodal(&self) -> bool {
|
|
self.model_config.is_multimodal
|
|
}
|
|
|
|
/// Bind address `host:port`. `host` is expected to be an IP — the result is
|
|
/// parsed as a `SocketAddr`, so a bare IPv6 host gets bracketed.
|
|
pub fn bind(&self) -> String {
|
|
join_host_port(&self.host, self.port)
|
|
}
|
|
|
|
/// Whether the HTTP access log is emitted, mirroring the Python server:
|
|
/// uvicorn runs at `log_level_http or log_level` and prints access lines
|
|
/// only at info/debug. `--log-level-http warning` turns them off.
|
|
pub fn http_access_log_enabled(&self) -> bool {
|
|
let level = self
|
|
.log_level_http
|
|
.as_deref()
|
|
.filter(|s| !s.is_empty())
|
|
.unwrap_or(&self.log_level);
|
|
matches!(
|
|
level.to_ascii_lowercase().as_str(),
|
|
"trace" | "debug" | "info"
|
|
)
|
|
}
|
|
|
|
/// Pinned API threads for the embedded HTTP api-server. Python `server_args`
|
|
/// has no such field — this is derived: enough to cover the widest pool.
|
|
pub fn http_api_worker_num(&self) -> usize {
|
|
4.max(self.tokenizer_worker_num)
|
|
.max(self.detokenizer_worker_num)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn bind_brackets_bare_ipv6() {
|
|
let sa = ServerArgs {
|
|
host: "::".into(),
|
|
port: 30001,
|
|
..Default::default()
|
|
};
|
|
assert_eq!(sa.bind(), "[::]:30001");
|
|
assert_eq!(ServerArgs::default().bind(), "127.0.0.1:30000");
|
|
}
|
|
|
|
#[test]
|
|
fn pd_role_derivations() {
|
|
let prefill = ServerArgs {
|
|
disaggregation_mode: DisaggregationMode::Prefill,
|
|
..Default::default()
|
|
};
|
|
assert!(prefill.is_disaggregation());
|
|
assert!(prefill.enable_pd_bootstrap());
|
|
let decode = ServerArgs {
|
|
disaggregation_mode: DisaggregationMode::Decode,
|
|
..Default::default()
|
|
};
|
|
assert!(decode.is_disaggregation());
|
|
assert!(!decode.enable_pd_bootstrap());
|
|
assert!(!ServerArgs::default().is_disaggregation());
|
|
}
|
|
|
|
#[test]
|
|
fn validate_requires_served_model_name() {
|
|
assert!(ServerArgs::default().validate().is_err());
|
|
let sa = ServerArgs {
|
|
served_model_name: "m".into(),
|
|
..Default::default()
|
|
};
|
|
assert!(sa.validate().is_ok());
|
|
}
|
|
|
|
/// `--log-level-http` overrides `--log-level` for the access log; unset or
|
|
/// empty falls through.
|
|
#[test]
|
|
fn access_log_follows_http_level_then_global() {
|
|
let mut sa = ServerArgs::default();
|
|
assert!(sa.http_access_log_enabled());
|
|
sa.log_level_http = Some("warning".into());
|
|
assert!(!sa.http_access_log_enabled());
|
|
sa.log_level_http = Some(String::new());
|
|
sa.log_level = "error".into();
|
|
assert!(!sa.http_access_log_enabled());
|
|
}
|
|
}
|