Coverage for wrapper/llamagen/wrapper_llamagen.py: 65%

211 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-09 04:47 +0000

1""" 

2Based on: 

3https://github.com/FoundationVision/LlamaGen/blob/main/autoregressive/sample/sample_t2i.py 

4""" 

5 

6import logging 

7import os 

8import sys 

9import random 

10import numpy as np 

11 

12from typing import override 

13from typing import Optional 

14from typing import Dict 

15from typing import List 

16from typing import Tuple 

17from typing import Any 

18from typing import Union 

19 

20from PIL import Image 

21 

22import torch 

23import torch.distributed as dist 

24from torch import inference_mode 

25 

26from wrapper_model import ModelGeneration 

27 

28from tokenizer.tokenizer_image.vq_model import VQ_models 

29from language.t5 import T5Embedder 

30from autoregressive.models.gpt import GPT_models 

31from autoregressive.models.generate import generate 

32 

33from xfuser.config import EngineConfig 

34 

35 

36class LlamaGenGeneration(ModelGeneration): 

37 """LlamaGen model for text-to-image generation.""" 

38 

39 def __init__( 

40 self, 

41 model_name: str = "llamagen", 

42 engine_config: EngineConfig = None, 

43 param_dtype: torch.dtype = torch.bfloat16, 

44 ) -> None: 

45 super().__init__(model_name) 

46 

47 self.engine_config = engine_config 

48 self.torch_compile = True 

49 if self.engine_config is not None: 

50 self.torch_compile = self.engine_config.runtime_config.use_torch_compile 

51 self.param_dtype = param_dtype 

52 

53 # LlamaGen specific configuration 

54 self.REPO_ID = "peizesun/llamagen_t2i" 

55 self.GPT_TYPE = "t2i" 

56 self.VQ_MODEL_NAME = "VQ-16" 

57 # GPT-B 111M 

58 # GPT-L 343M 

59 # GPT-XL 775M 

60 # GPT-3B 3.1B 

61 self.GPT_MODEL_NAME = "GPT-XL" 

62 

63 # LlamaGen specific parameters 

64 self.downsample_size = 16 

65 self.codebook_size = 16384 

66 self.codebook_embed_dim = 8 

67 self.cls_token_num = 120 

68 

69 # T5 model configuration - match this with your trained GPT model 

70 # Common T5 models and their dimensions: 

71 # - flan-t5-xl: 2048 dimensions 

72 # - t5-v1_1-xl: 2048 dimensions 

73 # - t5-v1_1-xxl: 4096 dimensions 

74 # - flan-t5-xxl: 4096 dimensions 

75 self.T5_MODEL_TYPE = "flan-t5-xl" 

76 self.t5_feature_max_len = 120 

77 self.t5_feature_dim = 2048 # This should match the T5 model's hidden size 

78 # It supports 256 and 512 

79 self.image_size = 512 # pixels 

80 self.latent_size = self.image_size // self.downsample_size 

81 

82 # Parallelism 

83 self.gpu: Optional[str] = None 

84 if torch.cuda.is_available(): 

85 self.gpu = torch.cuda.get_device_name(0) 

86 

87 # Model components 

88 self.vq_model: Optional[torch.nn.Module] = None 

89 self.gpt_model: Optional[torch.nn.Module] = None 

90 self.t5_model: Optional[T5Embedder] = None 

91 

92 def __del__(self) -> None: 

93 if self.vq_model is not None: 

94 self.vq_model = None 

95 if self.gpt_model is not None: 

96 self.gpt_model = None 

97 if self.t5_model is not None: 

98 self.t5_model = None 

99 if dist.is_initialized(): 

100 dist.destroy_process_group() 

101 

102 def init_parallelism(self) -> None: 

103 self.load_timer.start("torch_dist") 

104 

105 self.rank = int(os.getenv("RANK", 0)) 

106 self.local_rank = int(os.getenv("LOCAL_RANK", 0)) 

107 self.world_size = int(os.getenv("WORLD_SIZE", 1)) 

108 

109 self.device_id = self.local_rank 

110 self.device = torch.device(f"cuda:{self.device_id}") 

111 

112 torch.cuda.set_device(self.local_rank) 

113 

114 if self.world_size > 1: 

115 logging.warning("LlamaGen is not optimized for multi-GPU setups (yet).") 

