Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
acb8044
push initial qwen block addition
Tyler-Odenthal Jun 17, 2026
e4b241f
remove strength from tests and generate
Tyler-Odenthal Jun 17, 2026
f010854
update v1
Tyler-Odenthal Jun 18, 2026
8561131
update cpu mem to false
Tyler-Odenthal Jun 18, 2026
994a344
device map auto
Tyler-Odenthal Jun 19, 2026
ec4f2df
Update qwen_image_edit_hf.py
Tyler-Odenthal Jun 19, 2026
cd98e7e
Merge branch 'roboflow:main' into qwen-image-edit-gpu-block
Tyler-Odenthal Jun 19, 2026
f9c950f
update diffuser pipeline
Tyler-Odenthal Jun 19, 2026
af6c51f
Merge branch 'qwen-image-edit-gpu-block' of https://github.com/Tyler-…
Tyler-Odenthal Jun 19, 2026
6cef518
LoRA flag for smaller GPUs
Tyler-Odenthal Jun 19, 2026
42088d3
needs gpu flag
Tyler-Odenthal Jun 19, 2026
a3a495b
make style
Tyler-Odenthal Jun 19, 2026
9006c7d
final edits and test
Tyler-Odenthal Jun 19, 2026
44b7c6b
Merge remote-tracking branch 'upstream/main' into qwen-image-edit-gpu…
Tyler-Odenthal Jun 19, 2026
bb64050
minor test fix
Tyler-Odenthal Jun 19, 2026
fa67d04
Merge branch 'main' into qwen-image-edit-gpu-block
Tyler-Odenthal Jun 26, 2026
40cbc7e
Merge branch 'main' into qwen-image-edit-gpu-block
PawelPeczek-Roboflow Jul 2, 2026
2ebc3c9
Merge branch 'main' into qwen-image-edit-gpu-block
Tyler-Odenthal Jul 15, 2026
4f4d3f2
Address review: version bumps, docs companions, env-var validation, i…
Tyler-Odenthal Jul 16, 2026
c6fb1a9
Merge branch 'main' into qwen-image-edit-gpu-block
Tyler-Odenthal Jul 16, 2026
019d002
Update inference/core/workflows/core_steps/models/foundation/qwen_ima…
Tyler-Odenthal Jul 16, 2026
5e6ed92
Format qwen_image_edit v1.py after kind fix; drop unused import
Tyler-Odenthal Jul 16, 2026
a1eb23e
Address review: lightning-by-default, load lock + single-entry cache …
Tyler-Odenthal Jul 16, 2026
c96c14a
test: fix flaky visual_search_classifier batch test
Tyler-Odenthal Jul 16, 2026
2c6d48e
test: make Qwen-Image-Edit integration tests strictly opt-in
Tyler-Odenthal Jul 16, 2026
5b04793
Merge branch 'main' into qwen-image-edit-gpu-block
Tyler-Odenthal Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion inference/core/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "1.3.5"
__version__ = "1.3.6"


if __name__ == "__main__":
Expand Down
4 changes: 4 additions & 0 deletions inference/core/workflows/core_steps/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@
from inference.core.workflows.core_steps.models.foundation.qwen.v1 import (
Qwen25VLBlockV1,
)
from inference.core.workflows.core_steps.models.foundation.qwen_image_edit.v1 import (
QwenImageEditBlockV1,
)
from inference.core.workflows.core_steps.models.foundation.qwen_vlm.v1 import (
QwenVlmBlockV1,
)
Expand Down Expand Up @@ -1010,6 +1013,7 @@ def load_blocks() -> List[Type[WorkflowBlock]]:
Qwen35OpenRouterBlockV1,
Qwen36OpenRouterBlockV1,
QwenVlmBlockV1,
QwenImageEditBlockV1,
Comment thread
Tyler-Odenthal marked this conversation as resolved.
OpenAICompatibleBlockV1,
KimiOpenRouterBlockV1,
KimiOpenrouterBlockV2,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
"""Qwen-Image-Edit workflow block (v1).

Takes an input image and a text editing instruction and returns an edited image.
Runs locally on GPU only — no remote/hosted execution path.

Model: Qwen/Qwen-Image-Edit (HuggingFace)
Architecture key: qwen-image-edit
Task: image-editing
"""

