Skip to content

[Bugfix] Reuse one Qwen3-Omni fault payload across concurrent requests - #9407

Merged
amy-why-3459 merged 3 commits into
vllm-project:mainfrom
yenuo26:fix/synthetic-audio-espeak-concurrency
Oct 26, 2026
Merged

amy-why-3459 merged 3 commits into
vllm-project:mainfrom
yenuo26:fix/synthetic-audio-espeak-concurrency

Conversation

@yenuo26

@yenuo26 yenuo26 commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

PLEASE FILL IN THE PR DESCRIPTION HERE.

Contributor intent (required human answers)

Answers below are the public description in #9405, which this PR fixes.

1. What outcome do you want to achieve?

The Qwen3-Omni process-kill reliability test should finish and observe HTTP 500 after the stage-0 worker is killed. It must not abort the pytest process with SIGSEGV.

2. What is the highest-leverage variable for that outcome?

Concurrent generate_synthetic_audio calls. The process-kill checker builds a multimodal body on each worker thread, and pyttsx3/espeak is process-global.

3. How will this work move that variable?

Build the short multimodal request once on the calling thread. The process-kill thread pool then sends that same body and does not call pyttsx3.

4. Whose customer or user needs does this address, and what are those needs?

Buildkite reliability CI for Qwen3-Omni. The job needs the fault injection to complete and check the API error, instead of dying inside espeak while the server is already returning 500.

Purpose

Before this change, ests/dfx/reliability/test_qwen3_omni.py::test_reliability_fault_process_kill_worker_no_load[qwen3_omni_reliability_async_chunk__sigterm] exits 139. The server already returns HTTP 500 after StageEngineCoreProc_stage0 is killed. The pytest process segfaults because four worker threads each call _multimodal_request, which calls generate_synthetic_audio. espeak keeps one synth callback and a fixed 350-entry voice table (N_VOICES_LIST).

After this change, _multimodal_request builds the image, audio, and message body once per model. warmup runs on the main thread before the pool, so that first call synthesizes the audio. �ssert_fast_fail and submit_concurrent reuse the same body. The four workers only send HTTP.

ests/helpers/media.py is unchanged. Other generate_synthetic_audio callers in this repo are single-threaded, so this failure does not need a process-wide espeak lock. Serving, deploy config, and examples are unchanged.

Fixes #9405

Test Plan

vLLM Version: 0.32.0 on the failing Buildkite job.

vLLM-Omni Commit: 0881d06 on top of main 2c32448

No new pytest was added. The change is request reuse inside the existing reliability test:

ext tests/dfx/reliability/test_qwen3_omni.py::test_reliability_fault_process_kill_worker_no_load

That test needs two H100s and Qwen/Qwen3-Omni-30B-A3B-Instruct.

Test Result

image

BEFORE SUBMITTING: read CONTRIBUTING.md and run the precheck-pr skill with the code agent for a self-check against project conventions.

Note

To avoid agent spam, vLLM-Omni limits contributors without write access to 5 open PRs. If this blocks well-intentioned critical work, please contact a committer to request bypass list access.

…sh espeak.

Reliability workers were building speech inside the thread pool. pyttsx3 shares one process-global espeak callback, and overlapping engines segfault pytest.

Signed-off-by: yenuo26 <410167048@qq.com>
@yenuo26
yenuo26 requested a review from NickCao as a code owner October 26, 2026 08:35
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-26T08:35:43.518667Z
We couldn’t complete this review. Comment @codex review to try again.
79c89bf PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@vllm-omni-review-bot

Copy link
Copy Markdown

This PR matches CODEOWNERS paths: /tests/.

Code owners: @NickCao

Routing: @NickCao via CODEOWNERS

@yenuo26, please review your own changes and leave a short self-review comment describing what you checked. PRs without author self-review may not be assigned a reviewer.

Please take a look when you have a chance. If you would like an automated review, mention @vllm-omni-review-bot in a comment.

@vllm-omni-review-bot

vllm-omni-review-bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Omni ReviewBot triage note

