Composable parallel strategies¶
In vLLM-Omni, the composable_parallel system layers a declarative parallel strategy on top of the existing stage configs. A strategy YAML maps each pipeline stage (by its model_stage name) to a stack of axis declarations — one per parallelism dimension — that the engine translates into per-stage parallel sizing (tensor_parallel_size, data_parallel_size, pipeline_parallel_size, expert-parallel toggle, replica count) and, when relevant, an orchestrator-wide load-balancer policy.
Strategies are intentionally narrow: each one only writes the axes it declares, leaving every other engine arg untouched. This makes a strategy file a small, reusable overlay you can swap independently of the deploy YAML (see configuration/stage_configs.md).
Note
Composable parallel is opt-in. Passing --strategy-config <path/to/strategy.yaml> overlays the derived sizing onto the registry-merged stages before any CLI overrides are applied. When --strategy-config is omitted, stages remain exactly as their deploy YAML produces them.
Top-level schema reference¶
A strategy file has a single top-level mapping, strategies, keyed by the model_stage name of each stage you want to overlay. Each value is a list of axis declarations (see StrategySpec fields below).
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
strategies | dict[str, list] | required | — | Maps model_stage name (e.g. thinker, talker, code2wav, dit) to a list of axis declarations. The key MUST be the stage name, not the integer stage_id. See vllm_omni/config/composable_parallel/strategy_loader.py. |
StrategySpec fields¶
Each entry in a stage's list becomes one StrategySpec (see vllm_omni/config/composable_parallel/spec.py). The loader fills in routing and aggregation from per-kind defaults, so the file stays compact.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
axis | str | required | — | Mesh-axis kind. One of tp, dp, pp, ep, stage_replica (wired today) or sp_ulysses, sp_ring, cfg, vae_pp, hsdp, stage_pp, cp (reserved; declaring one raises AxisTranslationError at translate time). See MeshAxisKind in vllm_omni/config/composable_parallel/spec.py. |
size | int | required | — | Axis degree. Must be > 0. |
routing | str | optional | per-kind default | Stateless routing policy. Accepted only for dp / stage_replica; other kinds reject it. Values: random, round_robin, least_queue. (hash is recognised but currently raises NotImplementedError.) See RouteByStage in vllm_omni/config/composable_parallel/routing.py. |
l1_owner | str | optional | per-kind default | Overrides which layer owns request routing for this axis. engine for tp/dp/pp/ep, delegated for stage_replica. Recorded into shard_extension. |
name | str | optional | axis kind | Human-readable label for this spec, used in error messages. |
Two additional StrategySpec slots — layer_hook_specs and kernel_specs — let advanced authors bind layer-walker hook IDs (LayerHookSpec) or kernel IDs (KernelSpec) onto an axis. These are not exposed by the YAML loader today and are intended for programmatic construction. See vllm_omni/config/composable_parallel/spec.py for the dataclasses.
CLI flags¶
| Flag | Description |
|---|---|
--strategy-config PATH | Path to a strategy.yaml. Only takes effect on registry-based deploy paths (--deploy-config or the bundled default deploy YAML). Its derived sizing is overlaid onto the merged stages before CLI overrides. |
--stage-configs-path PATH | Legacy. Loads the old stage_args YAML schema for un-migrated models. Passing --strategy-config together with this path is silently inapplicable — vLLM-Omni emits a warning and the strategy is ignored (see vllm_omni/entrypoints/utils.py, lines 372–380). |
--omni-lb-policy POLICY | Orchestrator-wide load-balancer policy. Strategy-derived from any stage_replica axis; an explicit CLI value wins and must match the derived one or AsyncOmniEngine raises Conflicting load-balancer policy at construction. |
Warning
--strategy-config cannot be used with --stage-configs-path (the legacy deploy path). If your model has not been migrated to the registry-based pipeline yet, the strategy is silently dropped with a warning. Migrate to a registry-based model or use --deploy-config to apply a strategy.
Sub-schemas¶
axis¶
The set of recognised axis kinds is the MeshAxisKind enum in vllm_omni/config/composable_parallel/spec.py:
# Translatable today (see `_SUPPORTED_KINDS` in translator.py).
axis: tp # tensor parallel — sets tensor_parallel_size
axis: dp # engine data parallel — sets data_parallel_size (vLLM's intra-engine DP LB)
axis: pp # pipeline parallel — sets pipeline_parallel_size
axis: ep # dense expert parallel — sets enable_expert_parallel=True; size must equal tp*dp
axis: stage_replica # omni-coordinator-level replica fan-out — sets num_replicas + omni_lb_policy
# Reserved (raise AxisTranslationError today; declared in the type system for forward compat).
axis: sp_ulysses
axis: sp_ring
axis: cfg
axis: vae_pp
axis: hsdp
axis: stage_pp
axis: cp
routing¶
The full closed hierarchy lives in vllm_omni/config/composable_parallel/routing.py (Broadcast, PartitionByHash, ShardSequence, ShardSpatial, RouteByExpert, PipelineMicrobatch, DuplicateWithCondTag, RouteByStage). For YAML authors only RouteByStage is exposed as a routing: string, and only for dp / stage_replica axes (other axes have fixed routing — tp/ep broadcast, pp microbatch). Recognised policies:
routing: random # uniform-random across replicas/DP ranks
routing: round_robin # cycle through replicas/DP ranks (the default for dp/stage_replica)
routing: least_queue # send to the replica with the smallest backlog
# routing: hash # recognised but raises NotImplementedError; no key-stable balancer yet
aggregation¶
Aggregation is filled in automatically by the loader (_default_aggregation in vllm_omni/config/composable_parallel/strategy_loader.py) from the axis kind, so users do not author it:
| Axis kind | Aggregation pattern |
|---|---|
tp, ep | TakeRank (all ranks agree; pick one) |
dp, ep (in some flows) | Union (disjoint replica results) |
pp | StitchPipeline |
stage_replica | FanInByStage |
Full pattern hierarchy: TakeRank, Union, GatherDim, AllGather, StitchSpatial, StitchPipeline, Combine, FanInByStage (see vllm_omni/config/composable_parallel/aggregation.py).
Precedence¶
From highest to lowest, when the same axis is touched by multiple layers:
- CLI overrides (
--stage_N_tensor_parallel_size,--stage-overrides,--omni-lb-policy, etc.). A CLI value always wins; a warning is emitted whenever it overrides a strategy-declared axis (see_reconcile_strategy_with_cliinvllm_omni/config/config_factory.py). - Strategy YAML (
--strategy-config). Derived sizing is written onto the merged stages with conflict-on-explicit semantics — if the deploy YAML already set a value to something different, application raisesStrategyApplyError. - Deploy YAML (
--deploy-configor the bundledvllm_omni/deploy/<model_type>.yaml). - Parser defaults.
After CLI overrides land, the device-layout guard re-runs (check_device_layout, called from _reconcile_strategy_with_cli) against the effective tp * dp * pp * num_replicas and the resolved devices string, so a --devices or --tensor-parallel-size that conflicts with the strategy's world size cannot slip past the pre-spawn check.
Worked example¶
Example 1 — strategy_tp2.yaml¶
Shard the thinker stage with TP=2 on top of the bundled Qwen2.5-Omni deploy YAML. Routing is fixed (broadcast) for TP, so no routing: field is needed.
Launch:
python3 end2end.py --query-type text --num-prompts 6 \
--strategy-config strategy_tp2.yaml \
--stage-overrides '{"0": {"devices": "0,1"}, "1": {"devices": "2"}, "2": {"devices": "2"}}'
Effective OmniParallelConfig per stage after the overlay (see OmniParallelConfig in vllm_omni/config/composable_parallel/translator.py):
Stage (model_stage) | tensor_parallel_size | data_parallel_size | pipeline_parallel_size | stage_replica_size | omni_lb_policy |
|---|---|---|---|---|---|
thinker | 2 (from strategy) | 1 | 1 | 1 | — |
talker | 1 | 1 | 1 | 1 | — |
code2wav | 1 | 1 | 1 | 1 | — |
Example 2 — strategy_stage_replica.yaml¶
Replicate the thinker stage across two independent engines and let omni's StagePool round-robin requests across them.
# examples/offline_inference/qwen2_5_omni/strategy_stage_replica.yaml
strategies:
thinker:
- axis: stage_replica
size: 2
routing: round_robin
Launch:
python3 end2end.py --query-type text --num-prompts 6 \
--strategy-config strategy_stage_replica.yaml \
--stage-overrides '{"0": {"devices": "0,1"}, "1": {"devices": "2"}, "2": {"devices": "2"}}'
Effective per-stage config and the derived orchestrator knob:
Stage (model_stage) | tensor_parallel_size | stage_replica_size | omni_lb_policy (derived) |
|---|---|---|---|
thinker | 1 | 2 | round-robin |
talker | 1 | 1 | — |
code2wav | 1 | 1 | — |
The derived omni_lb_policy = round-robin is not written into any stage's engine args. Instead it is surfaced on StrategyApplyResult.omni_lb_policy and applied once at orchestrator construction by AsyncOmniEngine._apply_strategy_lb_policy (see vllm_omni/engine/async_omni_engine.py, lines 1107–1134). If you also pass --omni-lb-policy round-robin the values must match; passing a different value raises Conflicting load-balancer policy.
Per-field deep-dive¶
name¶
Default: the axis kind (e.g. "tp").
Human-readable label that appears in AxisTranslationError and StrategyApplyError messages. Set it to disambiguate two stages declaring the same axis kind when scanning logs.
mesh_axis (axis + size)¶
Default: no default — both fields are required.
axis chooses the parallelism dimension; size is its degree. Five kinds are wired end-to-end today (tp, dp, pp, ep, stage_replica); the remaining kinds in MeshAxisKind are reserved and raise AxisTranslationError at translate time so a typo cannot silently become a no-op (see _SUPPORTED_KINDS in vllm_omni/config/composable_parallel/translator.py). For ep, the size MUST equal tensor_parallel_size * data_parallel_size.
routing¶
Default: per-kind — RouteByStage(round_robin) for dp and stage_replica; PipelineMicrobatch() for pp; Broadcast() for tp/ep and any other kind.
User-facing string only on dp and stage_replica axes. The four recognised values are random, round_robin, least_queue, hash; hash is rejected today with NotImplementedError. See RouteByStage in vllm_omni/config/composable_parallel/routing.py.
l1_owner¶
Default: per-kind — engine for tp/dp/pp/ep, delegated for stage_replica.
Overrides which layer owns request routing along this axis. engine means vLLM realizes routing intra-engine; delegated means the omni coordinator owns routing across independent replicas. Set this only when overriding the per-kind default in _DEFAULT_L1_OWNER (vllm_omni/config/composable_parallel/translator.py); invalid values fail fast in _resolve_l1_owner.
stage_replica and omni_lb_policy¶
stage_replica is not a vLLM world dimension; it spins up size independent engine replicas of one stage, coordinated by the omni StagePool load balancer. The axis writes both:
- The stage's
num_replicas(a per-stage deploy knob). - The pipeline-wide
omni_lb_policystring (an orchestrator-level knob).
Because omni_lb_policy is read once at AsyncOmniEngine construction — not from per-stage engine args — only one value is allowed across the whole pipeline. If two stages declare conflicting policies, application raises (see apply_strategy_specs in vllm_omni/config/composable_parallel/apply.py, lines 253–260). To set this knob manually instead, use --omni-lb-policy on the CLI; see the model stage docs for how stage replicas plug into the orchestrator at runtime.
model_stage keys¶
Default: no default — required for every entry under strategies:.
Strategy keys MUST be model_stage names (strings), the same labels stage configs use for engine_args.model_stage (see model_stage in stage_configs.md). Integer stage_id values are not accepted; the resolver matches on model_stage exactly (_resolve_stage in vllm_omni/config/composable_parallel/apply.py).
Common pitfalls¶
Using stage_id integers as keys
Strategy keys are model_stage names (str), not integer stage IDs. A file that uses an integer index will raise at apply time:
StrategyApplyError: strategy role model_stage=0 did not match any stage;
available stages: 'thinker'(id=0), 'talker'(id=1), 'code2wav'(id=2)
Use the model_stage name (e.g. thinker) instead.
Mixing CLI overrides with a strategy on the same axis
A strategy is meant to be the single writer for the axes it declares. If a CLI override (--stage_0_tensor_parallel_size, --stage-overrides '{"0": {...}}') sets the same axis, the CLI value wins and a warning is emitted from _reconcile_strategy_with_cli. The device-layout guard then re-runs against the effective layout, so a CLI override that breaks tp * dp * pp * num_replicas will still fail fast at config time.
Passing --strategy-config against the legacy --stage-configs-path
Composable parallel only applies on the registry-based deploy path. If a model still resolves through the legacy stage_args YAML (loaded via --stage-configs-path), --strategy-config is silently inapplicable and emits:
--strategy-config (<path>) was provided but model '<model>' resolves via the
legacy stage_configs YAML path, which does not support composable-parallel
strategies; the strategy is ignored. Use a registry-based model to apply it.
Migrate the model to the new deploy schema (see configuration/stage_configs.md) before applying a strategy.
Conflicting omni_lb_policy across stages
omni_lb_policy is a single pipeline-wide knob. If two stage_replica axes in different stages derive different policies, apply_strategy_specs raises:
StrategyApplyError: role 'talker' derives omni_lb_policy='least-queue-length'
but role 'thinker' already derived 'round-robin'. The load-balancer policy is
pipeline-wide; only one value is allowed.
Make the policies match, drop one, or supply the value explicitly via --omni-lb-policy.