import os
import uuid
from typing import Dict, List, Literal, Optional, Type, Union

import numpy as np
from pydantic import ConfigDict, Field

from inference.core.workflows.core_steps.common.entities import StepExecutionMode
from inference.core.workflows.execution_engine.entities.base import (
Batch,
ImageParentMetadata,
OutputDefinition,
WorkflowImageData,
)
from inference.core.workflows.execution_engine.entities.types import (
BOOLEAN_KIND,
FLOAT_KIND,
FLOAT_ZERO_TO_ONE_KIND,
IMAGE_KIND,
INTEGER_KIND,
STRING_KIND,
ImageInputField,
Selector,
)
from inference.core.workflows.prototypes.block import (
AirGappedAvailability,
BlockResult,
Runtime,
RuntimeRestriction,
Severity,
WorkflowBlock,
WorkflowBlockManifest,
)

LONG_DESCRIPTION = """
Edit an image using a text instruction with **Qwen-Image-Edit**, Alibaba's
diffusion-based image editing model.

Provide a source image and describe the change you want to make (e.g. *"change
the sky to a sunset"*, *"add a red hat"*, *"remove the background"*). The block
returns the edited image so it can be passed to downstream blocks or saved as an
output.

#### ⚠️ Requirements

* Requires a **local GPU** — this block cannot run on CPU or hosted inference.
* Weights are loaded from `local_weights_path` if provided, otherwise fetched
from the Roboflow model registry using `model_id`.

#### Lightning LoRA (fast / low-VRAM)

Enable `use_lightning_lora` to fuse the lightx2v **Qwen-Image-Lightning**
step-distillation LoRA into the pipeline. The model then runs in ~4 diffusion
steps with guidance disabled — dramatically faster and feasible on consumer
GPUs. When enabled with no `local_weights_path`, the base model and LoRA are
pulled directly from HuggingFace (no Roboflow registry / API key needed). On
GPUs with limited VRAM set `INFERENCE_MODELS_QWEN_IMAGE_EDIT_CPU_OFFLOAD=sequential`.

#### Parameters

| Parameter | Default | Notes |
|---|---|---|
| `prompt` | — | Required editing instruction |
| `local_weights_path` | None | Absolute path to locally downloaded weights directory |
| `use_lightning_lora` | False | Fuse the 4-step Qwen-Image-Lightning LoRA |
| `num_inference_steps` | auto | Auto = 4 with LoRA, 28 otherwise |
| `guidance_scale` | auto | Auto = 1.0 with LoRA, 5.0 otherwise |
| `seed` | None | Set for reproducible outputs |
"""

DEFAULT_MODEL_ID = "qwen-image-edit/1"


class BlockManifest(WorkflowBlockManifest):
model_config = ConfigDict(
json_schema_extra={
"name": "Qwen-Image-Edit",
"version": "v1",
"short_description": "Edit an image with a text instruction using Qwen-Image-Edit.",
"long_description": LONG_DESCRIPTION,
"license": "Apache-2.0",
"block_type": "model",
"search_keywords": [
"Qwen",
"image edit",
"image editing",
"diffusion",
"Alibaba",
"generative",
],
"ui_manifest": {
"section": "model",
"icon": "fal fa-paintbrush",
"blockPriority": 6,
"needsGPU": True,
},
},
protected_namespaces=(),
)
type: Literal["roboflow_core/qwen_image_edit@v1"]

images: Selector(kind=[IMAGE_KIND]) = ImageInputField

prompt: Union[Selector(kind=[STRING_KIND]), str] = Field(
description="Text instruction describing the desired edit (e.g. 'make the sky orange at sunset').",
examples=["remove the background", "$inputs.edit_prompt"],
json_schema_extra={"multiline": True},
)

model_id: Union[Selector(kind=[STRING_KIND]), str] = Field(
default=DEFAULT_MODEL_ID,
description="Roboflow model-registry id for the Qwen-Image-Edit weights. Ignored when local_weights_path is set.",
examples=[DEFAULT_MODEL_ID],
)

