Systems like vLLM and SGLang were built on one core assumption:
Input prompt → [Prefill] → [Decode token 1] → [Decode token 2] → ... → Stop
This is called autoregressive (AR) generation — one token at a time, in a single loop.
Today's multimodal models are composite — they are built from structurally different pieces wired together:
| Model Type | Example | Components |
|---|---|---|
| Unified Multimodal | BAGEL | Vision encoder + LLM + Diffusion head + VAE |
| Speech LM | Orpheus | LLM + Audio codec |
| Omni Model | Qwen3-Omni | Thinker LLM + Talker + Waveform codec |
| Robot Action | π0.5 | Vision + LLM + Action predictor |
| World Model | V-JEPA 2 | Encoder + Rollout predictor |
These models introduce three things a single AR loop cannot handle:
The structure of modern model inference is best described as a dataflow graph, not a single loop. A request is a walk through that graph.
In M*, a model is declared as:
Model = Graph of Components + Set of Named Walks
Request = A sequence of Walks chosen by a state machine
BAGEL has 4 core components:
vit_encoder → (understands images)
vae_encoder → (encodes images for editing)
LLM → (backbone for text and image generation)
vae_decoder → (decodes latents to pixels)
Different requests take different walks through these components:
Generate image (text → image):
prefill_text → image_gen
Understand image (image → text):
prefill_text → prefill_vit → decode
Edit image (image → image):
prefill_text → prefill_vae → prefill_vit → image_gen
Key benefit: An image-understanding request never touches the diffusion loop or VAE decoder. An image-generation request never runs the ViT understanding path. You only execute what you need.
The author writes a small function that picks the next Walk based on current state:
def next_walk(self, state):
if state.prefill_steps: # still consuming inputs
return state.prefill_steps.pop(0) # prefill_text / prefill_vae / prefill_vit
if state.target == "image":
return "image_gen" # go to diffusion loop
return "decode" # otherwise, autoregressive text
The author only writes the graph and the walks. Everything physical — placement, scheduling, batching, tensor transport — is the runtime's job.
A node represents one model component. It declares:
vae_decoder = GraphNode(
name="vae_decoder",
input_names=["latents"],
outputs=[
GraphEdge(next_node=EMIT_TO_CLIENT, name="image_output",
output_modality="image"),
],
)
The graph only describes wiring. The actual computation lives in a NodeSubmodule:
class VAEDecoderSubmodule(NodeSubmodule): # is a torch.nn.Module
def prepare_inputs(self, graph_walk, fwd_info, inputs):
return NodeInputs(tensor_inputs={"latents": inputs["latents"][0]})
def forward(self, graph_walk, engine_inputs, latents):
image = self.vae_model.decode(unpatchify(latents))
return {"image_output": [image]}
Separation of concerns:
GraphNode → describes structure (wiring)
NodeSubmodule → describes compute (math)
Runtime → handles batching, KV cache, CUDA graphs, transport
Diffusion models don't generate an image in one step — they run the same denoising operation 49 times, each time refining the latents. World models run rollouts for many steps. Even autoregressive text decode is a loop.
Traditional systems handle loops inside a single stage — they can't express loops that span multiple components generically.
image_gen = Sequential([
Loop(
section=GraphNode(
name="LLM",
input_names=["latents", "time_index"],
outputs=[
GraphEdge(next_node="LLM", name="latents"), # feeds back to itself
GraphEdge(next_node="LLM", name="time_index"),
],
),
max_iters=49, # 49 denoising steps
outputs=[GraphEdge(next_node="vae_decoder", name="latents")],
),
vae_decoder,
])
The loop:
LLM node 49 times, feeding each step's output back as the next step's inputvae_decoderBecause Loop is a generic primitive, M* applies the same optimizations to every loop:
| Loop type | Stop condition | Optimization applied |
|---|---|---|
| Diffusion denoising | Fixed count (49 steps) | Continuous batching + CUDA graph replay |
| Autoregressive text | End-of-sequence token | Continuous batching + CUDA graph replay |
| World model rollout | Horizon reached | Persistent KV cache across steps |
World model rollout gets up to 12.5× speedup over Meta's native implementation because M* keeps the KV cache alive across loop steps instead of recomputing it from scratch each step.
CFG is a technique used in diffusion image generation. Each denoising step requires 3 forward passes through the LLM:
Pass 1: unconditional (no guidance)
Pass 2: text-conditioned
Pass 3: image-conditioned
→ Combine all three → refined latents
Running these in parallel minimizes latency. But flat pipeline systems like vLLM-Omni can't express this natively — they need bespoke per-model plugin code.
image_gen_cfg = Sequential([
Loop(
section=Sequential([
Parallel([
GraphNode(name="LLM", ...outputs to combine_cfg...),
GraphNode(name="LLM_cfg_text", ...outputs to combine_cfg...),
GraphNode(name="LLM_cfg_img", ...outputs to combine_cfg...),
]),
GraphNode(
name="combine_cfg",
input_names=["v_main", "v_cfg_text", "v_cfg_img", "latents", "time_index"],
outputs=[...routes updated latents back to all three LLM branches...],
),
]),
max_iters=49,
outputs=[GraphEdge(next_node="vae_decoder", name="latents")],
),
vae_decoder,
])
The three LLM "views" are the same model weights accessed under three names, each reading/writing its own guidance cache. The runtime fans them out, runs them in parallel, and merges results — no per-model glue code needed.
Placement is declared in a YAML file that maps logical node names to physical GPU ranks. The model code never changes.
model: "bagel"
node_groups:
- { node_names: [vit_encoder, vae_encoder, vae_decoder, LLM], ranks: [0] }
Everything runs on GPU 0.
model: "bagel"
node_groups:
- { node_names: [vit_encoder, vae_encoder, vae_decoder], ranks: [0] }
- { node_names: [LLM, combine_cfg], ranks: [0] }
- { node_names: [LLM_cfg_text], ranks: [1], graph_walks: [image_gen_cfg] }
- { node_names: [LLM_cfg_img], ranks: [2], graph_walks: [image_gen_cfg] }
The graph_walks key means: only use this placement during the image_gen_cfg walk. During text understanding, LLM_cfg_text and LLM_cfg_img don't exist — no wasted GPU memory.
| Capability | How |
|---|---|
| Encoder/decoder disaggregation | Put ViT on GPU 0, VAE on GPU 1 |
| Prefill/decode disaggregation | Different ranks per walk |
| Tensor parallelism | Set tp_size: 2 to shard one large component across 2 GPUs |
| Independent component scaling | Scale only the hot components |
Qwen3-Omni generates speech through a pipeline of 3 components:
Thinker (LLM) → produces hidden states + text
↓ streams hidden states
Talker (AR model) → produces audio codec tokens
↓ streams codec frames
Code2Wav (codec) → produces audio waveform
To start playing audio before the full response is computed, these components must overlap in time — the Talker should start as soon as the Thinker produces its first hidden state.
# Inside Thinker's walk: stream hidden states to Talker
StreamingGraphEdge(next_node="Talker", name="thinker_states",
target_partition="Talker")
# Inside Talker's walk: stream codec frames to Code2Wav
StreamingGraphEdge(next_node="Code2Wav", name="codec_tokens",
target_partition="Code2Wav")
Each streaming edge declares how the consumer reassembles the stream:
PartitionTopology(
connections=[
Connection(
from_partition="Thinker", to_partition="Talker",
edge_name="thinker_states",
chunk_policy_factory=lambda: FixedChunkPolicy(chunk_size=1,
continue_after_done=True)
# Feed Talker one hidden state at a time
),
Connection(
from_partition="Talker", to_partition="Code2Wav",
edge_name="codec_tokens",
chunk_policy_factory=lambda: LeftContextChunkPolicy(chunk=25,
left_context=25)
# Feed Code2Wav 25 frames + 25 frames of left context
# (needed to warm up causal convolutions)
),
],
)
| Policy | Behavior | Used for |
|---|---|---|
FixedChunkPolicy | Send N items at a time | Thinker → Talker (1 state/step) |
LeftContextChunkPolicy | Send chunk + left context window | Talker → Code2Wav |
| Sliding window | Rolling window of recent items | Orpheus SNAC decoder |
Result: Three components on three GPUs, running simultaneously, emitting audio incrementally — with no bespoke per-model streaming code.
Client Request
↓
Conductor ← tracks each request's current Walk, dispatches work
↓
Workers (one per GPU) ← execute nodes, route tensors to each other
↓
Engines ← the actual compute
1. Engines
| Engine | Purpose |
|---|---|
| AR engine | Autoregressive loops with FlashInfer paged-attention KV cache |
| Stateless engine | Encoders, decoders, audio codecs (no KV state) |
Both support continuous batching and CUDA-graph replay.
2. Pluggable data plane
Components exchange tensors over the best available transport:
Same GPU → shared memory (zero copy)
Different GPUs, same machine → RDMA
Across machines → TCP (via Mooncake)
3. Overlapped scheduling
GPU: [Step N running] [Step N+1 running] [Step N+2 running]
CPU: [Prepare N+1] [Prepare N+2] [Prepare N+3]
While the GPU executes step N, the CPU prepares the next batch and attention plan on a separate stream. Loop stop checks are deferred by one iteration so the GPU rarely stalls waiting for the CPU.
4. Tensor parallelism
Set tp_size: 2 in the placement file → the runtime automatically handles parallel linear layers, vocab-parallel embeddings, sharded MoE, sharded KV cache, and NCCL collectives.
| Model · Task | Baseline | Speedup |
|---|---|---|
| BAGEL text→image | vLLM-Omni | ~1.3× lower latency |
| BAGEL image editing | vLLM-Omni | up to 2.6× lower latency |
| BAGEL image→text | vLLM-Omni | ~1.6× faster first token |
| Qwen3-Omni TTS | vLLM-Omni | ~2.7× higher throughput |
| Qwen3-Omni TTS | SGLang-Omni | ~4× higher throughput |
| Qwen3-Omni TTS (TP-2) | SGLang-Omni | ~3.8× higher throughput |
| Orpheus TTS | VoxServe | ~1.3× higher throughput + lower RTF |
| V-JEPA 2 rollout | Meta native | up to 12.5× faster |
BAGEL image editing (2.6×):
Qwen3-Omni TTS (2.7–4×):
V-JEPA 2 rollout (12.5×):
| Feature | vLLM-Omni | SGLang-Omni | M* |
|---|---|---|---|
| Graph node | Engine-instance stage | Worker-pool stage | Model component |
| Composition | Flat DAG | Flat DAG | Sequential / Parallel / Loop / Stream |
| Paths per model | Prefill + decode only | Prefill + decode only | Flexible (any walk) |
| Loops | Within one stage | Within one stage | Across any subgraph |
| Placement granularity | Per stage | Per stage | Per component, per walk |
Key insight: vLLM-Omni and SGLang-Omni are restricted subsets of the Walk Graph. M* strictly generalizes them.
vLLM-Omni / SGLang-Omni model:
Stage 1 → Stage 2 → Stage 3
(each stage is a black box; loops stay inside a stage)
M* model:
Any subgraph can loop, run in parallel, or stream to any other subgraph
CFG parallelism in vLLM-Omni requires a bespoke CFGParallelMixin plugin using torch.distributed.all_gather. In M*, it's just a Parallel block — the same primitive used for any other parallelism.
Automatically search for the best node-to-GPU placement that meets a Service Level Objective (throughput, latency, or cost). Scale only the components on hot walks; offload cold components to host memory.
An AI agent is itself a graph of model calls — the same shape M* runs inside one model. The goal: place the inter-model agent graph and the intra-model component graph under one runtime, so calls across many agents share scheduling, batching, and cached state.
Treat the Walk Graph as an Intermediate Representation for a compiler that can:
┌─────────────────────────────────────────────────────┐
│ M* System │
│ │
│ Model Author provides: │
│ • Graph of nodes (components) │
│ • Named Walks (phases of behavior) │
│ • State machine (which walk comes next) │
│ • Placement YAML (which GPU each node uses) │
│ │
│ Runtime provides: │
│ • Scheduling & batching │
│ • KV cache management │
│ • CUDA graphs & FlashInfer kernels │
│ • Tensor transport (shared mem / RDMA / TCP) │
│ • Loop execution with persistent state │
│ • Parallel branch execution │
│ • Streaming with chunk policies │
└─────────────────────────────────────────────────────┘
The core philosophy: Decouple what the model does (the graph + walks) from how it runs (the runtime). This decoupling is what enables both flexibility for new model architectures and performance through reusable, generic optimizations.