Views
No views yet
A 4B-parameter multimodal model with identity-aware representation anchoring, designed for edge deployment in embodied AI and autonomous systems.
| Approach | Identity Stability | Quantization Robustness | Edge Deployability |
|---|---|---|---|
| Prompt Engineering | ❌ Fragile | ❌ Lost after compression | ⚠️ Context window limited |
| LoRA Fine-tuning | ⚠️ Adapter-dependent | ⚠️ Merge artifacts | ✅ Lightweight |
| GDU-Liang-4B (Ours) | ✅ Anchored in weights | ✅ Preserved post-quant | ✅ Full-stack ready |
1pip install -r requirements.txt
2#或者运行下面:
3!pip install -q \
4 "transformers==4.57.1" \
5 "peft>=0.12.0" \
6 "bitsandbytes>=0.43.3" \
7 "accelerate>=0.33.0" \
8 "datasets" \
9 "trl" \
10 "pillow" \
11 "einops" \
12 "torchvision" \
13 "decord2"
14
15print("✅ 安装完成,正在重启内核...")
16
17import IPython
18IPython.Application.instance().kernel.do_shutdown(True)transformers==4.57.x. Molmo2's custom architecture depends on this version; other versions may fail silently.注:Kaggle 环境常预装与 Molmo2 自定义代码冲突的包(例如torchao),在 Kaggle 上请先卸载该包:
pip uninstall torchao -y1import torch
2from transformers import AutoTokenizer, AutoModelForImageTextToText
3
4model_id = "zhenqiangliang6/GDU-Liang-4B"
5
6# 加载模型和分词器
7tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
8model = AutoModelForImageTextToText.from_pretrained(
9 model_id,
10 torch_dtype=torch.float16,
11 device_map="auto",
12 trust_remote_code=True,
13)
14
15# 定义推理函数
16def generate(user_input: str, system_prompt: str = None) -> str:
17 if system_prompt is None:
18 system_prompt = getattr(tokenizer, "default_system_message", None)
19
20 messages = [{"role": "user", "content": user_input}]
21 template_kwargs = {"add_generation_prompt": True}
22 if system_prompt:
23 template_kwargs["system"] = system_prompt
24
25 prompt_text = tokenizer.apply_chat_template(messages, tokenize=False, **template_kwargs)
26 inputs = tokenizer(prompt_text, return_tensors="pt").to(model.device)
27
28 outputs = model.generate(
29 **inputs,
30 max_new_tokens=256,
31 do_sample=True,
32 temperature=0.9,
33 top_p=0.92,
34 repetition_penalty=1.15,
35 eos_token_id=tokenizer.eos_token_id,
36 pad_token_id=tokenizer.pad_token_id,
37 )
38
39 new_tokens = outputs[0][inputs["input_ids"].shape[-1]:]
40 return tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
41
42# 测试
43print(generate("你是谁?"))
441# ================================================================
2# Molmo2-4B / GDU-liang-4B
3# 完整图文推理脚本(含 Embedding Resize 修复)
4#
5# 环境要求:
6# transformers==4.57.1
7# torch>=2.0
8# modelscope
9# Pillow
10# numpy
11# ================================================================
12
13# ================================================================
14# GDU-liang-4B 图文推理(精简版)
15# 依赖: transformers>=4.57.1, torch>=2.0, modelscope, Pillow, numpy
16# ================================================================
17import os, sys, importlib.util
18import torch, numpy as np
19from PIL import Image
20from modelscope import snapshot_download, MsDataset
21from transformers import AutoConfig, AutoProcessor, BatchFeature
22
23# ── 1. 加载数据 & 模型 ──────────────────────────────────────────
24ds = MsDataset.load("AlphapilotOS/GDU_Liang", split="train")
25raw_image = ds[0]["image"]
26if not isinstance(raw_image, Image.Image):
27 raw_image = Image.fromarray(raw_image) if isinstance(raw_image, np.ndarray) else Image.open(raw_image)
28raw_image = raw_image.convert("RGB")
29
30model_dir = snapshot_download("AlphapilotOS/GDU-liang-4B")
31
32# 动态加载自定义 modeling
33spec = importlib.util.spec_from_file_location(
34 "molmo2_mod", os.path.join(model_dir, "modeling_molmo2.py"),
35 submodule_search_locations=[model_dir],
36)
37mod = importlib.util.module_from_spec(spec)
38sys.modules["molmo2_mod"] = mod
39spec.loader.exec_module(mod)
40
41config = AutoConfig.from_pretrained(model_dir, trust_remote_code=True, local_files_only=True)
42model = mod.Molmo2ForConditionalGeneration.from_pretrained(
43 model_dir, config=config, trust_remote_code=True, dtype=torch.bfloat16, device_map="auto",
44)
45processor = AutoProcessor.from_pretrained(model_dir, trust_remote_code=True, use_fast=False, local_files_only=True)
46model.eval()
47
48# ── 2. Embedding Resize(仅需要时执行)──────────────────────────
49tok_vocab = len(processor.tokenizer)
50emb = model.get_input_embeddings().embedding # Molmo2Embedding.embedding
51if tok_vocab > emb.shape[0]:
52 diff = tok_vocab - emb.shape[0]
53 mean_vec = emb.data.mean(dim=0)
54 new_emb = torch.zeros(tok_vocab, emb.shape[1], dtype=emb.dtype, device=emb.device)
55 new_emb[:emb.shape[0]] = emb.data
56 new_emb[emb.shape[0]:] = mean_vec.unsqueeze(0).expand(diff, -1)
57 emb.data = new_emb
58
59 lm_w = model.lm_head.weight
60 if lm_w.shape[0] == emb.shape[0] - diff: # 旧尺寸才resize
61 new_lm = torch.zeros(tok_vocab, lm_w.shape[1], dtype=lm_w.dtype, device=lm_w.device)
62 new_lm[:lm_w.shape[0]] = lm_w.data
63 new_lm[lm_w.shape[0]:] = lm_w.data.mean(dim=0).unsqueeze(0).expand(diff, -1)
64 lm_w.data = new_lm
65
66# ── 3. 预处理 & 构造输入 ────────────────────────────────────────
67prompt = "请详细描述这张图片的内容"
68
69vis = processor.image_processor(images=[raw_image], return_tensors="np")
70img_tokens = processor.get_image_tokens(vis["image_grids"][0])
71img_ids = processor.tokenizer.convert_tokens_to_ids(
72 img_tokens.tolist() if isinstance(img_tokens, np.ndarray) else list(img_tokens)
73)
74
75user_seg = f"<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n"
76input_ids = img_ids + processor.tokenizer.encode(user_seg, add_special_tokens=False)
77
78inputs = BatchFeature({
79 "input_ids": [input_ids],
80 "attention_mask": [[1] * len(input_ids)],
81})
82for k, v in vis.items():
83 inputs[k] = v
84
85# ── 4. 转 Tensor & 推理 ────────────────────────────────────────
86device = next(model.parameters()).device
87tensor_inputs = {}
88for k, v in inputs.items():
89 if isinstance(v, np.ndarray):
90 t = torch.from_numpy(v)
91 t = t.to(torch.bfloat16) if t.is_floating_point() else t.to(torch.long)
92 elif isinstance(v, list):
93 t = torch.tensor(v, dtype=torch.long)
94 else:
95 t = v
96 tensor_inputs[k] = t.to(device) if isinstance(t, torch.Tensor) else t
97
98with torch.inference_mode():
99 out = model.generate(**tensor_inputs, max_new_tokens=512, do_sample=False)
100
101new_tokens = out[0, tensor_inputs["input_ids"].shape[1]:]
102print(processor.tokenizer.decode(new_tokens, skip_special_tokens=True))examples/ 获取视频与音频推理脚本示例。1!modelscope download \
2 --dataset AlphapilotOS/vido \
3 --include "微信视频2026-08-14_144752_279.mp4" \
4 --local_dir /mnt/workspace/vido_data1import subprocess, os
2from pathlib import Path
3
4def normalize_video_inplace(video_path: str):
5 """标准化视频并原地替换原文件"""
6 p = Path(video_path)
7 if not p.is_file():
8 print(f"❌ 文件不存在: {video_path}"); return False
9
10 tmp = str(p.parent / f".{p.stem}_normalizing.mp4")
11 cmd = [
12 "ffmpeg", "-y", "-i", str(p),
13 "-c:v", "libx264", "-preset", "ultrafast", "-crf", "18",
14 "-pix_fmt", "yuv420p", "-movflags", "+faststart",
15 "-c:a", "aac", "-b:a", "128k",
16 "-map", "0:v:0", "-map", "0:a?", "-sn", "-dn",
17 tmp,
18 ]
19 r = subprocess.run(cmd, capture_output=True, text=True)
20 if r.returncode != 0:
21 # 回退 stream copy
22 cmd_fb = ["ffmpeg", "-y", "-i", str(p), "-c", "copy",
23 "-movflags", "+faststart", "-map", "0:v:0",
24 "-map", "0:a?", "-sn", "-dn", tmp]
25 r2 = subprocess.run(cmd_fb, capture_output=True, text=True)
26 if r2.returncode != 0:
27 print(f"❌ {p.name}: {r2.stderr[-200:]}"); return False
28
29 # 原子替换:先删原文件再重命名,避免写入中途损坏
30 os.replace(tmp, str(p))
31 size = os.path.getsize(str(p)) / (1024 * 1024)
32 print(f"✅ {p.name} ({size:.1f} MB)")
33 return True
34
35
36# ====== 👇 使用方式 ======
37
38# 单个文件
39normalize_video_inplace("/mnt/workspace/vido_data/微信视频2026-08-14_144804_187.mp4")
40
41# 或批量处理整个目录
42# for f in sorted(Path("/mnt/workspace/vido_data").glob("*.mp4")):
43# normalize_video_inplace(str(f))1#!/usr/bin/env python3
2"""
3GDU-liang-4B Video Understanding - v9.15 Industrial Edition
4✅ 自动同步缺失 modeling 文件到 transformers_modules 缓存
5✅ 优先加载 ForCausalLM(跳过 base Molmo2Model)
6✅ Embedding + LM Head 同步扩展
7✅ A10 Safe: 8帧 + fp16 + dtype(非torch_dtype)
8"""
9
10import os, sys, shutil, importlib
11import torch
12import torchvision.io as tv_io
13from transformers import AutoTokenizer, AutoConfig
14
15
16# ==================== 模块同步 ====================
17
18def sync_modeling_to_cache(model_dir):
19 """将模型目录 .py 同步到 HF transformers_modules 缓存(v9.16: 直接定位路径)"""
20 # 🔑 直接通过 HF cache 路径定位,不依赖 import
21 repo_name = os.path.basename(os.path.normpath(model_dir))
22
23 # 尝试多个可能的缓存位置
24 possible_dirs = [
25 os.path.expanduser(f"~/.cache/huggingface/modules/transformers_modules/{repo_name}"),
26 f"/root/.cache/huggingface/modules/transformers_modules/{repo_name}",
27 ]
28
29 # 也尝试从已加载的模块反推路径
30 for key, mod in sys.modules.items():
31 if "transformers_modules" in key and hasattr(mod, "__file__") and mod.__file__:
32 candidate = os.path.join(os.path.dirname(mod.__file__), repo_name)
33 if candidate not in possible_dirs:
34 possible_dirs.insert(0, candidate)
35
36 target_dir = None
37 for d in possible_dirs:
38 parent = os.path.dirname(d)
39 if os.path.isdir(parent):
40 target_dir = d
41 break
42
43 if target_dir is None:
44 # 最后兜底:用第一个路径并创建
45 target_dir = possible_dirs[0]
46 print(f" ⚠️ No existing cache dir found, creating: {target_dir}")
47
48 os.makedirs(target_dir, exist_ok=True)
49
50 synced = []
51 for fname in os.listdir(model_dir):
52 if fname.endswith(".py"):
53 src = os.path.join(model_dir, fname)
54 dst = os.path.join(target_dir, fname)
55 if not os.path.exists(dst) or os.path.getmtime(src) > os.path.getmtime(dst):
56 shutil.copy2(src, dst)
57 synced.append(fname)
58
59 if synced:
60 print(f" 🔄 Synced {len(synced)} files → {target_dir}")
61 for f in synced:
62 print(f" ✅ {f}")
63 mods_to_remove = [k for k in sys.modules if f"transformers_modules.{repo_name}" in k]
64 for k in mods_to_remove:
65 del sys.modules[k]
66 print(f" 🧹 Cleared {len(mods_to_remove)} cached modules")
67 else:
68 print(f" ✅ All .py files already synced in {target_dir}")
69
70# ==================== 模型类加载 ====================
71
72def load_model_class(model_dir):
73 """🔑 v9.15: 跳过 base Model,优先选含 generate() 的 CausalLM 类"""
74 repo_name = os.path.basename(os.path.normpath(model_dir))
75 module_path = f"transformers_modules.{repo_name}.modeling_molmo2"
76 mod = importlib.import_module(module_path)
77
78 from transformers import PreTrainedModel
79 candidates = []
80 for attr_name in dir(mod):
81 obj = getattr(mod, attr_name)
82 if (isinstance(obj, type)
83 and issubclass(obj, PreTrainedModel)
84 and obj is not PreTrainedModel
85 and "config" not in attr_name.lower()):
86 candidates.append((attr_name, obj))
87
88 if not candidates:
89 raise RuntimeError(f"No model class in {module_path}!")
90
91 print(f" 🔍 Available classes: {[c[0] for c in candidates]}")
92
93 def score(item):
94 n, cls = item
95 nl = n.lower()
96 has_gen = hasattr(cls, "generate")
97 is_base = n.endswith("Model") and "for" not in nl
98 if has_gen and not is_base:
99 return 0
100 if "forcausallm" in nl:
101 return 1
102 if is_base:
103 return 99
104 return 50
105
106 candidates.sort(key=score)
107 chosen_name, chosen_cls = candidates[0]
108 print(f" ✅ Selected: {chosen_name} (has_generate={hasattr(chosen_cls, 'generate')})")
109 return chosen_cls
110
111
112# ==================== Vocab 工具 ====================
113
114def safe_set_vocab_size(config, size):
115 for attr in ["vocab_size", "text_config.vocab_size"]:
116 parts = attr.split(".")
117 obj = config
118 try:
119 for p in parts[:-1]:
120 obj = getattr(obj, p)
121 setattr(obj, parts[-1], size)
122 return
123 except AttributeError:
124 continue
125 if hasattr(config, "vocab_size"):
126 config.vocab_size = size
127
128
129def expand_vocab_if_needed(model, tokenizer, cfg):
130 tok_vocab = len(tokenizer)
131 model_vocab = cfg.vocab_size
132 print(f" Tokenizer vocab: {tok_vocab}, Model vocab: {model_vocab}")
133
134 if tok_vocab == model_vocab:
135 print(f" ✅ Vocab matched: {tok_vocab}")
136 return
137
138 # --- 1. Expand Embedding ---
139 embed_param = embed_name = None
140 for name, param in model.named_parameters():
141 if "wte.embedding" in name.lower() and param.dim() == 2:
142 n_lower = name.lower()
143 if not any(ex in n_lower for ex in ["vision", "vit", "new_embedding"]):
144 embed_param, embed_name = param, name
145 break
146 if embed_param is None:
147 raise RuntimeError("Cannot find text embedding!")
148
149 cur_rows, hidden = embed_param.shape
150 diff = tok_vocab - cur_rows
151 print(f" 🎯 Expanding {embed_name}: [{cur_rows}x{hidden}] → +{diff}")
152
153 special_ids = []
154 for tok_str in tokenizer.get_vocab():
155 tid = tokenizer.convert_tokens_to_ids(tok_str)
156 if tid is not None and tid < cur_rows:
157 if any(k in tok_str.lower() for k in
158 ["<", ">", "[", "]", "bos", "eos", "pad", "unk",
159 "sep", "cls", "mask", "frame", "image", "video"]):
160 special_ids.append(tid)
161
162 init_vec = (embed_param.data[special_ids].mean(dim=0, keepdim=True)
163 if special_ids else embed_param.data.mean(dim=0, keepdim=True))
164 new_rows = init_vec.expand(diff, -1).clone() + torch.randn(diff, hidden, device=embed_param.device, dtype=embed_param.dtype) * 1e-4
165 new_embed = torch.cat([embed_param.data, new_rows], dim=0)
166
167 parts = embed_name.rsplit(".", 1)
168 parent = model.get_submodule(parts[0]) if len(parts) > 1 else model
169 delattr(parent, parts[-1])
170 setattr(parent, parts[-1], torch.nn.Parameter(new_embed))
171 print(f" ✅ Expanded embedding to [{tok_vocab}x{hidden}]")
172
173 # --- 2. Expand LM Head (多策略) ---
174 expanded = False
175
176 # 策略A: 按名称搜索
177 for pname, param in list(model.named_parameters()):
178 pl = pname.lower()
179 if param.dim() == 2 and param.shape[1] == hidden:
180 if any(k in pl for k in ["lm_head", "output_head"]) and \
181 not any(ex in pl for ex in ["vision", "vit", "visual"]):
182 if param.shape[0] < tok_vocab:
183 hd = tok_vocab - param.shape[0]
184 print(f" 🎯 Expanding {pname}: [{param.shape[0]}x{hidden}] → +{hd}")
185 nh = torch.cat([param.data, new_embed[-hd:, :].clone()], dim=0)
186 hp = pname.rsplit(".", 1)
187 hpar = model.get_submodule(hp[0]) if len(hp) > 1 else model
188 delattr(hpar, hp[-1])
189 setattr(hpar, hp[-1], torch.nn.Parameter(nh))
190 expanded = True
191 break
192
193 # 策略B: fallback 路径
194 if not expanded:
195 for path in ["lm_head", "transformer.lm_head", "model.lm_head", "head"]:
196 try:
197 m = model.get_submodule(path)
198 if hasattr(m, "weight") and m.weight.dim() == 2 and m.weight.shape[1] == hidden:
199 if m.weight.shape[0] < tok_vocab:
200 hd = tok_vocab - m.weight.shape[0]
201 nh = torch.cat([m.weight.data, new_embed[-hd:, :].clone()], dim=0)
202 delattr(m, "weight")
203 setattr(m, "weight", torch.nn.Parameter(nh))
204 expanded = True
205 break
206 except (AttributeError, KeyError):
207 continue
208
209 # 策略C: tied weights — lm_head 可能共享 embedding,无需单独扩展
210 if not expanded:
211 # 检查是否有 tied weight(即 lm_head.weight is embedding)
212 for pname, param in model.named_parameters():
213 if param.dim() == 2 and param.shape == new_embed.shape:
214 if param.data_ptr() == new_embed.data_ptr() or torch.equal(param.data, new_embed.data):
215 print(f" 🔗 LM head is tied to embedding ({pname}), no separate expansion needed")
216 expanded = True
217 break
218
219 if not expanded:
220 print(" ⚠️ No separate lm_head found. Checking if model uses tied weights...")
221 # 如果模型使用 weight tying,generate 时会自动使用 embedding 作为 output projection
222 # 这种情况下不需要手动扩展 lm_head
223 print(" ℹ️ Assuming tied weights. If generation fails, model needs explicit lm_head.")
224
225 safe_set_vocab_size(cfg, tok_vocab)
226 safe_set_vocab_size(model.config, tok_vocab)
227 print(f" ✅ Config vocab → {tok_vocab}")
228
229
230# ==================== 视频处理 ====================
231
232def load_and_sample_video(video_path, max_frames=8, target_fps=1.0):
233 video, audio, info = tv_io.read_video(video_path, pts_unit="sec")
234 fps = info.get("video_fps", 30.0)
235 total = video.shape[0]
236 n = min(max_frames, total)
237 idx = torch.linspace(0, total - 1, n).long()
238 frames = video[idx]
239 ts = [f"{t:.1f}" for t in (idx.float() / fps).tolist()]
240 print(f" 📹 {total} frames, {fps:.1f} FPS → sampled {n} frames")
241 return frames, ts
242
243
244
245# ==================== 主推理 ====================(显存不足)
246@torch.inference_mode()
247def generate_video_response(video_path, user_query, max_new_tokens=512):
248 model_dir = "/mnt/workspace/.cache/modelscope/models/AlphapilotOS--GDU-liang-4B/snapshots/master"
249
250 sync_modeling_to_cache(model_dir)
251
252 tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)
253 cfg = AutoConfig.from_pretrained(model_dir, trust_remote_code=True)
254
255 ModelClass = load_model_class(model_dir)
256 print(f" 🏗️ Loading: {ModelClass.__name__}")
257
258 # 🔑 v9.19: 24GB GPU 显存不足,限制 GPU 用量 + CPU offload
259 import gc
260 gc.collect()
261 torch.cuda.empty_cache()
262
263 max_memory = {0: "20GiB", "cpu": "30GiB"}
264
265 model = ModelClass.from_pretrained(
266 model_dir,
267 config=cfg,
268 torch_dtype=torch.bfloat16,
269 device_map="auto",
270 max_memory=max_memory,
271 offload_folder="/tmp/offload",
272 trust_remote_code=True,
273 )
274 model.eval()
275
276 # 🔑 v9.18: 确保 meta tensor 全部物化后再 expand vocab
277 if any(p.device.type == "meta" for p in model.parameters()):
278 print(" ⚠️ Meta tensors detected, materializing...")
279 if not hasattr(model, "hf_device_map"):
280 model.to("cpu")
281 meta_count = sum(1 for p in model.parameters() if p.device.type == "meta")
282 if meta_count > 0:
283 print(f" ⚠️ Still {meta_count} meta params after first pass, retrying...")
284 model.to("cpu")
285
286 expand_vocab_if_needed(model, tokenizer, cfg)
287
288 if tokenizer.eos_token_id is None:
289 tokenizer.eos_token_id = tokenizer.pad_token_id or 0
290
291 frames, timestamps = load_and_sample_video(video_path, max_frames=8)
292
293 # 🔑 v9.20: Processor 加载
294 from transformers import AutoProcessor
295 try:
296 processor = AutoProcessor.from_pretrained(model_dir, trust_remote_code=True)
297 print(f" ✅ Loaded processor: {type(processor).__name__}")
298 except Exception as e:
299 repo_name = os.path.basename(os.path.normpath(model_dir))
300 proc_module = importlib.import_module(f"transformers_modules.{repo_name}.processing_molmo2")
301 for attr_name in dir(proc_module):
302 obj = getattr(proc_module, attr_name)
303 if isinstance(obj, type) and "processor" in attr_name.lower() and attr_name != "AutoProcessor":
304 processor = obj.from_pretrained(model_dir, trust_remote_code=True)
305 print(f" ✅ Loaded custom processor: {attr_name}")
306 break
307 else:
308 raise RuntimeError(f"Cannot load processor! AutoProcessor failed: {e}")
309
310 # 🔑 v9.26: Molmo2-GDU-liang-4B 确认使用 <|video|> (token id: 151945)
311 video_placeholder = "<|video|>"
312 print(f" 🎬 Using video placeholder: '{video_placeholder}'")
313
314 messages = [{"role": "user", "content": f"{video_placeholder}\n{user_query}"}]
315 prompt_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
316 # print(f" 💬 Prompt preview: {prompt_text[:200]}...")
317
318 # 🔑 v9.22: Molmo2VideoProcessor 期望 (T, H, W, C),不要 permute
319 video_tensor = frames # 已经是 (T, H, W, C)
320 print(f" 📐 Video tensor shape: {video_tensor.shape}") # 应为 (8, H, W, 3)
321
322 # 🔑 v9.21: VideoMetadata 需要 fps + total_num_frames
323 proc_inputs = None
324 fps_value = 30.0
325 num_frames = video_tensor.shape[0] # 8 frames
326
327 # 方式1: video_metadata 带完整字段
328 try:
329 proc_inputs = processor(
330 text=prompt_text,
331 videos=video_tensor,
332 video_metadata=[{"fps": fps_value, "total_num_frames": num_frames}],
333 return_tensors="pt",
334 )
335 print(f" ✅ Processor accepted video_metadata (dict)")
336 except Exception as e1:
337 print(f" ⚠️ video_metadata dict failed: {e1}")
338
339 # 方式1b: 用 processor 内部的 VideoMetadata 类
340 try:
341 from transformers.video_processing_utils import VideoMetadata as VM
342 proc_inputs = processor(
343 text=prompt_text,
344 videos=video_tensor,
345 video_metadata=[VM(fps=fps_value, total_num_frames=num_frames)],
346 return_tensors="pt",
347 )
348 print(f" ✅ Processor accepted VideoMetadata class")
349 except Exception as e1b:
350 print(f" ⚠️ VideoMetadata class failed: {e1b}")
351
352 # 方式2: fps 关键字参数
353 try:
354 proc_inputs = processor(
355 text=prompt_text,
356 videos=video_tensor,
357 fps=fps_value,
358 return_tensors="pt",
359 )
360 print(f" ✅ Processor accepted fps kwarg")
361 except Exception as e2:
362 raise RuntimeError(
363 f"All processor call methods failed!\n"
364 f" dict metadata: {e1}\n"
365 f" class metadata: {e1b}\n"
366 f" fps kwarg: {e2}"
367 )
368
369 # 🔑 v9.19: device_map="auto" 时 model.device 不可靠,用首个参数的设备
370 target_device = next(model.parameters()).device
371 inp = {k: v.to(target_device) if isinstance(v, torch.Tensor) else v for k, v in proc_inputs.items()}
372 print(f" 🚀 Generating with keys: {list(inp.keys())}, device: {target_device}")
373
374 outputs = model.generate(
375 **inp, max_new_tokens=max_new_tokens, do_sample=False,
376 eos_token_id=tokenizer.eos_token_id,
377 pad_token_id=tokenizer.pad_token_id or tokenizer.eos_token_id,
378 )
379
380 new_tok = outputs[0][inp["input_ids"].shape[1]:]
381 return tokenizer.decode(new_tok, skip_special_tokens=True).strip()
382
383# ==================== 入口 ====================
384
385print("=" * 60)
386print("【🚀 v9.15 - CausalLM Priority + Tied Weights Fallback】")
387print("=" * 60)
388
389result = generate_video_response(
390 "/mnt/workspace/vido_data/微信视频2026-08-14_144752_279.mp4",
391 "请详细描述视频中先后发生的动作与变化,注意时间顺序。",
392)
393
394print("\n" + "=" * 60)
395print("📝 RESPONSE:")
396print("=" * 60)
397print(result)1#!/usr/bin/env python3
2"""
3GDU-liang-4B Video Understanding - v9.26 Production Edition
4✅ 自动同步缺失 modeling 文件到 transformers_modules 缓存
5✅ 优先加载 ForCausalLM(跳过 base Molmo2Model)
6✅ Embedding + LM Head 同步扩展
7✅ A10 Safe: 8帧 + bf16 + CPU offload (24GB GPU)
8✅ <|video|> placeholder 硬编码
9这是显存大于24GB的时候最优运行代码(显存富裕请使用这代码)
10"""
11
12import os, sys, shutil, importlib
13import torch
14import torchvision.io as tv_io
15from transformers import AutoTokenizer, AutoConfig
16
17
18# ==================== 模块同步 ====================
19
20def sync_modeling_to_cache(model_dir):
21 """将模型目录 .py 同步到 HF transformers_modules 缓存"""
22 repo_name = os.path.basename(os.path.normpath(model_dir))
23
24 possible_dirs = [
25 os.path.expanduser(f"~/.cache/huggingface/modules/transformers_modules/{repo_name}"),
26 f"/root/.cache/huggingface/modules/transformers_modules/{repo_name}",
27 ]
28
29 for key, mod in sys.modules.items():
30 if "transformers_modules" in key and hasattr(mod, "__file__") and mod.__file__:
31 candidate = os.path.join(os.path.dirname(mod.__file__), repo_name)
32 if candidate not in possible_dirs:
33 possible_dirs.insert(0, candidate)
34
35 target_dir = None
36 for d in possible_dirs:
37 if os.path.isdir(os.path.dirname(d)):
38 target_dir = d
39 break
40
41 if target_dir is None:
42 target_dir = possible_dirs[0]
43 os.makedirs(target_dir, exist_ok=True)
44
45 synced = []
46 for fname in os.listdir(model_dir):
47 if fname.endswith(".py"):
48 src = os.path.join(model_dir, fname)
49 dst = os.path.join(target_dir, fname)
50 if not os.path.exists(dst) or os.path.getmtime(src) > os.path.getmtime(dst):
51 shutil.copy2(src, dst)
52 synced.append(fname)
53
54 if synced:
55 print(f" 🔄 Synced {len(synced)} files → {target_dir}")
56 mods_to_remove = [k for k in sys.modules if f"transformers_modules.{repo_name}" in k]
57 for k in mods_to_remove:
58 del sys.modules[k]
59 print(f" 🧹 Cleared {len(mods_to_remove)} cached modules")
60
61
62# ==================== 模型类加载 ====================
63
64def load_model_class(model_dir):
65 """优先选含 generate() 的 CausalLM 类,跳过 base Model"""
66 repo_name = os.path.basename(os.path.normpath(model_dir))
67 module_path = f"transformers_modules.{repo_name}.modeling_molmo2"
68 mod = importlib.import_module(module_path)
69
70 from transformers import PreTrainedModel
71 candidates = []
72 for attr_name in dir(mod):
73 obj = getattr(mod, attr_name)
74 if (isinstance(obj, type)
75 and issubclass(obj, PreTrainedModel)
76 and obj is not PreTrainedModel
77 and "config" not in attr_name.lower()):
78 candidates.append((attr_name, obj))
79
80 if not candidates:
81 raise RuntimeError(f"No model class in {module_path}!")
82
83 def score(item):
84 n, cls = item
85 nl = n.lower()
86 has_gen = hasattr(cls, "generate")
87 is_base = n.endswith("Model") and "for" not in nl
88 if has_gen and not is_base:
89 return 0
90 if "forcausallm" in nl:
91 return 1
92 if is_base:
93 return 99
94 return 50
95
96 candidates.sort(key=score)
97 chosen_name, chosen_cls = candidates[0]
98 print(f" ✅ Model: {chosen_name}")
99 return chosen_cls
100
101
102# ==================== Vocab 工具 ====================
103
104def safe_set_vocab_size(config, size):
105 for attr in ["vocab_size", "text_config.vocab_size"]:
106 parts = attr.split(".")
107 obj = config
108 try:
109 for p in parts[:-1]:
110 obj = getattr(obj, p)
111 setattr(obj, parts[-1], size)
112 return
113 except AttributeError:
114 continue
115 if hasattr(config, "vocab_size"):
116 config.vocab_size = size
117
118
119def expand_vocab_if_needed(model, tokenizer, cfg):
120 tok_vocab = len(tokenizer)
121 model_vocab = cfg.vocab_size
122
123 if tok_vocab == model_vocab:
124 return
125
126 # --- Expand Embedding ---
127 embed_param = embed_name = None
128 for name, param in model.named_parameters():
129 if "wte.embedding" in name.lower() and param.dim() == 2:
130 n_lower = name.lower()
131 if not any(ex in n_lower for ex in ["vision", "vit", "new_embedding"]):
132 embed_param, embed_name = param, name
133 break
134 if embed_param is None:
135 raise RuntimeError("Cannot find text embedding!")
136
137 cur_rows, hidden = embed_param.shape
138 diff = tok_vocab - cur_rows
139
140 special_ids = []
141 for tok_str in tokenizer.get_vocab():
142 tid = tokenizer.convert_tokens_to_ids(tok_str)
143 if tid is not None and tid < cur_rows:
144 if any(k in tok_str.lower() for k in
145 ["<", ">", "[", "]", "bos", "eos", "pad", "unk",
146 "sep", "cls", "mask", "frame", "image", "video"]):
147 special_ids.append(tid)
148
149 init_vec = (embed_param.data[special_ids].mean(dim=0, keepdim=True)
150 if special_ids else embed_param.data.mean(dim=0, keepdim=True))
151 new_rows = init_vec.expand(diff, -1).clone() + \
152 torch.randn(diff, hidden, device=embed_param.device, dtype=embed_param.dtype) * 1e-4
153 new_embed = torch.cat([embed_param.data, new_rows], dim=0)
154
155 parts = embed_name.rsplit(".", 1)
156 parent = model.get_submodule(parts[0]) if len(parts) > 1 else model
157 delattr(parent, parts[-1])
158 setattr(parent, parts[-1], torch.nn.Parameter(new_embed))
159 print(f" 🎯 Expanded embedding: [{cur_rows}x{hidden}] → [{tok_vocab}x{hidden}]")
160
161 # --- Expand LM Head ---
162 expanded = False
163
164 for pname, param in list(model.named_parameters()):
165 pl = pname.lower()
166 if param.dim() == 2 and param.shape[1] == hidden:
167 if any(k in pl for k in ["lm_head", "output_head"]) and \
168 not any(ex in pl for ex in ["vision", "vit", "visual"]):
169 if param.shape[0] < tok_vocab:
170 hd = tok_vocab - param.shape[0]
171 nh = torch.cat([param.data, new_embed[-hd:, :].clone()], dim=0)
172 hp = pname.rsplit(".", 1)
173 hpar = model.get_submodule(hp[0]) if len(hp) > 1 else model
174 delattr(hpar, hp[-1])
175 setattr(hpar, hp[-1], torch.nn.Parameter(nh))
176 expanded = True
177 break
178
179 if not expanded:
180 for path in ["lm_head", "transformer.lm_head", "model.lm_head", "head"]:
181 try:
182 m = model.get_submodule(path)
183 if hasattr(m, "weight") and m.weight.dim() == 2 and m.weight.shape[1] == hidden:
184 if m.weight.shape[0] < tok_vocab:
185 hd = tok_vocab - m.weight.shape[0]
186 nh = torch.cat([m.weight.data, new_embed[-hd:, :].clone()], dim=0)
187 delattr(m, "weight")
188 setattr(m, "weight", torch.nn.Parameter(nh))
189 expanded = True
190 break
191 except (AttributeError, KeyError):
192 continue
193
194 if not expanded:
195 for pname, param in model.named_parameters():
196 if param.dim() == 2 and param.shape == new_embed.shape:
197 if param.data_ptr() == new_embed.data_ptr() or torch.equal(param.data, new_embed.data):
198 expanded = True
199 break
200
201 safe_set_vocab_size(cfg, tok_vocab)
202 safe_set_vocab_size(model.config, tok_vocab)
203 print(f" ✅ Vocab aligned to {tok_vocab}")
204
205
206# ==================== 视频处理 ====================
207
208def load_and_sample_video(video_path, max_frames=8):
209 video, audio, info = tv_io.read_video(video_path, pts_unit="sec")
210 fps = info.get("video_fps", 30.0)
211 total = video.shape[0]
212 n = min(max_frames, total)
213 idx = torch.linspace(0, total - 1, n).long()
214 frames = video[idx]
215 ts = [f"{t:.1f}" for t in (idx.float() / fps).tolist()]
216 print(f" 📹 Sampled {n}/{total} frames @ {fps:.1f} FPS")
217 return frames, ts
218
219
220# ==================== 主推理 ====================
221
222@torch.inference_mode()
223def generate_video_response(video_path, user_query, max_new_tokens=512):
224 model_dir = "/mnt/workspace/.cache/modelscope/models/AlphapilotOS--GDU-liang-4B/snapshots/master"
225
226 sync_modeling_to_cache(model_dir)
227
228 tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)
229 cfg = AutoConfig.from_pretrained(model_dir, trust_remote_code=True)
230
231 ModelClass = load_model_class(model_dir)
232
233 # 🔑 24GB GPU 显存安全策略
234 import gc
235 gc.collect()
236 torch.cuda.empty_cache()
237
238 max_memory = {0: "20GiB", "cpu": "30GiB"}
239
240 model = ModelClass.from_pretrained(
241 model_dir,
242 config=cfg,
243 dtype=torch.bfloat16,
244 device_map="auto",
245 max_memory=max_memory,
246 offload_folder="/tmp/offload",
247 trust_remote_code=True,
248 )
249 model.eval()
250
251 # 确保 meta tensor 全部物化后再 expand vocab
252 if any(p.device.type == "meta" for p in model.parameters()):
253 if not hasattr(model, "hf_device_map"):
254 model.to("cpu")
255 meta_count = sum(1 for p in model.parameters() if p.device.type == "meta")
256 if meta_count > 0:
257 model.to("cpu")
258
259 expand_vocab_if_needed(model, tokenizer, cfg)
260
261 if tokenizer.eos_token_id is None:
262 tokenizer.eos_token_id = tokenizer.pad_token_id or 0
263
264 frames, timestamps = load_and_sample_video(video_path, max_frames=8)
265
266 # Processor 加载
267 from transformers import AutoProcessor
268 try:
269 processor = AutoProcessor.from_pretrained(model_dir, trust_remote_code=True)
270 except Exception as e:
271 repo_name = os.path.basename(os.path.normpath(model_dir))
272 proc_module = importlib.import_module(f"transformers_modules.{repo_name}.processing_molmo2")
273 for attr_name in dir(proc_module):
274 obj = getattr(proc_module, attr_name)
275 if isinstance(obj, type) and "processor" in attr_name.lower() and attr_name != "AutoProcessor":
276 processor = obj.from_pretrained(model_dir, trust_remote_code=True)
277 break
278 else:
279 raise RuntimeError(f"Cannot load processor! AutoProcessor failed: {e}")
280
281 print(f" ✅ Processor: {type(processor).__name__}")
282
283 # 🔑 Molmo2-GDU-liang-4B 确认使用 <|video|>
284 video_placeholder = "<|video|>"
285 messages = [{"role": "user", "content": f"{video_placeholder}\n{user_query}"}]
286 prompt_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
287
288 # Molmo2VideoProcessor 期望 (T, H, W, C)
289 video_tensor = frames
290 fps_value = 30.0
291 num_frames = video_tensor.shape[0]
292
293 # 多策略调用 processor
294 proc_inputs = None
295 try:
296 proc_inputs = processor(
297 text=prompt_text, videos=video_tensor,
298 video_metadata=[{"fps": fps_value, "total_num_frames": num_frames}],
299 return_tensors="pt",
300 )
301 except Exception:
302 try:
303 from transformers.video_processing_utils import VideoMetadata as VM
304 proc_inputs = processor(
305 text=prompt_text, videos=video_tensor,
306 video_metadata=[VM(fps=fps_value, total_num_frames=num_frames)],
307 return_tensors="pt",
308 )
309 except Exception:
310 proc_inputs = processor(
311 text=prompt_text, videos=video_tensor,
312 fps=fps_value, return_tensors="pt",
313 )
314
315 # device_map="auto" 时用首个参数的设备
316 target_device = next(model.parameters()).device
317 inp = {k: v.to(target_device) if isinstance(v, torch.Tensor) else v for k, v in proc_inputs.items()}
318
319 print(f" 🚀 Generating ({list(inp.keys())})...")
320
321 outputs = model.generate(
322 **inp, max_new_tokens=max_new_tokens, do_sample=False,
323 eos_token_id=tokenizer.eos_token_id,
324 pad_token_id=tokenizer.pad_token_id or tokenizer.eos_token_id,
325 )
326
327 new_tok = outputs[0][inp["input_ids"].shape[1]:]
328 return tokenizer.decode(new_tok, skip_special_tokens=True).strip()
329
330
331# ==================== 入口 ====================
332
333if __name__ == "__main__":
334 print("=" * 60)
335 print("【🚀 v9.26 - GDU-liang-4B Video Understanding】")
336 print("=" * 60)
337
338 result = generate_video_response(
339 "/mnt/workspace/vido_data/微信视频2026-08-14_144752_279.mp4",
340 "请详细描述视频中先后发生的动作与变化,注意时间顺序。",
341 )
342
343 print("\n" + "=" * 60)
344 print("📝 RESPONSE:")
345 print("=" * 60)
346 print(result)
347## 🎯 Roadmap: From Multimodal to Embodied Intelligence
348
349该项目为完整具身智能闭环的基石,重点方向:
350
351- 边缘量化与硬件适配(INT4/INT8)
352- 低延迟在线推理(目标延迟 < 50ms)
353- 硬件在环(HIL)验证与实机飞行测试
354- 从感知到动作(VLA Head → 飞控命令)端到端闭环
355
## 👤 作者与背景
作者为广东外语外贸大学人工智能方向学生梁子羿和AlphapilotOS组员,具备量化交易与轻量化部署实战经验(2026 年 4 月,华林证券 & 赛富溪栈 AI 股票交易挑战赛季军),目前专注于 4B 级别多模态模型的身份表征固化与端侧工程化。
## 🌍 开源与伦理
- 开源策略:提供模型权重与训练/推理流水线;坚持底层改造与工程化落地,不追逐热点式 Demo。
- 使用限制:请遵守当地法规与无人机飞行安全规定;该模型未经完整实战验证,不适用于未经额外安全验证的关键控制系统。
## 📋 Model Card 详情
| Field | Value |
|-------|-------|
| Base Model | allenai/Molmo2-4B |
| Parameters | ~4B |
| Modalities | Text, Image, Video, Audio |
| Precision | float16 (quantizable to INT4/INT8) |
| Context Length | 4096 tokens |
| License | Apache 2.0 |
| Framework | Transformers + Custom Code |
## 📚 Citation
```bibtex
@misc{gdu-liang-4b,
title={GDU-Liang-4B: Identity-Anchored Multimodal Model for Edge Embodied AI},
author={zhenqiangliang6},
year={2026},
url={https://huggingface.co/zhenqiangliang6/GDU-Liang-4B}
}examples/:图像、视频、音频推理示例(建议包含 Colab 与本地脚本)scripts/:量化、转换、硬件适配脚本docs/:技术报告、模型卡扩展、评测结果存档pip install -r requirements.txtexamples/ 中的脚本完成多模态输入的推理验证