- Resolved conflict in vllm_omni/engine/output_processor.py: use tracing_enabled for vLLM v0.17.0 API - Resolved conflict in vllm_omni/model_executor/models/qwen3_tts/qwen3_tts_code_predictor_vllm.py: keep correct _CodePredictorDecoderLayer.forward() - Update Dockerfile.ci vLLM wheel commit to b31e9326a (releases/v0.17.0) Made-with: Cursor Signed-off-by: Taichang Zhou <tzhouam@connect.ust.hk>
11 KiB
RFC-004: Long Function Decomposition for Improved Readability
| Field | Value |
|---|---|
| Status | Draft |
| Authors | vLLM-Omni Team |
| Created | 2026-03-01 |
1. Summary
This RFC proposes systematically decomposing the long functions in vllm-omni's engine and worker modules. Several functions span hundreds of lines, making them difficult to read, test, and maintain. We identify the worst offenders, propose decomposition strategies, and define guidelines to prevent future regression.
2. Motivation
Identified Long Functions
A survey of the vllm-omni codebase reveals the following functions that significantly exceed reasonable length limits:
| Function | File | Lines | Description |
|---|---|---|---|
_stage_worker_async |
entrypoints/omni_stage.py |
~400 | Async stage worker: device setup, engine init, main loop, profiler handling, output processing |
_stage_worker |
entrypoints/omni_stage.py |
~400 | Sync stage worker: near-identical structure to async version |
handle_profiler_task_local |
entrypoints/omni_stage.py (nested) |
~270 | Profiler start/stop/dump handling for LLM and diffusion engines |
_dummy_run |
worker/gpu_model_runner.py |
~320 | CUDA graph warm-up/capture with multiple execution paths |
_preprocess |
worker/gpu_model_runner.py |
~175 | Input preparation with multiple branches for mm/embeds/text/preprocess |
process_inputs |
engine/input_processor.py |
~193 | Multimodal input processing with tokenization, embeddings, LoRA |
create_model_config |
engine/arg_utils.py |
~146 | Config construction (duplicated for sync/async) |
_sequential_init_lock |
entrypoints/omni_stage.py |
~146 | Lock management for sequential GPU initialization |
process_engine_inputs |
entrypoints/omni_stage.py (class method) |
~50 | Input wiring between stages |
Problems Caused by Long Functions
| Problem | Impact |
|---|---|
| Cognitive load | Developers must hold 200-400 lines of context in their head to understand control flow |
| Testing difficulty | Cannot unit-test internal logic phases; must test the entire function end-to-end |
| Merge conflicts | Long functions increase the probability of merge conflicts during rebases |
| Code duplication | _stage_worker and _stage_worker_async are near-duplicates (~800 lines combined) with minor async/await differences |
| Debugging difficulty | Stack traces point to a single long function; hard to isolate which phase failed |
3. Decomposition Proposals
3.1 _stage_worker and _stage_worker_async (omni_stage.py)
These two functions share ~90% of their logic. The proposed decomposition:
Phase Extraction
# New helper functions (shared between sync and async):
def _init_stage_environment(stage_payload: dict) -> StageContext:
"""Phase 1: Environment setup — ZMQ, device mapping, plugins."""
...
def _init_stage_engine(
model: str, stage_payload: dict, stage_ctx: StageContext
) -> LLMEngine | OmniDiffusion:
"""Phase 2: Engine initialization with sequential lock."""
...
def _init_stage_connectors(
stage_id: str, connectors_config: dict
) -> dict[tuple[str, str], OmniConnectorBase]:
"""Phase 3: Build OmniConnectors."""
...
def _handle_profiler_task(
task_type: OmniStageTaskType, engine: Any, stage_id: str, stage_type: str
) -> dict:
"""Phase 4: Profiler start/stop/dump (extracted from nested function)."""
...
def _process_single_batch(
batch: list[dict], engine: Any, stage_ctx: StageContext
) -> list[dict]:
"""Phase 5: Execute a single batch through the engine."""
...
def _format_and_send_outputs(
outputs: list, out_q: Queue, stage_ctx: StageContext
) -> None:
"""Phase 6: Format outputs and send via IPC."""
...
Sync/Async Unification
def _stage_worker(model, stage_payload, in_q, out_q, ...):
ctx = _init_stage_environment(stage_payload)
engine = _init_stage_engine(model, stage_payload, ctx)
connectors = _init_stage_connectors(ctx.stage_id, ctx.connectors_config)
while True:
batch = _collect_batch(in_q, ctx.max_batch_size, ctx.batch_timeout)
if _is_shutdown(batch):
break
if _is_profiler_task(batch):
result = _handle_profiler_task(batch.task_type, engine, ...)
out_q.put(result)
continue
outputs = _process_single_batch(batch, engine, ctx)
_format_and_send_outputs(outputs, out_q, ctx)
async def _stage_worker_async(model, stage_payload, in_q, out_q, ...):
ctx = _init_stage_environment(stage_payload)
engine = _init_stage_engine(model, stage_payload, ctx) # async variant
connectors = _init_stage_connectors(ctx.stage_id, ctx.connectors_config)
while True:
batch = await _collect_batch_async(in_q, ...)
# ... same structure, async versions of batch processing
3.2 _dummy_run (gpu_model_runner.py)
This function handles CUDA graph capture, warm-up, and profiling. Decompose into:
def _dummy_run(self, num_tokens, ...):
num_scheduled_tokens, attn_metadata = self._prepare_dummy_batch(num_tokens, ...)
hidden_states = self._execute_dummy_forward(
num_scheduled_tokens, attn_metadata, ...
)
self._run_dummy_drafter(num_tokens, is_graph_capturing, ...)
self._finalize_dummy_run(hidden_states, num_scheduled_tokens, ...)
def _prepare_dummy_batch(self, num_tokens, ...):
"""Set up fake batch data, attention metadata, LoRA if needed."""
...
def _execute_dummy_forward(self, num_scheduled_tokens, attn_metadata, ...):
"""Run the model forward pass for CUDA graph capture or warm-up."""
...
def _run_dummy_drafter(self, num_tokens, is_graph_capturing, ...):
"""Run speculative decoding drafter dummy pass if configured."""
...
def _finalize_dummy_run(self, hidden_states, num_scheduled_tokens, ...):
"""Register NVTX hooks, run EPLB step, compute logit indices."""
...
3.3 _preprocess (gpu_model_runner.py)
The current function has 4 major branches. Decompose into:
def _preprocess(self, scheduler_output, num_input_tokens, intermediate_tensors=None):
input_ids, inputs_embeds, model_kwargs, ec_connector_output = (
self._prepare_model_inputs(scheduler_output, num_input_tokens)
)
positions = self._compute_positions(num_input_tokens)
intermediate_tensors = self._prepare_intermediate_tensors(
num_input_tokens, intermediate_tensors
)
self._apply_model_specific_preprocessing(
scheduler_output, input_ids, inputs_embeds, model_kwargs
)
return input_ids, inputs_embeds, positions, intermediate_tensors, model_kwargs, ec_connector_output
def _prepare_model_inputs(self, scheduler_output, num_input_tokens):
"""Branch: mm inputs / prompt embeds / has_preprocess / text-only."""
...
def _compute_positions(self, num_input_tokens):
"""Select M-RoPE / XD-RoPE / standard positions."""
...
def _apply_model_specific_preprocessing(self, ...):
"""Overlay prompt_embeds, run has_preprocess, talker MTP."""
...
3.4 process_inputs (input_processor.py)
Decompose into phases:
def process_inputs(self, request_id, prompt, params):
preprocessed = self._preprocess_prompt(request_id, prompt, params)
tokenized = self._tokenize_and_validate(preprocessed, params)
mm_data = self._extract_multimodal_data(tokenized, params)
prompt_embeds = self._extract_prompt_embeds(tokenized, params)
additional_info = self._extract_additional_information(tokenized, params)
return self._build_engine_core_request(
request_id, tokenized, mm_data, prompt_embeds, additional_info, params
)
3.5 create_model_config (arg_utils.py)
This function is duplicated between OmniEngineArgs and AsyncOmniEngineArgs (~260 lines combined). Decompose and deduplicate:
class OmniEngineArgs:
def create_model_config(self):
return _create_model_config_common(self, is_async=False)
class AsyncOmniEngineArgs:
def create_model_config(self):
return _create_model_config_common(self, is_async=True)
def _create_model_config_common(args, is_async: bool):
"""Shared config construction logic."""
base_config = _build_base_config(args)
_apply_omni_overrides(base_config, args)
_validate_config(base_config)
return base_config
4. Guidelines for Future Code
To prevent regression, we propose the following guidelines:
4.1 Function Length Limits
| Severity | Line Count | Action |
|---|---|---|
| OK | < 50 lines | No action needed |
| Warning | 50-100 lines | Consider decomposition |
| Required | > 100 lines | Must decompose before merge |
4.2 Lint Rule
Add a custom lint check (or use ruff with pylint.max-statements) to flag functions exceeding the threshold:
# pyproject.toml
[tool.ruff.pylint]
max-statements = 50
4.3 Decomposition Principles
- Extract by phase: Each phase of a multi-step function becomes its own method
- Extract by branch: If a function has N major branches, each branch becomes a helper
- Extract by concern: Separate I/O, computation, and error handling
- Preserve hot paths: Do not add unnecessary function call overhead in performance-critical paths (profile after decomposition)
- Keep related logic together: Extracted functions should be in the same module, not scattered
5. Implementation Plan
| Phase | Target | Functions | Est. Impact |
|---|---|---|---|
| Phase 1 | omni_stage.py |
_stage_worker, _stage_worker_async, handle_profiler_task_local |
~800 lines → ~200 + shared helpers |
| Phase 2 | gpu_model_runner.py |
_dummy_run, _preprocess |
~500 lines → ~200 + helpers |
| Phase 3 | input_processor.py |
process_inputs |
~193 lines → ~80 + helpers |
| Phase 4 | arg_utils.py |
create_model_config (dedup) |
~260 lines → ~130 shared |
| Phase 5 | Add lint rules | Configure ruff/pylint | Prevents regression |
6. Risk Mitigation
| Risk | Mitigation |
|---|---|
| Performance regression | Benchmark critical paths (model runner _preprocess, _dummy_run) before and after |
| Behavioral changes | Each decomposition should be a pure refactor — no logic changes |
| Merge conflicts | Coordinate with ongoing PRs; complete in small incremental PRs |
| Variable scoping issues | Use dataclass or NamedTuple for passing multi-value state between phases rather than long parameter lists |
7. Success Metrics
- No function in the codebase exceeds 100 lines (excluding comments/docstrings)
_stage_workerand_stage_worker_asyncshare > 80% of logic via shared helpers- No performance regression in model runner benchmarks
- Improved code review velocity for PRs touching refactored files
8. Open Questions
- Should we introduce a
StageContextdataclass to pass state between phases in_stage_worker, or use simpler parameter passing? - Is the 100-line limit too aggressive for GPU kernel-related code paths where inlining matters?
- Should we use
ruffmax-statements or a custom pre-commit hook for enforcement? - How do we handle the sync/async duplication — use a code generation approach or manual unification?