Multi-Stage Graph#
Qwen3-Omni runs several engines for one request: a thinker (the LLM backbone)
that writes the text reply, a talker and its code predictor that turn the reply
into codec tokens, a vocoder (code2wav) that turns codec tokens into audio, and
the audio and visual encoders in front of the thinker. Qwen3-TTS has no thinker;
its talker speaks the text it is given. The multi-stage graph records which
engines a deployment has, and which output feeds which input, in one file,
multistage_config.json, that the builder writes next to the engines.
multistage_inference loads that file and runs requests through it.
A build whose only components are an llm and its audio or visual encoders gets
a one-stage graph, so LLMs, VLMs, and ASR models also run through
multistage_inference. MiniMax-H3 runs its text encoder, denoiser, and audio
and video decoders as a graph too; see MiniMax-H3.
graph LR
R[request] --> T[thinker]
T -- text --> TO[text output]
T -- hidden states and text --> K[talker and code predictor]
K -- codec tokens --> V[code2wav]
V -- audio --> AO[audio output]
Build the engines#
The experimental tensorrt-edgellm-build writes multistage_config.json into
--engine-dir after it builds the components:
tensorrt-edgellm-build --model-dir $CKPT --engine-dir $ENG \
--max-input-len 1024 --max-kv-cache-capacity 4096 --max-batch-size 4
--max-batch-size caps how many requests run together; see
Batch requests.
Engines from the supported workflow, tensorrt-edgellm-export and the C++
builders, come without a graph. To write one for the engines already in $ENG
without building anything, run:
tensorrt-edgellm-build --graph-only --model-dir $CKPT --engine-dir $ENG
It looks for the backbone in $ENG, $ENG/thinker, or $ENG/llm, for the
encoders in audio/ and visual/ under $ENG or $ENG/multimodal, and for
every other engine in its own subdirectory, such as $ENG/talker,
$ENG/code_predictor, and $ENG/code2wav. An engine anywhere else is left out
of the graph.
The builder writes no graph, and logs why, for a tensor-parallel build, a
qwen3_omni_moe build without its llm component, or a build
with a component that no stage runs, such as an action expert.
Run requests#
Run one request from the command line:
# Text in; the reply on stdout and the speech in hello.wav.
./build/bin/multistage_inference --config $ENG --prompt "Hello" --out-audio hello.wav
# Audio in.
./build/bin/multistage_inference --config $ENG --audio question.wav --out-audio answer.wav
Without --out-audio, or --output-audio-dir for a request file, only the stages
that produce text run, so a speech model answers in text and skips the talker
and the vocoder. A graph without a text output, such as Qwen3-TTS, always runs
its speech stages. --voice SPEAKER selects one of the checkpoint’s speakers.
Run a request file:
./build/bin/multistage_inference --config $ENG --input-file requests.json \
--output-file predictions.json --output-audio-dir out --batch-size 4
The request file has the format that llm_inference reads (see
Input format) or, for Qwen3-TTS, the
format that qwen3_tts_inference reads (see
TTS input file format).
Each entry in predictions.json has the fields llm_inference writes, the
audio fields of qwen3_tts_inference for speech, and ok, error, and
per-stage details in stages. --output-audio-dir receives audio_req<i>.wav
and the talker’s codes as rvq_req<i>.safetensors.
The following table lists common flags; multistage_inference --help lists the
rest.
Flag |
Effect |
|---|---|
|
Vocode each utterance after the talker finishes it, in one pass when it fits the vocoder engine. By default, audio is vocoded in windows while the talker runs. |
|
Default sampling settings, such as |
|
Style settings of one request, such as a video’s |
|
Write the frames of a video terminal as raw 8-bit RGB, one file per request. |
|
With |
|
|
Batch requests#
--batch-size N runs up to N requests from a file as one cohort: the backbone
decodes them as one batch, the talker and code predictor step them together one
codec frame at a time, and the vocoder processes each in turn. A cohort is also
capped by the max_batch that the build wrote. Its requests must run the same
stages with the same request fields, such as a voice, and share batch-wide
settings such as sampling. A request that does not fit runs in a later cohort.
The graph file#
An abridged Qwen3-Omni graph; the builder writes the complete file:
{
"schema_version": 1,
"terminals": ["thinker:text", "code2wav:audio"],
"stages": [
{
"id": "thinker", "runner": "llm.backbone", "artifact": ".", "kind": "autoregressive",
"inputs": [
{"name": "prompt", "from": "request:messages", "on_missing": "fail"},
{"name": "audio", "from": "request:audio", "on_missing": "skip"}
],
"emits": [
{"id": "text", "kind": "text", "transport": "chunk_stream", "terminal": true},
{"id": "accept_hidden", "kind": "hidden", "transport": "chunk_stream"}
]
},
{
"id": "talker", "runner": "qwen3_omni.talker", "artifact": "talker", "kind": "autoregressive",
"inputs": [
{"name": "hidden", "from": "thinker:accept_hidden", "on_missing": "fail"},
{"name": "text_ids", "from": "thinker:text", "on_missing": "fail"},
{"name": "voice", "from": "request:voice", "on_missing": "skip"}
],
"emits": [{"id": "codes", "kind": "codec", "transport": "chunk_stream"}]
},
{
"id": "code2wav", "runner": "qwen3_omni.code2wav", "artifact": "code2wav", "kind": "iterative",
"inputs": [{"name": "codes", "from": "talker:codes", "on_missing": "fail"}],
"emits": [{"id": "audio", "kind": "audio", "transport": "chunk_stream", "terminal": true}]
}
]
}
Field |
Meaning |
|---|---|
|
Stage name. Other stages read its outputs as |
|
Stage implementation: |
|
Engine directory, relative to the directory that holds |
|
Shape of the work, such as |
|
|
|
What happens when the source is missing for a request: |
|
Output ports. |
|
Cohort width and sequence budget, set by the build. |
|
Settings that only the runner reads. |
For each request, only the stages it needs run: a stage runs when the request supplies its inputs and its output reaches a requested terminal.
To check a graph that you edit by hand, run
multistage_inference --config $ENG --validate-config. It checks the structure
of multistage_config.json and that the graph matches the runners that
multistage_inference links, without loading plugins or engines, and then
exits.
Add a model#
A model whose encoders feed one decoder needs no work: it gets the one-stage graph.
A new topology adds a
multistage_topology(bundle, components, args)hook to the family’sconfiguration.pyundertensorrt_edgellm/builder/models/. It returns the stages and terminals for the components that were built, orNonewhen the build cannot run as a graph.argsisNoneunder--graph-only.A new kind of stage implements
IStageand adds its row, aRunnerDecland a factory, to the runner table incpp/runtime/multiStage/stageFactories.cpp; seecpp/runtime/multiStage/README.md.
Differences from llm_inference and qwen3_tts_inference#
multistage_inference runs on the same engine directory as these binaries, so
you can compare their outputs request by request. It differs from them in these
ways:
Speech starts after the thinker finishes every reply in the cohort; there is no thinker-talker streaming.
Requests are batched into cohorts as described in Batch requests;
llm_inferencebatches requests by their position in the file.Audio is vocoded in windows unless
--whole-utteranceis set. Audio files are namedaudio_req<i>.wavafter the request’s position<i>in the file;llm_inferencewritesaudio_req<batch>_batch<row>.wav.Talker and code-predictor keys (
talker_*,cp_*,subtalker_*) take effect at the top level and in each request’ssamplingobject;llm_inferencereads only the top-leveltalker_*keys andrepetition_penalty.
Limits#
A cohort is fixed when a run starts; there is no in-flight batching.
No tensor parallelism. A speculative-decoding backbone serves text only.
Context-cache reuse and visual-token pruning are off for requests that feed the talker.
The Python server does not read the graph.