116 self.world_size = 1 

117 

118 self.load_timer.end("torch_dist") 

119 

120 def load_model(self) -> None: 

121 assert torch.cuda.is_available() 

122 

123 # Setup PyTorch optimizations like in the original code 

124 torch.backends.cuda.matmul.allow_tf32 = True 

125 torch.backends.cudnn.allow_tf32 = True 

126 torch.set_float32_matmul_precision('high') 

127 setattr(torch.nn.Linear, 'reset_parameters', lambda self: None) 

128 setattr(torch.nn.LayerNorm, 'reset_parameters', lambda self: None) 

129 os.environ["TOKENIZERS_PARALLELISM"] = "false" 

130 

131 self.load_timer.start("vq_model") 

132 # Load VQ model 

133 self.vq_model = VQ_models[self.VQ_MODEL_NAME]( 

134 codebook_size=self.codebook_size, 

135 codebook_embed_dim=self.codebook_embed_dim 

136 ) 

137 self.vq_model.to(self.device) 

138 self.vq_model.eval() 

139 vq_file = f"{self.REPO_ID}/vq_ds16_t2i.pt" 

140 checkpoint = torch.load( 

141 vq_file, 

142 map_location="cpu", 

143 weights_only=False) # nosec B614 - trusted HuggingFace model checkpoint 

144 self.vq_model.load_state_dict(checkpoint["model"]) 

145 self.load_timer.end("vq_model") 

146 

147 self.load_timer.start("gpt_model") 

148 self.gpt_model = GPT_models[self.GPT_MODEL_NAME]( 

149 block_size=self.latent_size ** 2, 

150 cls_token_num=self.cls_token_num, 

151 model_type=self.gpt_type, 

152 ) 

153 assert self.gpt_model is not None 

154 self.gpt_model = self.gpt_model.to(dtype=self.param_dtype) 

155 self.gpt_model = self.gpt_model.to(device=self.device) 

156 if self.image_size not in [256, 512]: 

157 raise ValueError(f"Image size {self.image_size} not supported. Must be 256 or 512.") 

158 t2i_file = f"{self.REPO_ID}/t2i_XL_stage2_{self.image_size}.pt" 

159 checkpoint = torch.load( 

160 t2i_file, 

161 map_location="cpu", 

162 weights_only=False) # nosec B614 - trusted HuggingFace model checkpoint 

163 model_weight = checkpoint.get("model", checkpoint.get("module", checkpoint.get("state_dict", checkpoint))) 

164 self.gpt_model.load_state_dict(model_weight, strict=False) 

165 self.gpt_model.eval() 

166 self.load_timer.end("gpt_model") 

167 

168 self.load_timer.start("t5_model") 

169 # Load T5 model for text embeddings which should be already downloaded: 

170 # For flan-t5-xl: huggingface-cli download google/flan-t5-xl --local-dir google/flan-t5-xl 

171 # https://github.com/FoundationVision/LlamaGen/blob/main/language/t5.py 

172 # self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_path) 

173 # self.model = T5EncoderModel.from_pretrained(path, **t5_model_kwargs).eval() 

174 # t5_path = "DeepFloyd" # "t5-v1_1-xxl" 

175 t5_path = "google" # "flan-t5-xl" 

176 logging.info(f"Loading T5 model from '{t5_path}'.") 

177 if not os.path.exists(t5_path): 

178 raise FileNotFoundError( 

179 f"T5 model directory '{t5_path}' does not exist. Please download the model using huggingface-cli.") 

180 if self.T5_MODEL_TYPE != ["flan-t5-xl"]: 

181 raise ValueError("Only 'flan-t5-xl' is available.") 

182 self.t5_model = T5Embedder( 

183 device=self.device, 

184 local_cache=True, 

185 cache_dir=t5_path if os.path.exists(t5_path) else None, 

186 dir_or_name=self.T5_MODEL_TYPE, 

187 torch_dtype=self.param_dtype, 

188 model_max_length=self.t5_feature_max_len, 

189 ) 

190 self.load_timer.end("t5_model") 

191 

192 logging.info(f"Loaded LlamaGen. VQ:{self.vq_model_name} GPT:{self.gpt_model_name} T5:{self.t5_model_type}.") 

193 

194 def init_model_parallelism(self) -> None: 

195 if self.world_size > 1: 

