treadon commited on
Commit
669b8a8
·
verified ·
1 Parent(s): 025033b

Upload image_understand.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. image_understand.py +152 -0
image_understand.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Image Understanding (VQA) — hybrid PyTorch + MLX.
2
+
3
+ - PyTorch image_tokenizer (ViT + VQVAE, 2.4 GB) encodes PIL image → VQ token IDs.
4
+ - MLX LLaDA2 backbone runs the block-diffusion text generation with the VQ-in-vocab
5
+ tokens spliced into the prompt.
6
+
7
+ The ViT/VQVAE loaded to PyTorch MPS is freed before MLX forward passes to stay
8
+ inside the ~64 GB unified memory budget.
9
+ """
10
+ import argparse
11
+ import gc
12
+ import json
13
+ import os
14
+ import sys
15
+ import time
16
+ from pathlib import Path
17
+
18
+ import mlx.core as mx
19
+ from huggingface_hub import snapshot_download
20
+ from PIL import Image
21
+ from transformers import AutoTokenizer
22
+
23
+ # Resolve official repo path (sibling to this package)
24
+ REPO_ROOT = Path(__file__).resolve().parent.parent / "llada2-uni-repo"
25
+ sys.path.insert(0, str(REPO_ROOT))
26
+
27
+ # Stub flash_attn (not on Apple Silicon). The decoder's dispatch_attention_fn
28
+ # fallback handles attention via diffusers + SDPA.
29
+ import types as _types, importlib.machinery as _im
30
+ if "flash_attn" not in sys.modules:
31
+ _stub = _types.ModuleType("flash_attn")
32
+ _stub.__spec__ = _im.ModuleSpec(name="flash_attn", loader=None)
33
+ _stub.__version__ = "0.0.0-stub"
34
+ _stub.flash_attn_func = lambda *a, **k: (_ for _ in ()).throw(
35
+ RuntimeError("flash_attn unavailable"))
36
+ sys.modules["flash_attn"] = _stub
37
+
38
+ from llada2.model import LLaDA2Config, LLaDA2Model
39
+ from llada2.weights import load_weights_into_model
40
+ from llada2.generate import generate_text
41
+
42
+
43
+ def encode_image(image_path: str, model_dir: Path):
44
+ """Return (token_ids, h, w) where tokens are VQ indices (no offset)."""
45
+ import torch
46
+ # Official encoder expects the dir layout of the HF snapshot.
47
+ from encoder.image_tokenizer import ImageTokenizer
48
+ from decoder.utils import generate_crop_size_list, var_center_crop
49
+
50
+ # Use CPU for image tokenizer — it's only 2.4 GB but MPS can OOM on
51
+ # concurrent allocations. CPU path works reliably and takes <30s.
52
+ use_mps = os.environ.get("LLADA2_ENCODER_DEVICE", "cpu") == "mps"
53
+ device = torch.device("mps" if use_mps and torch.backends.mps.is_available() else "cpu")
54
+ dtype = torch.bfloat16 if device.type == "mps" else torch.float32
55
+
56
+ print(f"[encode] loading ImageTokenizer on {device}…")
57
+ t0 = time.time()
58
+ tokenizer = ImageTokenizer(model_path=str(model_dir), device=str(device), dtype=dtype)
59
+ print(f"[encode] loaded in {time.time()-t0:.1f}s")
60
+
61
+ # Default crop target: 512x512 with 32-multiple aspect ratios (matches official script)
62
+ crop_sizes = generate_crop_size_list((512 // 32) ** 2, 32)
63
+ pil = var_center_crop(Image.open(image_path).convert("RGB"), crop_size_list=crop_sizes)
64
+ print(f"[encode] cropped image to {pil.size}")
65
+
66
+ info = tokenizer.encode_with_info(pil)
67
+ t, h, w = info["grid_thw"]
68
+ print(f"[encode] VQ grid: {t}x{h}x{w}, {info['num_tokens']} tokens")
69
+
70
+ # Free the PyTorch model before MLX loads
71
+ del tokenizer
72
+ gc.collect()
73
+
74
+ return info["token_ids"], h, w
75
+
76
+
77
+ def build_prompt(tokenizer, image_tokens: list[int], image_h: int, image_w: int,
78
+ question: str, offset: int) -> list[int]:
79
+ """<|image|><h-token><w-token><boi>[image tokens][<|/image|>] [question]"""
80
+ soi = tokenizer("<|image|>").input_ids
81
+ eoi = tokenizer("<|/image|>").input_ids
82
+ boi = tokenizer("<boi>").input_ids
83
+ h_tok = tokenizer(f"<|reserved_token_{image_h}|>").input_ids
84
+ w_tok = tokenizer(f"<|reserved_token_{image_w}|>").input_ids
85
+ pfx = tokenizer(question).input_ids if question else []
86
+ img_vocab = [t + offset for t in image_tokens]
87
+ return soi + h_tok + w_tok + boi + img_vocab + eoi + pfx
88
+
89
+
90
+ def main():
91
+ ap = argparse.ArgumentParser()
92
+ ap.add_argument("--image", required=True, type=str)
93
+ ap.add_argument("--question", default="Describe this image in detail.", type=str)
94
+ ap.add_argument("--gen-length", default=256, type=int)
95
+ ap.add_argument("--block-length", default=32, type=int)
96
+ ap.add_argument("--steps-per-block", default=16, type=int)
97
+ ap.add_argument("--threshold", default=0.95, type=float)
98
+ ap.add_argument("--repo-id", default="inclusionAI/LLaDA2.0-Uni", type=str)
99
+ args = ap.parse_args()
100
+
101
+ print("[mmu] fetching model files…", flush=True)
102
+ snap = Path(snapshot_download(
103
+ args.repo_id,
104
+ allow_patterns=[
105
+ "model-*.safetensors", "model.safetensors.index.json",
106
+ "config.json", "tokenizer*", "special_tokens_map.json",
107
+ "image_tokenizer/*",
108
+ ],
109
+ ))
110
+ print(f"[mmu] snap dir: {snap}", flush=True)
111
+
112
+ # ---------- Phase 1: encode image to VQ tokens in PyTorch ----------
113
+ image_tokens, h, w = encode_image(args.image, snap)
114
+
115
+ # ---------- Phase 2: run MLX backbone ----------
116
+ tokenizer = AutoTokenizer.from_pretrained(str(snap), trust_remote_code=True)
117
+ config = LLaDA2Config.from_hf(json.loads((snap / "config.json").read_text()))
118
+ model = LLaDA2Model(config)
119
+
120
+ print("[mmu] loading MLX backbone weights…")
121
+ t0 = time.time()
122
+ load_weights_into_model(model, snap, dtype=mx.bfloat16, verbose=False)
123
+ print(f"[mmu] backbone loaded in {time.time()-t0:.1f}s")
124
+
125
+ ids = build_prompt(tokenizer, image_tokens, h, w, args.question, config.image_token_offset)
126
+ prompt_ids = mx.array([ids], dtype=mx.int32)
127
+ print(f"[mmu] prompt token count: {len(ids)} (image tokens: {len(image_tokens)}, question: '{args.question}')")
128
+
129
+ t0 = time.time()
130
+ out = generate_text(
131
+ model, prompt_ids,
132
+ gen_length=args.gen_length,
133
+ block_length=args.block_length,
134
+ steps_per_block=args.steps_per_block,
135
+ temperature=0.0, threshold=args.threshold,
136
+ mask_token_id=config.mask_token_id, eos_token_id=config.eos_token_id,
137
+ verbose=True,
138
+ )
139
+ mx.eval(out)
140
+ dt = time.time() - t0
141
+
142
+ gen_ids = out[0, len(ids):].tolist()
143
+ text = tokenizer.decode(gen_ids, skip_special_tokens=True)
144
+ print(f"\n{'='*60}")
145
+ print(f"Q: {args.question}")
146
+ print(f"A: {text}")
147
+ print(f"{'='*60}")
148
+ print(f"(generated in {dt:.1f}s)")
149
+
150
+
151
+ if __name__ == "__main__":
152
+ main()