Resolved as of 3d7f7b489356: the high-priority or low-quality signal noted on an earlier commit no longer applies.

…quests.

Process-kill workers were synthesizing speech with pyttsx3. Reuse one multimodal body so they only send HTTP.

Signed-off-by: yenuo26 <410167048@qq.com>
@yenuo26

yenuo26 commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

@amy-why-3459 PTAL

@amy-why-3459 amy-why-3459 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clear root-cause write-up on #9405 — concurrent generate_synthetic_audio / pyttsx3 from the process-kill thread pool is a convincing explanation for the pytest SIGSEGV while the server was already returning 500.

What looks good

  • Caching one multimodal body per model in _multimodal_request matches how ProcessKillWorkerNoLoad actually runs: warmup and assert_fast_fail both call it on the main thread before _assert_concurrent_all_fail submits submit_concurrent into the pool (concurrent_workers=4). After that, the four workers only serialize HTTP and no longer touch espeak.
  • Scoped to the reliability test helper; tests/helpers/media.py and other single-threaded generate_synthetic_audio call sites stay untouched, which is a reasonable cut for this failure mode.
  • Returning a fresh top-level dict (and a new modalities list) while reusing the cached payload avoids accidental cross-request mutation of the request envelope.

Notes (non-blocking)

  1. The cache populate path is still check-then-set with no lock (see inline). Safe given the current main-thread warmup / fast-fail ordering; a cold hit from the pool would reintroduce overlapping pyttsx3. A small threading.Lock around populate, or building the body once outside _assert_concurrent_all_fail and passing it in, would make the invariant local rather than call-order dependent.
  2. messages is shared by reference across concurrent submits. Fine while callers only JSON-encode it; worth keeping that assumption if anything later mutates the message list in place.
  3. Title says “Serialize … TTS”; the change is request reuse/caching, not a lock around espeak — minor naming nit vs the implementation.
  4. No local re-run of the H100 process-kill case is claimed; Buildkite confirmation on this head is the real signal for #9405.

Direction looks right for stopping the client-side espeak crash on this path.

Comment on lines +145 to +156
cached = _MULTIMODAL_REQUESTS.get(model)
if cached is None:
image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(224, 224)['base64']}"
audio_data_url = f"data:audio/wav;base64,{generate_synthetic_audio(5, 1)['base64']}"
messages = dummy_messages_from_mix_data(
system_prompt=_system_prompt(),
image_data_url=image_data_url,
audio_data_url=audio_data_url,
content_text="Describe the image and the audio in one short sentence.",
)
cached = {"model": model, "messages": messages, "stream": False, "modalities": ["text", "audio"]}
_MULTIMODAL_REQUESTS[model] = cached

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this get → build → assign sequence has no lock. With ProcessKillWorkerNoLoad it is fine because warmup / assert_fast_fail populate the cache on the main thread before _assert_concurrent_all_fail fans out submit_concurrent. If a future caller hits a cold cache from the pool, several workers could still enter generate_synthetic_audio together. A threading.Lock around the populate, or constructing the body once outside the executor and reusing that object, would make the “no concurrent pyttsx3” guarantee independent of call order.

@yenuo26 yenuo26 changed the title [Bugfix] Serialize synthetic audio TTS so concurrent tests do not crash espeak [Bugfix] Reuse one Qwen3-Omni fault payload across concurrent requests Oct 26, 2026
@yenuo26

yenuo26 commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

@amy-why-3459 Updated the title to [Bugfix] Reuse one Qwen3-Omni fault payload across concurrent requests so it matches the request-reuse change. Thanks for the catch.

@yenuo26 yenuo26 added the ready label to trigger buildkite CI label Oct 26, 2026
@amy-why-3459
amy-why-3459 enabled auto-merge (squash) October 26, 2026 12:59
@amy-why-3459
amy-why-3459 merged commit b5bd597 into vllm-project:main Oct 26, 2026
9 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready label to trigger buildkite CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: weekly CI, Qwen3-Omni process-kill reliability test segfaults in pyttsx3/espeak

3 participants