Download infer.py from OpenOneRec/Kelix-AEDIT: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/OpenOneRec/Kelix-AEDIT/resolve/main/infer.py
- Command line
-
hf download hf://OpenOneRec/Kelix-AEDIT/infer.py
-
curl -L -o infer.py https://huggingface.co/OpenOneRec/Kelix-AEDIT/resolve/main/infer.py
10.5 kB
| """Single-GPU Kelix-AEDIT inference: an image or local VQ indices -> PNG.""" | |
| import argparse | |
| import json | |
| import sys | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parent | |
| # Support running the demo or a downloaded HF script inside a Kelix checkout. | |
| for parent in (ROOT, *ROOT.parents): | |
| if (parent / 'muse' / '__init__.py').is_file(): | |
| sys.path.insert(0, str(parent)) | |
| break | |
| def parse_args(): | |
| p = argparse.ArgumentParser(description=__doc__) | |
| p.add_argument('--ckpt', default=str(ROOT) if (ROOT / 'config.json').is_file() else 'OpenOneRec/Kelix-AEDIT', help='Local DiT directory or HF repo ID') | |
| p.add_argument('--tokenizer', default='OpenOneRec/Kelix-SFT', help='Kelix-SFT repo/directory or a standalone image tokenizer directory') | |
| p.add_argument('--vae', default='Efficient-Large-Model/SANA1.5_1.6B_1024px_diffusers/vae', help='Local DC-AE directory or HF repo/subfolder') | |
| inputs = p.add_mutually_exclusive_group(required=True) | |
| inputs.add_argument('--image', help='Image to encode and reconstruct') | |
| inputs.add_argument('--tokens', help='Safely loaded .pt LongTensor [H*W,8], local codebook indices') | |
| p.add_argument('--grid', type=int, default=18, help='Condition grid width/height; 18 means 324 tokens') | |
| p.add_argument('--output', default='output.png') | |
| p.add_argument('--image-size', type=int, default=512) | |
| p.add_argument('--steps', type=int, default=20) | |
| p.add_argument('--cfg', type=float, default=4.5) | |
| p.add_argument('--flow-shift', type=float, default=3.0) | |
| p.add_argument('--seed', type=int, default=42) | |
| p.add_argument('--device', default='cuda:0') | |
| p.add_argument('--dtype', choices=['bfloat16', 'float16', 'float32'], default='bfloat16') | |
| return p.parse_args() | |
| def positions(h, w, device): | |
| import torch | |
| ids = torch.arange(h * w, device=device) | |
| return {'height': ids // w, 'width': ids % w} | |
| def resolve_directory(source, allow_patterns=None): | |
| path = Path(source) | |
| if path.is_dir(): | |
| return path | |
| from huggingface_hub import snapshot_download | |
| parts = str(source).split('/') | |
| repo_id = '/'.join(parts[:2]) | |
| subfolder = '/'.join(parts[2:]) | |
| patterns = [f'{subfolder}/*'] if subfolder else allow_patterns | |
| root = Path(snapshot_download(repo_id, allow_patterns=patterns)) | |
| return root / subfolder if subfolder else root | |
| def load_image_tokenizer(source, device, dtype): | |
| """Load the visual tokenizer without instantiating or downloading the LLM.""" | |
| import torch | |
| from accelerate import init_empty_weights | |
| from safetensors import safe_open | |
| from muse.config.model_config import KeyeTokenizerConfig | |
| from muse.models.keye_tokenizer import KeyeImageTokenizer | |
| folder = resolve_directory(source, ['*.json', '*.txt', '*.py', '*.jinja']) | |
| config = json.loads((folder / 'config.json').read_text()) | |
| if 'tokenizer_config' not in config: | |
| tokenizer = KeyeImageTokenizer.from_pretrained( | |
| str(folder), strict=True, fusion_type='mean' | |
| ).to(device=device, dtype=dtype).eval() | |
| return tokenizer, folder | |
| tokenizer_config = KeyeTokenizerConfig(**config['tokenizer_config']) | |
| tokenizer_config.fusion_type = 'mean' | |
| with init_empty_weights(): | |
| tokenizer = KeyeImageTokenizer(tokenizer_config) | |
| index = json.loads((folder / 'model.safetensors.index.json').read_text()) | |
| prefix = 'visual_tokenizer.' | |
| selected = {k: v for k, v in index['weight_map'].items() if k.startswith(prefix)} | |
| if not selected: | |
| raise ValueError('No visual_tokenizer weights found in the checkpoint index') | |
| state = {} | |
| for shard in sorted(set(selected.values())): | |
| path = folder / shard | |
| if not path.is_file(): | |
| if Path(source).is_dir(): | |
| raise FileNotFoundError(f'Missing tokenizer shard: {shard}') | |
| from huggingface_hub import hf_hub_download | |
| path = Path(hf_hub_download(source, shard, revision=folder.name)) | |
| with safe_open(str(path), framework='pt', device='cpu') as weights: | |
| for key, filename in selected.items(): | |
| if filename == shard: | |
| state[key[len(prefix):]] = weights.get_tensor(key) | |
| tokenizer.load_state_dict(state, strict=True, assign=True) | |
| tokenizer = tokenizer.to(device=device, dtype=dtype).eval() | |
| return tokenizer, folder | |
| def main(args): | |
| import torch | |
| import torch.nn.functional as F | |
| from PIL import Image | |
| from diffusers import AutoencoderDC, FlowMatchEulerDiscreteScheduler | |
| from transformers import AutoProcessor | |
| from muse.models.base import Model | |
| from muse.training.common import set_default_dtype | |
| if args.grid < 1 or args.steps < 1 or args.image_size < 32 or args.image_size % 32: | |
| raise ValueError('grid and steps must be positive; image-size must be a positive multiple of 32') | |
| ckpt = resolve_directory(args.ckpt, ['config.json', '*.safetensors', '*.index.json']) | |
| dit_dir = ckpt / 'dit' if (ckpt / 'dit').is_dir() else ckpt | |
| device, dtype = torch.device(args.device), getattr(torch, args.dtype) | |
| if device.type == 'cuda' and not torch.cuda.is_available(): | |
| raise RuntimeError('CUDA is required for the FlashAttention vision encoder') | |
| torch.manual_seed(args.seed) | |
| with torch.inference_mode(), set_default_dtype(dtype): | |
| print('Loading DiT (strict=True)...', flush=True) | |
| model = Model.from_pretrained(str(dit_dir), strict=True).to(device=device, dtype=dtype).eval() | |
| print('Loading image tokenizer (strict=True)...', flush=True) | |
| tokenizer, tokenizer_dir = load_image_tokenizer(args.tokenizer, device, dtype) | |
| vae_dir = resolve_directory(args.vae, ['config.json', '*.safetensors']) | |
| vae = AutoencoderDC.from_pretrained(str(vae_dir), torch_dtype=dtype).to(device).eval() | |
| length = args.grid ** 2 | |
| if args.tokens: | |
| indices = torch.load(args.tokens, map_location='cpu', weights_only=True) | |
| if not isinstance(indices, torch.Tensor) or tuple(indices.shape) != (length, tokenizer.n_q_tokens): | |
| raise ValueError(f'Expected integer tensor [{length}, {tokenizer.n_q_tokens}]') | |
| if indices.dtype not in (torch.int32, torch.int64): | |
| raise ValueError('Token indices must have integer dtype') | |
| limit = tokenizer.config.codebook_size // tokenizer.config.split_voc | |
| if indices.min() < 0 or indices.max() >= limit: | |
| raise ValueError(f'Expected per-codebook indices in [0, {limit}) (not AR vocabulary IDs)') | |
| cond, mask, seq_len = tokenizer.tokenize_indices( | |
| indices.to(device=device, dtype=torch.long), max_pad_to=length, | |
| lengths=[length], return_attention_mask=True, | |
| ) | |
| else: | |
| processor = AutoProcessor.from_pretrained( | |
| str(tokenizer_dir), trust_remote_code=True, use_fast=False, | |
| ) | |
| # Match the original inference's square resize, then use an exact grid. | |
| # A merged Keye token covers 2*14 pixels; 18*28=504 for 324 tokens. | |
| image = Image.open(args.image).convert('RGB').resize( | |
| (args.image_size, args.image_size), Image.Resampling.LANCZOS, | |
| ).resize((args.grid * 28, args.grid * 28), Image.Resampling.BICUBIC) | |
| inputs = processor(text=[''], images=[image], return_tensors='pt') | |
| actual_grid = inputs['image_grid_thw'][0, 1:] // 2 | |
| if actual_grid.tolist() != [args.grid, args.grid]: | |
| raise ValueError(f'Unexpected processor grid: {actual_grid.tolist()}') | |
| encoded = tokenizer( | |
| pixel_values=inputs['pixel_values'].to(device=device, dtype=dtype), | |
| image_grid_thw=inputs['image_grid_thw'].to(device), | |
| ) | |
| features = encoded.get('token_embeds') | |
| if features is None: | |
| # Older Kelix releases compute fused features without returning them. | |
| features = torch.stack([ | |
| projector(z_q) for projector, z_q in | |
| zip(tokenizer.up_projectors, encoded['z_q']) | |
| ], dim=1).mean(dim=1) | |
| if features.shape[0] != length: | |
| raise ValueError(f'Expected {length} image tokens, got {features.shape[0]}') | |
| cond, seq_len = features[None], length | |
| mask = torch.ones((1, 1, 1, length), device=device) | |
| if cond.shape[-1] != model.config.caption_channels: | |
| raise ValueError('Tokenizer condition dimension does not match the DiT config') | |
| null = model.y_embedder.y_embedding[:seq_len].to(device=device, dtype=dtype) | |
| valid = null.shape[0] | |
| null = F.pad(null, (0, 0, 0, seq_len - valid))[None] | |
| null_mask = torch.zeros_like(mask) | |
| null_mask[..., :valid] = 1 | |
| cond_cfg = torch.cat([null, cond]) | |
| mask_cfg = torch.cat([null_mask, mask]) | |
| latent_size = args.image_size // 32 | |
| generator = torch.Generator(device=device).manual_seed(args.seed) | |
| latents = torch.randn((1, model.config.in_channels, latent_size, latent_size), | |
| device=device, dtype=dtype, generator=generator) | |
| scheduler = FlowMatchEulerDiscreteScheduler(shift=args.flow_shift) | |
| scheduler.set_timesteps(args.steps, device=device) | |
| x_pos = positions(latent_size, latent_size, device) | |
| y_pos = positions(args.grid, args.grid, device) | |
| print(f'Sampling {args.steps} steps...', flush=True) | |
| for t in scheduler.timesteps: | |
| pred = model.forward_with_dpmsolver( | |
| torch.cat([latents, latents]), t.expand(2), cond_cfg, mask=mask_cfg, | |
| x_input_pos=x_pos, cond_input_pos=y_pos, | |
| ) | |
| uncond, conditional = pred.chunk(2) | |
| pred = uncond + args.cfg * (conditional - uncond) | |
| latents = scheduler.step(pred, t, latents, return_dict=False)[0] | |
| image = (vae.decode(latents / vae.config.scaling_factor).sample[0] / 2 + 0.5).clamp(0, 1) | |
| if not torch.isfinite(image).all(): | |
| raise RuntimeError('Non-finite pixels encountered') | |
| pixels = (image.float().permute(1, 2, 0).cpu().numpy() * 255).round().astype('uint8') | |
| output = Path(args.output) | |
| output.parent.mkdir(parents=True, exist_ok=True) | |
| Image.fromarray(pixels).save(output) | |
| print(f'Saved {output.resolve()}') | |
| if __name__ == '__main__': | |
| main(parse_args()) | |