196 logging.warning("LlamaGen does not support model parallelism yet.") 

197 

198 def model_compile(self) -> None: 

199 if not self.torch_compile: 

200 return 

201 

202 self.load_timer.start("model_compile") 

203 assert self.gpt_model is not None 

204 self.gpt_model = torch.compile( # type: ignore[assignment] 

205 self.gpt_model, 

206 mode="reduce-overhead", 

207 fullgraph=True 

208 ) 

209 self.load_timer.end("model_compile") 

210 

211 def _assert_model_init(self) -> None: 

212 assert self.vq_model is not None 

213 assert self.gpt_model is not None 

214 assert self.t5_model is not None 

215 

216 def _assert_args(self, image_size: int) -> None: 

217 if image_size not in [256, 384, 512]: 

218 raise ValueError(f"Image size {image_size} not supported. Must be 256, 384, or 512.") 

219 if image_size != self.image_size: 

220 raise ValueError(f"Image size {image_size} does not match model's size {self.image_size}.") 

221 

222 def _prepare_text_embeddings( 

223 self, 

224 prompts: List[str], 

225 no_left_padding: bool = False) -> Tuple[torch.Tensor, torch.Tensor]: 

226 """ 

227 Prepare text embeddings using T5 model. 

228 """ 

229 assert self.t5_model is not None 

230 caption_embs, emb_masks = self.t5_model.get_text_embeddings(prompts) 

231 

232 if not no_left_padding: 

233 # Implement left-padding as in the original code 

234 new_emb_masks = torch.flip(emb_masks, dims=[-1]) 

235 new_caption_embs_list = [] 

236 for idx, (caption_emb, emb_mask) in enumerate(zip(caption_embs, emb_masks)): 

237 valid_num = int(emb_mask.sum().item()) 

238 logging.debug(f'Prompt {idx} token len: {valid_num}') 

239 new_caption_emb = torch.cat([caption_emb[valid_num:], caption_emb[:valid_num]]) 

240 new_caption_embs_list.append(new_caption_emb) 

241 new_caption_embs = torch.stack(new_caption_embs_list) 

242 else: 

243 new_caption_embs, new_emb_masks = caption_embs, emb_masks 

244 

245 c_indices = new_caption_embs * new_emb_masks[:, :, None] 

246 c_emb_masks = new_emb_masks 

247 

248 return c_indices, c_emb_masks 

249 

250 def _sample_to_image(self, sample: torch.Tensor) -> Image.Image: 

251 # "sample" is a tensor with shape [C, H, W] [3, H, W] and values in [-1, 1] 

252 sample = (sample + 1) / 2 # Convert from [-1, 1] to [0, 1] 

253 sample = torch.clamp(sample, 0, 1) 

254 # Convert to numpy and transpose to HWC format 

255 sample_np = sample.cpu().numpy().transpose(1, 2, 0) 

256 sample_np = (sample_np * 255).astype(np.uint8) # Convert to [0, 255] uint8 

257 pil_image = Image.fromarray(sample_np) 

258 return pil_image 

259 

260 @inference_mode() 

261 async def warmup(self) -> None: 

262 logging.info(f"[{self.rank}] Warmup for LlamaGen generation.") 

263 await self.generate( 

264 prompt="A warmup image to initialize the model.", 

265 image_size=512, 

266 cfg_scale=7.5, 

267 temperature=1.0, 

268 top_k=1000, 

269 ) 

270 

271 @override 

272 @inference_mode() 

273 async def generate( 

274 self, 

275 prompt: str, 

276 image_size: int = 512, # pixels (512 x 512) 

277 cfg_scale: float = 7.5, 

278 temperature: float = 1.0, 

279 top_k: int = 1000, 

280 top_p: float = 1.0, 

281 seed: Optional[int] = None, 

282 no_left_padding: bool = False, 

283 job_id: Optional[str] = None, 

284 ) -> Image.Image: 

285 """ 

286 Generate an image from a prompt using the LlamaGen model. 

287 Args: 

288 prompt (str): Text prompt to guide the image generation. 

289 image_size (int): Size of the generated image (256, 384, or 512). 

290 cfg_scale (float): Classifier-free guidance scale. 

291 temperature (float): Sampling temperature. 

292 top_k (int): Top-k sampling parameter. 

293 top_p (float): Top-p (nucleus) sampling parameter. 

294 seed (int): Random seed for reproducibility. 

295 no_left_padding (bool): Whether to skip left padding for text embeddings. 

296 Returns: 

297 Image.Image: Generated PIL Image. 

298 """ 

