Skip to content

Commit 259394a

Browse files
authored
feat: update the kontext examples and models (#495)
* update kontext examples * update tests * add tests for kontext * remove the warning of txt_ids and img_ids * chore: add kontext to be synced from hf to ms * add kontext demo * make linter happy * style: make linter happy * update docs
1 parent 865561d commit 259394a

13 files changed

Lines changed: 426 additions & 12 deletions

File tree

.github/workflows/sync-hf-to-ms.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jobs:
1919
- nunchaku-flux.1-canny-dev
2020
- nunchaku-shuttle-jaguar
2121
- nunchaku-sana
22+
- nunchaku-flux.1-kontext-dev
2223
- svdq-fp4-flux.1-schnell
2324
- svdq-int4-flux.1-schnell
2425
- svdq-fp4-flux.1-dev

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,5 @@ cython_debug/
208208
*.safetensors
209209
*.onnx
210210
.gitattributes
211+
nunchaku-models/
212+
*.png

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Join our user groups on [**Slack**](https://join.slack.com/t/nunchaku/shared_inv
1515

1616
## News
1717

18+
- **[2025-06-29]** 🔥 Support **FLUX.1-Kontext**! Try out our [example script](./examples/flux.1-kontext-dev.py) to see it in action!
1819
- **[2025-06-01]** 🚀 **Release v0.3.0!** This update adds support for multiple-batch inference, [**ControlNet-Union-Pro 2.0**](https://huggingface.co/Shakker-Labs/FLUX.1-dev-ControlNet-Union-Pro-2.0), initial integration of [**PuLID**](https://github.com/ToTheBeginning/PuLID), and introduces [**Double FB Cache**](examples/flux.1-dev-double_cache.py). You can now load Nunchaku FLUX models as a single file, and our upgraded [**4-bit T5 encoder**](https://huggingface.co/mit-han-lab/nunchaku-t5) now matches **FP8 T5** in quality!
1920
- **[2025-04-16]** 🎥 Released tutorial videos in both [**English**](https://youtu.be/YHAVe-oM7U8?si=cM9zaby_aEHiFXk0) and [**Chinese**](https://www.bilibili.com/video/BV1BTocYjEk5/?share_source=copy_web&vd_source=8926212fef622f25cc95380515ac74ee) to assist installation and usage.
2021
- **[2025-04-09]** 📢 Published the [April roadmap](https://github.com/mit-han-lab/nunchaku/issues/266) and an [FAQ](https://github.com/mit-han-lab/nunchaku/discussions/262) to help the community get started and stay up to date with Nunchaku’s development.
@@ -275,6 +276,12 @@ You can specify individual strengths for each LoRA in the list. For a complete e
275276

276277
**For ComfyUI users, you can directly use our LoRA loader. The converted LoRA is deprecated. Please refer to [mit-han-lab/ComfyUI-nunchaku](https://github.com/mit-han-lab/ComfyUI-nunchaku) for more details.**
277278

279+
## Kontext
280+
281+
Nunchaku supports [FLUX.1-Kontext-dev](https://huggingface.co/black-forest-labs/FLUX.1-Kontext-dev), which enables natural language image editing. You can find the [example script](./examples/flux.1-kontext-dev.py) in our examples directory. **Note:** This feature requires diffusers>=0.35.
282+
283+
![kontext](https://huggingface.co/mit-han-lab/nunchaku-artifacts/resolve/main/nunchaku/assets/kontext.png)
284+
278285
## ControlNets
279286

280287
Nunchaku supports both the [FLUX.1-tools](https://blackforestlabs.ai/flux-1-tools/) and the [FLUX.1-dev-ControlNet-Union-Pro](https://huggingface.co/Shakker-Labs/FLUX.1-dev-ControlNet-Union-Pro) models. Example scripts can be found in the [`examples`](examples) directory.

app/flux.1/kontext/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Nunchaku INT4 FLUX.1 Kontext Demo
2+
3+
![demo](https://huggingface.co/mit-han-lab/nunchaku-artifacts/resolve/main/nunchaku/assets/kontext.png)
4+
5+
This interactive Gradio application allows you to edit an image with natural language. Simply run:
6+
7+
```shell
8+
python run_gradio.py
9+
```
10+
11+
- To further reduce GPU memory usage, you can enable the W4A16 text encoder by specifying `--use-qencoder`.
12+
- By default, we use our INT4 model. Use `-p bf16` to switch to the BF16 model.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<div style="display: flex; justify-content: center; align-items: center; text-align: center;">
2+
<div>
3+
<!-- Logo Row -->
4+
<div style="display: flex; justify-content: center; align-items: center; gap: 10px; margin-bottom: 10px;">
5+
<a href="https://github.com/mit-han-lab/nunchaku">
6+
<img src="https://github.com/mit-han-lab/nunchaku/raw/refs/heads/main/assets/nunchaku.svg"
7+
alt="nunchaku logo" style="height: 150px; width: auto;" />
8+
</a>
9+
<a href="https://hanlab.mit.edu/projects/svdquant">
10+
<img src="https://github.com/mit-han-lab/nunchaku/raw/refs/heads/main/assets/svdquant.svg"
11+
alt="svdquant logo" style="height: 40px; width: auto;" />
12+
</a>
13+
</div>
14+
<h1 style="margin-top: 0;">INT4 FLUX.1-Kontext-dev Demo</h1>
15+
16+
<div style="display: flex; justify-content: center; align-items: center; text-align: center;">
17+
{device_info}
18+
</div>
19+
<div style="display: flex; justify-content: center; align-items: center; text-align: center;">
20+
{notice}
21+
</div>
22+
{count_info}
23+
</div>
24+
</div>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css');
2+
3+
.gradio-container {
4+
max-width: 1200px !important;
5+
margin: auto; /* Centers the element horizontally */
6+
}
7+
8+
h1 {
9+
text-align: center
10+
}
11+
12+
.wrap.svelte-p4aq0j.svelte-p4aq0j {
13+
display: none;
14+
}
15+
16+
#column_input, #column_output {
17+
width: 500px;
18+
display: flex;
19+
align-items: center;
20+
}
21+
22+
#input_header, #output_header {
23+
display: flex;
24+
justify-content: center;
25+
align-items: center;
26+
width: 400px;
27+
}
28+
29+
#accessibility {
30+
text-align: center; /* Center-aligns the text */
31+
margin: auto; /* Centers the element horizontally */
32+
}
33+
34+
#random_seed {
35+
height: 71px;
36+
}
37+
38+
#run_button {
39+
height: 87px;
40+
}

app/flux.1/kontext/run_gradio.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# Changed from https://github.com/GaParmar/img2img-turbo/blob/main/gradio_sketch2image.py
2+
import os
3+
import random
4+
import time
5+
from datetime import datetime
6+
7+
import torch
8+
from diffusers import FluxKontextPipeline
9+
from PIL import Image
10+
from utils import get_args
11+
from vars import EXAMPLES, MAX_SEED
12+
13+
from nunchaku.models.transformers.transformer_flux import NunchakuFluxTransformer2dModel
14+
15+
# import gradio last to avoid conflicts with other imports
16+
import gradio as gr # noqa: isort: skip
17+
18+
args = get_args()
19+
20+
if args.precision == "bf16":
21+
pipeline = FluxKontextPipeline.from_pretrained("black-forest-labs/FLUX.1-Kontext-dev", torch_dtype=torch.bfloat16)
22+
pipeline = pipeline.to("cuda")
23+
pipeline.precision = "bf16"
24+
else:
25+
assert args.precision == "int4"
26+
pipeline_init_kwargs = {}
27+
transformer = NunchakuFluxTransformer2dModel.from_pretrained(
28+
"mit-han-lab/nunchaku-flux.1-kontext-dev/svdq-int4_r32-flux.1-kontext-dev.safetensors"
29+
)
30+
pipeline_init_kwargs["transformer"] = transformer
31+
if args.use_qencoder:
32+
from nunchaku.models.text_encoders.t5_encoder import NunchakuT5EncoderModel
33+
34+
text_encoder_2 = NunchakuT5EncoderModel.from_pretrained(
35+
"mit-han-lab/nunchaku-t5/awq-int4-flux.1-t5xxl.safetensors"
36+
)
37+
pipeline_init_kwargs["text_encoder_2"] = text_encoder_2
38+
39+
pipeline = FluxKontextPipeline.from_pretrained(
40+
"black-forest-labs/FLUX.1-Kontext-dev", torch_dtype=torch.bfloat16, **pipeline_init_kwargs
41+
)
42+
pipeline = pipeline.to("cuda")
43+
pipeline.precision = "int4"
44+
45+
46+
def run(image, prompt: str, num_inference_steps: int, guidance_scale: float, seed: int) -> tuple[Image, str]:
47+
img = image["composite"].convert("RGB")
48+
49+
start_time = time.time()
50+
result_image = pipeline(
51+
prompt=prompt,
52+
image=img,
53+
height=img.height,
54+
width=img.width,
55+
num_inference_steps=num_inference_steps,
56+
guidance_scale=guidance_scale,
57+
generator=torch.Generator().manual_seed(seed),
58+
).images[0]
59+
60+
latency = time.time() - start_time
61+
if latency < 1:
62+
latency = latency * 1000
63+
latency_str = f"{latency:.2f}ms"
64+
else:
65+
latency_str = f"{latency:.2f}s"
66+
torch.cuda.empty_cache()
67+
if args.count_use:
68+
if os.path.exists(f"{args.model}-use_count.txt"):
69+
with open(f"{args.model}-use_count.txt", "r") as f:
70+
count = int(f.read())
71+
else:
72+
count = 0
73+
count += 1
74+
current_time = datetime.now()
75+
print(f"{current_time}: {count}")
76+
with open(f"{args.model}-use_count.txt", "w") as f:
77+
f.write(str(count))
78+
with open(f"{args.model}-use_record.txt", "a") as f:
79+
f.write(f"{current_time}: {count}\n")
80+
return result_image, latency_str
81+
82+
83+
with gr.Blocks(css_paths="assets/style.css", title="Nunchaku FLUX.1-Kontext Demo") as demo:
84+
with open("assets/description.html", "r") as f:
85+
DESCRIPTION = f.read()
86+
# Get the GPU properties
87+
if torch.cuda.device_count() > 0:
88+
gpu_properties = torch.cuda.get_device_properties(0)
89+
gpu_memory = gpu_properties.total_memory / (1024**3) # Convert to GiB
90+
gpu_name = torch.cuda.get_device_name(0)
91+
device_info = f"Running on {gpu_name} with {gpu_memory:.0f} GiB memory."
92+
else:
93+
device_info = "Running on CPU 🥶 This demo does not work on CPU."
94+
notice = '<strong>Notice:</strong>&nbsp;We will replace unsafe prompts with a default prompt: "A peaceful world."'
95+
96+
def get_header_str():
97+
98+
if args.count_use:
99+
if os.path.exists("use_count.txt"):
100+
with open("use_count.txt", "r") as f:
101+
count = int(f.read())
102+
else:
103+
count = 0
104+
count_info = (
105+
f"<div style='display: flex; justify-content: center; align-items: center; text-align: center;'>"
106+
f"<span style='font-size: 18px; font-weight: bold;'>Total inference runs: </span>"
107+
f"<span style='font-size: 18px; color:red; font-weight: bold;'>&nbsp;{count}</span></div>"
108+
)
109+
else:
110+
count_info = ""
111+
header_str = DESCRIPTION.format(device_info=device_info, notice=notice, count_info=count_info)
112+
return header_str
113+
114+
header = gr.HTML(get_header_str())
115+
demo.load(fn=get_header_str, outputs=header)
116+
117+
with gr.Row(elem_id="main_row"):
118+
with gr.Column(elem_id="column_input"):
119+
gr.Markdown("## INPUT", elem_id="input_header")
120+
with gr.Group():
121+
canvas = gr.ImageEditor(
122+
height=640,
123+
image_mode="RGB",
124+
sources=["upload", "clipboard"],
125+
type="pil",
126+
label="Input",
127+
show_label=False,
128+
show_download_button=True,
129+
interactive=True,
130+
transforms=[],
131+
canvas_size=(1024, 1024),
132+
scale=1,
133+
format="png",
134+
layers=False,
135+
)
136+
with gr.Row():
137+
prompt = gr.Text(label="Prompt", placeholder="Enter your prompt", scale=6)
138+
run_button = gr.Button("Run", scale=1, elem_id="run_button")
139+
140+
with gr.Row():
141+
seed = gr.Slider(label="Seed", show_label=True, minimum=0, maximum=MAX_SEED, value=233, step=1, scale=4)
142+
randomize_seed = gr.Button("Random Seed", scale=1, min_width=50, elem_id="random_seed")
143+
with gr.Accordion("Advanced options", open=False):
144+
with gr.Group():
145+
num_inference_steps = gr.Slider(label="Inference Steps", minimum=10, maximum=50, step=1, value=28)
146+
guidance_scale = gr.Slider(label="Guidance Scale", minimum=1, maximum=10, step=0.1, value=2.5)
147+
148+
with gr.Column(elem_id="column_output"):
149+
gr.Markdown("## OUTPUT", elem_id="output_header")
150+
with gr.Group():
151+
result = gr.Image(
152+
format="png",
153+
height=640,
154+
image_mode="RGB",
155+
type="pil",
156+
label="Result",
157+
show_label=False,
158+
show_download_button=True,
159+
interactive=False,
160+
elem_id="output_image",
161+
)
162+
latency_result = gr.Text(label="Inference Latency", show_label=True)
163+
164+
gr.Markdown("### Instructions")
165+
gr.Markdown("**1**. Enter a text prompt")
166+
gr.Markdown("**2**. Upload an image")
167+
gr.Markdown("**3**. Try different seeds to generate different results")
168+
169+
run_inputs = [canvas, prompt, num_inference_steps, guidance_scale, seed]
170+
run_outputs = [result, latency_result]
171+
172+
gr.Examples(examples=EXAMPLES, inputs=run_inputs, outputs=run_outputs, fn=run)
173+
174+
randomize_seed.click(
175+
lambda: random.randint(0, MAX_SEED), inputs=[], outputs=seed, api_name=False, queue=False
176+
).then(run, inputs=run_inputs, outputs=run_outputs, api_name=False)
177+
178+
gr.on(
179+
triggers=[prompt.submit, run_button.click],
180+
fn=run,
181+
inputs=run_inputs,
182+
outputs=run_outputs,
183+
api_name=False,
184+
)
185+
186+
gr.Markdown("MIT Accessibility: https://accessibility.mit.edu/", elem_id="accessibility")
187+
188+
189+
if __name__ == "__main__":
190+
demo.queue().launch(debug=True, share=True, root_path=args.gradio_root_path)

app/flux.1/kontext/utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import argparse
2+
3+
4+
def get_args() -> argparse.Namespace:
5+
parser = argparse.ArgumentParser()
6+
parser.add_argument(
7+
"-p", "--precision", type=str, default="int4", choices=["int4", "bf16"], help="Which precisions to use"
8+
)
9+
parser.add_argument("--use-qencoder", action="store_true", help="Whether to use 4-bit text encoder")
10+
parser.add_argument("--no-safety-checker", action="store_true", help="Disable safety checker")
11+
parser.add_argument("--count-use", action="store_true", help="Whether to count the number of uses")
12+
parser.add_argument("--gradio-root-path", type=str, default="")
13+
args = parser.parse_args()
14+
return args

app/flux.1/kontext/vars.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
MAX_SEED = 1000000000
2+
3+
EXAMPLES = [
4+
[
5+
"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/yarn-art-pikachu.png",
6+
"Make Pikachu hold a sign that says 'Nunchaku is awesome', yarn art style, detailed, vibrant colors",
7+
28,
8+
2.5,
9+
3,
10+
],
11+
]

examples/flux.1-kontext-dev.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import torch
2+
from diffusers import FluxKontextPipeline
3+
from diffusers.utils import load_image
4+
5+
from nunchaku import NunchakuFluxTransformer2dModel
6+
from nunchaku.utils import get_precision
7+
8+
transformer = NunchakuFluxTransformer2dModel.from_pretrained(
9+
f"mit-han-lab/nunchaku-flux.1-kontext-dev/svdq-{get_precision()}_r32-flux.1-kontext-dev.safetensors"
10+
)
11+
12+
pipeline = FluxKontextPipeline.from_pretrained(
13+
"black-forest-labs/FLUX.1-Kontext-dev", transformer=transformer, torch_dtype=torch.bfloat16
14+
).to("cuda")
15+
16+
image = load_image(
17+
"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/yarn-art-pikachu.png"
18+
).convert("RGB")
19+
20+
prompt = "Make Pikachu hold a sign that says 'Nunchaku is awesome', yarn art style, detailed, vibrant colors"
21+
image = pipeline(image=image, prompt=prompt, guidance_scale=2.5).images[0]
22+
image.save("flux-kontext-dev.png")

0 commit comments

Comments
 (0)