local_weights_path: Optional[Union[Selector(kind=[STRING_KIND]), str]] = Field(
default=None,
description=(
"Absolute path to a locally downloaded Qwen-Image-Edit weights directory. "
"When set, skips the Roboflow registry and loads directly from disk. "
"Useful for development before weights are registered."
),
examples=["/tmp/qwen-image-edit-weights", "$inputs.weights_path"],
)

use_lightning_lora: Union[Selector(kind=[BOOLEAN_KIND]), bool] = Field(
default=False,
description=(
"Fuse the lightx2v Qwen-Image-Lightning step-distillation LoRA into the "
"pipeline. This lets the model run in ~4 diffusion steps (guidance "
"disabled), making it dramatically faster and feasible on consumer GPUs. "
"When enabled and no weights path is given, the base model and LoRA are "
"pulled directly from HuggingFace (no Roboflow registry / API key needed)."
),
examples=[True, "$inputs.use_lightning_lora"],
)

num_inference_steps: Optional[Union[Selector(kind=[INTEGER_KIND]), int]] = Field(
default=None,
description=(
"Number of diffusion denoising steps. More steps improve quality at the "
"cost of speed. Leave unset to auto-select (4 with the Lightning LoRA, "
"28 otherwise)."
),
examples=[4, 28, 50],
)

guidance_scale: Optional[Union[Selector(kind=[FLOAT_ZERO_TO_ONE_KIND]), float]] = (
Field(
default=None,
description=(
"Classifier-free guidance scale. Higher values make the output adhere "
"more strongly to the prompt. Leave unset to auto-select (1.0 with the "
"Lightning LoRA, 5.0 otherwise)."
),
examples=[1.0, 5.0, 7.5],
)
)
Comment thread
Tyler-Odenthal marked this conversation as resolved.
Outdated
Comment thread
Tyler-Odenthal marked this conversation as resolved.
Outdated

seed: Optional[Union[Selector(kind=[INTEGER_KIND]), int]] = Field(
default=None,
description="Optional RNG seed for reproducible outputs. Leave unset for random results.",
examples=[42, "$inputs.seed"],
)

scale_megapixels: Optional[Union[Selector(kind=[FLOAT_KIND]), float]] = Field(
default=None,
description=(
"Downscale inputs larger than this many megapixels before inference "
"(never upscales). Diffusion VRAM/latency scales with pixel count, so a "
"small cap is what keeps the model on a consumer GPU. Leave unset to "
"auto-select (~0.35 MP with the Lightning LoRA, full size otherwise)."
),
examples=[0.35, 1.0],
)

@classmethod
def get_air_gapped_availability(cls) -> AirGappedAvailability:
return AirGappedAvailability(available=True, reason=None)

@classmethod
def get_parameters_accepting_batches(cls) -> List[str]:
return ["images"]

@classmethod
def describe_outputs(cls) -> List[OutputDefinition]:
return [
OutputDefinition(
name="image",
kind=[IMAGE_KIND],
description="The edited output image.",
),
]

@classmethod
def get_execution_engine_compatibility(cls) -> Optional[str]:
return ">=1.4.0,<2.0.0"

@classmethod
def get_supported_model_variants(cls) -> Optional[List[str]]:
return [DEFAULT_MODEL_ID]

@classmethod
def get_restrictions(cls) -> List[RuntimeRestriction]:
return [
RuntimeRestriction(
severity=Severity.HARD,
note="Qwen-Image-Edit requires a GPU. This block cannot run on CPU-only infrastructure.",
applies_to_runtimes=[Runtime.SELF_HOSTED_CPU],
applies_to_step_execution_modes=[StepExecutionMode.LOCAL],
),
RuntimeRestriction(
severity=Severity.HARD,
note="Qwen-Image-Edit is not available on Roboflow Hosted Inference. Run a local GPU inference server.",
applies_to_runtimes=[Runtime.HOSTED_SERVERLESS],
applies_to_step_execution_modes=[StepExecutionMode.LOCAL],
),
]


class QwenImageEditBlockV1(WorkflowBlock):
"""Workflow block that wraps QwenImageEditHF.

Model instances are cached by their load path so weights are only loaded
once per process regardless of how many workflow steps use this block.
"""