299 gen_timer = self._new_gen_timer(job_id) 

300 

301 self._assert_model_init() 

302 self._assert_args(image_size) 

303 

304 self.running = True # Mark running to avoid concurrent calls 

305 

306 try: 

307 # Set seed for reproducibility 

308 if seed is not None: 

309 torch.manual_seed(seed) 

310 torch.cuda.manual_seed(seed) 

311 else: 

312 seed = random.randint(0, sys.maxsize) 

313 torch.manual_seed(seed) 

314 

315 torch.set_grad_enabled(False) 

316 latent_size = image_size // self.downsample_size 

317 

318 logging.info(f"Generating image with prompt: '{prompt}'.") 

319 logging.info(f"Image size: {image_size}, Latent size: {latent_size}.") 

320 

321 # Prepare text embeddings 

322 gen_timer.start("text_embeddings") 

323 prompts = [prompt] # LlamaGen expects a list 

324 c_indices, c_emb_masks = self._prepare_text_embeddings(prompts, no_left_padding) 

325 gen_timer.end("text_embeddings") 

326 

327 # Generate token indices 

328 gen_timer.start("sampling") 

329 qzshape = [len(c_indices), self.codebook_embed_dim, latent_size, latent_size] 

330 

331 index_sample = generate( 

332 self.gpt_model, 

333 c_indices, 

334 latent_size ** 2, 

335 c_emb_masks, 

336 cfg_scale=cfg_scale, 

337 temperature=temperature, 

338 top_k=top_k, 

339 top_p=top_p, 

340 sample_logits=True, 

341 ) 

342 gen_timer.end("sampling") 

343 

344 # Decode to image 

345 gen_timer.start("decoding") 

346 assert self.vq_model is not None 

347 # output in [-1, 1] 

348 samples = self.vq_model.decode_code(index_sample, qzshape) # type: ignore [operator] 

349 sample = samples[0] 

350 gen_timer.end("decoding") 

351 

352 # Convert to PIL Image 

353 gen_timer.start("convert_pil") 

354 pil_image = self._sample_to_image(sample) 

355 gen_timer.end("convert_pil") 

356 

357 return pil_image 

358 finally: 

359 self.running = False 

360 gen_timer.end("total") 

361 

362 def get_health(self) -> Dict[str, Any]: 

363 ret = super().get_health() 

364 ret.update({ 

365 "gpu": self.gpu, 

366 "rank": self.rank, 

367 "world_size": self.world_size, 

368 "torch_compile": self.torch_compile, 

369 "dtype": str(self.param_dtype), 

370 "vq_model": self.vq_model_name, 

371 "gpt_model": self.gpt_model_name, 

372 "gpt_type": self.gpt_type, 

373 "t5_model": self.t5_model_type, 

374 }) 

375 return ret 

376 

377 async def get_rest_args( 

378 self, 

379 data_json: Dict[str, Union[str, int, float]] 

380 ) -> Dict[str, Any]: 

381 if data_json is None or not isinstance(data_json, dict): 

382 raise ValueError("Missing JSON body") 

383 

384 prompt = data_json.get("prompt", None) 

385 if prompt is None: 

386 raise ValueError("Missing 'prompt' parameter") 

387 

388 image_size = int(data_json.get("image_size", 512)) 

389 cfg_scale = float(data_json.get("cfg_scale", 7.5)) 

390 temperature = float(data_json.get("temperature", 1.0)) 

391 top_k = int(data_json.get("top_k", 1000)) 

392 top_p = float(data_json.get("top_p", 1.0)) 

393 seed = data_json.get("seed", None) 

394 if seed is not None: 

395 seed = int(seed) 

396 no_left_padding = bool(data_json.get("no_left_padding", False)) 

397 

398 return { 

399 "task": self.model_name, 

400 "args": { 

401 "prompt": prompt, 

402 "image_size": image_size, 

403 "cfg_scale": cfg_scale, 

404 "temperature": temperature, 

405 "top_k": top_k, 

406 "top_p": top_p, 

407 "seed": seed, 

408 "no_left_padding": no_left_padding, 

409 } 

410 }