mymage_test.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. ###第四版,优化可在T416G跑起来,模拟实时分析
  2. import os
  3. import gc
  4. import time
  5. import warnings
  6. import subprocess
  7. import importlib
  8. from pathlib import Path
  9. import torch
  10. from transformers import AutoModelForCausalLM, AutoProcessor
  11. # ==========================================
  12. # 1. 环境变量与 CUDA 分配策略优化
  13. # ==========================================
  14. os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True,max_split_size_mb:128")
  15. os.environ.setdefault("ONLINE_CODEC_CACHE_DIR", "/tmp/mage_vl_codec_cache")
  16. os.environ.setdefault("STREAMMIND_CLIP_CACHE", "/tmp/mage_vl_clips")
  17. os.makedirs(os.environ["ONLINE_CODEC_CACHE_DIR"], exist_ok=True)
  18. os.makedirs(os.environ["STREAMMIND_CLIP_CACHE"], exist_ok=True)
  19. MODEL_ID = "microsoft/Mage-VL"
  20. CODEC_MAX_PIXELS = 64_000 # 优化1:稍微降低分辨率上限,显著减少 Token 显存占用
  21. STREAM_PROMPT = "Please describe the video content in detail based on the provided information."
  22. print("[init] 加载模型中...")
  23. processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True)
  24. model = AutoModelForCausalLM.from_pretrained(
  25. MODEL_ID,
  26. trust_remote_code=True,
  27. torch_dtype=torch.bfloat16,
  28. attn_implementation="sdpa",
  29. ).to("cuda").eval()
  30. _REMOTE_PKG = type(processor).__module__.rsplit(".", 1)[0]
  31. codec_mod = importlib.import_module(_REMOTE_PKG + ".codec_video_processing_mage_vl")
  32. CodecConfig = codec_mod.CodecConfig
  33. # 加载 Cognition Gate
  34. try:
  35. model.model._load_streammind_gate()
  36. print("[gate] 认知门控加载成功")
  37. except Exception as exc:
  38. print(f"[gate] 加载失败: {exc}")
  39. # ==========================================
  40. # 2. 辅助工具函数
  41. # ==========================================
  42. def _ffprobe_duration(video_path):
  43. try:
  44. out = subprocess.run(
  45. ["ffprobe", "-v", "error", "-show_entries", "format=duration",
  46. "-of", "default=nw=1:nk=1", str(video_path)],
  47. check=True, capture_output=True, timeout=60, text=True,
  48. )
  49. return float(out.stdout.strip())
  50. except Exception:
  51. return 0.0
  52. def _codec_cfg(target_canvas=12, max_pixels=CODEC_MAX_PIXELS): # 优化2:target_canvas 从 32 降到 24
  53. override = {"engine": "hevc", "target_canvas": int(target_canvas), "patch": 16}
  54. kwargs = dict(processor._codec_config_defaults)
  55. merged_dcvc = dict(kwargs.get("dcvc") or {})
  56. merged_dcvc.update(override.get("dcvc") or {})
  57. kwargs.update(override)
  58. if merged_dcvc:
  59. kwargs["dcvc"] = merged_dcvc
  60. kwargs["max_pixels"] = int(max_pixels)
  61. return CodecConfig(**kwargs), override
  62. def _prompt(kind, question):
  63. messages = [{"role": "user", "content": [{"type": kind}, {"type": "text", "text": question}]}]
  64. return processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
  65. def _to_cuda(inputs):
  66. out = {}
  67. for key, value in inputs.items():
  68. if not hasattr(value, "to"):
  69. continue
  70. out[key] = value.to("cuda")
  71. if key == "pixel_values":
  72. out[key] = out[key].to(model.dtype)
  73. return out
  74. def _extract_subclip(src, start, duration, out_path):
  75. if out_path.exists() and out_path.stat().st_size > 0:
  76. return out_path
  77. tmp = out_path.with_name(f".tmp_{out_path.name}")
  78. subprocess.run(
  79. ["ffmpeg", "-y", "-loglevel", "error", "-ss", f"{max(0.0, start):.3f}",
  80. "-t", f"{duration:.3f}", "-i", str(src), "-c", "copy",
  81. "-avoid_negative_ts", "make_zero", str(tmp)],
  82. check=True, capture_output=True, timeout=180,
  83. )
  84. tmp.replace(out_path)
  85. return out_path
  86. @torch.inference_mode()
  87. def _generate(inputs, max_new_tokens):
  88. # 清理缓存以保证生成拥有最大连续显存块
  89. gc.collect()
  90. torch.cuda.empty_cache()
  91. out = model.generate(
  92. **inputs,
  93. max_new_tokens=int(max_new_tokens),
  94. do_sample=False,
  95. use_cache=True
  96. )
  97. new = out[0, inputs["input_ids"].shape[1]:]
  98. res = processor.tokenizer.decode(new, skip_special_tokens=True).strip()
  99. # 手动释放临时缓存
  100. del out, new
  101. gc.collect()
  102. torch.cuda.empty_cache()
  103. return res
  104. # ==========================================
  105. # 3. 优化后的流式分析函数
  106. # ==========================================
  107. def stream_video_analysis(
  108. video_path: str,
  109. segment_sec: float = 4.0,
  110. gate_threshold: float = 0.5,
  111. max_segments: int = 8,
  112. max_new_tokens: int = 64 # 优化3:适度缩减 token 生成长度,流式推理推荐 48-64
  113. ):
  114. path = Path(video_path)
  115. if not path.exists():
  116. raise FileNotFoundError(f"找不到视频文件: {video_path}")
  117. duration = _ffprobe_duration(path)
  118. clip_dir = Path("/tmp/mage_vl_clips")
  119. clip_dir.mkdir(parents=True, exist_ok=True)
  120. stem = path.stem
  121. text = _prompt("video", STREAM_PROMPT)
  122. cfg, override = _codec_cfg(24)
  123. raw_cpu_segments, spans = [], []
  124. start = 0.0
  125. index = 0
  126. yield f"开始切片视频 (总时长: {duration:.1f}s)..."
  127. # 1. 切片与预处理(保留在 CPU,避免占用 GPU 显存)
  128. while start < duration - 0.2 and index < max_segments:
  129. end = min(duration, start + segment_sec)
  130. clip = clip_dir / f"{stem}__t{int(start * 100):08d}__d{int((end - start) * 100):08d}.mp4"
  131. yield f"正在进行第 {index + 1} 个片段的 Codec 分析 (t={start:.1f}–{end:.1f}s)..."
  132. try:
  133. _extract_subclip(path, start, end - start, clip)
  134. with warnings.catch_warnings():
  135. warnings.simplefilter("ignore")
  136. inputs = processor(
  137. text=[text], videos=[str(clip)], video_backend="codec",
  138. max_pixels=CODEC_MAX_PIXELS, codec_config=override,
  139. return_tensors="pt", padding=False,
  140. )
  141. except Exception as exc:
  142. print(f"[stream] segment {start:.1f}-{end:.1f} unusable: {exc}", flush=True)
  143. start, index = end, index + 1
  144. continue
  145. raw_cpu_segments.append(inputs)
  146. spans.append((start, end))
  147. start, index = end, index + 1
  148. if not raw_cpu_segments:
  149. yield "未检测到可用片段。"
  150. return
  151. # 2. 评估阶段:仅将 Gate 关注的轻量级特征输入 GPU 计算分数
  152. yield f"正在使用认知门控 (System 1) 评估 {len(raw_cpu_segments)} 个片段..."
  153. cuda_visual_features = []
  154. for seg in raw_cpu_segments:
  155. feat = {}
  156. for k in ("pixel_values", "image_grid_thw", "patch_positions"):
  157. if k in seg:
  158. feat[k] = seg[k].to("cuda")
  159. if k == "pixel_values":
  160. feat[k] = feat[k].to(model.dtype)
  161. cuda_visual_features.append(feat)
  162. with torch.inference_mode():
  163. logits = model.model.streammind_gate_forward_segments(cuda_visual_features)[0]
  164. lengths = [int(seg["image_grid_thw"][:, 0].sum()) for seg in cuda_visual_features]
  165. boundaries = torch.tensor(lengths, device=logits.device).cumsum(0) - 1
  166. probs = torch.softmax(logits[boundaries].float(), dim=-1)[:, 1].tolist()
  167. # 释放 gate 特征占用的显存
  168. del cuda_visual_features, logits, boundaries
  169. gc.collect()
  170. torch.cuda.empty_cache()
  171. # 3. 按片段迭代生成并按需按时传输至 GPU
  172. lines = []
  173. for (span, cpu_inputs, prob) in zip(spans, raw_cpu_segments, probs):
  174. head = f"**t = {span[0]:.1f}–{span[1]:.1f}s** · p(respond) = `{prob:.2f}`"
  175. if prob < float(gate_threshold):
  176. lines.append(f"{head} → 🔇 *silence*")
  177. yield "\n\n".join(lines)
  178. continue
  179. lines.append(f"{head} → 🗣️ *generating…*")
  180. yield "\n\n".join(lines)
  181. try:
  182. # 仅在真正需要生成时,将当前片段输入移至 GPU
  183. cuda_inputs = _to_cuda(cpu_inputs)
  184. said = _generate(cuda_inputs, max_new_tokens)
  185. del cuda_inputs
  186. except Exception as exc:
  187. said = f"(generation failed: {type(exc).__name__}: {exc})"
  188. lines[-1] = f"{head} → 🗣️ {said}"
  189. yield "\n\n".join(lines)
  190. yield "\n\n".join(lines) + f"\n\n> 处理完成。"
  191. # ==========================================
  192. # 4. 执行测试
  193. # ==========================================
  194. if __name__ == "__main__":
  195. VIDEO_FILE = "/content/soccer-broadcast.mp4"
  196. for chunk_output in stream_video_analysis(
  197. video_path=VIDEO_FILE,
  198. segment_sec=4.0,
  199. gate_threshold=0.5,
  200. max_segments=8,
  201. max_new_tokens=64
  202. ):
  203. print("\033[H\033[J")
  204. print("=== 实时流式分析状态 ===")
  205. print(chunk_output)
  206. print("-" * 40)