_model_cache: Dict[str, object] = {}

def __init__(self, api_key: Optional[str]):
self._api_key = api_key

@classmethod
def get_init_parameters(cls) -> List[str]:
return ["api_key"]

@classmethod
def get_manifest(cls) -> Type[WorkflowBlockManifest]:
return BlockManifest

def run(
self,
images: Batch[WorkflowImageData],
prompt: str,
model_id: str,
local_weights_path: Optional[str],
use_lightning_lora: bool,
num_inference_steps: Optional[int],
guidance_scale: Optional[float],
seed: Optional[int],
scale_megapixels: Optional[float],
) -> BlockResult:
model = self._get_model(
model_id=model_id,
local_weights_path=local_weights_path,
use_lightning_lora=use_lightning_lora,
)
results = []
for image in images:
edited_pil = model.edit(
image=image.numpy_image,
prompt=prompt,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
seed=seed,
scale_megapixels=scale_megapixels,
)
edited_np = np.array(edited_pil)[:, :, ::-1] # RGB → BGR
parent_metadata = ImageParentMetadata(parent_id=str(uuid.uuid4()))
results.append(
{
"image": WorkflowImageData(
parent_metadata=parent_metadata,
numpy_image=edited_np,
)
}
)
return results

def _get_model(
self,
model_id: str,
local_weights_path: Optional[str],
use_lightning_lora: bool,
):
# Use the load path as cache key when provided so switching paths forces
# a reload, while the same path reuses the cached instance. The Lightning
# toggle is part of the key so flipping it loads a distinct instance.
base_key = local_weights_path if local_weights_path else model_id
cache_key = (base_key, bool(use_lightning_lora))

if cache_key not in QwenImageEditBlockV1._model_cache:
# Validate the cheap precondition before importing the (heavy) backend
# so a bad path fails fast with a clear error regardless of whether the
# GPU model stack is importable.
if local_weights_path and not os.path.isdir(local_weights_path):
raise ValueError(
f"local_weights_path '{local_weights_path}' does not exist or is not a directory."
)

from inference_models.models.qwen_image_edit.qwen_image_edit_hf import (
MODEL_ID,
QwenImageEditHF,
)

if local_weights_path:
QwenImageEditBlockV1._model_cache[cache_key] = (
QwenImageEditHF.from_pretrained(
model_name_or_path=local_weights_path,
local_files_only=True,
use_lightning_lora=use_lightning_lora,
)
)
elif use_lightning_lora:
# Dev / offline-friendly path: pull the base model and LoRA
# straight from HuggingFace, bypassing the Roboflow registry.
QwenImageEditBlockV1._model_cache[cache_key] = (
QwenImageEditHF.from_pretrained(
model_name_or_path=MODEL_ID,
local_files_only=False,
use_lightning_lora=True,
)
)
else:
from inference_models import AutoModel

QwenImageEditBlockV1._model_cache[cache_key] = (
AutoModel.from_pretrained(
model_id_or_path=model_id,
api_key=self._api_key,
use_lightning_lora=False,
)
)
return QwenImageEditBlockV1._model_cache[cache_key]
Comment thread
Tyler-Odenthal marked this conversation as resolved.
Outdated
17 changes: 17 additions & 0 deletions inference_models/docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## `0.32.0`

### Added

- Support for [Qwen-Image-Edit](https://huggingface.co/Qwen/Qwen-Image-Edit), Alibaba's
diffusion-based instruction-following image editing model (`qwen-image-edit`, HF backend,
GPU only). Includes an optional lightx2v
[Qwen-Image-Lightning](https://huggingface.co/lightx2v/Qwen-Image-Lightning) LoRA path
enabling ~4-step, low-VRAM inference on consumer GPUs. See the
[model documentation](models/qwen-image-edit.md) for details. Requires the optional
`diffusers` dependency.
- Validation of the `INFERENCE_MODELS_QWEN_IMAGE_EDIT_CPU_OFFLOAD` environment variable —
values other than `model`, `sequential` or `none` now raise `InvalidEnvVariable` instead
of silently falling back to the default offload strategy.

---

## `0.31.0`

### Fixed
Expand Down
Loading
Loading