Config#
SGLang-Omni uses declarative config as the contract between model-specific
pipeline definitions and the model-agnostic runtime. PipelineConfig describes
the whole pipeline: model path, stage list, endpoints, and relay backend.
StageConfig describes one logical stage: how to construct it, where it runs,
where its normal results go, and whether it participates in fan-in or streaming
edges.
The config layer is intentionally static. It should make topology, placement, and stage construction visible before the runtime starts; request-time behavior belongs in stages, schedulers, model runners, and model-local payload logic.
Declarative Config#
Pipelines are declared with PipelineConfig and StageConfig in the model’s
config.py. Stage topology — which stages exist, how they route, where
requests enter — lives here and only here; config files and CLI flags override
settings on these stages but never add or remove them.
Example:
# Every non-TP stage must declare `process` explicitly — there is no implicit
# default. Each stage below runs in its own OS process; multiple stages can
# share an OS process by giving them the same `process` value (see
# `Qwen3OmniSpeechColocatedPipelineConfig` for that pattern).
stages = [
StageConfig(
name="preprocessing",
process="preprocessing",
factory_path="...create_preprocessing_executor",
next=["image_encoder", "audio_encoder", "mm_aggregate"],
project_payload={
"image_encoder": "...project_preprocessing_to_image_encoder",
"audio_encoder": "...project_preprocessing_to_audio_encoder",
"mm_aggregate": "...project_preprocessing_to_mm_aggregate",
},
),
StageConfig(
name="mm_aggregate",
process="mm_aggregate",
factory_path="...create_aggregate_executor",
wait_for=["preprocessing", "image_encoder", "audio_encoder"],
merge_fn="...merge_for_thinker",
next="thinker",
),
EngineStageConfig( # drives an SGLang engine, so engine.* exists
name="thinker",
process="thinker",
factory_path="...create_sglang_thinker_executor_from_config",
factory=FactoryArgs(max_seq_len=8192),
gpu=0,
next=["decode", "talker_ar"],
stream_to=["talker_ar"],
),
StageConfig(
name="decode",
process="decode",
factory_path="...create_decode_executor",
terminal=True,
),
]
Consumer Groups#
Stage settings are grouped by the module that consumes them:
Group |
Consumer |
Examples |
|---|---|---|
stage top level |
parent process: placement, process planning, wiring |
|
|
SGLang |
|
|
the stage factory’s signature |
|
Each group declares its commonly tuned fields, which validate eagerly. Any
other key passes through untouched: the group vocabularies belong to their
consumers, so the entry side never format-checks unknown keys. A free-form
factory.* key reaches the stage factory under its own name; a free-form
engine.* key travels in the server_args_overrides mapping to SGLang,
which rules on it. This is how factory-specific knobs are passed — what used
to be a factory_args entry is now written under factory.*:
stages:
latent_engine:
factory:
num_steps: 4 # not a declared FactoryArgs field; passed to the
# factory as num_steps=4, validated by its signature
A value the factory does not accept is an error at stage construction, not a silent no-op.
Setting Values: YAML and CLI#
There are exactly two user-facing spellings of one path language.
YAML — the stages: mapping, keyed by stage name:
config_cls: MossTTSPipelineConfig
model_path: OpenMOSS-Team/MOSS-TTS
stages:
tts_engine:
tp_size: 2
engine:
mem_fraction_static: 0.7
vocoder:
factory:
dtype: bfloat16
Entries merge by name: a field the file does not write keeps the model’s default. Naming a stage the config class does not define is an error that lists the real stage names.
CLI — dotted flags with the stages. prefix implied; the flag starts
from the stage name exactly as the mapping does:
sgl-omni serve --config omni.yaml \
--tts_engine.tp_size 2 \
--tts_engine.engine.mem_fraction_static 0.7 \
--vocoder.factory.dtype bfloat16 \
--vocoder.process vocoder
CLI text is coerced by the declared field type; free-form group keys fall
back to YAML scalar parsing (true → bool, 7 → int). Writing one path
twice at the same precedence is refused, never silently last-one-wins.
The command line outranks the config file; an explicit dotted path outranks
the broadcast flag below.
Shared values — the shared: selector list writes one value into several
stages at once:
shared:
- select:
stages: [preprocessing, latent_engine] # or engine: true, exclude: [...]
factory:
num_steps: 4
The entry expands to one patch per matched stage before resolution. An
explicit per-stage entry (under stages: or as a dotted flag) overrides the
expansion; two shared entries writing one leaf conflict.
Broadcast flag — --mem-fraction-static 0.7 fans one value out to every
SGLang engine stage’s engine.mem_fraction_static. It is the one convenience
flag left; a dotted per-stage spelling overrides it without being a conflict.
Inspection — sgl-omni config resolve prints the configuration a launch
with the same arguments would use (--show config|diff|provenance), and
sgl-omni config explain PATH names the source that set a value and what it
overrode. Both run the same merge as serve.
StageConfig Reference#
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
required |
Unique stage identifier. Set by the config class; a config file addresses stages by name and never renames them. |
|
|
required |
Dotted import path to the stage factory. |
|
|
|
SGLang ServerArgs overrides. Exists only on |
|
|
empty |
Constructor kwargs for the stage factory, passed by field name. Unknown keys pass through. |
|
|
|
Static downstream stage or stages for normal result routing. |
|
|
|
Marks a stage as terminal; terminal results are sent to the coordinator. |
|
|
|
Dotted function path for request-aware result routing. The function receives |
|
|
|
GPU id for the stage. |
|
|
|
Number of tensor-parallel ranks. Must match |
|
|
|
Per-stage-rank budget as a fraction of total physical GPU memory. Required per stage when multiple processes share one GPU. |
|
|
|
OS process group identifier. Non-TP stages with the same |
|
|
|
Per-stage env defaults applied in this stage’s worker process at spawn; never overrides |
|
|
|
Upstream stages required before this stage can execute a request. |
|
|
|
Dotted function path for request-aware fan-in source selection. |
|
|
|
Dotted import path to the fan-in merge function. Required when |
|
|
|
Static superset of streaming targets for chunks such as hidden states or codec codes. |
|
|
|
Dotted function path for request-aware stream-completion targets. |
|
|
|
Optional target-stage to dotted projection function mapping used before writing a downstream payload. |
|
|
|
Per-stage communication pool and Mooncake options. |
Routing rule: set exactly one of next or terminal=True. route_fn is an
optional request-aware override for stages that already declare next.
Fan-in follows the same static-superset pattern: keep wait_for as the full
set of possible upstream stages, and use wait_for_fn only to select the
active per-request subset. When using stream_done_to_fn, keep stream_to
as the static superset because runtime prep derives stream receivers from it.
PipelineConfig Reference#
Field |
Type |
Default |
Description |
|---|---|---|---|
|
|
required |
Hugging Face model id or local checkpoint path. |
|
|
required |
Stage definitions. Config files override fields on them by name and cannot add or remove stages. |
|
|
|
Pipeline name. Used for reporting and runtime identification. |
|
|
first stage |
Declared by the config class when the first stage is not the entry. Not settable from a config file or the CLI. |
|
|
|
Adjacent linear stage groups to colocate in one runtime process. |
|
|
|
Environment defaults applied before stage factory imports. Existing process values take precedence. |
|
|
|
Native CUDA MPS policy for eligible same-GPU worker processes. |
|
|
IPC defaults |
Endpoint allocation settings. |
|
|
defaults |
Placement planning limits, e.g. |
|
|
|
Dotted function path for request-aware terminal-stage resolution. |
|
|
class name |
Stored automatically and used when loading a saved config file. |
Class-level hooks a model config may declare:
stage_config_types: ClassVar[dict[str, type[StageConfig]]]— theStageConfigsubclass a named stage compiles paths against; mapping a stage toEngineStageConfigis what makesengine.*exist on it.stage_factory_kwargs(stage_name)— constructor kwargs the pipeline author passes to the factory in code (wiring, not configuration). A user-setfactory.*value with the same name overrides the hook’s value.tensor_parallel_server_args_overrides(stage_name, tp_size)/topology_gated_custom_all_reduce_stages()— engine overrides derived from the resolved TP topology at launch.
Derived values are computed from stages, not manually maintained:
resolved_entry_stage, terminal_stages, gpu_placement.
Stage Fusion#
fused_stages is a framework-level colocation hint. It keeps every listed
logical stage as a normal Stage; it does not create a synthetic scheduler.
At runtime prep, each fused group adds a process-colocation constraint, and
ordinary Stage routing can then use process-local dispatch for eligible hops.
A group must be adjacent, linear, non-TP, and fit on at most one GPU.
How Values Reach a Factory#
Two channels feed a stage factory, resolved in the parent process and applied against the factory signature in the worker:
PipelineConfig.stage_factory_kwargs(name) # author channel: code wiring
stage.factory.* # config channel: by field name
stage.engine.* -> server_args_overrides # config channel: one dict to SGLang
Per key, the config channel wins over the author channel;
server_args_overrides merges per key the same way. A configured key the
factory does not accept raises at construction. Standard kwargs
(model_path, gpu_id, total_gpu_memory_fraction) are injected only when
the factory signature declares them; gpu_id is owned by placement and is
rejected from the author channel.
Device and GPU placement contract#
A stage’s card is placement’s decision, made in the parent process from
stage.gpu (or processes.<name>.replica_devices for a replicated process)
and handed to the factory as gpu_id. The factory’s device argument is
only ever a device type: cpu to keep a stage on the host, or a platform
type such as cuda/npu that must match the host. It never carries an
index. cuda:1 in a config is refused, and so are factory.gpu_id and the
two memory-fraction kwargs listed in PLACEMENT_OWNED_FACTORY_KWARGS
(sglang_omni/config/schema.py), because those are injected by placement.
Inside a factory the two values meet in one helper:
Declare both parameters,
device: str | None = Noneandgpu_id: int | None = None. A GPU-placed stage whose factory has nogpu_idparameter is refused by name before any weight is loaded.Call
sglang_omni.utils.device.resolve_concrete_device(device, gpu_id). It checks the requested type against the host platform, takes the index fromgpu_id, and only when neither side supplied one asks the host which card the process already sits on. Do not callresolve_device_specfrom a factory and do not combine the two values by hand.A factory that builds SGLang
ServerArgsitself writes the resolved type intoserver_args_overrides["device"]throughsglang_omni.scheduling.sglang_backend.pin_resolved_device_type, which also rejects an operator override that names a different device. The sharedSGLangGenerationEngineBuilderalready does this.
tests/unit_test/test_stage_device_contract.py sweeps every model, topology
and stage in the repository against these rules; a new model is covered
without registering anything. Because every GPU factory follows them, any
GPU process can be given num_replicas/replica_devices
(basic_usage/process_topology.md).
Runtime Prep and Runner#
Runtime prep builds the resolved state used by the runner:
validate stage names and static topology
compute the entry stage and terminal stages
allocate ZMQ endpoints
carry dotted factory, merge, route, and projection paths into worker specs
resolve both kwargs channels without importing stage factories
build relay config from stage placement and relay backend
wire stream targets and same-GPU stream fast paths
Serving uses MultiProcessPipelineRunner for both single-process and
multi-process topologies. Runtime prep first resolves GPU placement, then
process topology: every non-TP stage must declare process explicitly, and
explicit stage.process groups non-TP stages declaratively. A process group
may contain CPU stages and stages on at most one GPU. Multiple process groups
may share the same GPU only when per-stage gpu_memory_fraction budgets are
explicit and fit the configured placement limit.
pipeline/
|-- stage_workers.py # StageLaunchConfig, subprocess entrypoint, StageGroup
|-- runtime_config.py # endpoint/runtime-dir/placement prep
`-- mp_runner.py # Cross-stage orchestration and coordinator ownership
The child process does not recompile the pipeline. The main process builds
fully resolved, picklable stage/process specs; the child imports stage
factories, builds schedulers, constructs Stage objects, signals ready, and
runs one or more non-TP stages in the same event loop.
Tensor Parallelism#
Tensor parallelism inside a stage is orthogonal to pipeline parallelism between stages.
sgl-omni serve --model-path ... --thinker.tp_size 4 --thinker.gpu "[0, 1, 2, 3]"
For tp_size > 1, the runner derives one process per TP rank. Each process runs
the stage scheduler and model worker with a different tp_rank and GPU. NCCL
collectives inside model forward keep TP ranks in lockstep. StageConfig.process
is optional for TP stages; if set, it acts as the prefix for the derived
per-rank process names ({process}_tp{rank}). TP ranks always own their OS
process exclusively.
Only rank 0 owns external stage IO:
rank 0 receives ZMQ messages from the coordinator or previous stage
rank 0 fans work and aborts out to follower ranks
all ranks make the same scheduling decisions
only rank 0 sends downstream results or terminal completions
Each TP stage gets its own NCCL port allocation so multiple TP groups can exist inside one pipeline.