| """ |
| deploy.py — FrozenVLA 本地部署脚本 |
| ==================================== |
| 加载 Qwen3-VL-2B-Instruct 基座 + 训练好的 checkpoint → 本地推理 |
| |
| 用法: |
| python deploy.py # 默认加载 epoch_3.pt |
| python deploy.py --checkpoint epoch_1.pt # 指定 checkpoint |
| python deploy.py --test-image cat.jpg # 用真实图片测试 |
| |
| 环境: Windows + RTX 4060 Laptop 8GB, 使用 sdpa 注意力 |
| """ |
|
|
| import sys |
| import argparse |
| from pathlib import Path |
|
|
| |
| sys.path.insert(0, str(Path(__file__).parent.parent / "train")) |
|
|
| import torch |
| from PIL import Image |
| from model import FrozenVLA |
|
|
|
|
| |
| BASE_MODEL_PATH = r"G:\LingArm\models\pretrained\Qwen3-VL-2B-Instruct" |
| CHECKPOINT_DIR = r"G:\LingArm\models\train\frozen_preview" |
| DEFAULT_CHECKPOINT = "epoch_4.pt" |
|
|
|
|
| def load_model(checkpoint_name: str = DEFAULT_CHECKPOINT, verbose: bool = True): |
| """ |
| 加载完整部署模型: 基座 + checkpoint head |
| |
| Returns: |
| model (FrozenVLA): 已加载权重、设为 eval 模式、在 GPU 上的模型 |
| """ |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
|
|
| if verbose: |
| print("=" * 60) |
| print(" FrozenVLA 本地部署加载") |
| print("=" * 60) |
| print(f" Base model : {BASE_MODEL_PATH}") |
| print(f" Checkpoint : {CHECKPOINT_DIR}/{checkpoint_name}") |
| print(f" Device : {device}") |
| print(f" Attention : sdpa (Windows/4060)") |
| print("-" * 60) |
|
|
| |
| if verbose: |
| print("\n[1/3] Loading Qwen3-VL-2B-Instruct base model...") |
| model = FrozenVLA( |
| llm_name=BASE_MODEL_PATH, |
| mlp_hidden_dim=512, |
| mlp_depth=2, |
| action_dim=7, |
| attn_implementation="sdpa", |
| ) |
|
|
| |
| if verbose: |
| print(f"\n[2/3] Loading checkpoint: {checkpoint_name}") |
| ckpt_path = Path(CHECKPOINT_DIR) / checkpoint_name |
| model.load_trainable(ckpt_path) |
|
|
| |
| if verbose: |
| print(f"\n[3/3] Moving to {device} + casting head to bfloat16...") |
| model = model.to(device) |
| |
| model.mlp_projector = model.mlp_projector.to(dtype=torch.bfloat16) |
| model.action_head = model.action_head.to(dtype=torch.bfloat16) |
| model.translation_scale.data = model.translation_scale.data.to(torch.bfloat16) |
| model.rotation_scale.data = model.rotation_scale.data.to(torch.bfloat16) |
| model.eval() |
|
|
| if verbose: |
| trainable = model.count_trainable_params() |
| frozen = model.count_frozen_params() |
| print(f"\n Trainable params : {trainable:,}") |
| print(f" Frozen params : {frozen:,}") |
| print(f" Total params : {trainable + frozen:,}") |
| print(f" VRAM used : {torch.cuda.memory_allocated()/1024**3:.2f} GB") |
| print("=" * 60) |
| print(" [OK] Model loaded successfully!") |
| print("=" * 60) |
|
|
| return model |
|
|
|
|
| def predict(model: FrozenVLA, images, instructions): |
| """ |
| 推理接口 |
| |
| Args: |
| model: 已加载的 FrozenVLA 模型 |
| images: PIL Image 或 list of PIL Images |
| instructions: str 或 list of str(任务描述) |
| |
| Returns: |
| action: numpy array, shape (N, 7), EEF delta [dx,dy,dz,ax,ay,az,gripper] |
| """ |
| |
| if isinstance(images, Image.Image): |
| images = [images] |
| if isinstance(instructions, str): |
| instructions = [instructions] |
|
|
| with torch.no_grad(): |
| action = model(images, instructions) |
| return action.float().cpu().numpy() |
|
|
|
|
| def basic_test(model: FrozenVLA): |
| """ |
| 基础冒烟测试:用两张假图片推理,验证 pipeline 通畅 |
| """ |
| print("\n" + "=" * 60) |
| print(" Basic Smoke Test") |
| print("=" * 60) |
|
|
| dummy_images = [ |
| Image.new("RGB", (448, 448), color=(128, 128, 128)), |
| Image.new("RGB", (448, 448), color=(64, 200, 64)), |
| ] |
| instructions = [ |
| "pick up the red block", |
| "place the spoon into the bowl", |
| ] |
|
|
| print(f" Input: {len(dummy_images)} images, {len(instructions)} instructions") |
| action = predict(model, dummy_images, instructions) |
|
|
| print(f"\n Action shape : {action.shape}") |
| print(f" Action range : [{action.min():.4f}, {action.max():.4f}]") |
| print(f" Action mean : {action.mean():.4f}") |
| for i in range(len(action)): |
| print(f" Sample {i}: {action[i]}") |
| print("\n [OK] Inference pipeline OK!") |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="FrozenVLA Local Deployment") |
| parser.add_argument("--checkpoint", default=DEFAULT_CHECKPOINT, |
| help=f"Checkpoint filename (default: {DEFAULT_CHECKPOINT})") |
| parser.add_argument("--test-image", type=str, default=None, |
| help="Path to a test image for real inference") |
| parser.add_argument("--skip-test", action="store_true", |
| help="Skip the basic smoke test") |
| args = parser.parse_args() |
|
|
| |
| model = load_model(args.checkpoint) |
|
|
| |
| if not args.skip_test: |
| basic_test(model) |
|
|
| |
| if args.test_image: |
| img_path = Path(args.test_image) |
| if not img_path.exists(): |
| print(f"\n [ERROR] Image not found: {img_path}") |
| sys.exit(1) |
| print(f"\n Testing with real image: {img_path.name}") |
| image = Image.open(img_path).convert("RGB") |
| action = predict(model, image, "manipulate the object in front of the robot") |
| print(f" Predicted action (EEF delta 7D): {action[0]}") |
| print(f" dx={action[0,0]:.6f} dy={action[0,1]:.6f} dz={action[0,2]:.6f}") |
| print(f" ax={action[0,3]:.6f} ay={action[0,4]:.6f} az={action[0,5]:.6f}") |
| print(f" gripper={action[0,6]:.6f}") |
|
|
| print("\n [OK] Deployment complete!") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|