uni-icl / app.py
multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
ef73c3f verified
Raw
History Blame Contribute Delete
13 kB
import os
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
import spaces
import torch
import sys
import json
import tempfile
from pathlib import Path
from PIL import Image
# Add the UniICL code directory to path
sys.path.insert(0, str(Path(__file__).parent / "UniICL"))
from modeling.uniicl import (
BagelConfig, Bagel, Qwen2Config, Qwen2ForCausalLM,
SiglipVisionConfig, SiglipVisionModel
)
from modeling.qwen2 import Qwen2Tokenizer
from modeling.autoencoder import load_ae
from models.capm import CapmConfig
from data.transforms import ImageTransform
from data.data_utils import add_special_tokens, pil_img2rgb
from inferencer import InterleaveInferencer
from copy import deepcopy
from safetensors import safe_open
from safetensors.torch import load_file
MODEL_ID = "xuyicheng-zju/UniICL"
def load_model():
"""Load the UniICL model from Hugging Face Hub."""
from huggingface_hub import snapshot_download
model_path = snapshot_download(MODEL_ID)
# LLM config
llm_config = Qwen2Config.from_json_file(os.path.join(model_path, "llm_config.json"))
llm_config.qk_norm = True
llm_config.tie_word_embeddings = False
llm_config.layer_module = "Qwen2MoTDecoderLayer"
# ViT config
vit_config = SiglipVisionConfig.from_json_file(os.path.join(model_path, "vit_config.json"))
vit_config.rope = False
vit_config.num_hidden_layers = vit_config.num_hidden_layers - 1
# VAE
vae_model, vae_config = load_ae(local_path=os.path.join(model_path, "ae.safetensors"))
vae_model = vae_model.to(device="cuda", dtype=torch.bfloat16)
vae_model.eval()
# CAPM config
capm_config = None
capm_config_path = os.path.join(model_path, "capm_config.json")
if os.path.exists(capm_config_path):
print(f"Loading CAPM config from {capm_config_path}...")
with open(capm_config_path, "r") as f:
capm_dict = json.load(f)
capm_config = CapmConfig(**capm_dict)
print(f"[OK] CAPM config loaded: d_capm={capm_config.d_capm}, num_inject_layers={capm_config.num_inject_layers}")
# Bagel config
config = BagelConfig(
visual_gen=True,
visual_und=True,
llm_config=llm_config,
vit_config=vit_config,
vae_config=vae_config,
capm_config=capm_config,
vit_max_num_patch_per_side=70,
connector_act="gelu_pytorch_tanh",
latent_patch_size=2,
max_latent_size=64,
)
tokenizer = Qwen2Tokenizer.from_pretrained(model_path)
tokenizer, new_token_ids, _ = add_special_tokens(tokenizer)
# Transforms
vae_transform = ImageTransform(1024, 512, 16)
vit_transform = ImageTransform(980, 224, 14)
default_dtype = torch.get_default_dtype()
torch.set_default_dtype(torch.bfloat16)
try:
language_model = Qwen2ForCausalLM(llm_config)
vit_model = SiglipVisionModel(vit_config)
model = Bagel(language_model, vit_model, config)
model.vit_model.vision_model.embeddings.convert_conv2d_to_linear(vit_config, meta=False)
finally:
torch.set_default_dtype(default_dtype)
# Load checkpoint
checkpoint_path = os.path.join(model_path, "ema.safetensors")
if not os.path.exists(checkpoint_path):
checkpoint_path = os.path.join(model_path, "model.safetensors")
print(f"Loading checkpoint from {checkpoint_path}...")
def _load_checkpoint_streaming(model, checkpoint_path):
model_state = model.state_dict()
loaded_keys = set()
unexpected_keys = []
remapped_count = 0
with safe_open(checkpoint_path, framework="pt", device="cpu") as f:
for key in f.keys():
load_key = key
if key.startswith("prism."):
candidate = "capm." + key[len("prism."):]
if candidate in model_state:
load_key = candidate
remapped_count += 1
elif key in model_state:
pass
else:
load_key = candidate
if load_key not in model_state:
unexpected_keys.append(load_key)
continue
tensor = f.get_tensor(key)
target = model_state[load_key]
if tuple(tensor.shape) != tuple(target.shape):
continue
if tensor.dtype != target.dtype:
tensor = tensor.to(dtype=target.dtype)
with torch.no_grad():
target.copy_(tensor)
loaded_keys.add(load_key)
if remapped_count > 0:
print(f" Remapped {remapped_count} legacy PRISM keys to CAPM keys")
missing_keys = [key for key in model_state.keys() if key not in loaded_keys]
if missing_keys:
print(f" Missing keys: {len(missing_keys)}")
if unexpected_keys:
print(f" Unexpected keys: {len(unexpected_keys)}")
_load_checkpoint_streaming(model, checkpoint_path)
print("Moving model to CUDA...")
model = model.to(device="cuda", dtype=torch.bfloat16)
model.eval()
return model, vae_model, tokenizer, vae_transform, vit_transform, new_token_ids
print("Loading UniICL model...")
model, vae_model, tokenizer, vae_transform, vit_transform, new_token_ids = load_model()
print("Model loaded successfully!")
inferencer = InterleaveInferencer(
model=model,
vae_model=vae_model,
tokenizer=tokenizer,
vae_transform=vae_transform,
vit_transform=vit_transform,
new_token_ids=new_token_ids,
)
@spaces.GPU(duration=120)
def run_understanding(
image: Image.Image,
prompt: str,
max_new_tokens: int = 512,
temperature: float = 0.3,
do_sample: bool = False,
) -> str:
"""Answer a question about the provided image using UniICL's understanding capability.
Args:
image: Input image to analyze.
prompt: Question or instruction about the image.
max_new_tokens: Maximum number of tokens to generate.
temperature: Sampling temperature for text generation.
do_sample: Whether to use sampling instead of greedy decoding.
"""
if image is None:
return "Please provide an image."
if not prompt:
return "Please provide a prompt/question."
image = pil_img2rgb(image)
result = inferencer(
image=image,
text=prompt,
understanding_output=True,
max_think_token_n=max_new_tokens,
do_sample=do_sample,
text_temperature=temperature,
)
return result.get("text", "No output generated.")
@spaces.GPU(duration=180)
def run_generation(
prompt: str,
num_timesteps: int = 28,
cfg_text_scale: float = 3.0,
cfg_img_scale: float = 1.5,
image: Image.Image = None,
) -> Image.Image:
"""Generate an image from a text prompt using UniICL's generation capability.
Args:
prompt: Text description of the image to generate.
num_timesteps: Number of diffusion timesteps (more = higher quality, slower).
cfg_text_scale: Text classifier-free guidance scale.
cfg_img_scale: Image classifier-free guidance scale.
image: Optional reference image for image-to-image generation.
"""
if not prompt:
return None
input_list = []
if image is not None:
input_list.append(pil_img2rgb(image))
input_list.append(prompt)
result = inferencer.interleave_inference(
input_list,
understanding_output=False,
num_timesteps=num_timesteps,
cfg_text_scale=cfg_text_scale,
cfg_img_scale=cfg_img_scale,
)
for item in result:
if isinstance(item, Image.Image):
return item
return None
import gradio as gr
CSS = """
#col-container { max-width: 1100px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
"""
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
gr.Markdown(
"""
# 🧠 UniICL: Unified Multimodal In-context Learning
UniICL is a unified multimodal in-context learning framework for both visual understanding and generation.
It demonstrates in-context learning across 6 capability categories: perception, imitation, conception, deduction, analogy, and discernment.
[Paper](https://arxiv.org/abs/2603.24690) | [GitHub](https://github.com/xuyicheng-zju/UniICL) | [Model](https://huggingface.co/xuyicheng-zju/UniICL)
"""
)
with gr.Tabs():
# ── Understanding Tab ──
with gr.Tab("Visual Understanding"):
with gr.Row():
with gr.Column(scale=1):
und_image = gr.Image(label="Input Image", type="pil")
und_prompt = gr.Textbox(
label="Question / Instruction",
placeholder="Describe what you see in this image.",
lines=3,
)
und_btn = gr.Button("Analyze", variant="primary")
with gr.Accordion("Advanced Settings", open=False):
und_max_tokens = gr.Slider(
minimum=64, maximum=2048, value=512, step=64,
label="Max New Tokens"
)
und_temp = gr.Slider(
minimum=0.0, maximum=2.0, value=0.3, step=0.1,
label="Temperature"
)
und_sample = gr.Checkbox(label="Use Sampling", value=False)
with gr.Column(scale=1):
und_output = gr.Textbox(label="Response", lines=12, interactive=False)
und_btn.click(
run_understanding,
inputs=[und_image, und_prompt, und_max_tokens, und_temp, und_sample],
outputs=und_output,
api_name="understand",
)
gr.Examples(
examples=[
["astronaut.jpg", "What is happening in this image? Describe the scene in detail."],
["autumn_forest_path.jpg", "Describe the colors and mood of this forest path."],
["bird_kingfisher.jpg", "What species of bird is this? Describe its key features."],
],
inputs=[und_image, und_prompt],
outputs=und_output,
fn=run_understanding,
cache_examples=True,
cache_mode="lazy",
)
# ── Generation Tab ──
with gr.Tab("Image Generation"):
with gr.Row():
with gr.Column(scale=1):
gen_prompt = gr.Textbox(
label="Generation Prompt",
placeholder="Generate an image of a lighthouse at dusk.",
lines=3,
)
gen_ref_image = gr.Image(
label="Reference Image (optional, for image editing)",
type="pil",
)
gen_btn = gr.Button("Generate", variant="primary")
with gr.Accordion("Advanced Settings", open=False):
gen_steps = gr.Slider(
minimum=4, maximum=100, value=28, step=4,
label="Diffusion Steps"
)
gen_cfg_text = gr.Slider(
minimum=1.0, maximum=10.0, value=3.0, step=0.5,
label="Text CFG Scale"
)
gen_cfg_img = gr.Slider(
minimum=1.0, maximum=5.0, value=1.5, step=0.5,
label="Image CFG Scale"
)
with gr.Column(scale=1):
gen_output = gr.Image(label="Generated Image", type="pil")
gen_btn.click(
run_generation,
inputs=[gen_prompt, gen_steps, gen_cfg_text, gen_cfg_img, gen_ref_image],
outputs=gen_output,
api_name="generate",
)
gr.Examples(
examples=[
["A serene lighthouse at dusk, with warm golden light reflecting off calm ocean waves", 28, 3.0, 1.5, None],
["A cozy cabin in a snow-covered forest, smoke rising from the chimney", 28, 3.0, 1.5, None],
["An abstract painting of a cosmic nebula with vibrant purples and blues", 28, 3.0, 1.5, None],
],
inputs=[gen_prompt, gen_steps, gen_cfg_text, gen_cfg_img, gen_ref_image],
outputs=gen_output,
fn=run_generation,
cache_examples=True,
cache_mode="lazy",
)
demo.launch(mcp_server=True)