Files
vllm-omni/recipes/Qwen/Qwen-Image-Edit.md
T

171 lines
7.6 KiB
Markdown
Raw Normal View History

2026-05-19 19:56:38 +08:00
# Qwen-Image-Edit
> Image editing serving
## Summary
- Vendor: Qwen
- Model: `Qwen/Qwen-Image-Edit`
- Task: Text-guided single-image editing
- Mode: Online serving with the OpenAI-compatible multimodal endpoint
- Maintainer: Community
## When to use this recipe
Use this recipe as a baseline for serving `Qwen/Qwen-Image-Edit` for single-image editing on H200 141GB. Two configurations are validated below, sharing one software environment:
| Configuration | Parallelism | Notes |
| --- | --- | --- |
| `1x H200` | none | Single-card baseline; ~60 GiB peak VRAM on a 141 GB card |
| `2x H200` | TP=2 | Lower single-request latency (≈ 1.60× speedup) and lower per-GPU VRAM |
## References
- Model: <https://huggingface.co/Qwen/Qwen-Image-Edit>
- Example guide: [`examples/online_serving/image_to_image/README.md`](../../examples/online_serving/image_to_image/README.md)
- User guide: [`docs/user_guide/examples/online_serving/image_to_image.md`](../../docs/user_guide/examples/online_serving/image_to_image.md)
## Hardware Support
### GPU
The two hardware configurations below share the same software environment.
#### Environment
- OS: Linux
- Python: 3.12+
- Driver / runtime: CUDA-capable runtime matching the repository build
- vLLM: use versions from your current checkout, >=0.20.0
- vLLM-Omni: use versions from your current checkout
#### 1x H200 141GB
##### Command
```bash
CUDA_VISIBLE_DEVICES=0 vllm serve Qwen/Qwen-Image-Edit --omni \
--port 8092
```
The bundled launcher is equivalent:
```bash
bash examples/online_serving/image_to_image/run_server.sh
```
The server is ready when the log shows `INFO: Application startup complete.` (preceded by `Started server process [PID]` and `Waiting for application startup.`).
##### Verification
Run a single-image edit against the public example asset:
```bash
wget https://vllm-public-assets.s3.us-west-2.amazonaws.com/omni-assets/qwen-bear.png
PROMPT="Let this mascot dance under the moon, surrounded by floating stars and poetic bubbles such as 'Be Kind'"
IMG_B64=$(base64 -w0 qwen-bear.png)
curl -s http://localhost:8092/v1/chat/completions \
-H "Content-Type: application/json" \
-d "$(jq -n --arg prompt "$PROMPT" --arg img "$IMG_B64" '{
messages: [{
role: "user",
content: [
{type: "text", text: $prompt},
{type: "image_url", image_url: {url: ("data:image/png;base64," + $img)}}
]
}],
extra_body: {
height: 1024,
width: 1024,
num_inference_steps: 50,
guidance_scale: 1.0,
seed: 42
}
}')" \
| jq -r '.choices[0].message.content[0].image_url.url' \
| cut -d',' -f2- | base64 -d > qwen_image_edit_1card.png
```
The decoded PNG is a 1024×1024 RGB image of the bear mascot dancing under stylized moonlight, surrounded by floating stars and a "Be Kind" text bubble.
You can also run the offline CLI without standing up a server:
```bash
python examples/offline_inference/image_to_image/image_edit.py \
--image qwen-bear.png \
--prompt "Let this mascot dance under the moon, ..." \
--output qwen_image_edit_offline.png \
--num-inference-steps 50 \
--cfg-scale 4.0
```
Validated reference numbers for 1024×1024, 50 denoising steps, batch size 1, `guidance_scale=1.0`, seed 42 (`Cold` = first request after startup, `Warm` = identical subsequent request):
| Cold latency | Warm latency | Peak GPU memory |
| ---: | ---: | ---: |
| 17.80 s | 17.78 s | 61,091 MiB (~59.66 GiB) |
##### Notes
- Generation parameters live under `extra_body` for `/v1/chat/completions`, or as top-level form fields when using `/v1/images/edits`. See [`examples/online_serving/image_to_image/README.md`](../../examples/online_serving/image_to_image/README.md) for the full request / response schema.
- Memory: a 1024×1024 / 50-step edit uses ~60 GiB on a single H200, well within the 141 GB headroom.
- Multi-image variants of Qwen-Image-Edit (e.g. `Qwen-Image-Edit-2509`) are intentionally out of scope for this recipe and are not validated here.
#### 2x H200 141GB (TP=2)
##### Command
```bash
CUDA_VISIBLE_DEVICES=0,1 vllm serve Qwen/Qwen-Image-Edit --omni \
--port 8092 \
--tensor-parallel-size 2
```
In addition to the `Application startup complete.` line, the TP=2 server's `non-default args` log line echoes `'tensor_parallel_size': 2`, and a separate `Worker N: Initialized device and distributed environment` line is emitted for each rank — one per visible GPU.
##### Verification
Run the same `curl` command as in the **1x H200** Verification section above against the TP=2 server (same prompt, same seed, same image). Save the result to `qwen_image_edit_2card_tp2.png` so the two outputs sit side by side for comparison. During TP=2 inference, `nvidia-smi` shows sustained `~99 %` utilization on both GPUs, which is the expected signal that TP wiring is active.
Same workload as the 1x table above (1024×1024, 50 steps, `guidance_scale=1.0`, seed 42):
| Configuration | Cold latency | Warm latency | Peak per-GPU memory |
| --- | ---: | ---: | ---: |
| 1x H200 | 17.80 s | 17.78 s | 61,091 MiB (~59.66 GiB) |
| 2x H200 TP=2 | 11.16 s | 11.09 s | 49,377 MiB (~48.22 GiB) |
TP=2 reduces single-request warm latency from `17.78 s` to `11.09 s` (≈ **1.60×**) and drops per-GPU peak VRAM by ~11.4 GiB.
##### Notes
- TP=2 shards model weights across both cards, which is what produces both the latency reduction and the per-GPU VRAM drop above.
- CFG-Parallel (`--cfg-parallel-size 2`) is supported on the Qwen-Image family but only when `cfg_scale > 1`. The verification command above uses `guidance_scale=1.0` to match the public example image, so it does not exercise CFG-parallel; a separate run with `cfg_scale > 1` is needed if you want to benchmark that path.
- For deeper acceleration knobs (Cache-DiT, sequence parallel, HSDP) see [`docs/user_guide/diffusion/parallelism/overview.md`](../../docs/user_guide/diffusion/parallelism/overview.md); this recipe intentionally documents only the validated TP=2 baseline.
#### Profiling
The numbers below are from a warm request at 1024×1024, 50 steps, and `guidance_scale=1.0`, measured with `--enable-diffusion-pipeline-profiler` and a 250 ms `nvidia-smi --query-gpu=memory.used` sampler. For TP=2, latency uses rank max, and VRAM is per GPU.
Per-stage latency:
| Stage | TP=1 | TP=2 | Notes |
| --- | ---: | ---: | --- |
| `text_encoder.forward` | 0.132 s | 0.135 s | ~1.0× |
| `vae.encode` | 0.040 s | 0.041 s | ~1.0× |
| `diffuse` | 17.105 s | 10.382 s | **1.65×** speedup |
| `vae.decode` | 0.064 s | 0.066 s | ~1.0× |
| `forward` | 17.372 s | 10.652 s | 1.63× speedup |
Per-GPU VRAM:
| Snapshot | TP=1 | TP=2 | Notes |
| --- | ---: | ---: | --- |
| Static VRAM | 58,777 MiB | 47,065 MiB | after model load, before request |
| Peak VRAM | 61,091 MiB | 49,377 MiB | during inference |
| Request-time increment | 2,314 MiB | 2,312 MiB | peak minus static |
The latency gain mainly comes from the DiT denoising stage (`diffuse` in the table above). The other measured stages, such as text encoder and VAE encode/decode, stay essentially unchanged between TP=1 and TP=2, which is consistent with them not benefiting meaningfully from TP in this pipeline. The `forward` row's 1.63× speedup tracks `diffuse`, since `diffuse` accounts for about 98% of total pipeline time.
For memory, TP=2 saves about 11.4 GiB per GPU, mostly from static model-state memory, likely dominated by tensor-parallel sharding of the DiT weights. The request-time increment is almost unchanged, so TP=2 does not materially reduce transient inference-time memory for this single-image, batch-1 workload.