[Docs] how to add a new multi stage model document (#417)
Signed-off-by: Rein Yang <ruiruyang2@gmail.com> Signed-off-by: root <root@hk01dgx028.cm.cluster> Co-authored-by: root <root@hk01dgx028.cm.cluster>
This commit is contained in:
1 parent
2d42bdcc40
commit
2a3c50147a
4 files changed
+652
-1
No files matched your search
@@ -40,6 +40,7 @@ nav:
|
|||||||
flatten_single_child_sections: true
|
flatten_single_child_sections: true
|
||||||
- Model Implementation:
|
- Model Implementation:
|
||||||
- contributing/model/README.md
|
- contributing/model/README.md
|
||||||
|
- contributing/model/adding_multi_stage_model.md
|
||||||
- CI: contributing/ci
|
- CI: contributing/ci
|
||||||
- Tests: contributing/tests
|
- Tests: contributing/tests
|
||||||
- Design Documents:
|
- Design Documents:
|
||||||
|
|||||||
@@ -1,2 +1,20 @@
|
|||||||
# Adding a New Model
|
# Adding a New Model
|
||||||
Stay tuned for guidance on how to add a new model on vLLM-Omni!
|
|
||||||
|
This section provides comprehensive guidance on how to add a new model to vLLM-Omni.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- **[Adding a New Model Guide](adding_multi_stage_model.md)**: Complete step-by-step guide using Qwen3-Omni as an example
|
||||||
|
|
||||||
|
The guide covers:
|
||||||
|
- Directory structure and organization
|
||||||
|
- Implementing stage components (thinker, talker, code2wav)
|
||||||
|
- Creating the unified model class
|
||||||
|
- Model registration
|
||||||
|
- Stage configuration
|
||||||
|
- Stage input processors
|
||||||
|
- Testing strategies
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
For a quick reference, see the [Adding a New Model Guide](adding_multi_stage_model.md) which walks through the complete implementation of Qwen3-Omni, a multi-stage omni-modality model.
|
||||||
@@ -0,0 +1,632 @@
|
|||||||
|
# Adding a New Multi-Stage Model to vLLM-Omni
|
||||||
|
|
||||||
|
This guide walks through the process of adding a new model to vLLM-Omni, using **Qwen3-Omni** as a comprehensive example. Qwen3-Omni is a multi-stage omni-modality model that demonstrates the full capabilities of vLLM-Omni's architecture.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Overview](#overview)
|
||||||
|
2. [Directory Structure](#directory-structure)
|
||||||
|
3. [Step-by-Step Implementation](#step-by-step-implementation)
|
||||||
|
4. [Key Components](#key-components)
|
||||||
|
5. [Model Registration](#model-registration)
|
||||||
|
6. [Stage Configuration](#stage-configuration)
|
||||||
|
7. [Stage Input Processors](#stage-input-processors)
|
||||||
|
8. [Testing](#testing)
|
||||||
|
9. [Adding a Model Recipe](#adding-a-model-recipe)
|
||||||
|
10. [Summary](#summary)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
vLLM-Omni supports multi-stage model architectures where different stages can run on different devices and process different modalities. The Qwen3-Omni model exemplifies this with three stages:
|
||||||
|
|
||||||
|
1. **Thinker Stage**: Multimodal understanding (text + audio + video) → text generation
|
||||||
|
2. **Talker Stage**: Text embeddings → RVQ codec codes
|
||||||
|
3. **Code2Wav Stage**: RVQ codes → audio waveform
|
||||||
|
|
||||||
|
Each stage is implemented as a separate model class that can be configured independently.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
When adding a new model, you'll need to create the following structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
vllm_omni/model_executor/models/
|
||||||
|
└── your_model_name/ # Model directory (e.g., qwen3_omni)
|
||||||
|
├── __init__.py # Exports main model class
|
||||||
|
├── your_model.py # Main unified model class
|
||||||
|
├── your_model_stage1_implementation.py # Stage 1 implementation (e.g., thinker)
|
||||||
|
├── your_model_stage2_implementation.py # Stage 2 implementation (e.g., talker)
|
||||||
|
└── your_model_stage3_implementation.py # Stage 3 implementation (e.g., code2wav)
|
||||||
|
└── ... maybe other stage implementations
|
||||||
|
|
||||||
|
vllm_omni/model_executor/stage_input_processors/
|
||||||
|
└── your_model_name.py # Stage transition processors
|
||||||
|
|
||||||
|
vllm_omni/model_executor/stage_configs/
|
||||||
|
└── your_model_name.yaml # Stage configuration file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step-by-Step Implementation
|
||||||
|
|
||||||
|
### Step 1: Create the Model Directory
|
||||||
|
|
||||||
|
Create a new directory under `vllm_omni/model_executor/models/`
|
||||||
|
|
||||||
|
### Step 2: Implement Stage Components
|
||||||
|
|
||||||
|
For Qwen3-Omni, we have three stage components:
|
||||||
|
|
||||||
|
#### 2.1 Thinker Stage (`qwen3_omni_moe_thinker.py`)
|
||||||
|
|
||||||
|
The thinker stage handles multimodal understanding. Key features:
|
||||||
|
|
||||||
|
- Inherits from base Qwen3 MoE model in vLLM, using vLLM fused ops & page attn to accelerate
|
||||||
|
- Implements multimodal processing interfaces
|
||||||
|
- Handles audio, video, and image inputs
|
||||||
|
- Generates text outputs
|
||||||
|
|
||||||
|
```python
|
||||||
|
from vllm.model_executor.models.interfaces import SupportsMultiModal, SupportsPP
|
||||||
|
from vllm.model_executor.models.qwen3_moe import Qwen3MoeForCausalLM
|
||||||
|
|
||||||
|
class Qwen3OmniMoeThinkerForConditionalGeneration(
|
||||||
|
Qwen3MoeForCausalLM,
|
||||||
|
SupportsMultiModal,
|
||||||
|
SupportsPP
|
||||||
|
):
|
||||||
|
"""Thinker stage: multimodal understanding → text generation."""
|
||||||
|
|
||||||
|
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||||
|
# Initialize base model
|
||||||
|
# Set up multimodal processors
|
||||||
|
# Configure audio/video/image encoders
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 Talker Stage (`qwen3_omni_moe_talker.py`)
|
||||||
|
|
||||||
|
The talker stage converts text embeddings to codec codes:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Qwen3OmniMoeTalkerForConditionalGeneration(
|
||||||
|
Qwen3MoeForCausalLM,
|
||||||
|
SupportsPP
|
||||||
|
):
|
||||||
|
"""Talker stage: text embeddings → RVQ codec codes."""
|
||||||
|
|
||||||
|
def __init__(self, vllm_config, talker_config, prefix):
|
||||||
|
# Initialize base model
|
||||||
|
# Replace LM head with codec head
|
||||||
|
# Set up text projection from thinker
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3 Code2Wav Stage (`qwen3_omni_code2wav.py`)
|
||||||
|
|
||||||
|
The code2wav stage generates audio waveforms:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Qwen3OmniMoeCode2Wav(nn.Module):
|
||||||
|
"""Code2Wav stage: RVQ codes → audio waveform."""
|
||||||
|
|
||||||
|
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||||
|
# Initialize audio decoder
|
||||||
|
# Set up codec processing
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Implement the Unified Model Class
|
||||||
|
|
||||||
|
The main model class (`qwen3_omni.py`) orchestrates all stages:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@MULTIMODAL_REGISTRY.register_processor(
|
||||||
|
Qwen3OmniMoeThinkerMultiModalProcessor,
|
||||||
|
info=Qwen3OmniMoeThinkerProcessingInfo,
|
||||||
|
dummy_inputs=Qwen3OmniMoeThinkerDummyInputsBuilder,
|
||||||
|
)
|
||||||
|
class Qwen3OmniMoeForConditionalGeneration(
|
||||||
|
nn.Module, SupportsMultiModal, SupportsPP, Qwen3OmniMoeConditionalGenerationMixin
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Unified Qwen3 Omni MoE model combining thinker, talker, and code2wav.
|
||||||
|
|
||||||
|
Architecture:
|
||||||
|
- Thinker: Multimodal understanding (text + audio + video) → text generation
|
||||||
|
- Talker: Text embeddings → RVQ codec codes
|
||||||
|
- Code2Wav: RVQ codes → audio waveform
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
Set `model_stage` in vllm_config to one of: "thinker", "talker", "code2wav"
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||||
|
super().__init__()
|
||||||
|
self.have_multimodal_outputs = True
|
||||||
|
config: Qwen3OmniMoeConfig = vllm_config.model_config.hf_config
|
||||||
|
|
||||||
|
# Determine which stage to initialize
|
||||||
|
self.model_stage = vllm_config.model_config.model_stage
|
||||||
|
|
||||||
|
if self.model_stage == "thinker":
|
||||||
|
# Initialize thinker model
|
||||||
|
thinker_vllm_config = vllm_config.with_hf_config(
|
||||||
|
config.thinker_config,
|
||||||
|
architectures=["Qwen3OmniMoeThinkerForConditionalGeneration"]
|
||||||
|
)
|
||||||
|
self.thinker = init_vllm_registered_model(
|
||||||
|
vllm_config=thinker_vllm_config,
|
||||||
|
prefix=maybe_prefix(prefix, "thinker"),
|
||||||
|
hf_config=config.thinker_config,
|
||||||
|
architectures=["Qwen3OmniMoeThinkerForConditionalGeneration"],
|
||||||
|
)
|
||||||
|
self.model = self.thinker
|
||||||
|
|
||||||
|
elif self.model_stage == "talker":
|
||||||
|
# Initialize talker model
|
||||||
|
talker_vllm_config = vllm_config.with_hf_config(
|
||||||
|
config.talker_config,
|
||||||
|
architectures=["Qwen3OmniMoeTalkerForConditionalGeneration"]
|
||||||
|
)
|
||||||
|
self.talker = init_vllm_registered_model(
|
||||||
|
vllm_config=talker_vllm_config,
|
||||||
|
prefix=maybe_prefix(prefix, "talker"),
|
||||||
|
hf_config=config.talker_config,
|
||||||
|
architectures=["Qwen3OmniMoeTalkerForConditionalGeneration"],
|
||||||
|
)
|
||||||
|
self.model = self.talker
|
||||||
|
|
||||||
|
elif self.model_stage == "code2wav":
|
||||||
|
# Initialize code2wav model
|
||||||
|
code2wav_vllm_config = vllm_config.with_hf_config(
|
||||||
|
config.code2wav_config,
|
||||||
|
architectures=["Qwen3OmniMoeCode2Wav"]
|
||||||
|
)
|
||||||
|
self.code2wav = init_vllm_registered_model(
|
||||||
|
vllm_config=code2wav_vllm_config,
|
||||||
|
prefix=maybe_prefix(prefix, "code2wav"),
|
||||||
|
hf_config=config.code2wav_config,
|
||||||
|
architectures=["Qwen3OmniMoeCode2Wav"],
|
||||||
|
)
|
||||||
|
self.model = self.code2wav
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid model_stage: {self.model_stage}. "
|
||||||
|
f"Must be one of: 'thinker', 'talker', 'code2wav'"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Key Methods to Implement
|
||||||
|
|
||||||
|
1. **`forward()`**: Handles the forward pass for each stage
|
||||||
|
2. **`embed_input_ids()`**: Embeds input token IDs
|
||||||
|
3. **`embed_multimodal()`**: Processes multimodal inputs (if applicable)
|
||||||
|
4. **`compute_logits()`**: Computes logits from hidden states
|
||||||
|
5. **`load_weights()`**: Loads model weights with proper prefixing of different stages
|
||||||
|
|
||||||
|
### Step 4: Create `__init__.py`
|
||||||
|
|
||||||
|
Export the main model class:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# vllm_omni/model_executor/models/qwen3_omni/__init__.py
|
||||||
|
from .qwen3_omni import Qwen3OmniMoeForConditionalGeneration
|
||||||
|
|
||||||
|
__all__ = ["Qwen3OmniMoeForConditionalGeneration"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Components
|
||||||
|
|
||||||
|
### 1. Model Interfaces
|
||||||
|
|
||||||
|
Your model should implement the appropriate interfaces:
|
||||||
|
|
||||||
|
- **`SupportsMultiModal`**: For models that process multimodal inputs
|
||||||
|
- **`SupportsPP`**: For models that support pipeline parallelism
|
||||||
|
- **`SupportsMRoPE`**: For models using multi-dimensional RoPE (if applicable)
|
||||||
|
|
||||||
|
### 2. Multimodal Registration
|
||||||
|
|
||||||
|
If your model processes multimodal inputs, register it with the multimodal registry:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@MULTIMODAL_REGISTRY.register_processor(
|
||||||
|
YourMultiModalProcessor,
|
||||||
|
info=YourProcessingInfo,
|
||||||
|
dummy_inputs=YourDummyInputsBuilder,
|
||||||
|
)
|
||||||
|
class YourModel(nn.Module, SupportsMultiModal):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Weight Loading
|
||||||
|
|
||||||
|
Implement `load_weights()` to handle weight loading with proper prefixing:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||||
|
"""Load weights for all components of the omni model."""
|
||||||
|
loaded_weights = set()
|
||||||
|
thinker_weights = []
|
||||||
|
talker_weights = []
|
||||||
|
code2wav_weights = []
|
||||||
|
|
||||||
|
# Separate weights by component
|
||||||
|
for k, v in weights:
|
||||||
|
if k.startswith("thinker."):
|
||||||
|
thinker_weights.append((k, v))
|
||||||
|
elif k.startswith("talker."):
|
||||||
|
talker_weights.append((k, v))
|
||||||
|
elif k.startswith("code2wav."):
|
||||||
|
code2wav_weights.append((k, v))
|
||||||
|
|
||||||
|
# Load each component's weights
|
||||||
|
if self.thinker and thinker_weights:
|
||||||
|
thinker_loaded = self.thinker.load_weights(thinker_weights)
|
||||||
|
thinker_loaded = add_prefix_to_loaded_weights(thinker_loaded, "thinker")
|
||||||
|
loaded_weights.update(thinker_loaded)
|
||||||
|
|
||||||
|
# Similar for talker and code2wav...
|
||||||
|
|
||||||
|
return loaded_weights
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Output Format
|
||||||
|
|
||||||
|
Use `OmniOutput` for stage outputs:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||||
|
|
||||||
|
# In forward method
|
||||||
|
return OmniOutput(
|
||||||
|
text_hidden_states=hidden_states,
|
||||||
|
multimodal_outputs={"additional_data": data},
|
||||||
|
next_token_id=next_token_id,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Model Registration
|
||||||
|
|
||||||
|
Register your model in `vllm_omni/model_executor/models/registry.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
_OMNI_MODELS = {
|
||||||
|
# ... existing models ...
|
||||||
|
|
||||||
|
# Your new model
|
||||||
|
"YourModelForConditionalGeneration": (
|
||||||
|
"your_model_name", # Module folder name
|
||||||
|
"your_model", # Module file name (without .py)
|
||||||
|
"YourModelForConditionalGeneration", # Class name
|
||||||
|
),
|
||||||
|
"YourModelThinkerForConditionalGeneration": (
|
||||||
|
"your_model_name",
|
||||||
|
"your_model_thinker",
|
||||||
|
"YourModelThinkerForConditionalGeneration",
|
||||||
|
),
|
||||||
|
# ... other stages ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The registry uses lazy loading, so the model class is imported only when needed.
|
||||||
|
|
||||||
|
## Stage Configuration
|
||||||
|
|
||||||
|
Create a YAML configuration file in `vllm_omni/model_executor/stage_configs/`. For a complete example, see the [Qwen3-Omni configuration file](gh-file:vllm_omni/model_executor/stage_configs/qwen3_omni_moe.yaml).
|
||||||
|
|
||||||
|
### Key Configuration Fields
|
||||||
|
|
||||||
|
- **`model_stage`**: Which stage to run ("thinker", "talker", "code2wav", etc.)
|
||||||
|
- **`model_arch`**: The model architecture name (must match registry)
|
||||||
|
- **`engine_input_source`**: List of stage IDs that provide input to this stage
|
||||||
|
- **`custom_process_input_func`**: Function to process inputs from previous stages
|
||||||
|
- **`final_output`**: Whether this stage produces the final output (True/False)
|
||||||
|
- **`final_output_type`**: Type of final output ("text", "audio", "image", etc.)
|
||||||
|
|
||||||
|
## Stage Input Processors
|
||||||
|
|
||||||
|
Stage transitions are the mechanism by which outputs from one stage are converted into inputs for the next stage. This section explains where and how stage transitions occur.
|
||||||
|
|
||||||
|
### Where Stage Transitions Are Called
|
||||||
|
|
||||||
|
Stage transitions happen automatically in the orchestrator (`OmniLLM` class) during the generation loop. Here's the detailed flow:
|
||||||
|
|
||||||
|
1. **Location**: `vllm_omni/entrypoints/omni_llm.py` in the `_run_generation()` method
|
||||||
|
2. **Trigger**: When a stage completes processing and produces outputs
|
||||||
|
3. **Execution Flow**:
|
||||||
|
```python
|
||||||
|
# In omni_llm.py, _run_generation() method (around line 345-460)
|
||||||
|
|
||||||
|
# Main orchestrator loop polls each stage for completed requests
|
||||||
|
for stage_id, stage in enumerate(self.stage_list):
|
||||||
|
result = stage.try_collect() # Get completed request
|
||||||
|
if result is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Store outputs from this stage
|
||||||
|
engine_outputs = _load(result, obj_key="engine_outputs", shm_key="engine_outputs_shm")
|
||||||
|
stage.set_engine_outputs(engine_outputs)
|
||||||
|
|
||||||
|
# Check if there's a next stage to forward to
|
||||||
|
next_stage_id = stage_id + 1
|
||||||
|
if next_stage_id < len(self.stage_list):
|
||||||
|
next_stage: OmniStage = self.stage_list[next_stage_id]
|
||||||
|
|
||||||
|
# THIS IS WHERE STAGE TRANSITION HAPPENS
|
||||||
|
next_inputs = next_stage.process_engine_inputs(
|
||||||
|
self.stage_list,
|
||||||
|
[request_id_to_prompt[req_id]]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Submit to next stage
|
||||||
|
task = {
|
||||||
|
"request_id": req_id,
|
||||||
|
"engine_inputs": next_inputs[0],
|
||||||
|
"sampling_params": sampling_params_list[next_stage_id],
|
||||||
|
}
|
||||||
|
next_stage.submit(task)
|
||||||
|
```
|
||||||
|
|
||||||
|
### How Stage Transitions Work
|
||||||
|
|
||||||
|
The stage transition process follows these steps:
|
||||||
|
|
||||||
|
1. **Stage Completion**: When a stage finishes processing a request, it stores outputs via `stage.set_engine_outputs(engine_outputs)`
|
||||||
|
|
||||||
|
2. **Transition Detection**: The orchestrator checks if there's a next stage and calls `process_engine_inputs()` on it
|
||||||
|
|
||||||
|
3. **Input Processing**: The `process_engine_inputs()` method in `OmniStage` (`omni_stage.py`) handles the transition:
|
||||||
|
```python
|
||||||
|
def process_engine_inputs(
|
||||||
|
self, stage_list: list[Any], prompt: OmniTokensPrompt | TextPrompt = None
|
||||||
|
) -> list[OmniTokensPrompt | TextPrompt]:
|
||||||
|
"""Process engine inputs for this stage from upstream stage outputs."""
|
||||||
|
|
||||||
|
if self.custom_process_input_func is None:
|
||||||
|
# Default behavior: pass token IDs directly
|
||||||
|
# Extract outputs from source stage
|
||||||
|
source_stage_id = self.engine_input_source[0]
|
||||||
|
source_outputs = stage_list[source_stage_id].engine_outputs
|
||||||
|
# ... create OmniTokensPrompt from token_ids ...
|
||||||
|
else:
|
||||||
|
# Custom transition function (YOUR CODE HERE)
|
||||||
|
return self.custom_process_input_func(
|
||||||
|
stage_list,
|
||||||
|
self.engine_input_source,
|
||||||
|
prompt,
|
||||||
|
self.requires_multimodal_data
|
||||||
|
)
|
||||||
|
```
|
||||||
|
- If `custom_process_input_func` is configured, it calls that function
|
||||||
|
- Otherwise, it uses default behavior (passing token IDs directly)
|
||||||
|
|
||||||
|
4. **Custom Function Execution**: Your custom function receives:
|
||||||
|
- `stage_list`: List of all stage objects (to access upstream stage outputs)
|
||||||
|
- `engine_input_source`: List of source stage IDs (e.g., `[0]` for stage 0)
|
||||||
|
- `prompt`: Original prompt data (for preserving multimodal data)
|
||||||
|
- `requires_multimodal_data`: Whether multimodal data is required
|
||||||
|
|
||||||
|
5. **Output Format**: The function must return a list of `OmniTokensPrompt` objects ready for the next stage
|
||||||
|
|
||||||
|
### Data Structures in Stage Transitions
|
||||||
|
|
||||||
|
Understanding the data structures is crucial for implementing stage transitions:
|
||||||
|
|
||||||
|
**Input to your function:**
|
||||||
|
- `stage_list[source_stage_id].engine_outputs`: List of `EngineCoreOutput` objects
|
||||||
|
- Each contains `outputs`: List of `RequestOutput` objects
|
||||||
|
- Each `RequestOutput` has:
|
||||||
|
- `token_ids`: Generated token IDs
|
||||||
|
- `multimodal_output`: Dict with keys like `"code_predictor_codes"`, etc.
|
||||||
|
- These are the hidden states or intermediate outputs from the model's forward pass
|
||||||
|
- `prompt_token_ids`: Original prompt token IDs
|
||||||
|
|
||||||
|
**Output from your function:**
|
||||||
|
- Must return `list[OmniTokensPrompt]` where each `OmniTokensPrompt` contains:
|
||||||
|
- `prompt_token_ids`: List[int] - Token IDs for the next stage
|
||||||
|
- `additional_information`: Dict[str, Any] - Optional metadata (e.g., embeddings, hidden states)
|
||||||
|
- `multi_modal_data`: Optional multimodal data if needed
|
||||||
|
|
||||||
|
### How Model Outputs Are Stored
|
||||||
|
|
||||||
|
The model's `forward()` method returns an `OmniOutput` object that contains:
|
||||||
|
- `text_hidden_states`: Final hidden states for text generation
|
||||||
|
- `multimodal_outputs`: Dict containing intermediate outputs
|
||||||
|
|
||||||
|
These outputs are captured during the forward pass and stored in `multimodal_output` with specific keys:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In your model's forward() method (e.g., qwen3_omni.py)
|
||||||
|
def forward(self, ...):
|
||||||
|
# ... processing ...
|
||||||
|
|
||||||
|
# For thinker stage: capture embeddings and hidden states
|
||||||
|
multimodal_outputs = {
|
||||||
|
"0": captured_embeddings, # Layer 0 embeddings
|
||||||
|
"24": captured_hidden_states, # Layer 24 hidden states
|
||||||
|
"tts_bos_embed": tts_bos_embed,
|
||||||
|
"tts_eos_embed": tts_eos_embed,
|
||||||
|
# ... other intermediate outputs ...
|
||||||
|
}
|
||||||
|
|
||||||
|
return OmniOutput(
|
||||||
|
text_hidden_states=hidden_states,
|
||||||
|
multimodal_outputs=multimodal_outputs,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
These keys are then accessible in your stage transition function:
|
||||||
|
```python
|
||||||
|
# In stage_input_processors/qwen3_omni.py
|
||||||
|
thinker_embeddings = output.multimodal_output["0"] # Access by key
|
||||||
|
thinker_hidden_states = output.multimodal_output["24"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Points
|
||||||
|
|
||||||
|
1. **Accessing Upstream Outputs**: Use `stage_list[source_stage_id].engine_outputs` to get outputs from the source stage
|
||||||
|
2. **Extracting Data**: Access `output.multimodal_output[key]` to get specific hidden states or intermediate results
|
||||||
|
- Keys are defined by your model's `forward()` method when it creates `multimodal_outputs`
|
||||||
|
3. **Device Management**: Move tensors to appropriate devices (CPU for serialization, GPU for processing)
|
||||||
|
4. **Shape Transformations**: Reshape tensors as needed for the next stage (e.g., flattening codec codes)
|
||||||
|
5. **Batch Handling**: Process each request in the batch separately and return a list
|
||||||
|
|
||||||
|
### Complete Flow Diagram
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" src="https://raw.githubusercontent.com/vllm-project/vllm-omni/refs/heads/main/docs/source/architecture/omni-modality-model-architecture.png">
|
||||||
|
<img alt="Data Flow between stages" src="image.png" width=55%>
|
||||||
|
</picture>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### Implementation Example
|
||||||
|
|
||||||
|
Create stage transition processors in `vllm_omni/model_executor/stage_input_processors/your_model_name.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# qwen3_omni.py
|
||||||
|
|
||||||
|
def thinker2talker(
|
||||||
|
stage_list: list[Any],
|
||||||
|
engine_input_source: list[int],
|
||||||
|
prompt: OmniTokensPrompt | TextPrompt | None = None,
|
||||||
|
requires_multimodal_data: bool = False,
|
||||||
|
) -> list[OmniTokensPrompt]:
|
||||||
|
"""
|
||||||
|
Process thinker outputs to create talker inputs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stage_list: List of stage objects
|
||||||
|
engine_input_source: Source stage IDs (typically [0] for thinker)
|
||||||
|
prompt: Original prompt data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of OmniTokensPrompt for talker stage
|
||||||
|
"""
|
||||||
|
source_stage_id = engine_input_source[0]
|
||||||
|
thinker_outputs = stage_list[source_stage_id].engine_outputs
|
||||||
|
talker_inputs = []
|
||||||
|
|
||||||
|
for thinker_output in thinker_outputs:
|
||||||
|
output = thinker_output.outputs[0]
|
||||||
|
# Extract thinker embeddings and hidden states
|
||||||
|
thinker_embeddings = output.multimodal_output["0"].float().clone().detach().cuda()
|
||||||
|
thinker_hidden_states = output.multimodal_output["24"].float().clone().detach().cuda()
|
||||||
|
|
||||||
|
info = {
|
||||||
|
"thinker_embeddings": thinker_embeddings,
|
||||||
|
"thinker_hidden_states": thinker_hidden_states,
|
||||||
|
"thinker_sequences": thinker_output.prompt_token_ids + output.token_ids,
|
||||||
|
"thinker_input_ids": thinker_output.prompt_token_ids,
|
||||||
|
}
|
||||||
|
|
||||||
|
talker_inputs.append(
|
||||||
|
OmniTokensPrompt(
|
||||||
|
prompt_token_ids=[0] * computed_length,
|
||||||
|
additional_information=info,
|
||||||
|
multi_modal_data=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return talker_inputs
|
||||||
|
|
||||||
|
|
||||||
|
def talker2code2wav(
|
||||||
|
stage_list: list[Any],
|
||||||
|
engine_input_source: list[int],
|
||||||
|
prompt: OmniTokensPrompt | TextPrompt | None = None,
|
||||||
|
requires_multimodal_data: bool = False,
|
||||||
|
) -> list[OmniTokensPrompt]:
|
||||||
|
"""
|
||||||
|
Process talker outputs to create code2wav inputs.
|
||||||
|
"""
|
||||||
|
source_stage_id = engine_input_source[0]
|
||||||
|
talker_outputs = stage_list[source_stage_id].engine_outputs
|
||||||
|
code2wav_inputs = []
|
||||||
|
|
||||||
|
for talker_output in talker_outputs:
|
||||||
|
output = talker_output.outputs[0]
|
||||||
|
# Extract codec codes
|
||||||
|
codec_codes = (
|
||||||
|
output.multimodal_output["code_predictor_codes"]
|
||||||
|
.to(torch.long)
|
||||||
|
.transpose(0, 1)
|
||||||
|
.cpu()
|
||||||
|
.to(torch.long)
|
||||||
|
.reshape(-1)
|
||||||
|
.tolist()
|
||||||
|
)
|
||||||
|
|
||||||
|
code2wav_inputs.append(
|
||||||
|
OmniTokensPrompt(
|
||||||
|
prompt_token_ids=codec_codes,
|
||||||
|
multi_modal_data=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return code2wav_inputs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
For comprehensive testing guidelines, please refer to the [Test File Structure and Style Guide](../tests/tests_style.md).
|
||||||
|
|
||||||
|
## Adding a Model Recipe
|
||||||
|
|
||||||
|
After implementing and testing your model, please add a model recipe to the [vllm-project/recipes](https://github.com/vllm-project/recipes) repository. This helps other users understand how to use your model with vLLM-Omni.
|
||||||
|
|
||||||
|
### What to Include
|
||||||
|
|
||||||
|
Your recipe should include:
|
||||||
|
|
||||||
|
1. **Model Overview**: Brief description of the model and its capabilities
|
||||||
|
2. **Installation Instructions**: Step-by-step setup instructions including:
|
||||||
|
- Installing vllm-omni and dependencies
|
||||||
|
- Installing any additional required packages (e.g., xformers, diffusers)
|
||||||
|
- Any version requirements
|
||||||
|
3. **Usage Examples**: Command-line examples demonstrating how to run the model
|
||||||
|
4. **Configuration Details**: Important configuration parameters and their meanings
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
For reference, see the [LongCat recipe example](https://github.com/vllm-project/recipes/pull/179) which demonstrates the expected format and structure.
|
||||||
|
|
||||||
|
### Recipe Location
|
||||||
|
|
||||||
|
Create your recipe file in the appropriate directory structure:
|
||||||
|
- For organization-specific models: `OrganizationName/ModelName.md`
|
||||||
|
- For general models: `ModelName.md`
|
||||||
|
|
||||||
|
The recipe should be a Markdown file that provides clear, reproducible instructions for users to get started with your model.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Adding a new model to vLLM-Omni involves:
|
||||||
|
|
||||||
|
1. **Create model directory structure** with stage implementations
|
||||||
|
2. **Implement unified model class** that orchestrates stages
|
||||||
|
3. **Register model** in `registry.py`
|
||||||
|
4. **Create stage configuration** YAML file
|
||||||
|
5. **Implement stage input processors** for stage transitions
|
||||||
|
6. **Write tests** to verify functionality
|
||||||
|
7. **Add model recipe** to the [vllm-project/recipes](https://github.com/vllm-project/recipes) repository (see [Adding a Model Recipe](#adding-a-model-recipe) section)
|
||||||
|
|
||||||
|
### Qwen3-Omni Reference Files
|
||||||
|
|
||||||
|
For a complete reference implementation, see:
|
||||||
|
|
||||||
|
- **Main model**: `vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py`
|
||||||
|
- **Thinker**: `vllm_omni/model_executor/models/qwen3_omni/qwen3_omni_moe_thinker.py`
|
||||||
|
- **Talker**: `vllm_omni/model_executor/models/qwen3_omni/qwen3_omni_moe_talker.py`
|
||||||
|
- **Code2Wav**: `vllm_omni/model_executor/models/qwen3_omni/qwen3_omni_code2wav.py`
|
||||||
|
- **Stage config**: `vllm_omni/model_executor/stage_configs/qwen3_omni_moe.yaml`
|
||||||
|
- **Input processors**: `vllm_omni/model_executor/stage_input_processors/qwen3_omni.py`
|
||||||
|
- **Registry**: `vllm_omni/model_executor/models/registry.py`
|
||||||
|
- **Testing**: `vllm_omni/tests/e2e/offline_inference/test_qwen3_omni.py`
|
||||||
|
|
||||||
|
For more information, see:
|
||||||
|
- [Architecture Overview](../../design/architecture_overview.md)
|
||||||
|
- [Supported Models](../../models/supported_models.md)
|
||||||
|
- [Stage Configuration Guide](../../configuration/stage_configs.md)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
Reference in new issue
Block a user