From 0082fa512b43a73a17a3dc8558cc246ef4cf8c7d Mon Sep 17 00:00:00 2001 From: yxwang1215 Date: Sat, 7 Mar 2026 16:35:24 +0800 Subject: [PATCH] fix bugs in original code, which offload VAE to CPU rather than GPUs --- .gitignore | 37 +++ .../robotwin/eval_polict_client_openpi.py | 160 +++++++--- example/robotwin/README.txt | 11 + example/robotwin/create_dummy_images.py | 36 +++ .../robotwin/observation.images.cam_high.png | Bin 50260 -> 828 bytes .../observation.images.cam_left_wrist.png | Bin 24723 -> 827 bytes .../observation.images.cam_right_wrist.png | Bin 50408 -> 824 bytes markdown/EVAL_ROBOTWIN.md | 92 ++++++ markdown/INFERENCE.md | 117 +++++++ INSTALL.md => markdown/INSTALL.md | 0 markdown/ideas/ideas_0305.md | 279 ++++++++++++++++ ...ideas_action_relevant_video_tokens_0305.md | 157 +++++++++ .../ideas/ideas_dynamic_step_budget_0305.md | 103 ++++++ .../ideas_training_free_wam_strong_0305.md | 109 +++++++ .../ideas/ideas_value_aware_compute_0306.md | 144 +++++++++ .../ideas/ideas_video_token_prune_0305.md | 127 ++++++++ markdown/ideas/vla_roi_tokens_methods.md | 135 ++++++++ .../world-action-model-acceleration_0301.md | 43 +++ pyproject.toml | 9 +- run_inference.sh | 33 ++ script/download/download_bench_robotwin.sh | 61 ++++ script/download/download_dataset.py | 44 +++ script/download/download_modelscope.py | 40 +++ .../download/download_posttrain_robotwin.py | 70 ++++ script/download/download_robotwin2.sh | 91 ++++++ script/download/download_robotwin_assets.py | 79 +++++ script/run_eval_robotwin.sh | 145 +++++++++ script/run_eval_robotwin_client_only.sh | 46 +++ script/run_eval_robotwin_full.sh | 298 ++++++++++++++++++ script/run_eval_robotwin_slurm.sh | 229 ++++++++++++++ script/run_i2va_single_gpu.sh | 38 +++ script/setup/install_flash_attn_cu124.sh | 75 +++++ script/setup/setup_cu124_mirror.md | 47 +++ script/setup/setup_env.sh | 60 ++++ script/setup/setup_env_cu124.sh | 53 ++++ wan_va/configs/va_demo_cfg.py | 5 +- wan_va/configs/va_robotwin_cfg.py | 13 +- wan_va/configs/va_robotwin_i2va.py | 4 +- wan_va/configs/va_robotwin_train_cfg.py | 4 +- wan_va/distributed/util.py | 5 +- wan_va/modules/model.py | 20 +- wan_va/wan_va_server.py | 228 ++++++++++---- 42 files changed, 3135 insertions(+), 112 deletions(-) create mode 100644 .gitignore create mode 100644 example/robotwin/README.txt create mode 100644 example/robotwin/create_dummy_images.py create mode 100644 markdown/EVAL_ROBOTWIN.md create mode 100644 markdown/INFERENCE.md rename INSTALL.md => markdown/INSTALL.md (100%) create mode 100644 markdown/ideas/ideas_0305.md create mode 100644 markdown/ideas/ideas_action_relevant_video_tokens_0305.md create mode 100644 markdown/ideas/ideas_dynamic_step_budget_0305.md create mode 100644 markdown/ideas/ideas_training_free_wam_strong_0305.md create mode 100644 markdown/ideas/ideas_value_aware_compute_0306.md create mode 100644 markdown/ideas/ideas_video_token_prune_0305.md create mode 100644 markdown/ideas/vla_roi_tokens_methods.md create mode 100644 markdown/ideas/world-action-model-acceleration_0301.md create mode 100644 run_inference.sh create mode 100644 script/download/download_bench_robotwin.sh create mode 100644 script/download/download_dataset.py create mode 100644 script/download/download_modelscope.py create mode 100644 script/download/download_posttrain_robotwin.py create mode 100755 script/download/download_robotwin2.sh create mode 100644 script/download/download_robotwin_assets.py create mode 100755 script/run_eval_robotwin.sh create mode 100755 script/run_eval_robotwin_client_only.sh create mode 100755 script/run_eval_robotwin_full.sh create mode 100755 script/run_eval_robotwin_slurm.sh create mode 100644 script/run_i2va_single_gpu.sh create mode 100755 script/setup/install_flash_attn_cu124.sh create mode 100644 script/setup/setup_cu124_mirror.md create mode 100644 script/setup/setup_env.sh create mode 100755 script/setup/setup_env_cu124.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..017d29a --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# 不纳入版本控制:模型与测评结果(仅保留核心代码) +models/ +results/ + +# 日志与临时 +logs/ +*.log + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +*.egg +.eggs/ +dist/ +build/ + +# 虚拟环境 / conda +.venv/ +venv/ +env/ + +# IDE / 编辑 +.idea/ +.vscode/ +*.swp +*.swo + +# Jupyter +.ipynb_checkpoints/ + +# 系统 +.DS_Store +Thumbs.db diff --git a/evaluation/robotwin/eval_polict_client_openpi.py b/evaluation/robotwin/eval_polict_client_openpi.py index 7ed5265..01cda25 100644 --- a/evaluation/robotwin/eval_polict_client_openpi.py +++ b/evaluation/robotwin/eval_polict_client_openpi.py @@ -1,12 +1,13 @@ import sys import os import subprocess +import time import matplotlib.pyplot as plt from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas import cv2 from pathlib import Path -robowin_root = Path("/path/to/your/robowin") +robowin_root = Path(os.environ.get("ROBOTWIN_ROOT", "/path/to/your/robowin")) if str(robowin_root) not in sys.path: sys.path.insert(0, str(robowin_root)) @@ -205,36 +206,42 @@ def save_comparison_video(real_obs_list, imagined_video, action_history, save_pa print(f"Saving video: Real {n_real} frames, Imagined {n_imagined} frames...") - final_frames = [] + # 首帧确定统一尺寸,保证整段视频每帧一致,避免 imageio "All images should have same size" + obs0 = real_obs_list[0] + base_h = obs0["observation.images.cam_high"].shape[0] + + def resize_h(img, h): + if img.shape[0] != h: + w = int(img.shape[1] * h / img.shape[0]) + img = cv2.resize(img, (w, h)) + img = np.ascontiguousarray(img) + if img.dtype != np.uint8: + img = (img * 255).astype(np.uint8) + return img + + # Real 行为左-中-右:High | Left wrist | Right wrist,与 obs_cam_keys 顺序一致 + part_high_0 = resize_h(obs0["observation.images.cam_high"], base_h) + part_left_0 = resize_h(obs0["observation.images.cam_left_wrist"], base_h) + part_right_0 = resize_h(obs0["observation.images.cam_right_wrist"], base_h) + w_high, w_left, w_right = part_high_0.shape[1], part_left_0.shape[1], part_right_0.shape[1] + row_real0 = np.hstack([part_high_0, part_left_0, part_right_0]) + row_real0 = add_title_bar(row_real0, "Real Observation (High / Left / Right)") + target_width = row_real0.shape[1] + real_row_h = row_real0.shape[0] + imagined_row_h = 256 + final_frames = [] for i in range(n_frames): obs = real_obs_list[i] - cam_high = obs["observation.images.cam_high"] - cam_left = obs["observation.images.cam_left_wrist"] - cam_right = obs["observation.images.cam_right_wrist"] - - base_h = cam_high.shape[0] - - def resize_h(img, h): - if img.shape[0] != h: - w = int(img.shape[1] * h / img.shape[0]) - img = cv2.resize(img, (w, h)) - img = np.ascontiguousarray(img) - if img.dtype != np.uint8: - img = (img * 255).astype(np.uint8) - return img - row_real = np.hstack([ - resize_h(cam_high, base_h), - resize_h(cam_left, base_h), - resize_h(cam_right, base_h) + resize_h(obs["observation.images.cam_high"], base_h), + resize_h(obs["observation.images.cam_left_wrist"], base_h), + resize_h(obs["observation.images.cam_right_wrist"], base_h), ]) - row_real = np.ascontiguousarray(row_real) - row_real = add_title_bar(row_real, "Real Observation (High / Left / Right)") - - target_width = row_real.shape[1] + if row_real.shape[1] != target_width or row_real.shape[0] != real_row_h: + row_real = cv2.resize(row_real, (target_width, real_row_h)) if imagined_video is not None and i < n_imagined: img_frame = imagined_video[i] @@ -242,20 +249,33 @@ def resize_h(img, h): img_frame = (img_frame * 255).astype(np.uint8) elif img_frame.dtype != np.uint8: img_frame = img_frame.astype(np.uint8) - - h = int(img_frame.shape[0] * target_width / img_frame.shape[1]) - row_imagined = cv2.resize(img_frame, (target_width, h)) + # 与 real 一致:左-中-右 = High | Left wrist | Right wrist(模型输出顺序与 obs_cam_keys 一致) + H_im, W_im = img_frame.shape[0], img_frame.shape[1] + if W_im >= 3: + third = W_im // 3 + im_high = cv2.resize(img_frame[:, 0:third], (w_high, imagined_row_h)) + im_left = cv2.resize(img_frame[:, third : 2 * third], (w_left, imagined_row_h)) + im_right = cv2.resize(img_frame[:, 2 * third :], (w_right, imagined_row_h)) + row_imagined = np.hstack([im_high, im_left, im_right]) + else: + row_imagined = cv2.resize(img_frame, (target_width, imagined_row_h)) else: - row_imagined = np.zeros((300, target_width, 3), dtype=np.uint8) - cv2.putText(row_imagined, "Coming soon", (target_width//2 - 100, 150), + row_imagined = np.zeros((imagined_row_h, target_width, 3), dtype=np.uint8) + cv2.putText(row_imagined, "Coming soon", (target_width//2 - 100, imagined_row_h//2 - 20), cv2.FONT_HERSHEY_SIMPLEX, 1, (100, 100, 100), 2) row_imagined = np.ascontiguousarray(row_imagined) - row_imagined = add_title_bar(row_imagined, "Imagined Video Stream") + row_imagined = add_title_bar(row_imagined, "Imagined Video (High / Left / Right)") full_frame = np.vstack([row_real, row_imagined]) full_frame = np.ascontiguousarray(full_frame) final_frames.append(full_frame) + # 统一为第一帧尺寸,防止 add_title_bar 等导致细微差异 + ref_h, ref_w = final_frames[0].shape[0], final_frames[0].shape[1] + for idx in range(len(final_frames)): + if final_frames[idx].shape[0] != ref_h or final_frames[idx].shape[1] != ref_w: + final_frames[idx] = cv2.resize(final_frames[idx], (ref_w, ref_h)) + imageio.mimsave(save_path, final_frames, fps=fps) print(f"Combined video saved to: {save_path}") @@ -305,6 +325,7 @@ def main(usr_args): policy_name = usr_args["policy_name"] video_guidance_scale = usr_args["video_guidance_scale"] action_guidance_scale = usr_args["action_guidance_scale"] + save_visualization = bool(usr_args.get("save_visualization", True)) instruction_type = 'seen' save_dir = None video_save_dir = None @@ -397,7 +418,7 @@ def get_embodiment_file(embodiment_type): test_num = usr_args["test_num"] - model = WebsocketClientPolicy(port=usr_args['port']) + model = WebsocketClientPolicy(host="127.0.0.1", port=usr_args['port']) st_seed, suc_num = eval_policy(task_name, TASK_ENV, @@ -407,7 +428,7 @@ def get_embodiment_file(embodiment_type): test_num=test_num, video_size=video_size, instruction_type=instruction_type, - save_visualization=True, + save_visualization=save_visualization, video_guidance_scale=video_guidance_scale, action_guidance_scale=action_guidance_scale) suc_nums.append(suc_num) @@ -462,7 +483,7 @@ def eval_policy(task_name, now_id = 0 succ_seed = 0 suc_test_seed_list = [] - + all_trajectory_timings = [] # 每条 trajectory 的耗时汇总,用于最后样本级统计 now_seed = st_seed clear_cache_freq = args["clear_cache_freq"] @@ -545,6 +566,7 @@ def eval_policy(task_name, full_obs_list = [] gen_video_list = [] full_action_history = [] + trajectory_timings = [] # 当前 trajectory 每次 infer 的 timing initial_obs = TASK_ENV.get_obs() inint_eef_pose = initial_obs['endpose']['left_endpose'] + \ @@ -561,16 +583,21 @@ def eval_policy(task_name, first_obs = format_obs(observation, prompt) ret = model.infer(dict(obs=first_obs, prompt=prompt, save_visualization=save_visualization, video_guidance_scale=video_guidance_scale, action_guidance_scale=action_guidance_scale)) #(TASK_ENV, model, observation) + if 'timing' in ret: + trajectory_timings.append(ret['timing']) action = ret['action'] if 'video' in ret: imagined_video = ret['video'] gen_video_list.append(imagined_video) + print(f" [eval] received predicted video chunk, shape {getattr(imagined_video, 'shape', '?')}") key_frame_list = [] assert action.shape[2] % 4 == 0 action_per_frame = action.shape[2] // 4 start_idx = 1 if first else 0 + t0_env_step = time.perf_counter() + env_step_count = 0 for i in range(start_idx, action.shape[1]): for j in range(action.shape[2]): raw_action_step = action[:, i, j].flatten() @@ -597,20 +624,58 @@ def eval_policy(task_name, else: raise NotImplementedError TASK_ENV.take_action(ee_action, action_type='ee') + env_step_count += 1 if (j+1) % action_per_frame == 0: obs = format_obs(TASK_ENV.get_obs(), prompt) full_obs_list.append(obs) key_frame_list.append(obs) + trajectory_timings.append( + dict(env_step_update=(time.perf_counter() - t0_env_step), env_step_count=env_step_count) + ) first = False - model.infer(dict(obs = key_frame_list, compute_kv_cache=True, imagine=False, save_visualization=save_visualization, state=action)) - + ret_kv = model.infer(dict(obs = key_frame_list, compute_kv_cache=True, imagine=False, save_visualization=save_visualization, state=action)) + if 'timing' in ret_kv: + trajectory_timings.append(ret_kv['timing']) + if TASK_ENV.eval_success: succ = True break - + + # 当前 trajectory 耗时汇总与占比(以 trajectory 为单位输出) + if trajectory_timings: + keys = ['encode_obs', 'video_denoise', 'action_denoise', 'kv_cache', 'env_step_update', 'other'] + summed = {k: sum(t.get(k, 0.0) for t in trajectory_timings) for k in keys} + total = sum(summed.values()) or 1e-9 + pct = {k: 100.0 * summed[k] / total for k in keys} + total_env_steps = int(sum(t.get('env_step_count', 0) for t in trajectory_timings)) + avg_env_step = summed['env_step_update'] / max(total_env_steps, 1) + print(f"\033[90m[Trajectory {TASK_ENV.test_num + 1}] 耗时(秒): encode_obs={summed['encode_obs']:.2f}, video_denoise={summed['video_denoise']:.2f}, action_denoise={summed['action_denoise']:.2f}, kv_cache={summed['kv_cache']:.2f}, env_step_update={summed['env_step_update']:.2f}, other={summed['other']:.2f} | 占比(%): encode_obs={pct['encode_obs']:.1f}%, video_denoise={pct['video_denoise']:.1f}%, action_denoise={pct['action_denoise']:.1f}%, kv_cache={pct['kv_cache']:.1f}%, env_step_update={pct['env_step_update']:.1f}%, other={pct['other']:.1f}% | env_step均值={avg_env_step*1000:.1f}ms ({total_env_steps} steps)\033[0m") + detail_keys = [ + 'encode_obs_cpu_preprocess', + 'encode_obs_to_vae_device', + 'encode_obs_vae_encode', + 'encode_obs_latent_postprocess', + ] + detail_summed = {k: sum(t.get(k, 0.0) for t in trajectory_timings) for k in detail_keys} + encode_total = summed['encode_obs'] or 1e-9 + detail_pct = {k: 100.0 * detail_summed[k] / encode_total for k in detail_keys} + print( + "\033[90m" + f" └─ encode_obs细分(秒): cpu_preprocess={detail_summed['encode_obs_cpu_preprocess']:.2f}, " + f"to_vae_device={detail_summed['encode_obs_to_vae_device']:.2f}, " + f"vae_encode={detail_summed['encode_obs_vae_encode']:.2f}, " + f"latent_postprocess={detail_summed['encode_obs_latent_postprocess']:.2f} " + f"| 占encode_obs(%): cpu_preprocess={detail_pct['encode_obs_cpu_preprocess']:.1f}%, " + f"to_vae_device={detail_pct['encode_obs_to_vae_device']:.1f}%, " + f"vae_encode={detail_pct['encode_obs_vae_encode']:.1f}%, " + f"latent_postprocess={detail_pct['encode_obs_latent_postprocess']:.1f}%" + "\033[0m" + ) + summed['env_step_count'] = total_env_steps + all_trajectory_timings.append(summed) vis_dir = Path(args['save_root']) / f'stseed-{st_seed}' / 'visualization' / task_name vis_dir.mkdir(parents=True, exist_ok=True) @@ -618,7 +683,7 @@ def eval_policy(task_name, out_img_file = vis_dir / video_name save_comparison_video( real_obs_list=full_obs_list, - imagined_video=None, #gen_video_list, + imagined_video=gen_video_list if gen_video_list else None, action_history=full_action_history, save_path=str(out_img_file), fps=15 # Suggest adjusting fps based on simulation step @@ -655,6 +720,25 @@ def eval_policy(task_name, ) now_seed += 1 + # 以样本为单位输出时间占比统计 + if all_trajectory_timings: + keys = ['encode_obs', 'video_denoise', 'action_denoise', 'kv_cache', 'env_step_update', 'other'] + total_summed = {k: sum(t.get(k, 0.0) for t in all_trajectory_timings) for k in keys} + total_sec = sum(total_summed.values()) or 1e-9 + total_pct = {k: 100.0 * total_summed[k] / total_sec for k in keys} + n_samples = len(all_trajectory_timings) + total_env_steps = int(sum(t.get('env_step_count', 0) for t in all_trajectory_timings)) + avg_env_step = total_summed['env_step_update'] / max(total_env_steps, 1) + print("\n\033[97m======== 样本级时间占比统计 ({} 条 trajectory) ========\033[0m".format(n_samples)) + print("\033[97m总耗时(秒): encode_obs={:.2f}, video_denoise={:.2f}, action_denoise={:.2f}, kv_cache={:.2f}, env_step_update={:.2f}, other={:.2f}\033[0m".format( + total_summed['encode_obs'], total_summed['video_denoise'], total_summed['action_denoise'], + total_summed['kv_cache'], total_summed['env_step_update'], total_summed['other'])) + print("\033[97m占比(%): encode_obs={:.1f}%, video_denoise={:.1f}%, action_denoise={:.1f}%, kv_cache={:.1f}%, env_step_update={:.1f}%, other={:.1f}%\033[0m".format( + total_pct['encode_obs'], total_pct['video_denoise'], total_pct['action_denoise'], + total_pct['kv_cache'], total_pct['env_step_update'], total_pct['other'])) + print("\033[97menv_step 平均耗时: {:.1f} ms/step ({} steps)\033[0m".format(avg_env_step * 1000.0, total_env_steps)) + print("\033[97m========================================================\033[0m\n") + return now_seed, TASK_ENV.suc @@ -667,6 +751,8 @@ def parse_args_and_config(): parser.add_argument("--video_guidance_scale", type=float, default=5.0) parser.add_argument("--action_guidance_scale", type=float, default=5.0) parser.add_argument("--test_num", type=int, default=100) + parser.add_argument("--save_visualization", type=lambda x: str(x).lower() not in ('0', 'false', 'no', 'off'), default=True, + help='是否渲染并保存预测视频(VAE 解码+对比视频),关闭可显著加速。传 0 或 false 关闭。') args = parser.parse_args() with open(args.config, "r", encoding="utf-8") as f: diff --git a/example/robotwin/README.txt b/example/robotwin/README.txt new file mode 100644 index 0000000..868b1f5 --- /dev/null +++ b/example/robotwin/README.txt @@ -0,0 +1,11 @@ +# 本目录用于 Image-to-Video-Action (i2va) 推理的「首帧图像」输入。 +# 使用 robotwin_i2av 配置时,需要以下 3 个 PNG 文件(与 obs_cam_keys 对应): +# +# observation.images.cam_high.png +# observation.images.cam_left_wrist.png +# observation.images.cam_right_wrist.png +# +# 图像尺寸会被代码自动 resize(如 256x320),用任意尺寸的 RGB 图即可。 +# +# 生成占位图(便于先跑通流程): +# python create_dummy_images.py diff --git a/example/robotwin/create_dummy_images.py b/example/robotwin/create_dummy_images.py new file mode 100644 index 0000000..a50fc35 --- /dev/null +++ b/example/robotwin/create_dummy_images.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""生成 robotwin i2va 所需的占位首帧图像,便于先跑通推理流程。""" +import os + +try: + from PIL import Image + import numpy as np +except ImportError: + print("请先安装: pip install Pillow numpy") + raise + +# 与 va_robotwin_cfg.obs_cam_keys 一致 +OBS_CAM_KEYS = [ + "observation.images.cam_high", + "observation.images.cam_left_wrist", + "observation.images.cam_right_wrist", +] +# robotwin 主视角尺寸 +HEIGHT, WIDTH = 256, 320 + +def main(): + script_dir = os.path.dirname(os.path.abspath(__file__)) + os.makedirs(script_dir, exist_ok=True) + for i, key in enumerate(OBS_CAM_KEYS): + # 简单渐变图,避免全 0 导致潜在数值问题 + arr = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8) + arr[:, :, 0] = 30 + i * 60 + arr[:, :, 1] = 60 + i * 40 + arr[:, :, 2] = 90 + i * 30 + path = os.path.join(script_dir, f"{key}.png") + Image.fromarray(arr).save(path) + print(f"Written: {path}") + print("Done. 可用 script/run_i2va_single_gpu.sh 跑 i2va 推理。") + +if __name__ == "__main__": + main() diff --git a/example/robotwin/observation.images.cam_high.png b/example/robotwin/observation.images.cam_high.png index 7546ec3aa2064e4e32d03250439bff582b9d6042..b493486efa2731df796a88c89e22e5925f9b1824 100644 GIT binary patch literal 828 zcmeAS@N?(olHy`uVBq!ia0y~yU~~Xt1`Z~mi0>+ki3|+P%$_ceAr*7pUUuX?pulk8 zfT5y>Bj>-?2aWuhMavh=@7wu}-Q@3j=7wsvHUnno8I6i(4hWy;Yl17 g9u1Qro*-l|moRPFxFN6sm@61OUHx3vIVCg!0Djv1AOHXW literal 50260 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 CgXVAm diff --git a/example/robotwin/observation.images.cam_left_wrist.png b/example/robotwin/observation.images.cam_left_wrist.png index 68323f37c47863b981821ab7c54ee6281008eec3..1e19e143a416cb14a64988eac1f555480e4cb003 100644 GIT binary patch literal 827 zcmeAS@N?(olHy`uVBq!ia0y~yU~~Xt1`Z~mi0>+ki3|+POr9=|Ar*7pUN+=qP-Hl8 z;CZT%GSknB1V!I>A|)?pWY;@f{%^&wBRzq`*q}vH;t&r{B3qjQv-6Bb#WM#4(nf_x f!(?bDh#QO_)tJrnJq~CAGX;aEtDnm{r-UW|&{+6V literal 24723 zcmY(r30zZW_C8*_474r~TnLLI1`rSfmnDc&sHhQT69XcOB3BYX6VOCqGNsU|hyfBY z$Px%@6#*3m0STs186{{4iB_me2+KHzSnGn5w6#%$|9OL*@9+N^%7o$iUN(X6lF^}o$OhTrciUF$#kXz53r)~}8E`z-Cvx$NrC zzbn6){d_9Uz9?xsFSXUOZ`QA8bAo!^+mm;WWKMV`zY3muH@)T8gavn>Cd9@}Ri27m zo*PMbuj_Qx^ghVO^xsIU)}<@N3Mxfczk*8X;kk*uOID|FFnj@xg3EXW4wf-n>e|i2 zu052;QLA>OQ`OXY93H_S5jEA`H5zo+=4)i3HP!c0f;}wUvq;BO(o@NU)5eVHMbRnN z0a=-}i<9$hxCd6B%8sVcX_T=o@}hm(4KK9&8mUT0DxVm@4t0 zW4@$PvX-IsLiZaydCG^@9swsSyhsu92loAZB6dtPwyD*(h$EM0y|dRk|YasC1Lwjoh#!EOs&^3B8JHh9Um?g5Yv*i8!FwB4P~Mt0!e2{ zqNcYL1Zek-5=a6zfkvV4rFHD7KZzffY$Cf`bIOXo!1QL!!uwN?a^K(CCcQ9xeztKs z^fHr8%Y6JbmCsZpCDX{a)%!8FH7WlT$d4y2>~w2Fl0>qFqv5jDaz??->ndm6{X;>= z&_@qtKCQuPc!?~@9<5o+IQ3& zExFV0#(uPF;20k*tYIp6Do=0y2K6OY*_a&#A1y8NHQiMVzl8!(i`%hiib>RHA8KBN zI) zz9ik+%lIO(ciYNlY0*Vkn2l9c#%J!3_PU4-(%DSeHk)QQ5<}Im+~2!l311X%rI~s< zKx6LHE*_89Ovbu14fU~Pg)V*3PW7aF89{R?!ni{@(KE@ZjH43ayF1$=oG&Jzg$Wu^ z3a9Nwbyid5F%O_wE%d(AN@C|y1coOC>G8!OBwgBPno*P_d} z5-36ic1n=UQybT@&$!|7@zgNbCg|1ZSCY&_AgWC= zFd1a3CWbP8;?PYn5GoU2B6AhD6cE4|k-X@#P?99cj>=Cs+)V}hB<%DH+bz2=dBLXb zUR2a*oDE0|oW^kB7S*RmPE}fLE>cC%VY_s3Rt%Mb&u@U3MMMUZmRwVrd@dzoR%K_B&YU^j*>uZ;TIEdT%Z8OIWbdX^ zi6&f0lm~T^g}6AYhZiexqrHN&)iE33mBdXfGQ$+xSo3Sb1Nh8BVU}r^I!gntT|*u9 z(}$I=F50af@^XNx7PK z*Mv9*52Tv*WGl55bnY*wDtEM^XfhL4n$JhDXw*4|?*4w$8(=W~l4}pac;KKrzz8w) z{@7OJ#`O6$%vTB*T|Go0-C90u7{77$?zp?IEot!CwGbsw*x=+}7E!0{zwF0uE{OG6 zAx6tsiM{(hOy5^faqT`*9T5v7`8dVoRDP555B+H;$Y{a{8?G0g4w`TN%LcaNG%A0v zR8_oUC!J=Owup*8bR$tPh{0eGC=_9MA%Qdjc@Ed^n)%z5kAz~;8CFAUQG!z)G2q4R z6{On%3knGg__FCcGRSFl=*yh@f-Gf^vZs#17U43bT-ut*==$cZc9{+rcX*GRmIN!p z@M~9wwqY$7S!q;{E48dL0VAz^F#D1DLK0PJ3P)fZsj%ouII9QTBCVV{kCd^gRmMxY zv6TW&2!ZT4k-`>=y&)7qRCG0og~wf&*E zhl@c}*3A*HhYX%6xkF}XmVqbWGI@L?w=3@p8ie9_ zzHiu!wSbk~HyQp0{~Kv`lCmo;SB6UC8{PFQUbaV_0oQRJLs>)E4f&yzK$6r(QzZpL zstx9S%R^OVyl}U&NNR)icu-rxh+pA6FjkONvvf1a32D<;4{^+dL$tQAqPjF%|qyY7@ZEj#l+tV2y6s)LJMU(}cDXRaW8_H!yE0k#bB_ zl%72ndGh{Mw?*#8qxmZ(V^e?h7Wi=daAc$MIwqJdM6i7j*dY1WNn$mZShCPCP#EZ@ zMRn&WXN_v2r*j2W8G($zl=wkXZg!9X{l{-6jFa;LZHUCeB75BlFf5KW%7@?!1Pp%# zLaVW1`{6N-cv*zBkYH#dkg~*V$lU}u^ycw9FTP7)Z61F#=mn_l-Q+VG^IC#-qad4g znx&9$P^x$lHX!UezCDG0KAw#4t2Z7jP;D$jR)mA~dc$WB2xg4P6GJD`zEe<@4hu-S zBmI|$N;mdy;0M}aMM?9pCKjzA;QV37;C*%)0D8fXDAuhaYiu7lT3(V9G8vF12FaP2 zRzPTFl)5pns%UCePbi7e`yioBWl?1O1kg;32I8DYnl>F7P1+)osw4$d=-MC@czNQl zsH0wN(K@hie~vkZPkj&hV_3NhmL*wZxTsdx;I{LqVnVOmp1KEVAk;lL z-L7PEeTUmlo?qDh-kzY`e|TFT_43y5fxHKn49+|#b47QAV}s}@bZ{In3W*T|yNNR; zE=X746^g$;qUOkvHS=Z?6f!=DYbLLNcSpVSZQeLom##?E!j0oQP1s58)RD<~gr|2~ zu7DNTO{*p9ZI!hwHjRg3W7#$BT+%UQD8npY&|Fyr3pO9tirgE_OtXMQQYy2`t_ZC0;IIe~!3)R<_aIk+Ecpysf>L#eL8g>ELf|m50BQz^uQ}V5b~rdIC97Fx z(s?5VV=oNzxddNV{^uH08gO&#rv|kL)yMzTJ>xwISQv>KZu4&PH|>hv4MBKvI?6!k zZeX_{a3|{S%AN(SgOmFptJ|sp^~iY9RNLk&%rdxW)Vei7s>zOV1-Ale;M%An<8eqM zbb}fACQ~6%!1_;H!`% zv^RN43QF!>N?FFH{i;iZR#A&71c8vKwpfvH7z6-CfYIhxW%fGdi2R7K9kgj!iuBrb z5@HC6%DfTEAMD)gi!XxHVfu6IvIgU_L*p8W%~}2YQu=8im{rReFF%K$S?_=x3$?ppAO9s1j>!sQ_qLH!co^-T*ij z*R6Z%PU0p)xdvEHnT5=?oFTJ^U=Kiz054cbvK*G;>S-dUf-X&H}WlI6qS+~ZD z1jsy$M4clJuGe2??uLu?5tt`@$yhG3a25Llm@zh*tOQI}QY*Z2cRXJCI6*PN>ZX8W zr}V~7m9ky|01%}h(^@+|7m7IHo$1kDgbUX=NtBxbl6h> z3Rm^$ES^F$_5gn1Zw`t8 z6uNd91s64lyp@R=GJCx%=pQsy>M1c^4;l4mgCu4_knNWPFb2zegwMmq{PgRP74QUs zeRkBR`ejs)l-|~)<+$#Wh(MBsmLPbbWwrK3%H+G|b@N}eM@5xW@lgus4*W)m3PB>m z>{d1LGw2^-1e(SjLZ`7vz_onp-2!$x-WI{_mggI1^;QxLvzmPkjwA@s<={W4j_wKd z@yGg=VgUF_i+sYo-L;MJ%2;K@j#pc`hgP2|to3WKXKYjLV4k`clv;n%o4&KQ(>rr@ zY{#FI&n`;82;g@n<;_G$li!0JAcCOcO|;Giz?zCp5HqKC?7fk>P2lBYwG%2pgft|^ z(~7Pigl1rXM6Ae-g4xlqvf)@tRS{$<(zai2t?2l#2Xzj;D4GQk07^z&drq%l)=SkH zXM!r7gTWX_kWV91nlVUJE2Nf-z;Qt$tbO9HhrM=p=?eI`_rY*D2%txqy>lE4hN0bG zBlN_vX(z~zVzID+3p)niqNr~#s9BMfmPTfbE?Sedh-BUI-wjpHphMoXgrGff*B+W* z?DW>8H4bIuzDu4u5uvUNM}We_j0^?>83VXmybl5g-}l3bS%m#ENQ+@^jE8Lpnb8E_zbD(rkb~bhA)yyuV#r@Zd@7n+4%T0?0Ki<3apW1Cu zP|(xsn73WUcT1qK26W^fq+y6NVgUz=L#%U#nrkXsqRGGhmb!-05*6j(LAfROgy<5X z4*ieVn*?9fX@W687|DcaytfV7MoqB!Jk_j%5g=Dlg# zuv5~a+8!R)7mv9hYFjMRZ8!}9iF4Yxu;X03$I2A$A^VMwhwCjIaI(%lx6cFO(SlY4 z(NS=uZ=*Lz$Wf1s6hu zL~Q!IZoA0bo#X8`Mvcy$QqsLU!vVN->_7-7*nLKj>_$)@GnOOw?sJv zSsK3lVAUwR|GoQ}gWH+$N3bJgM*?3xjRD?SqbfB}y6y)<1(-2rZD5kiCtbuiAf&`Ouz+}NX(De zPn}~tSNZ7pldsy`%Dk(3I^7m=2t`7$moIuZH0s}`%!38ROo{_0-OAt&#WQNnQ zA1w6YK7>e(j9<6n&cW*i;~x4(qDg9(1lKM`@ByCJ07gk!*xEV}H3JhAIoFhlBQwU#P27a<(Fh$HWw&WQBqwM(@6B_^pxn?zhK#SIZv&ZOn8^)XaD* zY~Fi@41gF-5VwFOG=be`HzY7~I=bzabw#m-9T8GHE+6+pY|2aE)VJbvQ?<4SfUzvf$8S>?4q6Nc{rO~JHxV8rzz_=`{hRHrD z5TzJdl<}LWGROGUuGVE3lTKry&_PK=Ov}a(oGL1K{&yQuWVtrw8hB;@ix)A1t4Y>D z;tl}ZNI(!m3A8$*dK(Mw#YM!Bv`AWn3bQ4$(q5gzn$G3=l!baAYiIThF~Q`ZPLz94 zlEF>%nwk4ZMb7>t>!bKK3Q&LO&cK#xtdQZD&>jMOut{rdrmm-py*G@#ONU3X9kK7@p{aen~o?m6H)9!St0yztQlq?Kw&vMB4d`U1fOv?H!fhjKMc zANEI=g~PzA*=6ENwGMa~C}W`{QuAsn#lB%Ra2=VdY@P9J3tTolzD$J^vD|x)zZB-i zRXLjg6v_prP@u<^n%ilIhVtUc`sZ*k^Z{QY{e1DOka72N2C4y7t)dQ52f%(HqY#w> zH6Okt4@6+PMT&95yRl=%+b-s@{d_cmcE-l|zwqOXF3|CS6GO!ce3uZ$_w5_usm9oU zzxOP7%UooK0-S{D+k|EupFY@qDI94rHX()(l@Ot(@~J-iVWH}j7_dzUl9)0M2Nvjm z%o@Mj2tDtCDZ(3UyanI5nx*gIlGkk*I! zB5(ENx$du*O-uHP6J*pI$T=X^00UB#bh4d4;7ZEklx3ML9hkfocGQ3c24>y`?J~GA zD$aCkk%qyBT=GTUAL4Ds{_zj?Ntg$$*7rR|lBuL1w8?zO#~ zSPGI^its#v-8Q>pDL;GvB5?|xCfR3)S%mzb4#C};3&!{DUIFXaVfPfJ_bI_IUw`s{ z7eK=OT94-qL&Q!8U7OfqMV+XD3x<1>H~E7=dxv^j#bdeqgM3TAXf}xq6hI9JK_Chm zanY!P7PI|lf*Ue;rAqzmCZ%=o!Z&YU3tJOnoOJ3;rr~%z6Hh}ffvm6&*b@RN1O4TK zyZc&HRFw2r4=z1_vA9wU z;acqi-3Yn?nJtcrarFPmG`KCrCi_naYaegW;ua8dSDa z#e(qc0L>t2%QuKXjNKP}Xv!o?(~GA1deL2<;@ii|idcg#I`vgf6Yk#X3tH3ORA!Go?kXt~dHVx1>Bu-QOi;xtV_$&`xzN+> z@uX62@Ugz&hC2jI2$@3HB+08i9^#AAl7U5N|k22AX2{8sGF7WLYQ4ZFQ@C`09+aSm{D(mEP8U1VBu7Ln%2{MVgqq4-Gbi~= zxF&Q6^?Qq37UJM-O6LR@uzE_DO2u1ymYf*r2Vxa{Wk?kGGy2gvQ1$Ntn!|F z-FfJE&YTRmAtXC29J_Oz^^N2&eIntRZVA#Iky}6UXi{dno^j@0?K*vWI3{NF+LP@{ zCBM}&3^>FHb%MQEwX4mI3KTrg4K+L=DM7@du-j3cO-hsjwE+tuUR<|x@6-lJ_}fyH z51K(5$OXj1AGQlgCKxEqoAMEO*Deh?KI~%E><5y4i=aOYz!3^0pl#vw;-Llaa_U@Eb?4*3W>`QXF7~{~Zx( z%>6VSBY7ts?(9 zFHPnFSbg{W*~OuUf{DTWU3;E0cJf`nL3xl{Sz+z$e6b^E2fq)~#Q zj*Eq)CPV+UO|?^@b133&Q~xX}1Yw~{8boP9%>kQ5l+j>m(!1#wH)mHR97{TN^Xdl2DF*8Qz&KC(SS|XGWC|xm^r~e$7{^{z9#L`1-e<-{Gya+oQ zkwWD<#~J?(P_0w1=LPh@zzDqYAsVVnuRyU4Wx5@70r}f)x!nNX9042=3^zfVMTC>b zxTwoCQMcC7pgKXC={akUv2?= zAfHr?Iq81RoqQI>X;Z}=-YX`SRR9%v@{1mG%v4M$sObtS>fA-rdw>9s#Oj3rOT0?0294qlAGbw>x4A(XfHz8vV(3lOotgS=xi~}N*mi|k zSWAGWMLE^A_d~%5KmydF?u?-jAO=FU0^5L$mI`m0DwouURiL}=w#GWZj?0a^eLxEF z;rwb!eR74{8OWH63=1*+RX1xZsVRLDo5>x(1VPxLcp_vTh0upxnoPMI!_%oXEp8p* z{4EGfAUXw^J4l|vvhJ!PKAXNVJ)q3`MX@L>vJwh2YS6_kjf^r23Iqw^F~m^Fc;i1G z8v5BqyidFj%F2BR@?DEaSETN=;Zl5QP%QBS0pFqaUbp;>jT%vca7m+!<^r+KbSULI zB_7nCl)kL2n0gwTGB4Hx2})J#HUavAS14$x5#25egvzp;;u+J39K9?~(i7u@NA4Am zQG!fP0|4qBVCXQs zw^dq*C|!D#ogyu$CHSz1oH|ZKfi8eczkk^Vv+54?_1UY5o*Rbz-1IZ28*u_U zUmWx|Xqw_c@)H@IsLHUK>&(-KlTSl#C?8hBRg6KMza{6_`Zp8r?VmYiktFSL?c;j@>==|J1njnSc;Jt zidwb>Dn4qYCQ#gv8j0FJL^veVU5t271ZbBaX+VgD)F&_%&{XBRIhyQ7pck34nBCAb zfiacllVjl^%844*!TbO0bY49#4qVnBtFMi>TKu7PH&Zjb!WV$=-!KsnOJ@4N&%Q!z z3<17CmD>TWTsq;kUsg?p0TK(%)d!DQh9g0=OgJuyjf6X@36Vk^$reAPfCrl2{*66EEtC zk@N&I4L3V$LjvZKbTxCW{q58nq=1CS-Rotj;Y6psg8bKA4Yl_xMJPoLW(aG5)x!Vy ztliI?>LfjKxB3JD|EgOJOOaNg%D8Z5v@0cuSVx3SGDDQEVmuNWL5tf==fLWtkq4aC zKK4G^qlKyp{+NkkA~eM@a&?_8bn&5Q&$lFH0PTp#Jbvf30$~J11ip9C+_RF4yIkOy zzKhl6yL2`2J{C}vrp-*?K;u;Ti2M7=&!u>e&?XZ#r7XB)vf0#MQ)32}v+e;pFtOq0P z<_Qy1X+#wAeZzc;xX4p+T2Sb28!l0uT%p?UgZgalsJELhTrOswM!1uAk%=tuqGTo} zVJz#WpCQPj@(gV(rZr#JVto1AK=FiMSY+#9wR=Xuaf&88{_OnYB)9~RMAy-s>TM1> zvW_c5_luqV4gGS6<|Lha94Q03x5(Q$?J~Jao@HzNj?gM|u^^fFrX+DO9^h0SC?0TS zT31zi+U2`Jp;BVam~q*fVO9%N9IRfh{v`dfo-FB_9*C! zjkT-K1~L`h9s)^EwKv^ejO*O%R_~MT)6fkgFzC7Nf+c(CIME770OWkeTI!ATWSV4? zj|0ce2a!PmCYgZj0%YwH(e4T+3i! zJ+2lc-8IH?|HTO2P@t-y8_J)^mHqw7*_e-kNtD_+&XnXl8)0q;D*?KaojX*k~RsjXbMR) ztn$$98+8AfpuaEiZB;$U=U&wNFpG@I;;7Z$(BsvV`YY+yU518do)e?ovsz2>_Mzl; zm_q($)n*gH1!dg0qwdmA3n5;qT4oDimF@RTLf3#{1>-X>mxK* zRkOuIr4y2fW|@xhfXCVmZ3+J#p7av5|Li2jQ(eVVZ=V#8!|dvAIp7rheDsaWP6)j? zxh#w0_`6fjdN00g^Pd(>e_xI|FBT-odtUXBsVpFC`?3c%;qxI}DEl)pjvuK!&_-2q zOc=pRuY)-;souG#aeVc94hdH8J+Js3W=&+G8FpG{F{f<8okM>NXqU08gqY zZGiB%i;U~y%2%609EAH2ezxV}WA{Cp-ISS?p0;2(dI?cS6~RCB-=%QkEB}> zV(#Ogv_3!}s)X^ZG2<~9SmF;umH^{opKPcss)VW6m`Kh-aO7g2uI)!*$|&#<+pkBN zwthTb+f^WYDrq6sDtv?PWPO8V+@kJPuYDG{B|*5JpIP6 ztClf*!;}v~mnTy6VbEG?Q>_DZ1-(t+kNW9BznF98a|(^$iE@`>HHh?w34CAuY(ZdU z8_a2M()8+R#QEwdgu4=9Hv&a1oZHX3Ca~C ze@_#!Ie$9oQdWpNs&V&i+`(kUz4ij<%$wzu#V`n|aVG_L%T++$)AB9&lV#k+x7GV7aQtRl8t17 zC}hZV^oqXhSUL?LovZf9)IEo(f3NB3ym)I>%jejyul?+Fb>6Lt;1U?Rf;`wS*;fwp z&+Rfjv@fIZMx8^SX(5QC%>p3(8mUWQIMQ0cAJkblXcvGZiH?Z|6^O8wWv9ymwJfPE zq~@Hn!ZWOEEB(i7jj2!}1Zo#>eCk>0AV{by09Qh5SY*LSHuR>J2l4~MH%GFqdGogfw&=? zp{;dj}|)n z*Nl{^Hafz1pHvegcjMAYN#Gt~8vc>l7qcr^@ccvsr9UBJR6?{Vf+tr<0RPCl@YB17 z&vT&yXqLGw$mjMYtOPr>QStojKqMS@2LKyB?9D#)J&|ziGJzK)TTGKQODu_axZxD-_P;Y&At{`m9y%BSG{PS{f#|4<@rJjd`ZX> zm`5sU5GWFQvR~nK^SevMVD~kAB7*^ZX+Y1sIF1jlt&tXR9jVHVbi;ijm0vy2JFa&l zL|}H?aTXvrKTdKaRL2ie^RZhz*7kYFhC(B2gaenI{ksoTcHO)juSA8Vh2hk%d$dVV zWddJJ)HyQpM1UnilqDiSc23xis>%m9dBOpYq~yG*M_p6TSNVF6&&Ed=NekV?QNOw} z^7mB$Ifi^%?=0*f*2eqc%nB$Q{6(pw-g>WF;eIV{7KRfD9Qy#4O51p2r*FNeUHcym z8A7{#Bfh&328Sd`F;wQLuRfV!cpjjIsRV!ISKxGPKMUYbY>jnu2k@z>0NNFhIgC zu*16gRCKZ{E^;unT&RRHn29 z4nbuQMBr5PO_qof35Mnb-9oG$V-9sq{dO{HIDN0Ia~VXMwDLFkB3w)APF+NTQISi8 zy<*kHz*X1Psw3GasJCx+y0640;Hjk+E(F0joJ~uh%(FCH3J~a1=ffryci8?F(9jnX zx!zeoc(`ZLs7nAh3Pj6E#rQ#zj;tdiY>I1-5=gginqw8H{YRB^ngdkwuj%mA&|Xo8 z)Cn4pSQZzR3D#7lvja@}O6|BkfrAn17m2xVd(BAoU%qzLMbA+KmZ&lVs{f%!9rJvA zIQHDhb8d%#SRkOE*2Y9)B~K~p3)Q-tJgN3aB2c2Q^jTG5WKw>>Vs3=8iAMlNX0%9X z!7f1bGJed0`UPeEqIWhUKm)&J<%cbs^?~Cdnc)m^`edh zot2@AdreE0dDK_q+D%I4$-d$7BSe@~R?G(Ui+O5ca?`usco?(5q1UPNJu`^Z{FFMV zy9UM4z)C2?RqV=*&Axg>z@F093gZNw{)>#CjF$|Grb~&~87#{vup-H_-6{3LJ{YVa z7l1CvSQO|_1}jPlxOQ#8czaa}l}xJ7KG8ui?AQ5FBaLgcrDi_UTvGi=ems5kb(YD# zHn#e*{fBhkEz(MSo5~8egGLY;26ApDgp_pcOy?%$0sgByVP+hh6EOZG28oI2sJoA} zzZ-NACYe%{rbrq_#kfMMVafff88ypif2(s@Ufv$nHNJv{oVHa{4ukgV@Td6jh zUD2VcA*@p_rw+?UObSy6gRKG>z*{bqNd+dx)Bk5ztgYLbj)#p^clMb_{L1+0{!ZILQx8<(2!z9B@JVMXFzWLHHg zcM;V?ig_XAZhJEvY$vH80tPd{n(g;4k!5e=|FWtY)U8kMUzF}yY}3vl+r5*2~d{^nv{W}u-!21 zS{lJkGF(^(8cZx7(5L2X;CpdhbiQ9mHQJlt;{$R!8S7dG5jw0sLLFE!wvV-JE2WtX z{=`?_p=?aomH=eGC{8!*9RKC@8YRpBjQsvh`B+-=Ua_<^+R+%1Za>UB=#|C2W~ah? zAEQvF?d92MM2}uP&6>zQcfm{XwSPyFsO#SF_|Ka}!VbsY5qa9~?Ll&S&L*XEmLbF` zzxV4hGrN2kg$@qqAM*CGP`Gq$SkfjA-H4MM9V)|gKL_^eqM;Z6Gi$^&>fb?l;yT_g z)})^{g!7xsPa_+%T|@B=HY!x$5=kW;1j zntQR73p{{bns$MqP&D@ZCl4wQhGwAgo=9O=76Xx=z>eRliv{IzU8o%es}`^;ij_6Z z%YfzWJpzE?S4q0)QICvQdwi1d^}qc#x;5FtoYrOOOWoSl0pr#(S+)m+Hlwpt>8vxP zx9`*^%Wmi7yt%tvp^H|Y)CgHH={yAUBg+At+eZR7n?$c3}!rGSU7k*gm$ocuG_#*2me&Lux<3IG*?Z8-#$(^#AQA) zx`1F9tqU9)X63B?oK9sLb~`$rY1hoHaToOja195#@8~*9540|(}`XA z23anA=@qW?4Z+xj(Mz=6$SZ+}wP5!i={_P%q{tT7alOiEkt=j-2bHxjgriNL+Cp-W zC9o@K3Qq4xpy(wft6=ryB_g*6LXTI(vg zF-yjOEaiGd>7pzRW_?H8iYZbhWUGwXSixcOwZLoVzR8gPuq)AFSyL#QYE}qH}<(-gMg@ICnp@8e{sXdaUJ%Vc=DV;6Y z?Y{Ng2vH!<5hemlRh$jS`bsMF;pyFs?!eWB=Y_P$-ZTiy>@U*tD~;cu?DMGa^v9Pl z%a&uUS84>njnT!u$kb{?WB7~AgKOp%Sd+*Cm+!hxFkE8MYbp!v!}3A%LAygGJ!Gc#Alt_Eq#Df>0k-bqK!XQj#99o!g>n_9Iu1Ah<* z!syi{*Vxpa2s_`)kzYmX>r#O+czH?KiwnXOMrog%9!b(2COIv@vd(r>hwF&FCxvn0@JzeySs3R* zR0V37xJcaUt0C@Gt(NS(lSBjfNhi(kPQX`%2b2jnn)$Ogj49bZ@1n@8d7bhIJlFyma-E0DZDPY}?C5p%St0HC3BkH^!};-<;xK3E2ohob z)(hXvz6w~KU4<7O-Y=?=9 zbQsMRa;yz2APSPD%3aD8_|!*qk}B;LyzvS@l)At%3u1izpnT+K*Y+#kL)GU1vk5z* zL8l9z9PqE7D;^f$_WYKl(v9F>>X_S)*HXiifQzk9Iv=m!bGqZ;-@OVCdMR9B5#Mhz=xWzeth%gAG()fhiUy2 z%|zGqTa{hA-Rc9S8iv2vbMnjrPKDXUZXfKPX~JS22YP#z9o=pf!-9m{@{^Dacw8?^ z5A}(BHs=R|2Fx*ww18tvH(U@6Twrn_diH>&tqbBg0<3L$fIUB?5!F_1#AL^ZTn6ak z>-Qew zL>o+dA6gUAHTKGkvW?h;%CmRGs~dV+`rQcS&vYAwPeatJh7lhE7kJkUt2|7Css*w${*afxGyLnu1TjNTcJf1GJChLDiYZ5r7!MnUp(+E= z3UTMmd|ITm!x7<)U^KNmqeQCm4)qlhSLDXD^v4oST;h+$S1&E5f9q}^`{vE>V*iej z1f6%{pA(LUwZ#XZC1H9R6#!8op=MeAV1lkBPU)fYV!N+bI@I8cDiblV*y%b* z(nLn?WM6Sc*YwnL|BT^uzVTLYi$b?b|8zUQldJ=p-wILxGjsUxWaJOR!USEb{#F7` zZg#SR)Ol1Y8>|}!ABXYNRa>Sav99#och3{m2M^ibNq_gpOO3-+Q&;wz_gRTu+$WiP zLD9m)_QsQQjiJCl0j7(41)M8HF*S&ZM8!gKc99J>3(E(HQZzhomkBM1a>gsR|M9KL z6nNrjgVIwyHZ|VFI`wx0gor5!5e~VNI+b`No$R0Z=bc9fwfC+C0-9VSs}A8aVe9}s zl1&X)I#b8VCe#PYl+$DK`NDfuQuq|6MqF#2$(uxGB;yDY%*)IoaR0Z*x+ebw;pe>n zLzU?>oc*{<>=ZM#y|My7ZF<5C_8$F8=c~}|?RCq&hONImq}xk3q{@IfmRaq*^Vq<+ zT{mao6Hkb%&N?ep;_Nr*H2%Sp1I1(0uiyJ;bRF(ci~3;QrsJgC?}^f657QQjPfu9X(t^OlpTK@>*!~4gc z!8z0K-+-+59#_Al>;-ez9JHSbtc2GV7N1V zo}sYXJzsV4kKE}=10=wKtg+lT55yV2e8p_n2#mLKGwvMxb6}2fJNNS*Ci9(wfgy);`2LCf3`2fWH0B6n9f5It;)Wo~nT4b#sA;F{_ip62JZ7M0wUy6a z2lS5U`rj5$zy4kgp&(cRvI#GOAYe$~i{emsz95>Py$sKRPHS$butVB6@=IoE$K%ZT zhVwv`FOHP*MPYgDAzlCKsqVn*##__AAQPXJ6;MlnULaDz%z~*i)GDHFAREGf2r?07 zS=e;^2)jTvGrtHMN!1?JrW3C`3059~q!z`oKNdUytOpU&n&}@qfzfctT3CgC>9ij_ zoaL1zwHx_qxCg@CJts*wJbsX6rnrh7fAAQc|H8QzR2)&29`0SHaTKkT>jra)h>Q}! zzx_j?K_CFiWD}sUPOsc+X}}!JJWIq`ogE4P07u0(Gs98OYS2Y}AsUc{!V&^HP|$Jm z+)81hZ$43l2f0EmHGU1rfXF>@b&z;vo^E#Hs>OF6N0kkHm^uXDfM%4xFf)iDlrINt zk#j78QVvfqfqh{3vNI=rIaOf|M~t_uBSCPjbYzPlI&`lZVD9zFV$*2kD@EOjt~ZLGxYxPxWDh{He>bp54Wy{kfC;_~)+uCG3xqtFK!Wq*C!SpH~yI>RDz?B%8Bfpde;w7s$Ugb>|) z{*?K)g4q9j<+$BD4}O;in&OGGDvY``uDx{$z_%gFI;Yrn_1rCTW&F@&2p^X$oM$q z`bUmSKh3*Jy|wy8SF-ui7MJyN2%m&}|M#e`$V-=gnp}4Ii~Z-k_x#JOb@M~9)6#~# z1JNO0nddG#F?;^4&&(eu2pcw@aFZtJQyzbu>~gt4@$D=JrC-8lv$Q372WEw=Dz>~! z`n2>@xW$PB3qSXEe(rhM+x?TjWqaK(X>j{KFY9x3s#%VmiC;~HoBP1Z@W;|T`2!M9 z39pp$4WA{uEX`fS_;@}2U*B5jY=i#B-nQzDB_j{^d|(OM`L`wLm!;)0IM7lb_|Nng zoXgd5E%}J$U8|2xtL7ajw|tm)@xmP9fq?1|?GqAQ_9;kdi&fr%p#Oad?*BLtT|e{t zuhnjG;SLP=Z`2bc;6=)do#lu&)OSAhzmMjBHCp3@whb#ql2No5#^W_sAm5E8uh3{%rU| zMEDNg2>m!@dldQ_h&d1Tb@j&j*(VwzK8Ev{G-29}>FB2KyMB&@W4etOlA!5myl*W} zaGcH27B6jymR?$hEYb3oGhBz1m{0=WKj8OS-nYs;o+E53fz|lfUHB2~;8u@mEjZ2h zPBTlJjNHofeQw?Xn~&M2zeR#BJ{HS99150d+WLnr9R1b_j$_&boNR-+!>+H)<>6nM zHnMko!LsLJw>ps8L2)xSvgL!^K73{79RJTBdBslQ8_?YNN zE&bGaExfnjuLVl}y77!$&;kb{E?Q~7=#7CGU>^FN<*0LBnB{jR-&>xT`D)&`Az#5x zK!fPN1IPc8!Q(HH602%HrMrM#!r?-`g^$+dedV^r((;oQ_q<@A|C2ZpwVaENC6Aw# z_c~{m)O0HJcTCLR=3K`X(r3*Yznu4m*tq)h|Hp7(#ajXJHO9wQWwSm>Ulk&9UpxD| z5*~=L`+@T;6gIvx-+b#R7z{EmBu)R8kj()rzP0@HR_wwpCTBDWv1z*@aaDf z{QjwB-hnhYGUz@8ZNH#&*)q$F-x+W$*d-<8RxwJy>7q?ed$EjXLDu^9A26?>rwfo|NO8J zR<^iuY1J{yrE8ysY~sl;{Zu>u*0=k^Vh?^<1Rtr~1EM0@-0ku#L;J7T9{zlm>9hY5 zdM`mh$ekN)^ZIO>+r-67wB=DZrgY+CV?izbO^2LzrFXoXpEZ2&&TWC+of<6ZUv2f5 zuAc^{IF?fOY2J;Y`|ZVnxqGI4I?lWwSkY9yn%ffFz#gcJ2%dawF)@1R&E#bEYR5U( zB{2&fj#`?|dh5=z%Y~*5t+RGHIVBg8zJBvk)5$e(-u>b5W#+ZFH`;P)AAFhkUUO&3 z`;i|Lr;izXY8z*N(ijli^Sj&U#IjiqCFOX;j@;i~lJ316Yd_p4`SRW??(?5C+m39% z5&)VE19mN)xw%l8SDg3@3O7LuK5K|P70CVNC(XMvV_kdi9RKp` zKb8b#Uw?bU>Ej1)SOX(j!BZV~y0~NQ*6H=4|lBFP}mYL z+4o>U+X(mR)BmrfYYj^(-NI^9TFqpane1qJnXybQ?bIlh7FL>>m8a272r5}a1{UTm zub|Cj=5!28(J@S#3@_leo8T?MvDC!UGR4Llo)k?<2@yfA=X0907)Xlt zD&&*}oj%SXLNF&ircIPO-^W)go|_}JlU=FFp4MwX?XE0eB*u-k<+sgy;vXNeaQPlo z@OsbK+?WC}oH$ic#=Y>@<&h@+y0!rH(k+|5XQJV#UJMS8j&F;Qs#;tr3p#0<;ikvT zhAyftpzA8OQC&_>-2ayN;ElH4E0ute$iJ{?k8aB9p)@t759~t2c7JYe&vjtC(d;W@ zj&p=HJkC!Iaen(1g#o$`6NdTc;P*3689KU5Yu21jzp_|LFEfjCG~*au%nb5$?FC_}va$AD=)3J-8{4^o9AUMPmHw7BGe3Y-$Qim% zrUq>nLqqS%o@fJ!MuVsY+nttNI-c%ghneyy3*g6n7CRuKM33d|znU82)N_+Wc$@c9 z?+zA`EUH1}NZQ>gK&!0;6p$ zE_TL(y-C`!m9D(nivz+?kA%kgDd(~P#@i=^>LL>s2L(sL{Xv8Shf7G{@GNdn2(c9H zC__6}hSX^%3>}G-Rn6(flC4fl?%C%AaefJOIAOi}ks-^0`-!5SM1-Y{zWVvT8{Y9g zgn}aBD6#?)e*1e|iR^qhiSnc6>+P>}BO0Vi?3%V9Zg79yMfGd@5)Bnh#1eF}vkvpq zDI^US?(&p>`lDv}&ao|+e0=Z`$LW2&7kXJn97%nhERhlpVmXSEMy|A3r+kW3rp8mE z*VuvCg>*lL36@-S_C#l8(O&Y@8+zvaw5UPe9%{O3dfuVaaUtjpUD*z~?R|WT z)JFeVDpIvJ-tb>}C(dibj?}|Jo4McJpqgC|UDr>=+K_Olf#U zEFz~>sJ=sl^6*e_e#V22^FLzHXf!5dWA@=(TD!=gLuxe!)pG=2_PT}Ha@)&fIS_`m zcB!B4$HcOQH*JvmHVeJ##rXDy%lnf)!iO$0AF|Ftkz}_H6dM!S%YxWWFbbc9f^IpE zx5mola({pS*x1os5nFJY{yx11kZC%K92uRIl@K2aiggM=@t7y2Zt|Gd@^thX5{1}S z^+P5#$c>GGMCxvLXfY-cT%0T1I`huhh9az~7VFpN7v#5%zAbDYlk$?gMybfl2C~vs z^rZ5(0Hu(9m%LASGGB#ssoK=R+qT+Jy0o98%Bg?1}Xj7WoD4|7{%t z2|Fhkrc0>%W}6bMYw@FSJHNnp=;N!gR|V4kxdC5_{7r&QzbXssXA*byAEb|+F`Hnqc9n@1 zfQ8Sz=aD7cO{O@CY_1}3J(`&hE1b#vZnY4^2P8iJh!s>Fva~#1aL0ZWaE}5|k4_>~ zSY>a?Z7<-@F-6+>Tbb6$=SMGEdF%y{qY)?T)&y_9x?1?Ya#0>URkQPOz)XLgL|lL? zE-Ji-55$T(r?4Un4F$1L5C*3Gwxa5QamKy$(7x=p(Ym??CbNaf6kmOpyDNb_S>oZ9kPlV_#L=&di=LEE9&*Y%H@@n$8K&yQbw9q29O50l8T|s;pC@`N;E#$y@N(! z0-QKR7^;9-7mh-N3g~phh{qW&TbK$NC_n~-YlugRqA5w}^ zn9Vq{C`yh_h;o(*i`X5os!=8!CZ_=7nb+!dZB*-rLRKX}N2r83JeVbXf!akRKzo+F zboy?Moo(RaB4TgcSn2QI;qQL~aw{yV(>SLoKc405%1n7F9j#bD!2L}=;$=|2y0aqi zVdK2Is;s!58!xvpsAk9JCTm%6HBSMvq^B?lJ$lP;6>(1!Oi6J_@r?f%(kz^y{_D{*zn& z)mY_Mcw$fXxWzc)!uv7B{l_VH??Bsn!a%z>$KJG+e1eY|R zo~tUmxp5B$hTJ$9z`95>n}vcEy+@0P&~HIB6M^Nw@8zej2YbXjZqFh;ExTM@{$5XY zUL34osKzcL)qhffKd0$GSnzijGk)E1x@qrk0>!H*JKn@kYG?>iYKn5EtcEmHpCBzR z$)s|LK^zp&W>A<49>jUVN(~@DSugWem>Kt7vd+kHEH&A=DAyK<3+S*AwEbK4tB4s@ zJC!o4vq?2{dPp~RQUA5$G}%X-T&qhKt_NM`$hH+Gg0g(Jwzs#c)pHx^wBo( zL;%cz-ez`Aw%6?K?{2v55<6O#(UQSrej}HUx*XuCTDlC9fHeJQrFFfI8xSA+1E3_1 zfby+9;rFfpm%M<~Po#O)CCTZT<~8jPI?7p6jj%>oGc3gHfgmCj49+@Mk5iMQaBJk+ zJjczoMX+ki3|)(|2f-_DXo#8zXPa<2J0kiXrM#VD+1kw^Xj15{OB@T@WkA}$* cPY}ELo-^pZ7VRCz^KLasz%7e%-ot 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 x!w`VX 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 diff --git a/markdown/EVAL_ROBOTWIN.md b/markdown/EVAL_ROBOTWIN.md new file mode 100644 index 0000000..8aa7653 --- /dev/null +++ b/markdown/EVAL_ROBOTWIN.md @@ -0,0 +1,92 @@ +# LingBot-VA 在 RoboTwin-2.0 上 Eval 说明 + +## 前置条件 + +- **RoboTwin-2.0** 已下载,且 **assets** 已就绪(你当前路径:`/mnt/users/wangyuxuan-20250915/EAI/RoboTwin-2.0`) +- **LingBot-VA** 模型目录:`lingbot-va-base`(或通过 `LINGBOT_VA_MODEL_PATH` 指定) +- 推理时 `lingbot-va-base/transformer/config.json` 中 `attn_mode` 为 `"torch"` 或 `"flashattn"`(你当前已是 `flashattn`,无需改) + +## 环境依赖(重要) + +Eval 时 **client 会启动 RoboTwin 仿真**,当前 Python 环境必须能 `import sapien`,否则会报错: + +```text +ModuleNotFoundError: No module named 'sapien' +``` + +请在本机 **用于跑 eval 的 conda 环境**(如 `lingbot-va`)中安装 RoboTwin 仿真依赖,例如: + +```bash +# 0. setuptools 需 <82,否则 sapien 的 pkg_resources 会报错 +pip install 'setuptools<82' + +# 1. 系统(若未装) +sudo apt install libvulkan1 mesa-vulkan-drivers vulkan-tools + +# 2. 在 lingbot-va 环境中安装(不覆盖已有 torch) +pip install sapien==3.0.0 # 或 3.0.0b1(若你从源码/其它源安装) +pip install open3d scipy mplib gymnasium trimesh imageio pydantic zarr h5py +pip install -r /mnt/users/wangyuxuan-20250915/EAI/RoboTwin-2.0/script/requirements.txt +``` + +**Curobo(运动规划)**:RoboTwin 的 env 依赖 [NVlabs/curobo](https://github.com/NVlabs/curobo),需在 RoboTwin 仓库下按官方步骤安装: + +```bash +cd /mnt/users/wangyuxuan-20250915/EAI/RoboTwin-2.0/envs +git clone https://github.com/NVlabs/curobo.git +cd curobo && pip install -e . --no-build-isolation && cd ../.. +``` + +若与当前 PyTorch/CUDA 版本不兼容,可参考 [RoboTwin 安装文档](https://robotwin-platform.github.io/doc/usage/robotwin-install.html) 使用与 LingBot-VA 兼容的 torch 版本后再装 curobo。 +并按需执行 RoboTwin 的 `script/_install.sh`(pytorch3d、mplib/sapien 补丁等)。 + +## 一键 Eval(同机 Server + Client) + +在 **lingbot-va 仓库根目录** 执行: + +```bash +export ROBOTWIN_ROOT=/mnt/users/wangyuxuan-20250915/EAI/RoboTwin-2.0 +export LINGBOT_VA_MODEL_PATH=/mnt/users/wangyuxuan-20250915/EAI/lingbot-va/lingbot-va-base # 可选,默认即仓库下 lingbot-va-base + +# 默认:任务 adjust_bottle,test_num=100,结果到 ./results +bash script/run_eval_robotwin.sh + +# 指定结果目录、任务、测试次数(快速试跑建议 test_num=2) +bash script/run_eval_robotwin.sh ./results adjust_bottle 2 +``` + +脚本会先启动 LingBot-VA server(WebSocket),再启动 RoboTwin eval client,结果在 `save_root`(默认 `./results`)下。 + +## 仅跑 Client(Server 已另起) + +若已在其他终端启动 LingBot-VA server(例如 `bash evaluation/robotwin/launch_server.sh`),可只跑 client: + +```bash +export ROBOTWIN_ROOT=/mnt/users/wangyuxuan-20250915/EAI/RoboTwin-2.0 +bash script/run_eval_robotwin_client_only.sh ./results adjust_bottle 2 +``` + +默认连接 `PORT=29056`,可通过 `export PORT=29056` 修改。 + +## 可选:快速试跑脚本 + +```bash +bash script/run_eval_robotwin_quick.sh +``` + +等价于:`run_eval_robotwin.sh ./results adjust_bottle 2`,用于快速验证流程。 + +## 结果与指标 + +- 视频与可视化:`save_root/stseed-/visualization//` +- 成功率等:`save_root/stseed-/metrics//res.json`(`succ_num`、`total_num`、`succ_rate`) + +## 常见问题 + +| 现象 | 处理 | +|------|------| +| `ModuleNotFoundError: No module named 'sapien'` | 在当前环境中安装 sapien 及 RoboTwin 依赖(见上方「环境依赖」) | +| `attn_mode` 相关报错 | 确认 `transformer/config.json` 中为 `"torch"` 或 `"flashattn"` | +| 未找到 `ROBOTWIN_ROOT/assets` | 先下载 RoboTwin assets,或设置 `ROBOTWIN_ROOT` 到正确路径 | +| 未找到 `curobo` / `CuroboPlanner` | 在 RoboTwin 下安装 curobo:`cd $ROBOTWIN_ROOT/envs && git clone https://github.com/NVlabs/curobo.git && cd curobo && pip install -e . --no-build-isolation` | +| Server 启动失败 | 检查 GPU、CUDA、以及模型路径 `LINGBOT_VA_MODEL_PATH` | diff --git a/markdown/INFERENCE.md b/markdown/INFERENCE.md new file mode 100644 index 0000000..32761e4 --- /dev/null +++ b/markdown/INFERENCE.md @@ -0,0 +1,117 @@ +# LingBot-VA 推理调试指南 + +按以下步骤可在单机上把 **Image-to-Video-Action (i2va)** 推理跑通。 + +## 1. 环境 + +- Python 3.10、PyTorch 2.9、CUDA 12.6(与 README 一致) +- 安装依赖后,**推理** 时 `transformer` 的 `attn_mode` 必须为 `"torch"` 或 `"flashattn"`,不能为 `"flex"` + +## 2. 下载模型 + +从 [HuggingFace](https://huggingface.co/robbyant/lingbot-va-base) 或 [ModelScope](https://modelscope.cn/models/Robbyant/lingbot-va-base) 下载 **lingbot-va-base**,得到本地目录,例如: + +```text +/path/to/lingbot-va-base/ +├── vae/ +├── tokenizer/ +├── text_encoder/ +└── transformer/ +``` + +## 3. 设置推理用 attn_mode + +编辑 **`<模型目录>/transformer/config.json`**,将 `"attn_mode"` 改为 `"torch"` 或 `"flashattn"`: + +```json +"attn_mode": "torch" +``` + +(训练时为 `"flex"`,推理必须改掉,否则会报错。) + +## 4. 准备首帧图像(i2va) + +使用 **robotwin_i2av** 配置时,需要 3 张首帧 PNG,放在 `example/robotwin/` 下,文件名为: + +- `observation.images.cam_high.png` +- `observation.images.cam_left_wrist.png` +- `observation.images.cam_right_wrist.png` + +**快速生成占位图**(仅用于跑通流程): + +```bash +cd /path/to/lingbot-va +python example/robotwin/create_dummy_images.py +``` + +会在这 3 个文件名下生成 256x320 的占位图。 + +## 5. 单 GPU 跑 i2va + +在仓库根目录下执行: + +```bash +export LINGBOT_VA_MODEL_PATH=/path/to/lingbot-va-base +bash script/run_i2va_single_gpu.sh +``` + +未设置 `LINGBOT_VA_MODEL_PATH` 或路径不对时,脚本会报错并提示。 + +- 结果会写到 `save_root` 下的 `real/_<时间>/`(默认 `./train_out` 可改)。 +- 其中会保存 `latents_*.pt`、`actions_*.pt`,以及用首帧 + 预测 latent 解码得到的 `demo.mp4`(在 generate 流程里)。 + +## 6. 可选:换配置 / 减少步数 + +- 使用 **demo** 配置(2 视角、不同 action 维度)时,可改用 `demo_i2av`,并设置 `example/demo/` 下对应名称的 PNG(见 `va_demo_cfg.obs_cam_keys`)。 +- 在对应 config 里可调: + - `num_inference_steps` / `action_num_inference_steps`:减小可加快推理(质量会下降); + - `num_chunks_to_infer`:i2va 生成的总 chunk 数; + - `frame_chunk_size`:每个 chunk 的帧数。 + +## 7. 常见错误 + +| 现象 | 处理 | +|------|------| +| `attn_mode` 相关报错 | 确认 `transformer/config.json` 里为 `"torch"` 或 `"flashattn"` | +| 找不到 `observation.images.*.png` | 在 `example/robotwin/` 下运行 `create_dummy_images.py` 或自行放置同名 PNG | +| CUDA OOM | 使用 README 中的 offload(VAE、text_encoder 放到 CPU),或减小 `frame_chunk_size` / 推理步数 | +| 找不到 `wan_va` 模块 | 在 **仓库根目录**(含 `wan_va` 的上一级)执行 `bash script/run_i2va_single_gpu.sh` | + +## 8. Server 模式(与仿真器联调) + +若要与 RoboTwin 等仿真器联调,使用 **server** 模式: + +- 启动推理服务:`bash evaluation/robotwin/launch_server.sh`(需先设好 `wan22_pretrained_model_name_or_path` 或 `LINGBOT_VA_MODEL_PATH`) +- 再在另一终端启动 client / 仿真器,见 README 的 RoboTwin 部署说明。 + +上述步骤可保证 i2va 推理从环境、模型、首帧到单 GPU 脚本整条链路打通;若某一步报错,把报错信息与对应步骤号贴出来即可继续排查。 + +--- + +## 9. 下载 Post-Training 数据集(用于微调) + +**该数据集仅在 HuggingFace 提供,ModelScope 无此数据集。** + +- 数据集:`robbyant/robotwin-clean-and-aug-lerobot`(LeRobot 格式,用于 post-training 微调) + +**一键下载到仓库下默认目录:** + +```bash +# 需先安装: pip install huggingface_hub +python script/download_dataset.py +``` + +**下载到指定目录:** + +```bash +python script/download_dataset.py /path/to/save +``` + +下载完成后,训练时设置环境变量即可: + +```bash +export LINGBOT_VA_DATASET_PATH=/path/to/robotwin-clean-and-aug-lerobot +NGPU=8 bash script/run_va_posttrain.sh +``` + +配置里已支持从 `LINGBOT_VA_DATASET_PATH` 读取数据集路径(见 `wan_va/configs/va_robotwin_train_cfg.py`)。 diff --git a/INSTALL.md b/markdown/INSTALL.md similarity index 100% rename from INSTALL.md rename to markdown/INSTALL.md diff --git a/markdown/ideas/ideas_0305.md b/markdown/ideas/ideas_0305.md new file mode 100644 index 0000000..0db7b3c --- /dev/null +++ b/markdown/ideas/ideas_0305.md @@ -0,0 +1,279 @@ +# WAM(World Action Model)推理加速:创新点清单(training-free 为主) + +下面的点尽量围绕 **WAM 的独特性**(动作会改变世界、需要闭环控制、world/action 联合生成且可复用缓存),避免照搬纯视频生成或常见 VLA(BC/RT/纯 policy)套路。每条都给出:机制、落点、加速来源、风险与验证方式。 + +--- + +## 背景:当前推理耗时主要来自哪里 + +以当前实现(`wan_va/wan_va_server.py`)为例,推理开销通常由以下部分主导: + +- **NFE(transformer 前向次数)**:video 分支 `num_inference_steps` + action 分支 `action_num_inference_steps`。 +- **Token 数 / 注意力有效边数**:world latent token 远多于 action token;action 阶段还会 attend 到 world 的 KV cache。 +- **Cache 维护成本**:KV cache 增长、slot 管理、以及(若设计不当)无约束的可见范围带来的注意力开销。 +- **观测编码与数据搬运**:多视角图像 resize/stack + VAE encode;offload 时还有 CPU↔GPU 传输。 + +因此,推理加速的“杠杆”主要是:**减少 NFE、减少有效 token/边、复用/压缩 cache、减少不必要的 world 去噪、减少数据搬运**。 + +--- + +## 设计原则(WAM 专属) + +- **动作优先**:若最终指标是控制成功率,世界生成只需提供“对动作有用”的信息(不是高保真视频)。 +- **干预因果**:动作是干预变量(intervention),world 必须对 action 敏感;world 的哪些 token 对 action 重要应可识别并被优先计算。 +- **闭环与低延迟**:每个控制周期必须准时输出动作;允许“粗动作 + 快速修正”的策略。 +- **缓存可迁移**:同一 episode 内、甚至相似场景间,许多计算(KV、prompt embedding、静态背景 token)可复用。 + +--- + +## 创新点 1:Action-first 世界“懒惰化”(Lazy World Denoising, LWD) + +- **WAM 特性**:world 只是 action 的上下文,不一定需要每个周期都高质量去噪。 +- **算法**: + - 用一个廉价的 **world-need 指标** 决定是否跑 world 去噪:例如观测变化幅度、接触/抓取状态变化、动作不确定性(action 方差/熵)、或“动作对 world token 的注意力集中度”。 + - 若指标低:本周期 **跳过/极少** world steps(例如 `video_exec_step=0~2`),直接用已有 world cache 生成 action。 + - 若指标高:再启用正常 world 去噪。 +- **落点**:`wan_va_server.py::_infer()` 中 video loop 的 step budget 动态化(per-chunk/per-cycle)。 +- **加速来源**:直接砍掉大量 world NFE。 +- **风险/验证**: + - 风险:遇到“需要精确世界状态”的时刻误判。 + - 验证:按任务阶段分桶(抓取前/抓取中/放置前)统计跳过 world 的成功率变化;并用“误判率”分析指标设计。 + +--- + +## 创新点 2:多速率联合生成(Multi-rate WAM Sampling) + +- **WAM 特性**:动作需要高频更新,世界状态(尤其背景/静态部分)可以低频更新。 +- **算法**: + - world 每 \(K\) 个控制周期才更新一次(或每个 chunk 只在首周期更新)。 + - action 每个周期都更新;action 的 attention 只看“最近一次更新的 world KV + 最新观测条件 token(很短)”。 + - 可加一个轻量的 **观测增量 token**:只编码/写入最新帧的少量 summary token,而不是全量世界 latent。 +- **落点**:server 模式下 `compute_kv_cache` 与 `infer` 的调用节奏;以及 cache 可见范围控制。 +- **加速来源**:world 侧 NFE 与 token 更新频次下降;action 侧每步 K/V 更短。 +- **风险/验证**: + - 风险:world 低频导致“状态滞后”。 + - 验证:对比不同 \(K\) 下的成功率/动作抖动/碰撞率,并量化延迟降低。 + +--- + +## 创新点 3:WAM 专属“因果 cache 视野裁剪”(Causal Cache Cropping) + +- **WAM 特性**:动作只需要与当前动作因果相关的 world 部分(例如手、被操作物体、容器口等)。 +- **算法**: + - 用 action→world 的注意力或梯度(近似)在推理时在线得到 **重要 world token 子集**。 + - 将 KV cache 按重要性裁剪:action 阶段只 attend 到 Top-\(M\) 的 world KV(或 ROI KV)。 + - 重要性可以是: + - 最近几步 action token 对 world token 的平均 attention; + - 或 “动作预测对 world token 的敏感度” 的低成本近似(例如 last-layer attention 聚合)。 +- **落点**:`modules/model.py` 的 cache 读出(`valid` selection)处引入“重要 token mask”;或在写入时就打标分类。 +- **加速来源**:减少 action 阶段的 K/V 长度,降低注意力计算。 +- **风险/验证**: + - 风险:裁剪错误导致关键几何信息缺失。 + - 验证:记录被裁剪 token 的类别/位置;对失败案例可视化 attention 以调参。 + +--- + +## 创新点 4:KV cache 在线压缩成“世界摘要 token”(Online World Summarization) + +- **WAM 特性**:对控制而言,世界只需少量摘要(物体相对位姿、可达性、接触)即可指导动作。 +- **算法(training-free)**: + - 周期性将较旧的 world KV 压缩成少数 \(K\) 个 **summary KV**(例如用 attention pooling / mean pooling / PCA 低秩投影)。 + - 将被压缩的原 KV slots 释放,保持 cache 上限。 + - action 阶段优先看 summary + 最近窗口内的高分辨 KV。 +- **落点**:`modules/model.py` cache 管理(slot 释放策略)+ 一个压缩函数(可放在 `utils/`)。 +- **加速来源**:控制 cache 长度,避免长 episode 变慢;action 注意力更短。 +- **风险/验证**: + - 风险:摘要丢失细粒度操作信息。 + - 验证:按“接触/精细操作阶段”关闭压缩或提高 \(K\);做阶段自适应。 + +--- + +## 创新点 5:用“速度场历史”做多步外推(AB2/AB3 for Flow, training-free) + +- **WAM 特性**:你们的 `FlowMatchScheduler.step()` 是显式 Euler(速度场积分),天然可做多步法。 +- **算法**: + - 保存前一步(或前两步)的速度 \(v_{t-1}, v_{t-2}\)。 + - 用 Adams–Bashforth(2/3 阶)更新: + - AB2:\(\Delta x \approx \Delta\sigma \cdot (3/2\,v_t - 1/2\,v_{t-1})\) + - AB3:\(\Delta x \approx \Delta\sigma \cdot (23/12\,v_t - 16/12\,v_{t-1} + 5/12\,v_{t-2})\) + - 在更少 steps 下维持质量(或相同 steps 下更好质量)。 +- **落点**:`wan_va/utils/scheduler.py` 新增 `step_ab2/ab3`;`wan_va_server.py` 循环里维护速度历史。 +- **加速来源**:允许把 steps 降得更激进而不崩(减少 NFE)。 +- **风险/验证**: + - 风险:外推不稳定,尤其在 guidance 强或噪声大区域。 + - 验证:对比 Euler/AB2 在 10/15/20 steps 下的成功率与动作平滑度。 + +--- + +## 创新点 6:WAM 版 speculative control:快动作提案 + 单步世界验真 + 局部修正 + +- **WAM 特性**:控制可以容忍“先给一个可执行动作”,再快速修正(闭环)。 +- **算法**: + 1) 用极少 steps(甚至仅 action 分支)产生 action 提案 \(a_t\)。 + 2) 用 1 次(或极少)world 更新预测 \(\hat{o}_{t+1}\) 的关键摘要(不需要整段视频)。 + 3) 若验真失败(违反约束/目标偏差大),再追加少量 steps 对 action 做 refinement(或回退到完整采样)。 +- **落点**:server 模式最自然;需要一个廉价的“验真器”(可用现有 world head 的低成本统计,或基于任务约束的几何判据)。 +- **加速来源**:大多数时刻只走快路径;仅少数困难时刻走慢路径。 +- **风险/验证**: + - 风险:验真器设计不良导致误放行。 + - 验证:统计快路径命中率、回退率、以及回退带来的尾延迟。 + +--- + +## 创新点 7:动作维度的“冻结/早停”(Action Token Freezing) + +- **WAM 特性**:很多动作维度在大多数阶段接近常量(例如部分关节、或某些夹爪维度)。 +- **算法(training-free)**: + - 在 action 去噪过程中,监测每个 action 维度(或 token)的变化量 \(|\Delta a|\)。 + - 若连续 \(m\) 步 \(|\Delta a| < \epsilon\),则将该维度标记为 frozen:后续 steps 不再更新(或仅做一次低频更新)。 + - 对 frozen 维度可以在 transformer 输出后做 mask,或在输入噪声中置零相应更新。 +- **落点**:`wan_va_server.py` action loop + `actions_mask` 扩展;或在 `postprocess_action` 前做冻结逻辑。 +- **加速来源**:减少有效 action token 更新与注意力计算(尤其在更细粒度 token 化时效果更明显)。 +- **风险/验证**:冻结阈值/窗口需要按任务阶段自适应。 + +--- + +## 创新点 8:观测编码复用(Obs/VAE Encode Reuse with Change Detection) + +- **WAM 特性**:相邻控制周期多视角图像变化可能很小;重复 VAE encode 浪费。 +- **算法**: + - 对输入图像做低成本 hash/差分(例如 downsample 后 L2、或 SSIM 近似)。 + - 若变化小于阈值:复用上次的 encoded latent(或仅对变化最大的相机视角重编码)。 + - 对 wrist camera 可按运动幅度自适应刷新频率。 +- **落点**:`wan_va_server.py::_encode_obs()` 外围加缓存与变化检测。 +- **加速来源**:减少 VAE encode 与 CPU↔GPU 传输(offload 时收益更大)。 +- **风险/验证**:快速运动/遮挡变化需要强制刷新。 + +--- + +## 创新点 9:WAM 目标导向的 step 自适应(Goal-Progress Adaptive Step Budget) + +- **WAM 特性**:控制目标通常有可计算的进度指标(距离、门角度、抓取状态、容器对齐)。 +- **算法(training-free)**: + - 在每次输出 action 前,用一个廉价的 proxy 估计“本周期动作对目标推进是否足够”(例如从 action 大小、方向一致性、或 world 摘要预测中估计)。 + - 若推进明显:减少后续采样 steps;若推进停滞/反向:增加 steps 或触发回退策略。 +- **落点**:server loop 中的 step controller;与上面的 speculative control 可组合。 +- **加速来源**:多数“简单阶段”少算,困难阶段多算。 +- **风险/验证**:需要设计任务无关的通用 proxy(或按任务族提供不同 proxy)。 + +--- + +## 创新点 10:控制专用的“低保真 world”通道(World-for-Control Channel) + +- **WAM 特性**:控制不需要高保真像素细节,而需要几何/接触/可达性。 +- **算法**: + - 不改变训练(或极少改动),在推理时从现有 latent 中提取一个极低维的控制特征(例如每帧若干统计:均值/方差、或固定池化得到的 K 个 token)。 + - action 阶段只 attend 到这些控制 token + 最近窗口内的少量高分辨 token(必要时再补全)。 +- **落点**:在 world cache 写入后追加一段 pooled token;action 阶段只读 pooled token(类似“在线瓶颈”)。 +- **加速来源**:大幅缩短 action 侧的 K/V。 +- **风险/验证**:对精细插入/对齐任务,可能需要阶段性打开高分辨 KV。 + +--- + +## 创新点 11:候选动作并行 + 单次世界评估(Batch Candidates, Shared World KV) + +- **WAM 特性**:world KV 可共享;动作候选可以并行评估,比串行多次采样更划算。 +- **算法**: + - 以 batch 方式并行生成 \(K\) 个 action 候选(少步数/不同噪声 seed)。 + - 复用同一份 world KV(以及 prompt embedding),用廉价 world 评估器/约束检查选最优。 + - 只对选中的候选做后续 refinement(可选)。 +- **落点**:`wan_va_server.py` 将 action 采样 batch 化(维持 world cache 相同),以及选择器实现。 +- **加速来源**:用并行吞吐换更少回退与更少长采样;在 GPU 上常更划算。 +- **风险/验证**:batch 增大显存;需要找到“选优 proxy”。 + +--- + +## 创新点 12:训练-推理对齐的“交错式联合步”但保持 cache(Interleaved Joint-Step with Cache) + +- **WAM 特性**:训练里 world/action 联合序列 + 因果 mask;推理里分阶段。对齐可减少步数敏感性。 +- **算法(尽量 training-free)**: + - 每个大步只做 **一次** transformer 调用,但在 cache 中交错写入: + 1) 读 world cache,更新一小步 world(写入 pred KV) + 2) 立刻在同一步用更新后的 KV 更新 action(或用同一 forward 的共享 trunk,输出两个 head) + - 目标是:在不显著增加 token 的前提下,把“2 次 forward/步”变成“1 次 forward/步”(或减少总步数)。 +- **落点**:需要对 `modules/model.py` 的前向做轻量封装(共享 block 计算、双 head 输出),或复用 `forward_train` 的拼接思路但用 cache 控制稀疏可见范围。 +- **加速来源**:减少总前向次数(NFE)。 +- **风险/验证**:实现复杂;需要确保 mask/缓存可见性不会引入未来泄露。 + +--- + +## 创新点 13:KV cache 冷热分层 + 冷层量化(Hot/Cold KV Tiering + Quantized Cold Cache) + +- **WAM 特性**:episode 变长时 cache 增长拖慢 action 注意力;但真正有用的通常是最近窗口 + 少量长期记忆。 +- **算法(training-free)**: + - 将 KV 分为 **Hot**(最近 \(W\) 帧 bf16/fp16)与 **Cold**(更旧部分 int8/fp8 或 cpu-pinned)。 + - action 阶段默认只 attend Hot + 少量 Cold summary;事件触发时临时扩大可见 Cold。 +- **落点**:`modules/model.py` cache 存取与 `valid` selection(读时拼接/解量化)。 +- **加速来源**:缩短有效 K/V、降显存、避免长 episode 变慢(offload 场景更明显)。 +- **风险/验证**:量化误差;用分阶段策略(精细操作阶段禁用或提高精度)。 + +--- + +## 创新点 14:Cache 的 Delta Write(无更新就不写,避免无效 cache 维护) + +- **WAM 特性**:很多周期 world 不更新或只更新 ROI,但 naive 仍写整段 KV,导致 cache 膨胀与带宽浪费。 +- **算法**:当本周期 world 不更新(或只更新 ROI),则 **禁止写入 pred cache**(或只写 ROI token 的 KV)。 +- **落点**:`wan_va_server.py` 的 `update_cache` 策略 + `modules/model.py` cache 写入逻辑。 +- **加速来源**:减少 cache 写带宽、控制 cache 长度增长、降低后续注意力开销。 +- **风险/验证**:需保证因果一致性;记录“跳写比例 vs 失败案例”。 + +--- + +## 创新点 15:World 的 ROI Token Update(按“动的/因果相关的”token 更新 world) + +- **WAM 特性**:背景静态,关键是“手+物体+接触区域”;全量 world token 去噪浪费。 +- **算法(training-free)**: + - 用廉价变化检测(多视角 downsample diff / 光流近似)得到 ROI。 + - 仅对 ROI token 做 world 更新,其余 token 直接复用上一周期 world latent/KV。 +- **落点**:ROI 从 `_encode_obs` 外围产出;world 数据打包/patch 逻辑支持子集 token(block 粒度先做)。 +- **加速来源**:world token 数显著下降 → 注意力边数下降 → NFE 成本下降。 +- **风险/验证**:ROI 映射误差;先用粗 block 降错杀风险。 + +--- + +## 创新点 16:Action Diffusion Warm-start(跨周期动作 latent 热启动) + +- **WAM 特性**:相邻控制周期动作连续;每次从纯噪声采样浪费。 +- **算法**:用上周期 action latent 末态做本周期初值(加小噪声/从中间 sigma 开始),并结合早停/冻结降低 steps。 +- **落点**:`wan_va_server.py` action loop 维护 `prev_action_latent`,改变初始化与 timesteps 起点。 +- **加速来源**:减少 action NFE(action_steps 高时收益更明显)。 +- **风险/验证**:突变场景会卡住;用事件触发强制重启(random init)。 + +--- + +## 创新点 17:Scheduler Early-Exit(用收敛判据提前结束扩散) + +- **WAM 特性**:很多周期去噪很快收敛,后续 steps 增益小。 +- **算法(training-free)**:监测 \(\|v_t-v_{t-1}\|\) / \(\|\Delta x\|\) 等,连续低于阈值则提前结束该分支(world 或 action)。 +- **落点**:`wan_va_server.py` 的 video/action loop 或 `FlowMatchScheduler.step()` 外围。 +- **加速来源**:直接减少 NFE。 +- **风险/验证**:阈值敏感;做离线 sweep 找 Pareto。 + +--- + +## 创新点 18:CPU↔GPU 搬运与算子重叠(Encode/Transfer Overlap with CUDA Streams) + +- **WAM 特性**:offload + 多视角 preprocess 常让 CPU↔GPU 搬运成为瓶颈,拉高尾延迟。 +- **算法**:pinned memory + 异步 H2D;VAE encode 独立 CUDA stream;与 transformer forward 重叠;CPU preprocess 线程化预取下一周期。 +- **落点**:`wan_va_server.py::_encode_obs*()` 与 `_compute_kv_cache()` 的 pipeline 拆分与重叠。 +- **加速来源**:减少 pipeline 泡沫,显著改善 P90/P99。 +- **风险/验证**:工程复杂;用 profiler 验证 overlap 生效。 + +--- + +## 建议的落地顺序(按“最可能立刻提速/风险最低”排序) + +1. **LWD(跳过/少跑 world)** + **自适应 step budget**(最直接砍 NFE) +2. **AB2/AB3 多步法**(不改模型,仅改 scheduler/循环) +3. **Causal Cache Cropping / Summary token**(减少 action 阶段 attention) +4. **Obs/VAE encode 复用**(工程上收益稳定) +5. **Speculative control(快路径 + 验真 + 回退)**(闭环友好,尾延迟需评估) + +--- + +## 评估指标(建议同时记录) + +- **吞吐/时延**:ms/step、ms/chunk、P50/P90/P99 时延、GPU 利用率、显存峰值。 +- **控制质量**:成功率、碰撞率、动作抖动(\|\Delta a\|)、阶段性失败分布(抓取/放置/开合)。 +- **计算分解**:world NFE vs action NFE、cache 长度随时间变化、VAE encode/CPU↔GPU 传输占比。 + diff --git a/markdown/ideas/ideas_action_relevant_video_tokens_0305.md b/markdown/ideas/ideas_action_relevant_video_tokens_0305.md new file mode 100644 index 0000000..d45b5e5 --- /dev/null +++ b/markdown/ideas/ideas_action_relevant_video_tokens_0305.md @@ -0,0 +1,157 @@ +# 识别“对下一个动作强相关”的 video/world tokens(training-free 创新点) + +目标:在 WAM(world/action 联合扩散)推理中,在线识别当前生成的 **video/world tokens** 里哪些对 **下一步动作** 最关键,并 **优先保留/更新** 这些 tokens(其余 token 可被裁剪为不可见、压缩为摘要、或低频刷新)。 + +> 关键建议:优先做 **KV 可见性裁剪**(token 仍存在,但后续注意力只看关键 token),工程风险远低于“真正删 token 让前向变短”。后者可作为第二阶段。 + +--- + +## 0) 定义:什么叫“对下一个动作强相关” + +给定 world token \(w_i\)、下一步动作输出 \(a\)(或 action 去噪过程中的噪声预测 \(\\epsilon_a\) / 速度场 \(v_a\)),我们希望估计某种 “重要性”: + +- **Sensitivity/Influence**:\( \\partial a / \\partial w_i \) 大 +- **Information**:移除/扰动 \(w_i\) 会明显改变动作分布(均值/方差) +- **Causal utility**:\(w_i\) 能解释 action 的决策(而不是仅相关) + +training-free 的核心:用 **模型本身的 attention/输出变化** 做 proxy,而不是再训练一个 importance head。 + +--- + +## 1) Action→World 注意力归因(最可行、WAM 专属) + +### 1.1 单层/多层 attention 聚合 + +**思路**:在 action 分支 forward 时,拿到 action tokens 对 world tokens 的 attention 权重,把它当作 “action 正在读取的信息”。 + +- **importance**: + \[ + \\mathrm{imp}(w_i) = \\sum_{a \\in \\mathcal{A}} \\mathrm{Attn}(a \\rightarrow w_i) + \] + 可对最后 1–2 层、或全部层加权求和(越靠后越贴近输出)。 + +### 1.2 跨 step 的 EMA 稳定化(减少抖动) + +- 维护 \( \\hat{\\mathrm{imp}}_t = \\alpha \\hat{\\mathrm{imp}}_{t-1} + (1-\\alpha)\\mathrm{imp}_t \) +- 用 \(\\hat{\\mathrm{imp}}_t\) 做 Top‑K/Top‑Blocks 选择。 + +### 1.3 强制保留项(防止“注意力偏见”漏掉关键几何) + +即便 attention 给低分,也强制保留: + +- **最近帧** tokens(短期因果) +- **变化显著 ROI** tokens(见第 3 节) +- **wrist/手附近** 的 token 块(可用 state/相机先验映射) + +**优势**:不改训练、信号天然存在;最贴合 “world 为 action 服务” 的 WAM 叙事。 +**风险**:attention ≠ 因果;需要用扰动验证(第 2 节)做 sanity check。 + +--- + +## 2) 反事实扰动评分(更因果,但更耗) + +### 2.1 Token dropout / mask 的 action 变化量 + +**思路**:对候选 token 集合做小扰动,看 action 输出改变多少。 + +- 扰动方式: + - **drop**:把 token 的 K/V 置零或替换为 mean token + - **noise**:加小噪声扰动 token 表示 +- 评分: + - \(\\Delta_a = \\| a - a^{(\\text{drop } i)} \\|\) + - 或动作分布差异(若你有多样采样):KL/方差变化 + +### 2.2 “二阶段”快速筛选(降低开销) + +先用 attention 得到 Top‑M 候选,再对这 M 个做反事实验证,最终保留 Top‑K(K≪M)。 + +**优势**:更接近因果影响,解释性强。 +**代价**:需要额外 forward(可只在低频或关键阶段运行)。 + +--- + +## 3) Change/Flow 驱动的 token 重要性(与多视角观测强耦合) + +### 3.1 观测变化 → latent block 变化 → token 重要性 + +**思路**:动作决策往往依赖 “正在变化的部分”(手、物体、接触)。用低成本图像变化检测做 ROI,再映射到 token 网格。 + +- 变化信号: + - 多视角 downsample diff(mean abs diff) + - 近似光流(低分辨率即可) + - 目标/手/物体 mask 变化(若你有分割器) +- 映射方式: + - 先按 **粗 block**(例如 latent 8×8 patch)做 ROI,降低错杀风险 + - 对多相机拼接的 latent:每个相机对应固定的宽度区间,ROI 映射到对应 slice + +### 3.2 与 attention 融合(最稳的组合) + +最终分数: + +\[ +\\mathrm{score}(w_i)=\\lambda\\,\\hat{\\mathrm{imp}}_{\\text{attn}}(w_i) + (1-\\lambda)\\,\\hat{\\mathrm{imp}}_{\\text{change}}(w_i) +\] + +**优势**:training-free、成本低、能覆盖“注意力没看但其实关键在动”的情况。 +**风险**:变化不等于因果(例如光照变化);靠强制保留/阈值鲁棒化。 + +--- + +## 4) Action-uncertainty 驱动的重要性(不确定性越大越需要更多 token) + +**思路**:当 action 分支对世界不确定时,通常需要更多上下文 token。反过来:能显著降低 action 不确定性的 token 更重要。 + +- 做法: + - 对同一 obs,采样多次 action(不同 seed/少量步)得到方差 \(\\mathrm{Var}(a)\) + - 逐步引入 token 子集(从 Top‑K 开始)观察方差下降速度 + - 能最快降低方差的 tokens 视为关键 + +**优势**:直接服务“下一步动作稳定/确定”。 +**代价**:需要少量多样采样(可低频做)。 + +--- + +## 5) 梯度/一阶敏感度近似(更直接,但需要可导获取) + +**思路**:用一阶近似度量 token 对动作的影响(类似 saliency)。不更新参数,但允许取梯度。 + +- 定义标量目标:例如 action 预测的 L2 范数、或某些维度的 logit/均值 +- 计算: + - \(\\mathrm{imp}(w_i)=\\|\\nabla_{w_i} \\mathcal{L}\\|\) 或 \(\\|w_i\\odot \\nabla_{w_i}\\mathcal{L}\\|\) +- 实用技巧: + - 只对最后 1–2 层 token 取梯度(降开销) + - 只在关键阶段触发(接触/遮挡) + +**优势**:比 attention 更接近“影响”。 +**风险**:实现侵入+开销;且梯度噪声大,需要平滑/分块。 + +--- + +## 6) 如何“保留这些 tokens”:三种落地形态(从易到难) + +### 6.1 KV 可见性裁剪(最推荐) + +- world token 仍写 cache,但 action 阶段只读取 Top‑K 的 world K/V(其余视为被 prune) +- 好处:不改 `mesh_id` / patch 打包;不破坏张量形状;容易 A/B。 + +### 6.2 Cache 生命周期管理(Hot/Cold + Summary) + +- 保留关键 tokens 为 Hot,低重要 tokens 变 Cold(压缩/量化/低频更新) +- 适合长 episode,避免 token 长度增长导致越跑越慢。 + +### 6.3 真正缩短序列(token merging / ToMe) + +- 对低重要 tokens 做 merge,减少 forward token 数 +- 工程侵入最大,建议在 6.1/6.2 验证收益后再做。 + +--- + +## 7) 实验与验证(必须做的 sanity checks) + +- **Token recall**:关键物体/手附近 tokens 是否在 Top‑K 覆盖率高(可视化到像素/patch)。 +- **Ablation**: + - 只用 attention / 只用 change / 融合 + - K 从小到大扫(Pareto:时延 vs 成功率) +- **反事实验证**:随机丢 token vs 丢低分 token vs 丢高分 token,比较 action 变化量与任务成功率。 +- **长序列曲线**:episode 越长,是否还能保持时延不增长(cache hygiene 成功与否)。 + diff --git a/markdown/ideas/ideas_dynamic_step_budget_0305.md b/markdown/ideas/ideas_dynamic_step_budget_0305.md new file mode 100644 index 0000000..e0cd5fd --- /dev/null +++ b/markdown/ideas/ideas_dynamic_step_budget_0305.md @@ -0,0 +1,103 @@ +# 动态 Step Budget:Lazy World Denoising / Multi-rate 实现思路 + +目标:**每控制周期动态决定 world/action 的步数**,在不动模型结构的前提下砍 world NFE(通常是最大头)。对应主清单 `ideas_0305.md` 创新点 1/2/9/11。 + +--- + +## 1. 核心直觉 + +- **World 分支**:token 多、attention 重,是推理大头;但很多周期里“世界没怎么变”或“action 不需要新世界信息”。 +- **做法**:大多数周期 world **少算/不算**(用旧 world cache/旧 world latent);少数关键周期 world **多算**。 +- **实现**:每个 control cycle 动态决定 `world_steps`、`action_steps`(以及是否强制刷新观测/缓存)。 + +--- + +## 2. Lazy World Denoising(LWD) + +**含义**:world 按需算——当“世界没怎么变 / action 不需要新世界”时,world 去噪步数从 full 降到 0~2。 + +**代码落点(最小改动)** +`wan_va_server.py::_infer()` 当前逻辑: + +- `self.scheduler.set_timesteps(video_inference_step)` +- `timesteps = timesteps[:video_step]`(当 `video_step != -1`) + +把 **`video_step` 从固定 config 改为每周期计算的 `world_steps`**: + +- **world_steps = 0**:跳过 video loop。需注意:若 `frame_st_id == 0`,仍需把 `latents[:, :, 0:1]` 设为 `init_latent`(第一帧条件注入),否则偏离有条件生成。 +- **world_steps = 1~3**:跑极少步,维持 KV/latent 的“新鲜度”。 +- **world_steps = full**:关键周期跑满。 + +**world_need 指标(training-free、低成本)** +结合已有观测变化检测(如 `_obs_change_score` / obs ref): + +- **obs_change**:多视角 downsample 的 mean-abs-diff。 +- **state_delta**:`np.max(np.abs(state - prev_state))`。 +- **action_delta**:上一周期输出动作幅度/jerk,如 `||a_t - a_{t-1}||`。 +- **外部强事件**:碰撞、抓取成功、目标切换等 → 强制 `world_steps = full`。 + +**简单分段规则示例**: + +```python +if force_refresh or obs_change > T_hi or state_delta > S_hi: + world_steps = FULL +elif obs_change < T_lo and state_delta < S_lo and action_delta < A_lo: + world_steps = 0 +else: + world_steps = 2 # 小步刷新 +``` + +--- + +## 3. Multi-rate(多速率 / 快慢双环) + +**含义**:world 低频更新、action 高频更新;不必每个控制周期都更新 world。 + +**实现要点**: + +- 维护计数器 `world_update_countdown`(或等价逻辑)。 +- 每次 `infer()`: + - 若 countdown == 0 或事件触发:跑一次 world 更新(少步或 full),然后 countdown = K。 + - 否则:本周期 `world_steps = 0`,只跑 action。 +- action 分支继续使用**上一轮** world 的 cache/latent;需保证在“更新周期”里 world cache 确实被更新(如 `_compute_kv_cache()` 或 video loop 最后一次 forward 的 `update_cache=1`)。 + +**落点**:server 内 `infer()` 的调用节奏 + `_infer()` 的 step 覆盖逻辑。 + +--- + +## 4. Goal-Progress Adaptive Budget(创新点 9 的加速版) + +**含义**:用任务进度 proxy 调预算——越接近目标/越简单 → 步数越少;停滞/反向/恢复 → 步数增加。 + +**实现**:step controller 多一个输入 `progress_proxy`(末端到目标距离、抓取状态、门角度等),并入 LWD 的分段规则,例如: + +- 进度明显推进 → 减少 world/action steps。 +- 进度停滞或反向 → 增加 steps 或触发回退策略。 + +--- + +## 5. Batch Candidates(创新点 11,用并行换更少大步数) + +**含义**:用**小步数**并行生成 K 个 action 候选,用廉价选择器(规则/约束/碰撞几何)选一个,再对选中者做少量 refinement(可选)。目标是降低“单路径长 steps 兜底”的概率,从而降低**平均** NFE 与尾延迟。 + +**实现要点**: + +- action latent 的 batch 维扩成 K,**共享同一份 world cache**(不复制 world token)。 +- 跑 `action_steps_small`,选优后再单独跑 `action_steps_refine`(可选)。 + +--- + +## 6. 最小可落地改法(建议起步) + +1. 在 `VA_Server` 增加 **step controller** 状态:`prev_state`、`prev_action`、`prev_obs_change`(或等价)。 +2. `_infer()` 支持 **override**:`world_steps_override`、`action_steps_override`,用它们截断 `timesteps` 长度。 +3. **world_steps = 0 的边界**:确保 `frame_st_id == 0` 时 `latent_cond` 仍注入(否则有条件生成会偏)。 +4. 先只用 **obs_change + state_delta** 做 ablation:`world_steps ∈ {0, 2, FULL}`,扫阈值看延迟/成功率 Pareto。 + +--- + +## 7. 评估指标建议 + +- 延迟:每周期 P50/P90/P99;world NFE 分布。 +- 控制质量:成功率、碰撞率、动作抖动;按阶段(抓取前/中/放置)分桶。 +- 统计:world_steps=0 / 2 / full 的占比;误判(关键周期被判为 0)与漏判比例。 diff --git a/markdown/ideas/ideas_training_free_wam_strong_0305.md b/markdown/ideas/ideas_training_free_wam_strong_0305.md new file mode 100644 index 0000000..de37e71 --- /dev/null +++ b/markdown/ideas/ideas_training_free_wam_strong_0305.md @@ -0,0 +1,109 @@ +# WAM(World Action Model)推理加速:补充创新点(training-free) + +这份文件专门补充 **training-free 的 accelerate WAM** 点子(不写 MPC/规划/奖励那类“提高成功率为主”的内容)。主清单在 `ideas/ideas_0305.md`,这里给你一些 **更工程化但依然“WAM 特有”** 的加速补充,重点围绕: + +- **KV cache 的冷热分层/压缩/量化** +- **world token 的 ROI 更新(只算“动的/因果相关”的部分)** +- **跨周期 warm-start 与 early-exit** +- **CPU↔GPU 搬运与算子并行重叠** + +--- + +## 创新点 A:KV Cache 冷热分层 + 冷层量化(Hot/Cold KV Tiering + Quantized Cold Cache) + +- **WAM 特性**:闭环 episode 里 cache 会增长;action 侧会反复 attend 历史 world KV,但真正“有效”的往往是最近窗口 + 少量长期记忆。 +- **算法(training-free)**: + - 维护 **Hot KV**(最近 \(W\) 帧,高精度 bf16/fp16)与 **Cold KV**(更旧部分,量化存储 int8/fp8 或 cpu-pinned)。 + - action 阶段默认只看 Hot + 少量 Cold summary(或按需解量化一小段)。 + - 当触发事件(接触/遮挡/目标切换)才临时扩大可见 Cold 范围。 +- **落点**:`modules/model.py` 的 cache 存取/valid selection(写入时打 hot/cold tag;读取时拼接/解量化)。 +- **加速来源**:缩短 action 注意力的有效 K/V;减少显存占用与 cache 管理成本;offload 场景收益更大。 +- **风险/验证**:量化误差影响精细操作;用分阶段策略(精细阶段禁用 cold 量化或增大 Hot 窗口)。 + +--- + +## 创新点 B:Cache 的 “Delta Write” (无更新即不写,避免无效 cache 维护) + +- **WAM 特性**:很多周期里 world 其实未更新(或只更新很小一块),但 naive 实现仍会写入整段 KV,带来 cache 膨胀与带宽浪费。 +- **算法**: + - 当触发器判定本周期 world 不需要更新(或只更新 ROI),则 **禁止写入 pred cache**(或只写 ROI token 的 KV)。 + - action 分支使用上次的 KV(或上次 + ROI 增量)。 +- **落点**:`wan_va_server.py` 调用 transformer 时的 `update_cache` 策略 + `modules/model.py` 中 cache 写入逻辑。 +- **加速来源**:减少 cache 写带宽、减少 cache 长度增长、减少后续注意力计算。 +- **风险/验证**:需要严格保证因果一致性;用日志记录每次“跳写”的比例与失败案例。 + +--- + +## 创新点 C:World 的 ROI Token Update(基于“运动/变化”更新 world token) + +- **WAM 特性**:控制里大量背景是静态的;真正重要的是 “手+物体+接触区域”。 +- **算法(training-free)**: + - 用廉价的变化检测(多视角 downsample diff / 光流近似 / mask 变化)得到 ROI。 + - 将 ROI 映射到 latent patch/token,只有 ROI token 进入 world 去噪更新;其余 token 直接复用上一周期 world latent(以及 KV)。 + - ROI token 的写入以 block 为粒度(利于 cache 管理)。 +- **落点**:`wan_va_server.py::_encode_obs_with_cache()` 产出 ROI mask;world 循环里按 mask 构建子序列(需要在数据打包/patch 逻辑处支持子集)。 +- **加速来源**:world token 数显著减少 → 注意力边数下降 → NFE 总成本下降。 +- **风险/验证**:ROI 映射误差;先用粗 block(如 8×8 patch)降低错杀风险。 + +--- + +## 创新点 D:Action Diffusion Warm-start(跨周期动作 latent 热启动) + +- **WAM 特性**:相邻控制周期最优动作通常连续;action 去噪从纯噪声开始浪费。 +- **算法**: + - 用上周期 action latent 的末态 \(a_{t}^{*}\) 作为本周期初值,加小噪声后只做少量去噪(或直接从中间 sigma 开始)。 + - 结合你的 “动作维度冻结/早停”(`ideas_0305.md` 的创新点 7)进一步减少步数。 +- **落点**:`wan_va_server.py` action loop:维护 `prev_action_latent`,改变初始化与 timesteps 起点。 +- **加速来源**:减少 action NFE(尤其 action_steps 很高时)。 +- **风险/验证**:遇到突变场景会陷入局部;用 “变化检测/事件触发” 强制重启(random init)。 + +--- + +## 创新点 E:Scheduler Early-Exit(用收敛判据提前结束扩散) + +- **WAM 特性**:很多周期里去噪很快收敛,后续 steps 的增益小。 +- **算法(training-free)**: + - **监测信号(不额外 forward)**:在每次 `scheduler.step()` 你本来就有 `model_pred_t`(如 \(v_t\)/\(\epsilon_t\))以及更新后的 latent \(x_{t+1}\)。用它们构造“相对变化量”: + - \(\Delta_v(t)=\dfrac{\|v_t-v_{t-1}\|_2}{\|v_t\|_2+\varepsilon}\)(或把 \(v\) 换成 \(\epsilon\)/\(\hat{x}_0\)) + - \(\Delta_x(t)=\dfrac{\|x_{t+1}-x_t\|_2}{\|x_t\|_2+\varepsilon}\) + - world 分支可只在 ROI/block 上算 \(\Delta_x\)(与“ROI token update”配合更稳);action 分支在 action latent 维度上算即可。 + - **抗抖/滞回**:对 \(\Delta_v,\Delta_x\) 做一个轻量 EMA(或滑窗均值)得到 \(\widehat{\Delta}(t)\),并设置“连续命中”计数器 `hit`: + - 若 \(\widehat{\Delta}(t) < \tau(t)\),则 `hit += 1`,否则 `hit = 0`。 + - 当 `hit >= m` 且已完成最少步数 `t >= t_min`,触发 early-exit。 + - **阈值怎么设(关键是归一化 + 随噪声阶段调度)**: + - 固定阈值起步:\(\tau(t)=\tau_0\),例如 \(\tau_0\in[10^{-3},10^{-2}]\)(依赖归一化后量纲)。 + - 更稳的做法:随噪声强度收紧/放宽,比如 \(\tau(t)=c\cdot \sigma_t\) 或 \(\tau(t)=c\cdot \sigma_t^2\)(早期 \(\sigma\) 大允许更大变化,后期更严格)。 + - 分阶段:接触/精细阶段使用更小阈值、更大的 \(m\)(更保守),粗阶段相反。 + - **防误退 guardrails**: + - 设 `t_min`(至少跑完前 \(p\%\) steps 才允许早停),避免一开始就误判“变化小”。 + - 若触发“事件/突变”(例如观测 diff/接触检测/目标切换),本周期禁用 early-exit 或直接重启(参考 D 的“变化触发强制重启”)。 + - 可加一个“最终质量兜底”检查:例如 early-exit 前再看一次 \(\Delta_x\) 是否也低于阈值(双条件)以降低误判。 +- **落点**:`wan_va_server.py` 的 video/action loop;或 `FlowMatchScheduler.step()` 外围做。 +- **加速来源**:直接减少 NFE。 +- **风险/验证**:阈值敏感;用离线 sweep 找 Pareto。 + +--- + +## 创新点 F:CPU↔GPU 搬运与算子重叠(Encode/Preprocess Overlap with CUDA Streams) + +- **WAM 特性**:offload + 多视角 preprocess 常让 CPU↔GPU 变成瓶颈,尤其 server 长跑时会出现尾延迟。 +- **算法**: + - 使用 pinned memory + 异步 H2D;VAE encode 放到独立 CUDA stream;与 transformer forward 重叠。 + - 图像 resize/stack 在 CPU 侧也可并行(线程池)并提前准备下一周期数据。 +- **落点**:`wan_va_server.py::_encode_obs_with_cache()` 与 `_compute_kv_cache()`:把 preprocess/transfer/encode 拆分成可重叠阶段。 +- **加速来源**:减少 pipeline 泡沫、降低 P90/P99 时延。 +- **风险/验证**:实现复杂;用 Nsight/torch profiler 验证 overlap 是否生效。 + +--- + +## 创新点 G:自适应 Offload 策略(基于命中率/负载的 VAE 常驻调度) + +- **WAM 特性**:offload 带来搬运开销,但在 encode reuse 命中率高时 VAE 常驻 GPU 可能更划算。 +- **算法**: + - 在线统计:encode reuse 命中率、VAE encode 的占比、GPU 余量。 + - 动态决定:VAE 是否常驻 GPU(或只常驻高频视角的编码路径)。 +- **落点**:`VA_Server.__init__` 与 `_encode_obs_with_cache()`:根据统计切换 `self.vae`/`self.streaming_vae` 的 device。 +- **加速来源**:避免“搬运主导”的坏工况,尤其在高帧率/多视角时。 +- **风险/验证**:频繁切换 device 反而更慢;需加入 hysteresis(滞回)。 + + diff --git a/markdown/ideas/ideas_value_aware_compute_0306.md b/markdown/ideas/ideas_value_aware_compute_0306.md new file mode 100644 index 0000000..57e6063 --- /dev/null +++ b/markdown/ideas/ideas_value_aware_compute_0306.md @@ -0,0 +1,144 @@ +# WAM 推理加速:Value-Aware Compute 创新点(0306) + +单纯把 `num_inference_steps` 调小(如 10→3)难以作为创新点——审稿人会认为 diffusion/flow matching 减步数、蒸馏、一致性采样已有大量工作。 + +真正能站住脚的角度:在 WAM 里,**world 和 action 不是两个独立生成任务**,而是一个闭环控制系统里**两种不同时间尺度、不同价值密度**的计算。创新应写成 **「如何把有限的 denoising budget 在 world/video 与 action 之间按控制价值动态分配」**,而不是「把 diffusion steps 调小」。 + +当前实现(`wan_va_server.py`)已给出切入点:先 video loop 再 action loop,两个 budget 分开配置;可在此基础上做**预算分配策略、world/action 耦合、停止准则、cache 复用**。 + +--- + +## 1. Action-Conditioned Dynamic Budget + +**核心**:不是固定 `video=3, action=10`,而是每个 control cycle 动态决定 +- 这一步要不要更新 world +- world 需要几步 +- action 需要几步 + +预算由 **action relevance** 决定,而非仅看图像变化。 + +**信号示例**: +- 当前 action 的不确定性 / 多样性 +- action 对 world token 的注意力集中度 +- 预测动作与上一步动作的差异 +- 接触 / 抓取 / 放置等关键事件 +- world prediction 对 action 的边际增益 + +**Novelty 表述**: +*"We allocate denoising budget according to the control value of world updates, rather than uniformly reducing diffusion steps."* + +与传统 diffusion 的区别:传统是样本质量导向;此处是**控制收益导向**;预算分配对象是**两个耦合分支**,不是单一生成器。 + +--- + +## 2. Multi-Rate World-Action Sampling + +**思想**: +- action **高频**更新 +- world **低频**更新 +- 大部分周期复用旧 world cache +- 仅在关键时刻刷新 video/world branch + +机器人控制里动作环通常比世界建模环更高频,设定自然。 + +**Novelty 表述**: +*"Asynchronous denoising for embodied world-action models."* + +与一般 video diffusion 的区别:world 不是最终输出目标,而是 action 的上下文;可允许 world 低频、action 高频;本质是控制系统里的**多速率采样**。 + +--- + +## 3. Action-Guided ROI World Denoising + +**思想**:不要让 world branch 每次对整张 latent grid 同等强度更新,而是 +- 重点更新与当前动作相关的区域(gripper 附近、被操作物体、容器口等) +- 背景区域复用 cache 或低频更新 + +**ROI 来源**: +- wrist camera / end-effector pose +- 上一步 action rollout 落点 +- action→world attention map +- optical flow / obs difference + +**Novelty 表述**: +*"We reduce world denoising by exploiting the action-conditioned spatial sparsity of embodied interaction."* + +比通用 sparse diffusion 更合理:ROI 不是纯视觉,而是 **causal to action**。 + +--- + +## 4. Speculative Control with World Verification + +**流程**: +1. 用极少 steps 快速生成 action proposal +2. 用 1 小步 world update 预测该 action 的后果 +3. 若 world verification 通过 → 直接执行 +4. 若不通过 → 追加 refinement steps + +类比 LLM speculative decoding,但此处:proposal 是 action,verifier 是 world model,目标是 control success。 + +**Novelty 表述**: +*"Fast action proposal, cheap world verification, selective refinement."* + +WAM-specific:利用同时有 world 与 action 两个头。 + +--- + +## 5. 论文主线 Framing + +**主标题建议**(避免「fewer diffusion steps」): +- **Value-Aware Compute Allocation for World-Action Models** +- 或 **Action-Centric Adaptive Sampling for World-Action Models** + +**主张**: +- WAM 的 world/video 与 action 分支对控制的价值密度不同 +- 统一固定步数低效 +- 应根据动作相关性、阶段、置信度,把 compute budget 动态分配到最有用的 branch / token / timestep + +--- + +## 6. 推荐组合方案 + +落地时建议做成一个完整系统: + +- **动态 world/action 步数分配** +- **world 低频、action 高频** +- **action-guided ROI world update** +- **world verification 触发 fallback refinement** + +**快路径**:不更新或少更新 world,小步数出 action,用局部 ROI 或 summary world 做验证。 +**慢路径**:关键周期 full world refresh,增加 action refinement,重建可靠 world context。 + +--- + +## 7. 避免被说「只是工程 trick」 + +需证明: + +1. **不是所有 step reduction 都一样**:相同 NFE 下,你的分配策略优于固定减步。 +2. **world/action 联合分配优于只压 video 或只压 action**:做 branch-level ablation。 +3. **改善来自 WAM 特有结构**:例如 + - action uncertainty 能预测 world refresh 必要性 + - ROI world tokens 与 action success 强相关 + - speculative verification 显著降低尾延迟而不掉成功率 + +**评测建议**:success rate、average NFE、P50/P90 latency、failure mode breakdown、不同任务阶段的 budget 分布。 + +--- + +## 8. 关于 env_step 占比 + +若 `env_step_update` 占端到端时间很高(如 80%+),则: + +- 减少 inference steps 对**模型推理时间**有效 +- 对**端到端 wall-clock** 改善会被环境仿真掩盖 + +论文中建议拆开报告:**model-only inference latency** 与 **end-to-end control latency**。 + +--- + +## 9. 一句话总结 + +最有新意的方向不是「把 diffusion steps 调小」,而是: +**让 world 和 action 在闭环控制中按价值、按阶段、按空间区域、按验证结果动态消耗计算。** +这才是从「生成模型加速」走向「world-action model 专属推理机制」的差异点。 diff --git a/markdown/ideas/ideas_video_token_prune_0305.md b/markdown/ideas/ideas_video_token_prune_0305.md new file mode 100644 index 0000000..29536a0 --- /dev/null +++ b/markdown/ideas/ideas_video_token_prune_0305.md @@ -0,0 +1,127 @@ +# Video / World Token Prune 创新点(training-free) + +目标:对 **生成的 video/world token 长度** 做裁剪或压缩,降低 attention 计算与 cache 成本,且不改变训练。最稳的做法是先做在 **KV/注意力侧**(等价于 token prune),再考虑真正缩短前向序列。 + +--- + +## 设计原则 + +- **优先做“谁 attend 谁”的裁剪**:world token 仍存在,但在后续 attention 里只让一部分 token 参与(缩短有效 K/V),不改输入 token 形状与 mesh_id,工程风险小。 +- **再考虑真正缩短序列**:merge/evict 等需动 cache 写入与打包逻辑,适合在 KV prune 验证后再做。 + +--- + +## 创新点 1:Action-conditioned KV Token Prune(最推荐、可行性最高) + +**目标**:action 分支 attend 的 world KV 从“全量视频 token”变为“Top-M 因果相关 token”。 + +**重要性打分(training-free)**: + +- 用 **action token → world token 的 attention 权重**(最后 1~2 层即可): + - \(\mathrm{imp}(w_i) = \sum_{a \in \text{action tokens}} \mathrm{Attn}(a \to w_i)\) +- 可在多 step/多层做 EMA 平滑,减少抖动。 + +**Prune 规则**: + +- 保留 **Top-M** world tokens(或按 block 保留 Top-B blocks)。 +- **强制保留**:wrist/手附近 ROI、最近帧 token、变化显著区域(见创新点 3)。 + +**落点**: + +- 在 transformer 的 attention **读 cache 时**做 K/V 的 gather(只给注意力一个缩短的 K/V),不改 latent / mesh_id 生成。 + +**加速来源**:显著降低 **action 阶段** attention 的 K/V 长度;长 episode 时延与 cache 增长更可控。 + +**创新性**:WAM 专属“因果裁剪”——world 为 action 服务,只保留对动作预测重要的 token。 + +**风险/验证**:M 过小可能丢关键几何;可先做 M 的 sweep,并记录被裁掉的 token 空间/时间分布。 + +--- + +## 创新点 2:Temporal Token Eviction + Summary Tokens + +**目标**:把“很久以前的 video tokens”从全量保留改为少量“世界摘要 token”,使有效 token 长度稳定在上限。 + +**做法**: + +- **Hot**:最近 \(W\) 帧 world tokens 全保留。 +- **Cold**:更早的帧压缩成 \(K\) 个 summary tokens(mean pooling / attention pooling / PCA 低秩等,均 training-free)。 +- action 分支默认只 attend:**summary + 最近 \(W\) 帧**。 + +**落点**:cache 的写入/淘汰策略;summary 作为额外 KV 写入,不改变主前向的 token 形状定义。 + +**加速来源**:控制 token 长度不随 episode 增长爆炸;降低长序列时延与显存。 + +**创新性**:把“世界记忆”做成可控的长期摘要,而非无限增长的 KV。 + +**风险/验证**:摘要丢失细粒度操作信息;可在接触/精细阶段关闭压缩或增大 \(K\)。 + +--- + +## 创新点 3:Change-driven Spatial ROI Prune + +**目标**:只保留“动的/相关的”空间区域对应 token,其余视为静态背景。 + +**ROI 信号(training-free)**: + +- 多视角下采样 diff、近似光流、分割 mask 变化。 +- 末端速度大时扩大 ROI;静止时缩小 ROI。 + +**工程要点**(当前 world latent 为多相机在宽度维拼接): + +- 每个相机 ROI 映射到其对应的 **latent 水平区间**。 +- 先按**粗 block**(如 8×8 latent patch)做,鲁棒且易实现。 + +**落点**: + +- 建议先做“ROI 决定 KV 可见性”(即 KV prune),不立刻做“减少前向 token 数”。 + +**加速来源**:action attention 与长序列 cache 均受益;ROI 也可作为动态 step budget 的触发器。 + +**创新性**:利用 WAM 多视角结构,把视觉变化直接映射到 token 级计算裁剪。 + +--- + +## 创新点 4:ToMe / Token Merging on World Tokens(真缩短序列,研究向) + +**目标**:在 world 分支去噪过程中,将相似/低重要性 token **合并**,真实减少序列长度。 + +**做法(training-free)**: + +- 每隔若干步,对 world tokens 做相似度(cosine / L2),将最相似的若干对 **merge**(如加权平均)。 +- 保留映射表,必要时可“反投影”回原网格(通常不完全可逆)。 + +**难点**: + +- 需同步处理 **mesh_id**、token 打包、cache slot 对应关系。 +- 对扩散去噪稳定性有影响(尤其 early steps)。 + +**建议**:作为论文亮点合适;工程上建议在 **KV prune(创新点 1/2/3)** 验证后再上。 + +--- + +## 落地顺序建议 + +| 顺序 | 方案 | 说明 | +|------|------|------| +| 1 | Action-conditioned KV prune(创新点 1) | 改动集中、不破坏形状,对“video token 长度带来的注意力成本”最直接 | +| 2 | Temporal eviction + summary(创新点 2) | 控制长 episode 的 token 上限,与 1 可组合 | +| 3 | Change-driven ROI(创新点 3) | 先用于 KV prune / 触发器,再考虑真减 token | +| 4 | ToMe / token merging(创新点 4) | 真剪序列长度,研究型,放在最后 | + +--- + +## 落点与依赖小结 + +- **创新点 1**:transformer 内 attention 读 cache 时的 K/V 索引/gather;需暴露 1~2 层 action→world attention 权重(或等价重要性)。 +- **创新点 2**:cache 管理(写入、淘汰、summary 生成与拼接)。 +- **创新点 3**:`_encode_obs` 外围的 ROI 映射 + 同上 cache/attention 的可见性。 +- **创新点 4**:world 数据打包、patch、mesh_id 与 cache slot 一致性。 + +--- + +## 评估指标建议 + +- 有效 world token 数 / K-V 长度分布;attention 计算量(FLOPs 或等价)。 +- 控制质量:成功率、碰撞率、阶段失败分布;与“不 prune”的 A/B。 +- 延迟与显存:P50/P90 时延;cache 峰值;长 episode 的时延增长曲线。 diff --git a/markdown/ideas/vla_roi_tokens_methods.md b/markdown/ideas/vla_roi_tokens_methods.md new file mode 100644 index 0000000..8d1bb6c --- /dev/null +++ b/markdown/ideas/vla_roi_tokens_methods.md @@ -0,0 +1,135 @@ +# VLA 中 ROI Tokens 的计算方法与创新点备忘 + +这里的 **ROI tokens** 指:从视觉 token 序列(patch tokens / video tokens / latent tokens / blocks)中挑出一小部分“更值得算/更该被模型关注”的子集,用于 **token pruning / 动态计算 / cache 选择 / ROI 更新** 等目的。目标通常是:在几乎不掉成功率的前提下,减少注意力边数、减少 NFE、或减少跨模态对齐成本。 + +--- + +## 1) ROI token 的常见定义(你到底想“保留”什么) + +- **Action-relevant ROI**:与下一步动作决策最相关(手/物体/接触点/目标区域)。 +- **Dynamics ROI**:随时间变化显著(运动、形变、遮挡变化),背景静态区域不重要。 +- **Uncertainty / Hard ROI**:模型最不确定/最难预测的区域(需要更多计算)。 +- **Task-conditioned ROI**:由语言指令/任务状态决定(“把红杯子拿起来” → 红杯区域优先)。 + +实践里往往是 **多信号融合**,并用时间一致性减少抖动。 + +--- + +## 2) Training-free(不训练新模块)ROI 计算 + +### 2.1 运动/变化驱动(最稳的第一选择) + +- **帧差 / 低分辨率 diff**:在像素或 VAE latent 上做 `|I_t - I_{t-1}|` 或 `|z_t - z_{t-1}|`,再映射回 patch/token。 + - **优点**:便宜、实现快、对“静态背景+局部运动”的场景很有效。 + - **缺点**:相机抖动/光照变化会误报;动作相关但静止的目标会漏掉。 +- **光流/特征流(近似)**:用轻量 flow 或用 backbone 特征做相关性匹配,得到运动 mask。 +- **时序一致性 + 滞回**:对 ROI mask 做 dilate/erode,或对 token 重要性做 EMA,并设置最小保持时长,减少“闪烁式 ROI”。 + +### 2.2 模型内部注意力信号(不额外 forward) + +适用于 Transformer/VLM/VLA:把“模型已经算出来的东西”拿来当重要性。 + +- **Cross-attention mass**(语言/动作 query → 视觉 token 的注意力权重): + - 计算每个视觉 token 被关注的总量 \(s_i=\sum_{q\in Q} \mathrm{Attn}(q\rightarrow i)\),Top-K 作为 ROI。 + - 变体:只统计与动词/名词相关的语言 token;或只统计 action head 的 query。 +- **Attention rollout / last-layer attention**: + - 使用最后几层、或 rollout 得到更“语义化”的重要性。 +- **KV cache usage proxy**: + - 统计某些 token 在注意力里被访问的频率/平均权重,作为长期重要性。 + +注意:纯注意力权重可能被“分布形状/温度”影响,建议做归一化和跨 step 平滑(EMA)。 + +### 2.3 预测误差/残差驱动(diffusion / world model 特别常见) + +- **去噪残差/更新幅度**: + - 例如对 latent patch 的 \(|\Delta x|\) 或 \(|\Delta \epsilon|\) 做聚合,大的区域当 ROI(“哪里还没收敛就多算哪里”)。 +- **重建误差/一致性误差**: + - 用轻量 decoder 或特征一致性评估,误差大 → ROI。 +- **不确定性 proxy**: + - 多次 dropout / 两个头的分歧 / 方差估计:分歧大 → ROI(计算更贵,但更稳)。 + +### 2.4 几何/先验驱动(机器人常用) + +- **手-眼先验**: + - 已知末端执行器投影/深度 → 以 gripper 投影点为中心取 ROI(环形/高斯窗)。 +- **目标框/检测/分割**(训练外部小模型也算 training-free for 主模型): + - 用现成 detector/segmenter 产生 mask → token 选择。 +- **接触/力觉事件触发 ROI 扩张**: + - 事件发生时扩大 ROI(避免错杀关键细节)。 + +--- + +## 3) 需要少量训练/可学习的 ROI 计算(质量更高,但要数据/训练) + +### 3.1 可学习的 Token Selector / Router(动态 Top-K) + +- **Gating network**:输入视觉 token(可加语言/动作条件)输出每个 token 的 keep score。 + - 训练信号:行为克隆 loss、成功率、或 distillation(保持与全算模型输出一致)。 +- **Block-level routing**: + - 先选 block 再选 token(两级稀疏),更适合工程落地与 cache 管理。 +- **Budget-aware / compute-aware 训练**: + - 把 FLOPs/NFE 当约束:在固定预算下最大化任务指标。 + +### 3.2 Action-conditioned ROI(VLA 特有的“更贴任务”方式) + +- **预测“动作敏感区域”**: + - 让 selector 直接预测“哪些视觉 token 会影响下一步 action distribution”。 + - 可用梯度近似监督:\(\|\partial \pi(a|s)/\partial x_i\|\) 大的 token 更重要(实践可用蒸馏近似,避免真梯度开销)。 +- **Counterfactual masking**: + - 训练时随机 mask 一部分 token,看 action 变化大不大;变化大 → 该 token 重要(可用于训练 selector)。 + +### 3.3 任务/语言对齐式 ROI(指令驱动) + +- **Phrase grounding**: + - 把指令中的实体/属性对齐到视觉区域,ROI = 被 grounding 的区域 + 邻域。 +- **Query-former / region queries**: + - 用少量 learnable queries 从视觉 token 中抽取“可控数量”的 region features,本质是软 ROI 压缩。 + +--- + +## 4) 组合策略(工业界常见的“稳 + 快”) + +ROI 质量通常来自 **多信号融合 + 时序稳定化**: + +- **融合打分**(例): + - \(s_i = w_m s^{motion}_i + w_a s^{attn}_i + w_u s^{uncert}_i + w_p s^{prior}_i\) + - 再做 Top-K / Top-blocks。 +- **跨 step EMA 稳定化**: + - \(\hat{s}_t = \alpha \hat{s}_{t-1} + (1-\alpha)s_t\),用 \(\hat{s}_t\) 选择,减少 ROI 抖动。 +- **hysteresis(滞回)**: + - 进入 ROI 用高阈值,退出 ROI 用低阈值;或设置最小驻留步数。 +- **Multi-res ROI**: + - 先粗分辨率找 ROI block,再在 block 内细选 token;可显著降低 selector 成本。 + +--- + +## 5) 可写成“创新点”的方向(更像论文/专利的表述) + +- **创新点 1:Action-conditioned + Dynamics 双通路 ROI** + - 一路用动作条件(cross-attn / action head),一路用变化检测(\(|\Delta x|\)/motion),再做可学习融合或规则融合。 + - 亮点:兼顾“动作相关但静止”和“变化但不相关”的两类误差。 + +- **创新点 2:ROI 的时序一致性约束(减少 flicker 的稳定化机制)** + - EMA + 滞回 + 最小驻留步数 + 事件触发扩张(接触/遮挡/目标切换)。 + - 亮点:把“选择稳定性”当成显式目标,提升闭环控制鲁棒性。 + +- **创新点 3:Uncertainty-driven 计算再分配(把算力花在难点上)** + - 用模型分歧/残差预测来扩张 ROI;easy 区域早停或降精度。 + - 亮点:与 diffusion/world model 的收敛特性天然匹配。 + +- **创新点 4:Block-sparse ROI + Cache-aware 选择** + - ROI 不只决定 forward 计算,还决定 KV 写入/保留/量化策略(hot/cold 分层、delta write)。 + - 亮点:把“token 选择”与“系统级瓶颈(带宽/显存)”统一优化。 + +- **创新点 5:Counterfactual token importance 的轻量蒸馏** + - 用训练时的 token masking 估计因果重要性,蒸馏给一个便宜 selector(推理时近似反事实)。 + - 亮点:比纯注意力权重更接近“对 action 的因果贡献”。 + +--- + +## 6) 验证与指标(避免只看速度不看闭环) + +- **速度**:token 数/注意力边数、wall-clock、显存峰值、NFE、P90/P99。 +- **质量**:成功率、轨迹偏差、接触稳定性、失败类型(漏 ROI / 误 ROI / 抖动)。 +- **稳定性**:ROI 集合变化率(churn)、mask 闪烁频率、平均驻留步数。 + diff --git a/markdown/ideas/world-action-model-acceleration_0301.md b/markdown/ideas/world-action-model-acceleration_0301.md new file mode 100644 index 0000000..2718909 --- /dev/null +++ b/markdown/ideas/world-action-model-acceleration_0301.md @@ -0,0 +1,43 @@ +# Acceleration Ideas for World-Action Models (WAM) + +In this project, inference cost is dominated by: +- **Video token count** (often much larger than action tokens; e.g., Robotwin is ~30:1). +- **Video diffusion steps** (many backbone calls per chunk). + +Below are algorithm-level acceleration directions, especially for optimizing **video latent** inference. + +## A) Fewer steps (distillation / consistency / rectified flow) +- Distill the video branch from many steps (e.g., 25) to **1–4 steps** using teacher trajectories. +- `FlowMatchScheduler.step()` is Euler-like integration, suitable for progressive distillation / consistency training. +- Keep action head unchanged initially; aggressively compress **video steps** first. + +## B) Fewer video tokens (structural latent compression) +1. **Low-res latent diffusion + latent super-resolution** + - Diffuse on smaller `(H', W')` latent grids (token count \(\propto\) area), then use a lightweight decoder/upsampler to recover full latent size. +2. **Learned bottleneck tokens (per-frame K summary tokens)** + - Encode each frame latent grid into **K \(\ll\) H×W** tokens; diffuse only these tokens; decode back only if needed. +3. **Camera/ROI-aware compression** + - Keep high-res tokens only for critical views/regions (e.g., wrist / end-effector neighborhood), downsample the rest. + +## C) Fewer tokens without changing output (dynamic token selection) +- Update only a subset of video tokens per step; reuse previous values for the rest. +- Token importance can be estimated by motion/changes, end-effector proximity, or early-step attention statistics. +- Combine with `flex_attention` masks to realize sparse compute beyond fixed windows. + +## D) Fewer backbone calls (share computation between video and action) +- Instead of running separate diffusion loops for video then action, predict actions from shared hidden states during the video loop. +- Alternatively reduce action diffusion to 1–2 steps, or switch action to deterministic regression + uncertainty head. + +## E) More reuse across chunks (beyond KV cache) +- Extend from KV cache to **state/token cache**: cache a compressed world-state representation across chunks and only update the increment. +- Use a **keyframe strategy**: refresh video latent features at low frequency; run action at high frequency; periodic correction. + +## F) Fast-path inference (no explicit video generation) +- If deployment only needs control success (not visualization), do not generate full video latents at inference time. +- Train with video loss to maintain representation quality, but deploy a fast-path that outputs only compact features needed for action. + +## Recommended MVP routes +- **Route 1 (most reliable):** video step distillation (25 → 4 → 2 → 1). +- **Route 2 (token bottleneck):** latent token compression (low-res or K-summary) + lightweight decoding. +- **Route 3 (research-y):** dynamic sparse attention + keyframe refresh policy. + diff --git a/pyproject.toml b/pyproject.toml index 9236cbc..710dddc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,16 +12,15 @@ authors = [ license = { file = "LICENSE.txt" } readme = "README.md" requires-python = ">=3.10,<4.0" +# 不在此声明 torch/torchvision/flash_attn,避免 pip install -e . 从 PyPI 拉取覆盖已安装的 cu124 版本 +# 请先按 README 或 script/setup_cu124_mirror.md 单独安装 PyTorch (cu124),再 pip install -e . --no-deps 或直接运行 dependencies = [ - "torch>=2.9.0", - "torchvision>=0.24.0", "diffusers>=0.36.0", "transformers>=4.55.4", "tokenizers>=0.21.4", "tqdm", "imageio", "easydict", - "flash_attn", "numpy>=1.26.4,<2" ] @@ -44,10 +43,10 @@ modelscope = "https://github.com/Robbyant" discord = "https://github.com/Robbyant" [tool.setuptools] -packages = ["lingbot_va"] +packages = ["wan_va"] [tool.setuptools.package-data] -"lingbot_va" = ["**/*.py"] +"wan_va" = ["**/*.py"] [tool.black] line-length = 88 diff --git a/run_inference.sh b/run_inference.sh new file mode 100644 index 0000000..1c7c18c --- /dev/null +++ b/run_inference.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# LingBot-VA 一键推理(自动激活 conda 环境 + 设置模型路径) +# 用法: bash run_inference.sh 或 bash script/ +set -e + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +export CUDA_VISIBLE_DEVICES="${CUDA_VISIBLE_DEVICES:-4}" + +CONDA_BASE="" +if [[ -n "$CONDA_EXE" ]]; then + CONDA_BASE="${CONDA_EXE%/bin/conda}" +fi +if [[ -z "$CONDA_BASE" ]]; then + CONDA_BASE="$(conda info --base 2>/dev/null)" || true +fi +if [[ -n "$CONDA_BASE" && -f "$CONDA_BASE/etc/profile.d/conda.sh" ]]; then + source "$CONDA_BASE/etc/profile.d/conda.sh" +else + echo "未检测到 conda,请先安装 Miniconda/Anaconda 或在本机已激活 lingbot-va 的终端中直接执行:" + echo " export LINGBOT_VA_MODEL_PATH=$REPO_ROOT/lingbot-va-base" + echo " bash script/run_i2va_single_gpu.sh" + exit 1 +fi + +echo ">>> 激活环境: lingbot-va" +conda activate lingbot-va + +echo ">>> 模型路径: $REPO_ROOT/lingbot-va-base" +export LINGBOT_VA_MODEL_PATH="${LINGBOT_VA_MODEL_PATH:-$REPO_ROOT/lingbot-va-base}" + +cd "$REPO_ROOT" +bash script/run_i2va_single_gpu.sh "$@" diff --git a/script/download/download_bench_robotwin.sh b/script/download/download_bench_robotwin.sh new file mode 100644 index 0000000..6ea1613 --- /dev/null +++ b/script/download/download_bench_robotwin.sh @@ -0,0 +1,61 @@ +#!/usr/bin/bash +# RoboTwin 2.0 测评环境与资源下载(测评 bench 用) +# 使用完整路径,从仓库根目录执行: bash script/download_bench_robotwin.sh +# +# 说明:测评不是在“数据集文件”上跑,而是在 RoboTwin 仿真里跑 50 个 task。 +# 需要先克隆 RoboTwin 仓库并下载其 assets,再按 README 启动 server + client。 + +set -e + +# 测评 bench 根目录(RoboTwin 克隆位置) +BENCH_ROOT="${BENCH_ROOT:-/mnt/users/wangyuxuan-20250915/EAI/RoboTwin}" +ROBOTWIN_COMMIT="${ROBOTWIN_COMMIT:-2eeec322}" + +echo "BENCH_ROOT (RoboTwin 克隆目录): $BENCH_ROOT" +echo "RoboTwin commit: $ROBOTWIN_COMMIT" +echo "" + +# 1. 克隆 RoboTwin 并 checkout +if [ ! -d "$BENCH_ROOT" ]; then + echo ">>> 克隆 RoboTwin 到 $BENCH_ROOT" + git clone https://github.com/RoboTwin-Platform/RoboTwin.git "$BENCH_ROOT" + cd "$BENCH_ROOT" + git checkout "$ROBOTWIN_COMMIT" + cd - > /dev/null +else + echo ">>> 目录已存在: $BENCH_ROOT,跳过 clone;如需重装请先删掉该目录" + cd "$BENCH_ROOT" + git fetch origin 2>/dev/null || true + git checkout "$ROBOTWIN_COMMIT" 2>/dev/null || true + cd - > /dev/null +fi + +# 2. 下载 assets(测评必需) +echo "" +echo ">>> 下载 RoboTwin assets(测评场景与资源)" +cd "$BENCH_ROOT" +if [ -f "script/_download_assets.sh" ]; then + if ! bash script/_download_assets.sh; then + echo "" + echo "官方脚本下载失败(多为 HuggingFace 连接中断)。请用带重试与镜像的脚本:" + echo " cd $(dirname "$0")/.." + echo " HF_ENDPOINT=https://hf-mirror.com python script/download_robotwin_assets.py $BENCH_ROOT" + exit 1 + fi +else + echo "未找到 script/_download_assets.sh,请先完成 RoboTwin 安装(见 README 步骤 2–4)。" + exit 1 +fi +cd - > /dev/null + +echo "" +echo "测评 bench 下载完成." +echo "RoboTwin 路径: $BENCH_ROOT" +echo "" +echo "后续步骤(需在 RoboTwin 文档中完成安装后再做):" +echo " 1) 安装依赖: sudo apt install libvulkan1 mesa-vulkan-drivers vulkan-tools" +echo " 2) 按 README 修改 script/requirements.txt 和 script/_install.sh 后执行: bash script/_install.sh" +echo " 3) 测评时设置 RoboTwin 路径并启动 client:" +echo " export ROBOTWIN_ROOT=$BENCH_ROOT" +echo " # 在 lingbot-va 仓库根目录执行: bash evaluation/robotwin/launch_client.sh " +echo " 4) 先启动 LingBot-VA server,再在同上目录运行 launch_client.sh 进行测评" diff --git a/script/download/download_dataset.py b/script/download/download_dataset.py new file mode 100644 index 0000000..cc4732d --- /dev/null +++ b/script/download/download_dataset.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +""" +从 HuggingFace 下载 LingBot-VA post-training 数据集 (robotwin-clean-and-aug-lerobot)。 +README 中该数据集仅在 HuggingFace 提供,ModelScope 无此数据集。 + +用法: + python script/download_dataset.py + python script/download_dataset.py /path/to/save +""" +import os +import sys + +def main(): + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + default_dir = os.path.join(repo_root, "robotwin-clean-and-aug-lerobot") + local_dir = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else default_dir + + try: + from huggingface_hub import snapshot_download + except ImportError: + print("请先安装: pip install huggingface_hub") + sys.exit(1) + + repo_id = "robbyant/robotwin-clean-and-aug-lerobot" + print(f"正在从 HuggingFace 下载数据集: {repo_id}") + print(f"保存到: {local_dir}") + os.makedirs(local_dir, exist_ok=True) + + try: + path = snapshot_download( + repo_id=repo_id, + repo_type="dataset", + local_dir=local_dir, + ) + except Exception as e: + print(f"下载失败: {e}") + sys.exit(1) + + print(f"\n下载完成: {path}") + print("训练时设置数据集路径:") + print(f" export LINGBOT_VA_DATASET_PATH={path}") + +if __name__ == "__main__": + main() diff --git a/script/download/download_modelscope.py b/script/download/download_modelscope.py new file mode 100644 index 0000000..2b03082 --- /dev/null +++ b/script/download/download_modelscope.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +""" +从 ModelScope 下载 LingBot-VA 模型 (lingbot-va-base)。 +用法: + python script/download_modelscope.py + python script/download_modelscope.py /path/to/save +""" +import os +import sys + +def main(): + # 默认保存到仓库根目录下的 lingbot-va-base + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + default_dir = os.path.join(repo_root, "lingbot-va-base") + local_dir = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else default_dir + + try: + from modelscope import snapshot_download + except ImportError: + print("请先安装 modelscope: pip install modelscope") + sys.exit(1) + + model_id = "Robbyant/lingbot-va-base" + print(f"正在从 ModelScope 下载: {model_id}") + print(f"保存到: {local_dir}") + os.makedirs(local_dir, exist_ok=True) + + try: + path = snapshot_download(model_id, local_dir=local_dir) + except Exception as e: + print(f"下载失败: {e}") + sys.exit(1) + + print(f"\n下载完成: {path}") + print("推理前请设置并修改 transformer 的 attn_mode:") + print(f" export LINGBOT_VA_MODEL_PATH={path}") + print(" # 编辑 {}/transformer/config.json 将 attn_mode 改为 \"torch\" 或 \"flashattn\"".format(path)) + +if __name__ == "__main__": + main() diff --git a/script/download/download_posttrain_robotwin.py b/script/download/download_posttrain_robotwin.py new file mode 100644 index 0000000..4f38681 --- /dev/null +++ b/script/download/download_posttrain_robotwin.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +""" +从 HuggingFace 或 ModelScope 下载 LingBot-VA RoboTwin 后训权重 (lingbot-va-posttrain-robotwin)。 +用于 RoboTwin 2.0 评测时达到论文报告成功率,需先下载再设置 LINGBOT_VA_MODEL_PATH。 + +用法: + python script/download/download_posttrain_robotwin.py + python script/download/download_posttrain_robotwin.py /path/to/save + HF_ENDPOINT=https://hf-mirror.com python script/download/download_posttrain_robotwin.py # 国内镜像 + +下载完成后评测前设置: + export LINGBOT_VA_MODEL_PATH=/path/to/lingbot-va-posttrain-robotwin + bash script/run_eval_robotwin.sh +""" +import os +import sys + +REPO_ID_HF = "robbyant/lingbot-va-posttrain-robotwin" +REPO_ID_MS = "Robbyant/lingbot-va-posttrain-robotwin" + + +def main(): + # 仓库根目录 = script/download 的上级的上级 + repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + default_dir = os.path.join(repo_root, "lingbot-va-posttrain-robotwin") + local_dir = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else default_dir + + use_modelscope = os.environ.get("USE_MODELSCOPE", "").lower() in ("1", "true", "yes") + + if use_modelscope: + try: + from modelscope import snapshot_download + except ImportError: + print("请先安装: pip install modelscope") + sys.exit(1) + print(f"正在从 ModelScope 下载: {REPO_ID_MS}") + print(f"保存到: {local_dir}") + os.makedirs(local_dir, exist_ok=True) + try: + path = snapshot_download(REPO_ID_MS, local_dir=local_dir) + except Exception as e: + print(f"下载失败: {e}") + sys.exit(1) + else: + try: + from huggingface_hub import snapshot_download + except ImportError: + print("请先安装: pip install huggingface_hub") + sys.exit(1) + print(f"正在从 HuggingFace 下载: {REPO_ID_HF}") + print(f"保存到: {local_dir}") + os.makedirs(local_dir, exist_ok=True) + try: + path = snapshot_download( + repo_id=REPO_ID_HF, + local_dir=local_dir, + ) + except Exception as e: + print(f"下载失败: {e}") + print(" 国内用户可试: HF_ENDPOINT=https://hf-mirror.com python script/download/download_posttrain_robotwin.py") + sys.exit(1) + + print(f"\n下载完成: {path}") + print("RoboTwin 评测前设置:") + print(f" export LINGBOT_VA_MODEL_PATH={path}") + print(" 或在运行脚本前: LINGBOT_VA_MODEL_PATH=%s bash script/run_eval_robotwin.sh" % path) + + +if __name__ == "__main__": + main() diff --git a/script/download/download_robotwin2.sh b/script/download/download_robotwin2.sh new file mode 100755 index 0000000..052fe77 --- /dev/null +++ b/script/download/download_robotwin2.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# RoboTwin 2.0 一键下载脚本 +# 功能:克隆 RoboTwin 仓库(若不存在)+ 下载并解压 **2.0 专用** assets,并写入资源路径配置。 +# +# 与 1.0 区分:默认安装到 RoboTwin-2.0,避免和已有 RoboTwin/1.0 共用同一 assets 目录造成混用或覆盖。 +# 若需同时保留 1.0:1.0 用例如 EAI/RoboTwin 或 EAI/RoboTwin-1.0,2.0 用本脚本默认 EAI/RoboTwin-2.0。 +# +# 用法(任选其一): +# # 从 lingbot-va 仓库根目录执行(推荐) +# bash script/download/download_robotwin2.sh +# bash script/download/download_robotwin2.sh /path/to/RoboTwin-2.0 +# +# # 国内镜像(HuggingFace 不稳定时) +# HF_ENDPOINT=https://hf-mirror.com bash script/download/download_robotwin2.sh +# BENCH_ROOT=/path/to/RoboTwin-2.0 bash script/download/download_robotwin2.sh +# +# 环境变量: +# BENCH_ROOT RoboTwin 2.0 安装目录,默认: .../EAI/RoboTwin-2.0(与 1.0 分目录) +# ROBOTWIN_COMMIT 使用的 git 提交,默认: 2eeec322(与官方测评一致) +# HF_ENDPOINT 可选,国内可设 https://hf-mirror.com 加速 assets 下载 + +set -e + +# 脚本所在目录 -> lingbot-va 仓库根 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +BENCH_ROOT="${1:-${BENCH_ROOT:-/mnt/users/wangyuxuan-20250915/EAI/RoboTwin-2.0}}" +ROBOTWIN_COMMIT="${ROBOTWIN_COMMIT:-2eeec322}" +# 转为绝对路径(若目录已存在) +if [ -d "$BENCH_ROOT" ]; then + BENCH_ROOT="$(cd "$BENCH_ROOT" && pwd)" +elif [ -d "$(dirname "$BENCH_ROOT")" ]; then + true +else + mkdir -p "$(dirname "$BENCH_ROOT")" +fi + +echo "============= RoboTwin 2.0 下载 ============= + lingbot-va 仓库: $REPO_ROOT + RoboTwin 目录: $BENCH_ROOT + git commit: $ROBOTWIN_COMMIT + HF_ENDPOINT: ${HF_ENDPOINT:-(未设置)} +================================================" + +# 1. 克隆 RoboTwin 仓库(若不存在) +if [ ! -d "$BENCH_ROOT" ] || [ ! -f "$BENCH_ROOT/script/_download_assets.sh" ]; then + if [ ! -d "$BENCH_ROOT" ]; then + echo ">>> 克隆 RoboTwin 到: $BENCH_ROOT" + mkdir -p "$(dirname "$BENCH_ROOT")" + git clone https://github.com/RoboTwin-Platform/RoboTwin.git "$BENCH_ROOT" + fi + cd "$BENCH_ROOT" + git fetch origin 2>/dev/null || true + git checkout "$ROBOTWIN_COMMIT" 2>/dev/null || true + cd - > /dev/null + echo ">>> 仓库就绪: $BENCH_ROOT" +else + echo ">>> 已存在 RoboTwin 目录,跳过 clone;如需指定 commit 可设 ROBOTWIN_COMMIT 后重新运行" + cd "$BENCH_ROOT" + git fetch origin 2>/dev/null || true + git checkout "$ROBOTWIN_COMMIT" 2>/dev/null || true + cd - > /dev/null +fi +# 确保之后使用绝对路径 +BENCH_ROOT="$(cd "$BENCH_ROOT" && pwd)" + +# 2. 下载并解压 assets(使用带重试与镜像的 Python 脚本) +echo "" +echo ">>> 下载 RoboTwin 2.0 assets(HuggingFace: TianxingChen/RoboTwin2.0)..." +if [ ! -f "$REPO_ROOT/script/download/download_robotwin_assets.py" ]; then + echo "Error: 未找到 $REPO_ROOT/script/download/download_robotwin_assets.py,请从 lingbot-va 仓库根目录执行本脚本。" + exit 1 +fi + +cd "$REPO_ROOT" +python script/download/download_robotwin_assets.py "$BENCH_ROOT" + +echo "" +echo "============= 下载完成 ============= + RoboTwin 2.0: $BENCH_ROOT + assets: $BENCH_ROOT/assets(仅 2.0 资源,与 1.0 分目录不混用) + +后续步骤(测评 / 运行仿真): + 1) 安装系统与 Python 依赖(见 RoboTwin 文档): + sudo apt install libvulkan1 mesa-vulkan-drivers vulkan-tools + cd $BENCH_ROOT && bash script/_install.sh + 2) 测评时指定 2.0 路径并运行 eval: + export ROBOTWIN_ROOT=$BENCH_ROOT + bash script/run_eval_robotwin.sh +==========================================" diff --git a/script/download/download_robotwin_assets.py b/script/download/download_robotwin_assets.py new file mode 100644 index 0000000..76794d8 --- /dev/null +++ b/script/download/download_robotwin_assets.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +""" +RoboTwin 2.0 测评 assets 下载(带重试 + 可选国内镜像)。 +HuggingFace 连接中断时可用此脚本重试;国内建议先设 HF_ENDPOINT 镜像。 + +用法: + python script/download_robotwin_assets.py + python script/download_robotwin_assets.py /mnt/users/wangyuxuan-20250915/EAI/RoboTwin + HF_ENDPOINT=https://hf-mirror.com python script/download_robotwin_assets.py /path/to/RoboTwin +""" +import os +import sys +import time + +def main(): + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + bench_root = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else os.environ.get("BENCH_ROOT", os.path.join(repo_root, "..", "RoboTwin")) + bench_root = os.path.normpath(bench_root) + assets_dir = os.path.join(bench_root, "assets") + os.makedirs(assets_dir, exist_ok=True) + + # 国内可设 HF_ENDPOINT=https://hf-mirror.com 加速/避免连接中断 + hf_endpoint = os.environ.get("HF_ENDPOINT", "") + if hf_endpoint: + os.environ["HF_ENDPOINT"] = hf_endpoint + print(f"Using HF_ENDPOINT: {hf_endpoint}") + + try: + from huggingface_hub import snapshot_download + except ImportError: + print("请先安装: pip install huggingface_hub") + sys.exit(1) + + repo_id = "TianxingChen/RoboTwin2.0" + patterns = ["background_texture.zip", "embodiments.zip", "objects.zip"] + max_retries = 3 + + print(f"下载 RoboTwin2.0 assets 到: {assets_dir}") + for attempt in range(1, max_retries + 1): + try: + snapshot_download( + repo_id=repo_id, + allow_patterns=patterns, + local_dir=assets_dir, + repo_type="dataset", + ) + print("下载完成.") + break + except Exception as e: + print(f"第 {attempt}/{max_retries} 次尝试失败: {e}") + if attempt < max_retries: + wait = 10 * attempt + print(f"{wait}s 后重试...") + time.sleep(wait) + else: + print("多次重试仍失败。建议:") + print(" 1) 国内用户设置镜像后重试: HF_ENDPOINT=https://hf-mirror.com python script/download_robotwin_assets.py", bench_root) + print(" 2) 或浏览器打开 https://huggingface.co/datasets/TianxingChen/RoboTwin2.0 手动下载上述 zip 放到", assets_dir, "后执行:") + print(" cd " + bench_root + " && bash script/_download_assets.sh # 仅解压与配置") + sys.exit(1) + + # 解压与配置(与 RoboTwin 原脚本一致) + import subprocess + orig_cwd = os.getcwd() + try: + os.chdir(assets_dir) + for name in ["background_texture.zip", "embodiments.zip", "objects.zip"]: + if os.path.isfile(name): + subprocess.check_call(["unzip", "-o", name], shell=False) + os.remove(name) + os.chdir(bench_root) + if os.path.isfile(os.path.join(bench_root, "script", "update_embodiment_config_path.py")): + subprocess.check_call([sys.executable, "script/update_embodiment_config_path.py"], cwd=bench_root) + print("解压与路径配置完成.") + finally: + os.chdir(orig_cwd) + +if __name__ == "__main__": + main() diff --git a/script/run_eval_robotwin.sh b/script/run_eval_robotwin.sh new file mode 100755 index 0000000..682b075 --- /dev/null +++ b/script/run_eval_robotwin.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# RoboTwin 2.0 测评脚本(LingBot-VA) +# 使用前:已下载 RoboTwin 及 assets 到 ROBOTWIN_ROOT(默认 EAI/RoboTwin) +# 重要:eval client 会启动 RoboTwin 仿真,当前 conda 环境需安装 RoboTwin 依赖(sapien、mplib 等), +# 否则会报 ModuleNotFoundError: No module named 'sapien'。见下方「依赖」说明。 +# +# 用法(在 lingbot-va 仓库根目录执行): +# bash script/run_eval_robotwin.sh # 单任务 adjust_bottle,结果到 ./results +# bash script/run_eval_robotwin.sh ./my_results # 指定结果目录 +# bash script/run_eval_robotwin.sh ./out stack_bowls_three # 指定任务名 +# bash script/run_eval_robotwin.sh ./out adjust_bottle 10 # 结果目录 + 任务 + test_num +# 测完全集(50 任务 × 100 条):bash script/run_eval_robotwin_full.sh [save_root] [test_num] +# 是否渲染/保存预测视频(VAE 解码+对比视频,较耗时):SAVE_VISUALIZATION=1 bash script/run_eval_robotwin.sh ... # 默认 0 关闭 +# 是否启用 offload(1=把 VAE 放 CPU 节省显存,0=VAE 常驻 GPU 加速 encode_obs):ENABLE_OFFLOAD=0 bash script/run_eval_robotwin.sh ... + +set -e + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# 最先激活 lingbot-va 环境,保证后续 python/sapien 检查及 server/client 均在该环境下运行 +CONDA_BASE="" +if [[ -n "$CONDA_EXE" ]]; then + CONDA_BASE="${CONDA_EXE%/bin/conda}" +fi +if [[ -z "$CONDA_BASE" ]]; then + CONDA_BASE="$(conda info --base 2>/dev/null)" || true +fi +if [[ -z "$CONDA_BASE" ]]; then + for d in "$HOME/miniconda3" "$HOME/miniforge3" "$HOME/anaconda3" "/opt/conda"; do + if [[ -f "${d}/etc/profile.d/conda.sh" ]]; then + CONDA_BASE="$d" + break + fi + done +fi +if [[ -n "$CONDA_BASE" && -f "$CONDA_BASE/etc/profile.d/conda.sh" ]]; then + source "$CONDA_BASE/etc/profile.d/conda.sh" + conda activate lingbot-va + echo ">>> 已自动激活 conda 环境: lingbot-va" +else + echo "Error: 未检测到 conda 或无法找到 lingbot-va 环境。" + echo " 请安装 conda 并创建环境: conda create -n lingbot-va python=3.x && conda activate lingbot-va" + exit 1 +fi + +ROBOTWIN_ROOT="${ROBOTWIN_ROOT:-/mnt/users/wangyuxuan-20250915/EAI/RoboTwin-2.0}" +export ROBOTWIN_ROOT + +# 模型路径:优先 models/,其次仓库根下 posttrain,否则 base +if [ -n "$LINGBOT_VA_MODEL_PATH" ]; then + : +elif [ -d "$REPO_ROOT/models/lingbot-va-posttrain-robotwin" ]; then + export LINGBOT_VA_MODEL_PATH="$REPO_ROOT/models/lingbot-va-posttrain-robotwin" +elif [ -d "$REPO_ROOT/lingbot-va-posttrain-robotwin" ]; then + export LINGBOT_VA_MODEL_PATH="$REPO_ROOT/lingbot-va-posttrain-robotwin" +else + export LINGBOT_VA_MODEL_PATH="${LINGBOT_VA_MODEL_PATH:-$REPO_ROOT/lingbot-va-base}" +fi +export CUDA_VISIBLE_DEVICES="${CUDA_VISIBLE_DEVICES:-5}" +export LD_LIBRARY_PATH="/usr/lib64:/usr/lib:${LD_LIBRARY_PATH:-}" + +# 可调参数:server 使用的 GPU 进程数(默认 1,多卡时需 server 支持) +NPROC_PER_NODE="${NPROC_PER_NODE:-1}" + +# 解析参数 +save_root="${1:-./results}" +task_name="${2:-adjust_bottle}" +test_num="${3:-10}" +PORT="${PORT:-29056}" +MASTER_PORT="${MASTER_PORT:-29501}" +# 是否渲染并保存预测视频(0=关闭可加速,1=开启保存对比视频) +save_visualization="${SAVE_VISUALIZATION:-0}" +# 是否把 VAE/text encoder offload 到 CPU(默认 0,优先速度) +enable_offload="${ENABLE_OFFLOAD:-0}" +export ENABLE_OFFLOAD="$enable_offload" + +# 实际推理为 LingBot-VA(WebsocketClientPolicy),结果目录与日志用 lingbot;deploy 配置沿用 RoboTwin 的 ACT 配置 +policy_name=lingbot +policy_config="${POLICY_CONFIG:-ACT}" +task_config=demo_clean +train_config_name=0 +model_name=0 +seed=0 + +echo "============= RoboTwin Eval (LingBot-VA) ============= + REPO_ROOT = $REPO_ROOT + ROBOTWIN_ROOT = $ROBOTWIN_ROOT + LINGBOT_VA_MODEL_PATH = $LINGBOT_VA_MODEL_PATH + policy_name = $policy_name (LingBot-VA) + policy_config = $policy_config (deploy yml) + save_root = $save_root + task_name = $task_name + test_num = $test_num + PORT = $PORT + NPROC_PER_NODE = $NPROC_PER_NODE + CUDA_VISIBLE_DEVICES = $CUDA_VISIBLE_DEVICES + SAVE_VISUALIZATION = $save_visualization (0=关 1=开预测视频) + ENABLE_OFFLOAD = $enable_offload (0=VAE在GPU加速 1=offload到CPU省显存) +========================================================" + +# 首次运行:更新 embodiment 配置中的资源路径(从 _tmp.yml 生成 .yml) +if [ -d "$ROBOTWIN_ROOT/assets/embodiments" ] && [ -f "$ROBOTWIN_ROOT/script/update_embodiment_config_path.py" ]; then + if [ -n "$(find "$ROBOTWIN_ROOT/assets/embodiments" -name '*_tmp.yml' 2>/dev/null | head -1)" ]; then + echo ">>> 更新 RoboTwin embodiment 配置路径..." + (cd "$ROBOTWIN_ROOT" && python script/update_embodiment_config_path.py >> 启动 LingBot-VA server (port=$PORT, nproc_per_node=$NPROC_PER_NODE),日志: $server_log ..." +PYTHONWARNINGS=ignore::UserWarning python -m torch.distributed.run \ + --nproc_per_node "$NPROC_PER_NODE" \ + --master_port "$MASTER_PORT" \ + wan_va/wan_va_server.py \ + --config-name robotwin \ + --port "$PORT" \ + --save_root "$save_root" \ + >> "$server_log" 2>&1 & +SERVER_PID=$! +trap "kill $SERVER_PID 2>/dev/null || true" EXIT + +echo ">>> 启动 eval client (task=$task_name, test_num=$test_num) ..." +PYTHONWARNINGS=ignore::UserWarning \ +XLA_PYTHON_CLIENT_MEM_FRACTION=0.9 python -m evaluation.robotwin.eval_polict_client_openpi \ + --config "$ROBOTWIN_ROOT/policy/$policy_config/deploy_policy.yml" \ + --overrides \ + --task_name "$task_name" \ + --task_config "$task_config" \ + --train_config_name "$train_config_name" \ + --model_name "$model_name" \ + --ckpt_setting "$model_name" \ + --seed "$seed" \ + --policy_name "$policy_name" \ + --save_root "$save_root" \ + --video_guidance_scale 5 \ + --action_guidance_scale 1 \ + --test_num "$test_num" \ + --save_visualization "$save_visualization" \ + --port "$PORT" + +echo ">>> Eval 结束,结果见: $save_root" diff --git a/script/run_eval_robotwin_client_only.sh b/script/run_eval_robotwin_client_only.sh new file mode 100755 index 0000000..58b2503 --- /dev/null +++ b/script/run_eval_robotwin_client_only.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# 仅运行 RoboTwin eval client(需先在另一终端启动 LingBot-VA server) +# 用法(在 lingbot-va 仓库根目录执行): +# bash script/run_eval_robotwin_client_only.sh +# bash script/run_eval_robotwin_client_only.sh ./results adjust_bottle 20 +# export PORT=29056; bash script/run_eval_robotwin_client_only.sh + +set -e + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ROBOTWIN_ROOT="${ROBOTWIN_ROOT:-/mnt/users/wangyuxuan-20250915/EAI/RoboTwin-2.0}" +export ROBOTWIN_ROOT + +save_root="${1:-./results}" +task_name="${2:-adjust_bottle}" +test_num="${3:-100}" +PORT="${PORT:-29056}" + +policy_name=ACT +task_config=demo_clean +train_config_name=0 +model_name=0 +seed=0 + +export LD_LIBRARY_PATH="/usr/lib64:/usr/lib:${LD_LIBRARY_PATH:-}" + +echo "Client only: ROBOTWIN_ROOT=$ROBOTWIN_ROOT save_root=$save_root task_name=$task_name test_num=$test_num port=$PORT" +cd "$REPO_ROOT" +mkdir -p "$save_root" + +PYTHONWARNINGS=ignore::UserWarning \ +XLA_PYTHON_CLIENT_MEM_FRACTION=0.9 python -m evaluation.robotwin.eval_polict_client_openpi \ + --config "$ROBOTWIN_ROOT/policy/$policy_name/deploy_policy.yml" \ + --overrides \ + --task_name "$task_name" \ + --task_config "$task_config" \ + --train_config_name "$train_config_name" \ + --model_name "$model_name" \ + --ckpt_setting "$model_name" \ + --seed "$seed" \ + --policy_name "$policy_name" \ + --save_root "$save_root" \ + --video_guidance_scale 5 \ + --action_guidance_scale 1 \ + --test_num "$test_num" \ + --port "$PORT" diff --git a/script/run_eval_robotwin_full.sh b/script/run_eval_robotwin_full.sh new file mode 100755 index 0000000..dd666a0 --- /dev/null +++ b/script/run_eval_robotwin_full.sh @@ -0,0 +1,298 @@ +#!/usr/bin/env bash +# RoboTwin 2.0 完全集测评(50 个任务 × test_num) +# 支持单卡顺序跑或多卡并行:通过 NUM_GPUS 或第 3 个参数指定 GPU 数,>1 时每卡起一个 server,任务按卡分批并行。 +# 与 run_eval_robotwin.sh 同环境要求。 +# +# 用法(在 lingbot-va 仓库根目录执行): +# bash script/run_eval_robotwin_full.sh # 单卡顺序,结果 ./results,每任务 100 条 +# bash script/run_eval_robotwin_full.sh ./my_results # 指定结果目录 +# bash script/run_eval_robotwin_full.sh ./my_results 100 4 # 结果目录 + test_num + 4 卡并行 +# NUM_GPUS=8 bash script/run_eval_robotwin_full.sh ./out # 8 卡并行(50 任务分 8 批) +# GPU_IDS=2,3,4,5 bash script/run_eval_robotwin_full.sh ./out 100 4 # 使用指定 GPU 4卡并行 +# GPU_IDS=1 bash script/run_eval_robotwin_full.sh ./out # 单卡运行,使用 GPU 1 +# +# 可选环境变量:ROBOTWIN_ROOT, LINGBOT_VA_MODEL_PATH, NUM_GPUS, GPU_IDS, START_PORT, MASTER_PORT_BASE + +set -e + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# 最先激活 lingbot-va 环境(与 run_eval_robotwin.sh 一致) +CONDA_BASE="" +if [[ -n "$CONDA_EXE" ]]; then + CONDA_BASE="${CONDA_EXE%/bin/conda}" +fi +if [[ -z "$CONDA_BASE" ]]; then + CONDA_BASE="$(conda info --base 2>/dev/null)" || true +fi +if [[ -z "$CONDA_BASE" ]]; then + for d in "$HOME/miniconda3" "$HOME/miniforge3" "$HOME/anaconda3" "/opt/conda"; do + if [[ -f "${d}/etc/profile.d/conda.sh" ]]; then + CONDA_BASE="$d" + break + fi + done +fi +if [[ -n "$CONDA_BASE" && -f "$CONDA_BASE/etc/profile.d/conda.sh" ]]; then + source "$CONDA_BASE/etc/profile.d/conda.sh" + conda activate lingbot-va + echo ">>> 已自动激活 conda 环境: lingbot-va" +else + echo "Error: 未检测到 conda 或无法找到 lingbot-va 环境。" + echo " 请安装 conda 并创建环境: conda create -n lingbot-va python=3.x && conda activate lingbot-va" + exit 1 +fi + +ROBOTWIN_ROOT="${ROBOTWIN_ROOT:-/mnt/users/wangyuxuan-20250915/EAI/RoboTwin-2.0}" +export ROBOTWIN_ROOT + +# 模型路径:优先 models/,其次仓库根下 posttrain,否则 base(与 run_eval_robotwin.sh 一致) +if [ -n "$LINGBOT_VA_MODEL_PATH" ]; then + : +elif [ -d "$REPO_ROOT/models/lingbot-va-posttrain-robotwin" ]; then + export LINGBOT_VA_MODEL_PATH="$REPO_ROOT/models/lingbot-va-posttrain-robotwin" +elif [ -d "$REPO_ROOT/lingbot-va-posttrain-robotwin" ]; then + export LINGBOT_VA_MODEL_PATH="$REPO_ROOT/lingbot-va-posttrain-robotwin" +else + export LINGBOT_VA_MODEL_PATH="${LINGBOT_VA_MODEL_PATH:-$REPO_ROOT/lingbot-va-base}" +fi +export CUDA_VISIBLE_DEVICES="${CUDA_VISIBLE_DEVICES:-0,1,2,3,4,5,6,7}" +export LD_LIBRARY_PATH="/usr/lib64:/usr/lib:${LD_LIBRARY_PATH:-}" + +save_root="${1:-./results}" +test_num="${2:-100}" +num_gpus="${3:-${NUM_GPUS:-6}}" +num_gpus=$((num_gpus)) +GPU_IDS="${GPU_IDS:-2,3,4,5,6,7}" +START_PORT="${START_PORT:-29556}" +MASTER_PORT_BASE="${MASTER_PORT_BASE:-29661}" +PORT="${PORT:-29056}" +MASTER_PORT="${MASTER_PORT:-29501}" + +# 实际推理为 LingBot-VA,结果目录与日志用 lingbot;deploy 配置沿用 RoboTwin 的 ACT +policy_name=lingbot +policy_config="${POLICY_CONFIG:-ACT}" +task_config=demo_clean +train_config_name=0 +model_name=0 +seed=0 + +# 50 个任务(与 evaluation/robotwin/calc_stat.py TASK_CLASS 一致) +# ROBOTWIN_ALL_TASKS=( +# adjust_bottle beat_block_hammer blocks_ranking_rgb blocks_ranking_size click_alarmclock +# click_bell dump_bin_bigbin grab_roller handover_block handover_mic hanging_mug +# lift_pot move_can_pot move_pillbottle_pad move_playingcard_away move_stapler_pad +# open_laptop open_microwave pick_diverse_bottles pick_dual_bottles place_a2b_left +# place_a2b_right place_bread_basket place_bread_skillet place_burger_fries place_can_basket +# place_cans_plasticbox place_container_plate place_dual_shoes place_empty_cup place_fan +# place_mouse_pad place_object_basket place_object_scale place_object_stand place_phone_stand +# place_shoe press_stapler put_bottles_dustbin put_object_cabinet rotate_qrcode scan_object +# shake_bottle shake_bottle_horizontally stack_blocks_three stack_blocks_two stack_bowls_three +# stack_bowls_two stamp_seal turn_switch +# ) + +ROBOTWIN_ALL_TASKS=( + adjust_bottle beat_block_hammer blocks_ranking_rgb blocks_ranking_size click_alarmclock + click_bell dump_bin_bigbin grab_roller handover_block handover_mic hanging_mug + lift_pot move_can_pot move_pillbottle_pad move_playingcard_away move_stapler_pad + open_laptop open_microwave pick_diverse_bottles pick_dual_bottles place_a2b_left + place_a2b_right place_bread_basket place_bread_skillet place_burger_fries place_can_basket + place_cans_plasticbox place_container_plate place_dual_shoes place_empty_cup place_fan + place_mouse_pad place_object_basket place_object_scale place_object_stand place_phone_stand + place_shoe press_stapler put_bottles_dustbin put_object_cabinet rotate_qrcode scan_object + shake_bottle shake_bottle_horizontally stack_blocks_three stack_blocks_two stack_bowls_three + stack_bowls_two stamp_seal turn_switch +) + +echo "============= RoboTwin 完全集 Eval (LingBot-VA) ============= + REPO_ROOT = $REPO_ROOT + ROBOTWIN_ROOT = $ROBOTWIN_ROOT + LINGBOT_VA_MODEL_PATH = $LINGBOT_VA_MODEL_PATH + policy_name = $policy_name (LingBot-VA) + policy_config = $policy_config (deploy yml) + save_root = $save_root + test_num per task = $test_num (每任务条数) + 任务数 = ${#ROBOTWIN_ALL_TASKS[@]} (共 50 个任务) + num_gpus = $num_gpus + GPU_IDS = ${GPU_IDS:-自动按卡分配 (0,1,2...)} + START_PORT = $START_PORT + PORT = $PORT + CUDA_VISIBLE_DEVICES = $CUDA_VISIBLE_DEVICES +================================================================" + +if [ ! -d "$ROBOTWIN_ROOT" ]; then + echo "Error: ROBOTWIN_ROOT 不存在: $ROBOTWIN_ROOT" + exit 1 +fi + +if [ ! -d "$ROBOTWIN_ROOT/assets" ]; then + echo "Error: 未找到 $ROBOTWIN_ROOT/assets,请先下载 RoboTwin assets" + echo " 例: cd $REPO_ROOT && python script/download/download_robotwin_assets.py $ROBOTWIN_ROOT" + exit 1 +fi + +# 检查 RoboTwin 仿真依赖(client 会 import envs -> sapien)(与 run_eval_robotwin.sh 一致) +if ! python -c "import sapien" 2>/dev/null; then + echo "Error: 当前环境缺少 RoboTwin 仿真依赖 'sapien',导致 client 报错 ModuleNotFoundError." + echo " 请在当前 conda 环境中安装 RoboTwin 依赖后再运行本脚本,例如:" + echo " pip install sapien==3.0.0b1" + echo " pip install -r $ROBOTWIN_ROOT/script/requirements.txt" + echo " 并按 RoboTwin 文档完成 script/_install.sh(pytorch3d、mplib 补丁、curobo 等)。" + echo " 详见: $ROBOTWIN_ROOT/INSTALLATION.md 或 README" + exit 1 +fi + +# 首次运行:更新 embodiment 配置中的资源路径(从 _tmp.yml 生成 .yml) +if [ -d "$ROBOTWIN_ROOT/assets/embodiments" ] && [ -f "$ROBOTWIN_ROOT/script/update_embodiment_config_path.py" ]; then + if [ -n "$(find "$ROBOTWIN_ROOT/assets/embodiments" -name '*_tmp.yml' 2>/dev/null | head -1)" ]; then + echo ">>> 更新 RoboTwin embodiment 配置路径..." + (cd "$ROBOTWIN_ROOT" && python script/update_embodiment_config_path.py >> 启动 LingBot-VA server (单卡 GPU=$CUDA_VISIBLE_DEVICES, port=$PORT) ..." + PYTHONWARNINGS=ignore::UserWarning python -m torch.distributed.run \ + --nproc_per_node "$num_gpus" \ + --master_port "$MASTER_PORT" \ + wan_va/wan_va_server.py \ + --config-name robotwin \ + --port "$PORT" \ + --save_root "$save_root" & + SERVER_PID=$! + trap "kill $SERVER_PID 2>/dev/null || true" EXIT + + sleep 15 + if ! kill -0 $SERVER_PID 2>/dev/null; then + echo "Error: Server 启动失败,请检查日志(如 attn_mode 是否为 torch/flashattn)" + exit 1 + fi + + total=${#ROBOTWIN_ALL_TASKS[@]} + for i in "${!ROBOTWIN_ALL_TASKS[@]}"; do + task_name="${ROBOTWIN_ALL_TASKS[$i]}" + echo "" + echo ">>> [$((i+1))/$total] 启动 eval client: task=$task_name, test_num=$test_num" + PYTHONWARNINGS=ignore::UserWarning \ + XLA_PYTHON_CLIENT_MEM_FRACTION=0.9 python -m evaluation.robotwin.eval_polict_client_openpi \ + --config "$ROBOTWIN_ROOT/policy/$policy_config/deploy_policy.yml" \ + --overrides \ + --task_name "$task_name" \ + --task_config "$task_config" \ + --train_config_name "$train_config_name" \ + --model_name "$model_name" \ + --ckpt_setting "$model_name" \ + --seed "$seed" \ + --policy_name "$policy_name" \ + --save_root "$save_root" \ + --video_guidance_scale 5 \ + --action_guidance_scale 1 \ + --test_num "$test_num" \ + --port "$PORT" + done +else + # ---------- 多卡:每卡一个 server,任务分批并行 ---------- + # 解析 GPU_IDS + if [ -z "$GPU_IDS" ]; then + GPU_ARRAY=() + for ((i=0; i/dev/null || true; done' EXIT + + echo ">>> 启动 $num_gpus 个 LingBot-VA server (GPU=${GPU_ARRAY[*]}, ports $START_PORT..$((START_PORT + num_gpus - 1))) ..." + for ((g=0; g/dev/null; then + echo "Error: Server GPU $g 启动失败,请检查日志(如 attn_mode 是否为 torch/flashattn)" + exit 1 + fi + done + + total=${#ROBOTWIN_ALL_TASKS[@]} + batch_time=$(date +%Y%m%d_%H%M%S) + log_dir="${save_root}/logs" + mkdir -p "$log_dir" + + for ((batch_start=0; batch_start>> 批次 $batch_num/$batch_total (任务 $((batch_start+1))..${end_idx}/$total)" + PIDS=() + for ((j=0; j "$log_file" 2>&1 & + PIDS+=($!) + done + for p in "${PIDS[@]}"; do wait "$p" || true; done + done +fi + +echo "" +echo ">>> 完全集 Eval 结束,结果见: $save_root" +if [ "$num_gpus" -gt 1 ]; then + echo " 各任务日志: $save_root/logs/" +fi +echo " 汇总成功率: python -m evaluation.robotwin.calc_stat $save_root/stseed-0/visualization" diff --git a/script/run_eval_robotwin_slurm.sh b/script/run_eval_robotwin_slurm.sh new file mode 100755 index 0000000..e012138 --- /dev/null +++ b/script/run_eval_robotwin_slurm.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +#SBATCH -p a100_global +#SBATCH -N 1 +#SBATCH --gres=gpu:4 +# 多卡并行:改为 --gres=gpu:4 则起 4 个 server+4 个 client 并行跑 4 个任务(每卡一个) +#SBATCH --cpus-per-task=12 +#SBATCH --ntasks=1 +#SBATCH --job-name=robotwin_va +#SBATCH --time=48:00:00 +#SBATCH --output=logs/robotwin_%x-%j.out +#SBATCH --error=logs/robotwin_%x-%j.err +# +# RoboTwin 2.0 测评脚本(LingBot-VA)- Slurm 版 +# 用法(在 lingbot-va 仓库根目录执行): +# sbatch script/run_eval_robotwin_slurm.sh +# sbatch script/run_eval_robotwin_slurm.sh ./my_results +# sbatch script/run_eval_robotwin_slurm.sh ./out stack_bowls_three +# sbatch script/run_eval_robotwin_slurm.sh ./out adjust_bottle 10 +# 可选环境变量(提交前 export 或 sbatch 前设置): +# SAVE_ROOT, TASK_NAME, TEST_NUM, PORT, NPROC_PER_NODE, CUDA_VISIBLE_DEVICES, SLURM_GPUS +# 多卡并行:sbatch --gres=gpu:4 时起 4 个 server(每卡一个)+ 4 个 client 并行跑 4 个任务; +# 可通过 TASK_LIST="t1 t2 t3 t4" 指定任务,否则用前 4 个 from 50 任务列表。 + +set -e + +NGPU="${SLURM_GPUS_ON_NODE:-${SLURM_GPUS:-1}}" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +ROBOTWIN_ROOT="${ROBOTWIN_ROOT:-/mnt/users/wangyuxuan-20250915/EAI/RoboTwin-2.0}" +export ROBOTWIN_ROOT + +# 50 任务列表(多卡时取前 N 个,或由 TASK_LIST 指定) +ROBOTWIN_ALL_TASKS=( + adjust_bottle beat_block_hammer blocks_ranking_rgb blocks_ranking_size click_alarmclock + click_bell dump_bin_bigbin grab_roller handover_block handover_mic hanging_mug + lift_pot move_can_pot move_pillbottle_pad move_playingcard_away move_stapler_pad + open_laptop open_microwave pick_diverse_bottles pick_dual_bottles place_a2b_left + place_a2b_right place_bread_basket place_bread_skillet place_burger_fries place_can_basket + place_cans_plasticbox place_container_plate place_dual_shoes place_empty_cup place_fan + place_mouse_pad place_object_basket place_object_scale place_object_stand place_phone_stand + place_shoe press_stapler put_bottles_dustbin put_object_cabinet rotate_qrcode scan_object + shake_bottle shake_bottle_horizontally stack_blocks_three stack_blocks_two stack_bowls_three + stack_bowls_two stamp_seal turn_switch +) + +# 模型路径(优先 models/ 下,其次仓库根下) +if [ -n "$LINGBOT_VA_MODEL_PATH" ]; then + : +elif [ -d "$REPO_ROOT/models/lingbot-va-posttrain-robotwin" ]; then + export LINGBOT_VA_MODEL_PATH="$REPO_ROOT/models/lingbot-va-posttrain-robotwin" +elif [ -d "$REPO_ROOT/lingbot-va-posttrain-robotwin" ]; then + export LINGBOT_VA_MODEL_PATH="$REPO_ROOT/lingbot-va-posttrain-robotwin" +else + export LINGBOT_VA_MODEL_PATH="${LINGBOT_VA_MODEL_PATH:-$REPO_ROOT/lingbot-va-base}" +fi +# 使用前 NGPU 张卡(未设 CUDA_VISIBLE_DEVICES 时;单卡即为 0) +if [[ -z "$CUDA_VISIBLE_DEVICES" ]]; then + export CUDA_VISIBLE_DEVICES="$(seq -s, 0 $((NGPU-1)))" +fi +export LD_LIBRARY_PATH="/usr/lib64:/usr/lib:${LD_LIBRARY_PATH:-}" + +save_root="${SAVE_ROOT:-${1:-./results}}" +task_name="${TASK_NAME:-${2:-adjust_bottle}}" +test_num="${TEST_NUM:-${3:-100}}" +PORT="${PORT:-29056}" +MASTER_PORT="${MASTER_PORT:-29501}" + +policy_name=ACT +task_config=demo_clean +train_config_name=0 +model_name=0 +seed=0 + +# 多卡时每卡 1 个 server(1 进程),单卡时也是 1 进程 +NPROC_PER_NODE=1 + +echo "============= RoboTwin Eval (LingBot-VA) [Slurm] ============= + JOB_ID = ${SLURM_JOB_ID:-N/A} + NGPU = $NGPU + REPO_ROOT = $REPO_ROOT + ROBOTWIN_ROOT = $ROBOTWIN_ROOT + LINGBOT_VA_MODEL_PATH = $LINGBOT_VA_MODEL_PATH + save_root = $save_root + task_name = $task_name + test_num = $test_num + PORT = $PORT + CUDA_VISIBLE_DEVICES = $CUDA_VISIBLE_DEVICES +========================================================" + +if [ ! -d "$ROBOTWIN_ROOT" ]; then + echo "Error: ROBOTWIN_ROOT 不存在: $ROBOTWIN_ROOT" + exit 1 +fi + +if [ ! -d "$ROBOTWIN_ROOT/assets" ]; then + echo "Error: 未找到 $ROBOTWIN_ROOT/assets,请先下载 RoboTwin assets" + exit 1 +fi + +if ! python -c "import sapien" 2>/dev/null; then + echo "Error: 当前环境缺少 RoboTwin 仿真依赖 'sapien'." + exit 1 +fi + +if [ -d "$ROBOTWIN_ROOT/assets/embodiments" ] && [ -f "$ROBOTWIN_ROOT/script/update_embodiment_config_path.py" ]; then + if [ -n "$(find "$ROBOTWIN_ROOT/assets/embodiments" -name '*_tmp.yml' 2>/dev/null | head -1)" ]; then + (cd "$ROBOTWIN_ROOT" && python script/update_embodiment_config_path.py >> 启动 LingBot-VA server (单卡 port=$PORT) ..." + PYTHONWARNINGS=ignore::UserWarning python -m torch.distributed.run \ + --nproc_per_node 1 \ + --master_port "$MASTER_PORT" \ + wan_va/wan_va_server.py \ + --config-name robotwin \ + --port "$PORT" \ + --save_root "$save_root" & + SERVER_PID=$! + trap "kill $SERVER_PID 2>/dev/null || true" EXIT + + sleep 15 + if ! kill -0 $SERVER_PID 2>/dev/null; then + echo "Error: Server 启动失败" + exit 1 + fi + + echo ">>> 启动 eval client (task=$task_name, test_num=$test_num) ..." + PYTHONWARNINGS=ignore::UserWarning \ + XLA_PYTHON_CLIENT_MEM_FRACTION=0.9 python -m evaluation.robotwin.eval_polict_client_openpi \ + --config "$ROBOTWIN_ROOT/policy/$policy_name/deploy_policy.yml" \ + --overrides \ + --task_name "$task_name" \ + --task_config "$task_config" \ + --train_config_name "$train_config_name" \ + --model_name "$model_name" \ + --ckpt_setting "$model_name" \ + --seed "$seed" \ + --policy_name "$policy_name" \ + --save_root "$save_root" \ + --video_guidance_scale 5 \ + --action_guidance_scale 1 \ + --test_num "$test_num" \ + --port "$PORT" +else + # ---------- 多卡:每卡 1 个 server + 1 个 client,并行跑 N 个任务 ---------- + if [ -n "$TASK_LIST" ]; then + TASKS=($TASK_LIST) + else + TASKS=("${ROBOTWIN_ALL_TASKS[@]:0:$NGPU}") + fi + if [ ${#TASKS[@]} -lt "$NGPU" ]; then + echo "Error: 任务数 ${#TASKS[@]} 小于 NGPU=$NGPU,请设置 TASK_LIST=\"t1 t2 ...\"" + exit 1 + fi + + SERVER_PIDS=() + trap 'for p in "${SERVER_PIDS[@]}"; do kill $p 2>/dev/null || true; done' EXIT + + echo ">>> 启动 $NGPU 个 LingBot-VA server (ports $PORT..$((PORT+NGPU-1))) ..." + for ((g=0; g/dev/null; then + echo "Error: Server GPU $g 启动失败" + exit 1 + fi + done + + echo ">>> 启动 $NGPU 个 eval client 并行 (tasks: ${TASKS[*]}) ..." + PIDS=() + for ((g=0; g>> Eval 结束,结果见: $save_root" diff --git a/script/run_i2va_single_gpu.sh b/script/run_i2va_single_gpu.sh new file mode 100644 index 0000000..da90bd7 --- /dev/null +++ b/script/run_i2va_single_gpu.sh @@ -0,0 +1,38 @@ +#!/usr/bin/bash +# 单 GPU 运行 Image-to-Video-Action 推理(用于调试/本地跑通) +# 使用前请设置模型路径并准备首帧图像,见 INFERENCE.md +# +# 从仓库根目录执行: bash script/run_i2va_single_gpu.sh + +set -e + +NGPU=${NGPU:-1} +CONFIG_NAME=${CONFIG_NAME:-robotwin_i2av} +# 模型目录:需包含 vae / tokenizer / text_encoder / transformer 子目录 +export LINGBOT_VA_MODEL_PATH=${LINGBOT_VA_MODEL_PATH:-"/path/to/pretrained/model"} + +if [ "$LINGBOT_VA_MODEL_PATH" = "/path/to/pretrained/model" ]; then + echo "Error: 请设置环境变量 LINGBOT_VA_MODEL_PATH 为已下载的 LingBot-VA 模型目录" + echo " export LINGBOT_VA_MODEL_PATH=/path/to/lingbot-va-base" + echo " 并确保该目录下存在: vae/ tokenizer/ text_encoder/ transformer/" + exit 1 +fi + +if [ ! -d "$LINGBOT_VA_MODEL_PATH/transformer" ]; then + echo "Error: 未找到 $LINGBOT_VA_MODEL_PATH/transformer" + echo " 请从 HuggingFace/ModelScope 下载 lingbot-va-base 并解压到该路径" + exit 1 +fi + +# 推理前请将 transformer/config.json 中 attn_mode 设为 "torch" 或 "flashattn"(不能是 "flex") +echo "Config: NGPU=$NGPU CONFIG_NAME=$CONFIG_NAME" +echo "Model: $LINGBOT_VA_MODEL_PATH" +echo "" + +export TOKENIZERS_PARALLELISM=false +cd "$(dirname "$0")/.." + +PYTORCH_CUDA_ALLOC_CONF="expandable_segments:True" python -m torch.distributed.run \ + --nproc_per_node="$NGPU" \ + --master_port=29501 \ + -m wan_va.wan_va_server --config-name "$CONFIG_NAME" "$@" diff --git a/script/setup/install_flash_attn_cu124.sh b/script/setup/install_flash_attn_cu124.sh new file mode 100755 index 0000000..fd70624 --- /dev/null +++ b/script/setup/install_flash_attn_cu124.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# 从根本上解决 flash_attn 与 PyTorch 2.6.0+cu124 的 ABI 兼容:安装匹配的 wheel 或从源码用正确 ABI 编译 +# 用法: bash script/install_flash_attn_cu124.sh [lingbot-va] +# 要求: 已安装 torch==2.6.0+cu124(且 torch._C._GLIBCXX_USE_CXX11_ABI == False) + +set -e + +CONDA_ENV="${1:-lingbot-va}" +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# 初始化 conda +CONDA_BASE="${CONDA_EXE%/bin/conda}" +[[ -z "$CONDA_BASE" ]] && CONDA_BASE="$(conda info --base 2>/dev/null)" || true +if [[ -n "$CONDA_BASE" && -f "$CONDA_BASE/etc/profile.d/conda.sh" ]]; then + source "$CONDA_BASE/etc/profile.d/conda.sh" +fi + +echo "=== 检查当前 PyTorch 与 ABI ===" +conda activate "$CONDA_ENV" +python -c " +import torch +v = torch.__version__ +cuda = torch.version.cuda +abi = torch._C._GLIBCXX_USE_CXX11_ABI +print(f'PyTorch: {v}, CUDA: {cuda}, CXX11_ABI: {abi}') +if not v.startswith('2.6') or cuda != '12.4': + print('Warning: 本脚本针对 torch 2.6.x+cu124 与 cxx11abiFALSE。当前环境可能不匹配。') +if abi is not False: + print('Warning: 官方 PyTorch wheel 通常为 CXX11_ABI=False。若用 cxx11abiTRUE 的 flash_attn 会报 undefined symbol。') +" + +echo "" +echo ">>> 卸载已有 flash-attn(若存在)" +pip uninstall -y flash-attn 2>/dev/null || true + +# 方案 1:使用社区预编译 wheel(torch2.6 + cu124 + cp310,manylinux 与官方 PyTorch ABI 一致) +# 来源: https://github.com/mjun0812/flash-attention-prebuild-wheels/releases (v0.7.16) +# 2.7.4 在 issue #1783 中 2.7.4.post1 确认与 torch 2.6.0+cu124 兼容;mjun0812 的 2.7.4+cu124torch2.6 为同组合 +WHEEL_URL_274="https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.7.16/flash_attn-2.7.4+cu124torch2.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + +echo "" +echo ">>> 方案 1: 安装预编译 wheel (2.7.4+cu124torch2.6, cp310)" +if pip install --no-cache-dir "$WHEEL_URL_274"; then + echo "" + echo ">>> 验证 flash_attn 导入" + if python -c "from flash_attn import flash_attn_func; print('flash_attn 导入成功')"; then + echo "" + echo "=== 安装成功。可将 transformer 的 attn_mode 设为 flashattn 以使用 Flash Attention。 ===" + exit 0 + fi + echo ">>> 预编译 wheel 导入失败(多为 ABI 不匹配),尝试方案 2" + pip uninstall -y flash-attn 2>/dev/null || true +fi + +# 方案 2:从源码编译,强制使用与 PyTorch 一致的旧 ABI(CXX11_ABI=0) +# 官方 PyTorch wheel 使用 _GLIBCXX_USE_CXX11_ABI=0,故设为 FALSE +echo "" +echo ">>> 方案 2: 从源码编译 flash-attn (FLASH_ATTENTION_FORCE_CXX11_ABI=FALSE)" +export FLASH_ATTENTION_FORCE_CXX11_ABI="FALSE" +export FLASH_ATTENTION_FORCE_BUILD="TRUE" +export MAX_JOBS="${MAX_JOBS:-4}" + +pip install --no-cache-dir --no-build-isolation "flash-attn>=2.6.3,<2.8" + +echo "" +echo ">>> 验证 flash_attn 导入" +if python -c "from flash_attn import flash_attn_func; print('flash_attn 导入成功')"; then + echo "" + echo "=== 安装成功。可将 transformer 的 attn_mode 设为 flashattn。 ===" + exit 0 +fi + +echo "" +echo "=== 安装或导入仍失败。请检查: 1) CUDA/nvcc 可用 2) 与当前 PyTorch 版本完全一致。 ===" +exit 1 diff --git a/script/setup/setup_cu124_mirror.md b/script/setup/setup_cu124_mirror.md new file mode 100644 index 0000000..0daec25 --- /dev/null +++ b/script/setup/setup_cu124_mirror.md @@ -0,0 +1,47 @@ +# LingBot-VA 环境:国内镜像安装 PyTorch (CUDA 12.4) + +阿里云 cu124 是**目录列表页**,不是 pip 的 simple index,不能用 `--index-url`,要用 **`--find-links`**。 + +## 1. 激活环境后安装(推荐) + +**阿里云镜像(--find-links):** +```bash +conda activate lingbot-va +pip install --upgrade pip +pip install torch==2.6.0+cu124 torchvision==0.21.0+cu124 torchaudio==2.6.0+cu124 \ + --find-links https://mirrors.aliyun.com/pytorch-wheels/cu124/ +``` + +**不指定版本(装镜像站里最新):** +```bash +conda activate lingbot-va +pip install torch torchvision torchaudio \ + --find-links https://mirrors.aliyun.com/pytorch-wheels/cu124/ +``` + +**南京大学镜像(若支持 find-links 可试):** +```bash +pip install torch torchvision torchaudio \ + --find-links https://mirror.nju.edu.cn/pytorch-wheels/cu124/ +``` + +## 2. 用脚本时走国内镜像 + +```bash +cd /mnt/users/wangyuxuan-20250915/EAI/lingbot-va +# 默认已用阿里云 --find-links;可改镜像: +# export PYTORCH_MIRROR=https://mirror.nju.edu.cn/pytorch-wheels/cu124/ +bash script/setup_env_cu124.sh +``` + +## 3. 若镜像没有 cu124 + +部分镜像只同步到 cu121,可改用 cu121 的包(需本机 CUDA 兼容): +```bash +pip install torch torchvision torchaudio --index-url https://mirrors.aliyun.com/pytorch-wheels/cu121 +``` + +或从 PyTorch 官方页下载对应 `.whl` 后本地安装: +```bash +pip install /path/to/torch-xxx-cu124-*.whl +``` diff --git a/script/setup/setup_env.sh b/script/setup/setup_env.sh new file mode 100644 index 0000000..2790627 --- /dev/null +++ b/script/setup/setup_env.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# LingBot-VA 环境配置脚本 +# 从 lingbot-va 仓库根目录执行: bash script/setup_env.sh +# 可选: bash script/setup_env.sh + +set -e +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +CONDA_ENV="${1:-lingbot-va}" +PYTHON_VERSION="3.10.16" + +echo "=== LingBot-VA 环境配置 ===" +echo "仓库根目录: $REPO_ROOT" +echo "Conda 环境名: $CONDA_ENV" +echo "" + +# 1. 创建 conda 环境 +if conda env list | grep -q "^${CONDA_ENV} "; then + echo ">>> 环境 $CONDA_ENV 已存在,将复用并更新依赖" +else + echo ">>> 创建 conda 环境: $CONDA_ENV (Python $PYTHON_VERSION)" + conda create -n "$CONDA_ENV" python="$PYTHON_VERSION" -y +fi + +# 2. 激活后安装(用 conda run 保证在正确 env 下执行) +echo ">>> 安装 PyTorch 2.9.0 (CUDA 12.6)" +conda run -n "$CONDA_ENV" pip install --upgrade pip +conda run -n "$CONDA_ENV" pip install torch==2.9.0 torchvision==0.24.0 torchaudio==2.9.0 \ + --index-url https://download.pytorch.org/whl/cu126 + +echo ">>> 安装基础依赖 (README)" +conda run -n "$CONDA_ENV" pip install \ + websockets einops diffusers==0.36.0 transformers==4.55.2 accelerate msgpack \ + opencv-python matplotlib ftfy easydict + +echo ">>> 安装 flash-attn (可能较慢)" +conda run -n "$CONDA_ENV" pip install flash-attn --no-build-isolation + +echo ">>> 安装 requirements.txt 中的其余依赖" +conda run -n "$CONDA_ENV" pip install -r "$REPO_ROOT/requirements.txt" + +echo ">>> 以可编辑方式安装当前项目" +conda run -n "$CONDA_ENV" pip install -e . + +echo "" +echo "=== 环境就绪 ===" +echo "激活环境: conda activate $CONDA_ENV" +echo "" +echo "可选环境变量(按需设置):" +echo " # RoboTwin 测评时指定测评仓库路径" +echo " export ROBOTWIN_ROOT=/mnt/users/wangyuxuan-20250915/EAI/RoboTwin" +echo "" +echo " # Post-training 训练时指定 LeRobot 数据集路径" +echo " export LINGBOT_VA_DATASET_PATH=$REPO_ROOT/robotwin-clean-and-aug-lerobot" +echo " # 若数据集尚未下载,可运行: python script/download/download_dataset.py" +echo "" +echo " # 推理/测评时指定模型路径(若未用默认 lingbot-va-base)" +echo " export LINGBOT_VA_MODEL_PATH=/path/to/your/model" +echo "" diff --git a/script/setup/setup_env_cu124.sh b/script/setup/setup_env_cu124.sh new file mode 100755 index 0000000..3e4560c --- /dev/null +++ b/script/setup/setup_env_cu124.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# LingBot-VA 环境配置(兼容 CUDA 12.4,且不依赖 Anaconda 官方频道,无需接受 ToS) +# 从 lingbot-va 仓库根目录执行: bash script/setup_env_cu124.sh +# 可选: bash script/setup_env_cu124.sh + +set -e +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +CONDA_ENV="${1:-lingbot-va}" +PYTHON_VERSION="3.10" + +echo "=== LingBot-VA 环境配置 (CUDA 12.4) ===" +echo "仓库根目录: $REPO_ROOT" +echo "Conda 环境名: $CONDA_ENV" +echo "" + +# 1. 仅用 conda-forge 创建环境,避免 Anaconda 官方频道 ToS +if conda env list | grep -q "^${CONDA_ENV} "; then + echo ">>> 环境 $CONDA_ENV 已存在,将复用并更新依赖" +else + echo ">>> 创建 conda 环境: $CONDA_ENV (Python $PYTHON_VERSION, 仅 conda-forge)" + conda create -n "$CONDA_ENV" python="$PYTHON_VERSION" -y -c conda-forge --override-channels +fi + +# 2. 安装 PyTorch (CUDA 12.4):阿里云为目录列表页,须用 --find-links 而非 --index-url +PYTORCH_MIRROR="${PYTORCH_MIRROR:-https://mirrors.aliyun.com/pytorch-wheels/cu124/}" +echo ">>> 安装 PyTorch 2.6.0 (CUDA 12.4), 镜像: $PYTORCH_MIRROR" +conda run -n "$CONDA_ENV" pip install --upgrade pip +conda run -n "$CONDA_ENV" pip install torch==2.6.0+cu124 torchvision==0.21.0+cu124 torchaudio==2.6.0+cu124 \ + --find-links "$PYTORCH_MIRROR" + +echo ">>> 安装基础依赖 (README)" +conda run -n "$CONDA_ENV" pip install \ + websockets einops diffusers==0.36.0 transformers==4.55.2 accelerate msgpack \ + opencv-python matplotlib ftfy easydict + +echo ">>> 安装 flash-attn (可能较慢)" +conda run -n "$CONDA_ENV" pip install flash-attn --no-build-isolation + +echo ">>> 安装其余依赖" +conda run -n "$CONDA_ENV" pip install \ + numpy==1.26.4 tqdm "imageio[ffmpeg]" safetensors Pillow \ + lerobot==0.3.3 scipy wandb + +echo ">>> 以可编辑方式安装当前项目" +conda run -n "$CONDA_ENV" pip install -e . + +echo "" +echo "=== 环境就绪 ===" +echo "激活: conda activate $CONDA_ENV" +echo "可选: export LINGBOT_VA_MODEL_PATH=$REPO_ROOT/lingbot-va-base" +echo "" diff --git a/wan_va/configs/va_demo_cfg.py b/wan_va/configs/va_demo_cfg.py index 6045eb7..6ad8e25 100644 --- a/wan_va/configs/va_demo_cfg.py +++ b/wan_va/configs/va_demo_cfg.py @@ -1,4 +1,5 @@ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved. +import os import torch from easydict import EasyDict @@ -8,7 +9,9 @@ va_demo_cfg.update(va_shared_cfg) va_shared_cfg.infer_mode = 'server' -va_demo_cfg.wan22_pretrained_model_name_or_path = "/path/to/pretrained/model" +va_demo_cfg.wan22_pretrained_model_name_or_path = os.environ.get( + "LINGBOT_VA_MODEL_PATH", "/path/to/pretrained/model" +) va_demo_cfg.attn_window = 30 va_demo_cfg.frame_chunk_size = 4 diff --git a/wan_va/configs/va_robotwin_cfg.py b/wan_va/configs/va_robotwin_cfg.py index 5fa703f..4cb1569 100644 --- a/wan_va/configs/va_robotwin_cfg.py +++ b/wan_va/configs/va_robotwin_cfg.py @@ -1,4 +1,5 @@ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved. +import os from easydict import EasyDict from .shared_config import va_shared_cfg @@ -6,10 +7,13 @@ va_robotwin_cfg = EasyDict(__name__='Config: VA robotwin') va_robotwin_cfg.update(va_shared_cfg) -va_robotwin_cfg.wan22_pretrained_model_name_or_path = "/path/to/pretrained/model" +va_robotwin_cfg.wan22_pretrained_model_name_or_path = os.environ.get( + "LINGBOT_VA_MODEL_PATH", "/path/to/pretrained/model" +) va_robotwin_cfg.attn_window = 72 -va_robotwin_cfg.frame_chunk_size = 2 +# 与论文一致: K=4 for deployment; 推理 3 steps video (to s=0.6), 10 steps action (to s=1.0) +va_robotwin_cfg.frame_chunk_size = 4 va_robotwin_cfg.env_type = 'robotwin_tshape' va_robotwin_cfg.height = 256 @@ -23,9 +27,10 @@ va_robotwin_cfg.guidance_scale = 5 va_robotwin_cfg.action_guidance_scale = 1 -va_robotwin_cfg.num_inference_steps = 25 +# 论文: Euler 3 steps video (to s=0.6), 10 steps action (to s=1.0); Video CFG 5.0, Action CFG 1.0 +va_robotwin_cfg.num_inference_steps = 3 va_robotwin_cfg.video_exec_step = -1 -va_robotwin_cfg.action_num_inference_steps = 50 +va_robotwin_cfg.action_num_inference_steps = 10 va_robotwin_cfg.snr_shift = 5.0 va_robotwin_cfg.action_snr_shift = 1.0 diff --git a/wan_va/configs/va_robotwin_i2va.py b/wan_va/configs/va_robotwin_i2va.py index 9e37872..3b94d73 100644 --- a/wan_va/configs/va_robotwin_i2va.py +++ b/wan_va/configs/va_robotwin_i2va.py @@ -8,4 +8,6 @@ va_robotwin_i2va_cfg.input_img_path = 'example/robotwin' va_robotwin_i2va_cfg.num_chunks_to_infer = 10 va_robotwin_i2va_cfg.prompt = 'Grab the medium-sized white mug, rotate it, place it on the table, and hook it onto the smooth dark gray rack.' -va_robotwin_i2va_cfg.infer_mode = 'i2va' \ No newline at end of file +va_robotwin_i2va_cfg.infer_mode = 'i2va' +# 导出视频时 2 倍上采样,观感更清晰(模型原生 256x320) +va_robotwin_i2va_cfg.export_scale = 2 \ No newline at end of file diff --git a/wan_va/configs/va_robotwin_train_cfg.py b/wan_va/configs/va_robotwin_train_cfg.py index 190a1b0..66f63f3 100644 --- a/wan_va/configs/va_robotwin_train_cfg.py +++ b/wan_va/configs/va_robotwin_train_cfg.py @@ -8,7 +8,9 @@ # va_robotwin_train_cfg.resume_from = '/robby/share/Robotics/lilin1/code/Wan_VA_Release/train_out/checkpoints/checkpoint_step_10' -va_robotwin_train_cfg.dataset_path = '/path/to/your/dataset' +va_robotwin_train_cfg.dataset_path = os.environ.get( + "LINGBOT_VA_DATASET_PATH", "/path/to/your/dataset" +) va_robotwin_train_cfg.empty_emb_path = os.path.join(va_robotwin_train_cfg.dataset_path, 'empty_emb.pt') va_robotwin_train_cfg.enable_wandb = True va_robotwin_train_cfg.load_worker = 16 diff --git a/wan_va/distributed/util.py b/wan_va/distributed/util.py index 8ad7c29..fb10775 100644 --- a/wan_va/distributed/util.py +++ b/wan_va/distributed/util.py @@ -12,10 +12,13 @@ def _configure_model(model, shard_fn, param_dtype, device, eval_mode=True): if dist.is_initialized(): dist.barrier() + # Unify parameter dtypes before FSDP wrap (FSDP requires uniform dtype). + # Some modules may be kept in fp32 by the model (e.g. _keep_in_fp32_modules). + model.to(param_dtype) + if dist.is_initialized(): model = shard_fn(model) else: - model.to(param_dtype) model.to(device) return model diff --git a/wan_va/modules/model.py b/wan_va/modules/model.py index 25b45e5..0c409ae 100644 --- a/wan_va/modules/model.py +++ b/wan_va/modules/model.py @@ -26,10 +26,16 @@ ) from functools import partial +flash_attn_func = None try: - from flash_attn_interface import flash_attn_func -except: - from flash_attn import flash_attn_func + from flash_attn_interface import flash_attn_func as _flash_attn_func + flash_attn_func = _flash_attn_func +except Exception: + try: + from flash_attn import flash_attn_func as _flash_attn_func + flash_attn_func = _flash_attn_func + except Exception: + flash_attn_func = None __all__ = ['WanTransformer3DModel'] @@ -302,12 +308,18 @@ def __init__( if attn_mode == 'torch': self.attn_op = custom_sdpa elif attn_mode == 'flashattn': + if flash_attn_func is None: + raise ImportError( + "attn_mode='flashattn' requires flash-attn, but it is not available " + "or failed to import. Please install a torch-compatible flash-attn " + "or switch attn_mode to 'torch'." + ) self.attn_op = flash_attn_func elif attn_mode == 'flex': self.attn_op = FlexAttnFunc(cross_attention_dim_head is not None) else: raise ValueError( - f"Unsupported attention mode: {attn_mode}, only support torch and flashattn" + f"Unsupported attention mode: {attn_mode}, only support torch, flashattn, and flex" ) self.inner_dim = dim_head * heads diff --git a/wan_va/wan_va_server.py b/wan_va/wan_va_server.py index 4abaf46..ba0aa28 100644 --- a/wan_va/wan_va_server.py +++ b/wan_va/wan_va_server.py @@ -321,13 +321,20 @@ def _prepare_latent_input(self, action_mask] *= 0 return input_dict - def _encode_obs(self, obs): + def _encode_obs(self, obs, profile=False): + detail = { + 'cpu_preprocess': 0.0, + 'to_vae_device': 0.0, + 'vae_encode': 0.0, + 'latent_postprocess': 0.0, + } images = obs['obs'] if not isinstance(images, list): images = [images] if len(images) < 1: - return None + return (None, detail) if profile else None videos = [] + t0_cpu = time.perf_counter() for k_i, k in enumerate(self.job_config.obs_cam_keys): if self.env_type == 'robotwin_tshape': if k_i == 0: # camera high @@ -345,16 +352,27 @@ def _encode_obs(self, obs): mode='bilinear', align_corners=False).unsqueeze(0) videos.append(history_video_k) + detail['cpu_preprocess'] = time.perf_counter() - t0_cpu if self.env_type == 'robotwin_tshape': videos_high = videos[0] / 255.0 * 2.0 - 1.0 videos_left_and_right = torch.cat(videos[1:], dim=0) / 255.0 * 2.0 - 1.0 vae_device = next(self.streaming_vae.vae.parameters()).device + t0_to = time.perf_counter() + videos_high = videos_high.to(vae_device).to(self.dtype) + videos_left_and_right = videos_left_and_right.to(vae_device).to(self.dtype) + if profile and vae_device.type == 'cuda': + torch.cuda.synchronize(vae_device) + detail['to_vae_device'] = time.perf_counter() - t0_to + t0_vae = time.perf_counter() enc_out_high = self.streaming_vae.encode_chunk( - videos_high.to(vae_device).to(self.dtype)) + videos_high) enc_out_left_and_right = self.streaming_vae_half.encode_chunk( - videos_left_and_right.to(vae_device).to(self.dtype)) + videos_left_and_right) + if profile and vae_device.type == 'cuda': + torch.cuda.synchronize(vae_device) + detail['vae_encode'] = time.perf_counter() - t0_vae enc_out = torch.cat([ torch.cat(enc_out_left_and_right.split(1, dim=0), dim=-1), enc_out_high @@ -363,15 +381,28 @@ def _encode_obs(self, obs): else: videos = torch.cat(videos, dim=0) / 255.0 * 2.0 - 1.0 vae_device = next(self.streaming_vae.vae.parameters()).device + t0_to = time.perf_counter() videos_chunk = videos.to(vae_device).to(self.dtype) + if profile and vae_device.type == 'cuda': + torch.cuda.synchronize(vae_device) + detail['to_vae_device'] = time.perf_counter() - t0_to + t0_vae = time.perf_counter() enc_out = self.streaming_vae.encode_chunk(videos_chunk) + if profile and vae_device.type == 'cuda': + torch.cuda.synchronize(vae_device) + detail['vae_encode'] = time.perf_counter() - t0_vae + t0_post = time.perf_counter() mu, logvar = torch.chunk(enc_out, 2, dim=1) latents_mean = torch.tensor(self.vae.config.latents_mean).to(mu.device) latents_std = torch.tensor(self.vae.config.latents_std).to(mu.device) mu_norm = self.normalize_latents(mu, latents_mean, 1.0 / latents_std) video_latent = torch.cat(mu_norm.split(1, dim=0), dim=-1) - return video_latent.to(self.device) + video_latent = video_latent.to(self.device) + if profile and self.device.type == 'cuda': + torch.cuda.synchronize(self.device) + detail['latent_postprocess'] = time.perf_counter() - t0_post + return (video_latent, detail) if profile else video_latent def _reset(self, prompt=None): logger.info('Reset.') @@ -379,6 +410,7 @@ def _reset(self, prompt=None): #### Reset all parameters self.frame_st_id = 0 self.init_latent = None + self.last_latents = None # 用于 skip video 时复用上一轮的 video latent(可视化等) #### clean vae and transformer cache self.transformer.clear_cache(self.cache_name) self.streaming_vae.clear_cache() @@ -440,18 +472,41 @@ def _reset(self, prompt=None): torch.cuda.empty_cache() def _infer(self, obs, frame_st_id=0): + timing = dict(encode_obs=0.0, video_denoise=0.0, action_denoise=0.0, kv_cache=0.0, other=0.0) frame_chunk_size = self.job_config.frame_chunk_size + # 当前回合是否跳过 video 预测,仅跑 action(复用上一轮的 KV cache / last_latents) + # 仅当 frame_st_id != 0 且显式传入 world_steps_override=0 时生效;首帧必须跑 video 以构建 cache + world_steps_override = obs.get('world_steps_override', None) + skip_video = ( + frame_st_id != 0 + and world_steps_override is not None + and world_steps_override == 0 + and self.last_latents is not None + ) + if skip_video: + logger.info(f"[Skip Video] frame_st_id={frame_st_id}, 本回合仅跑 action,复用上轮 KV cache") + if frame_st_id == 0: - init_latent = self._encode_obs(obs) + init_latent, encode_detail = self._encode_obs(obs, profile=True) + timing['encode_obs'] = sum(encode_detail.values()) + timing['encode_obs_cpu_preprocess'] = encode_detail['cpu_preprocess'] + timing['encode_obs_to_vae_device'] = encode_detail['to_vae_device'] + timing['encode_obs_vae_encode'] = encode_detail['vae_encode'] + timing['encode_obs_latent_postprocess'] = encode_detail['latent_postprocess'] self.init_latent = init_latent + else: + init_latent = self.init_latent - latents = torch.randn(1, - 48, - frame_chunk_size, - self.latent_height, - self.latent_width, - device=self.device, - dtype=self.dtype) + if skip_video: + latents = self.last_latents # 复用上一轮 video 结果,仅用于返回/可视化 + else: + latents = torch.randn(1, + 48, + frame_chunk_size, + self.latent_height, + self.latent_width, + device=self.device, + dtype=self.dtype) actions = torch.randn(1, self.job_config.action_dim, frame_chunk_size, @@ -484,42 +539,47 @@ def _infer(self, obs, frame_st_id=0): with ( torch.no_grad(), ): - # 1. Video Generation Loop - for i, t in enumerate(tqdm(timesteps)): - last_step = i == len(timesteps) - 1 - latent_cond = init_latent[:, :, 0:1].to( - self.dtype) if frame_st_id == 0 else None - input_dict = self._prepare_latent_input( - latents, - None, - t, - t, - latent_cond, - None, - frame_st_id=frame_st_id) - - video_noise_pred = self.transformer( - self._repeat_input_for_cfg(input_dict['latent_res_lst']), - update_cache=1 if last_step else 0, - cache_name=self.cache_name, - action_mode=False) - - if not last_step or video_step != -1: - video_noise_pred = data_seq_to_patch( - self.job_config.patch_size, video_noise_pred, - frame_chunk_size, self.latent_height, - self.latent_width, batch_size=2 if self.use_cfg else 1) - if self.job_config.guidance_scale > 1: - video_noise_pred = video_noise_pred[1:] + self.job_config.guidance_scale * (video_noise_pred[:1] - video_noise_pred[1:]) - else: - video_noise_pred = video_noise_pred[:1] - latents = self.scheduler.step(video_noise_pred, - t, - latents, - return_dict=False) - - latents[:, :, 0:1] = latent_cond if frame_st_id == 0 else latents[:, :, 0:1] - + # 1. Video Generation Loop(skip_video 时跳过,直接用上一轮 cache,只跑 action) + t0_video = time.perf_counter() + if not skip_video: + for i, t in enumerate(tqdm(timesteps)): + last_step = i == len(timesteps) - 1 + latent_cond = init_latent[:, :, 0:1].to( + self.dtype) if frame_st_id == 0 else None + input_dict = self._prepare_latent_input( + latents, + None, + t, + t, + latent_cond, + None, + frame_st_id=frame_st_id) + + video_noise_pred = self.transformer( + self._repeat_input_for_cfg(input_dict['latent_res_lst']), + update_cache=1 if last_step else 0, + cache_name=self.cache_name, + action_mode=False) + + if not last_step or video_step != -1: + video_noise_pred = data_seq_to_patch( + self.job_config.patch_size, video_noise_pred, + frame_chunk_size, self.latent_height, + self.latent_width, batch_size=2 if self.use_cfg else 1) + if self.job_config.guidance_scale > 1: + video_noise_pred = video_noise_pred[1:] + self.job_config.guidance_scale * (video_noise_pred[:1] - video_noise_pred[1:]) + else: + video_noise_pred = video_noise_pred[:1] + latents = self.scheduler.step(video_noise_pred, + t, + latents, + return_dict=False) + + latents[:, :, 0:1] = latent_cond if frame_st_id == 0 else latents[:, :, 0:1] + self.last_latents = latents.detach().clone() + timing['video_denoise'] = time.perf_counter() - t0_video + + t0_action = time.perf_counter() for i, t in enumerate(tqdm(action_timesteps)): last_step = i == len(action_timesteps) - 1 action_cond = torch.zeros( @@ -558,26 +618,38 @@ def _infer(self, obs, frame_st_id=0): return_dict=False) actions[:, :, 0:1] = action_cond if frame_st_id == 0 else actions[:, :, 0:1] + timing['action_denoise'] = time.perf_counter() - t0_action actions[:, ~self.action_mask] *= 0 + t0_other = time.perf_counter() save_async(latents, os.path.join(self.exp_save_root, f'latents_{frame_st_id}.pt')) save_async(actions, os.path.join(self.exp_save_root, f'actions_{frame_st_id}.pt')) actions = self.postprocess_action(actions) torch.cuda.empty_cache() - return actions, latents + timing['other'] = time.perf_counter() - t0_other + return actions, latents, timing def _compute_kv_cache(self, obs): + timing = dict(encode_obs=0.0, video_denoise=0.0, action_denoise=0.0, kv_cache=0.0, other=0.0) ### optional async save obs for debug self.transformer.clear_pred_cache(self.cache_name) save_async(obs['obs'], os.path.join(self.exp_save_root, f'obs_data_{self.frame_st_id}.pt')) - latent_model_input = self._encode_obs(obs) + logger.info("[KV Cache] 阶段 1/2: 编码观测 (encode_obs, VAE)...") + latent_model_input, encode_detail = self._encode_obs(obs, profile=True) + timing['encode_obs'] = sum(encode_detail.values()) + timing['encode_obs_cpu_preprocess'] = encode_detail['cpu_preprocess'] + timing['encode_obs_to_vae_device'] = encode_detail['to_vae_device'] + timing['encode_obs_vae_encode'] = encode_detail['vae_encode'] + timing['encode_obs_latent_postprocess'] = encode_detail['latent_postprocess'] + logger.info(f"[KV Cache] encode_obs 完成, 耗时 {timing['encode_obs']:.2f}s") if self.frame_st_id == 0: latent_model_input = torch.cat( [self.init_latent, latent_model_input], dim=2) if latent_model_input is not None else self.init_latent + t0_prep = time.perf_counter() action_model_input = self.preprocess_action(obs['state']) action_model_input = action_model_input.to(latent_model_input) logger.info( @@ -586,10 +658,13 @@ def _compute_kv_cache(self, obs): input_dict = self._prepare_latent_input(latent_model_input, action_model_input, frame_st_id=self.frame_st_id) + timing['other'] = time.perf_counter() - t0_prep with ( torch.no_grad(), ): + logger.info("[KV Cache] 阶段 2/2: 更新 KV cache (transformer)...") + t0_kv = time.perf_counter() self.transformer(self._repeat_input_for_cfg(input_dict['latent_res_lst']), update_cache=2, cache_name=self.cache_name, @@ -599,8 +674,11 @@ def _compute_kv_cache(self, obs): update_cache=2, cache_name=self.cache_name, action_mode=True) + timing['kv_cache'] = time.perf_counter() - t0_kv + logger.info(f"[KV Cache] kv_cache 完成, 耗时 {timing['kv_cache']:.2f}s (本请求总耗时 encode_obs+kv_cache 即上述两阶段)") torch.cuda.empty_cache() self.frame_st_id += latent_model_input.shape[2] + return timing @torch.no_grad() def infer(self, obs): @@ -614,14 +692,31 @@ def infer(self, obs): return dict() elif compute_kv_cache: logger.info( - f"################# Compute KV Cache #################") - self._compute_kv_cache(obs) - return dict() + "################# Compute KV Cache(本请求先做 encode_obs 再做 kv_cache)#################") + kv_timing = self._compute_kv_cache(obs) + return dict(timing=kv_timing) else: logger.info(f"################# Infer One Chunk #################") - action, _ = self._infer(obs, frame_st_id=self.frame_st_id) - return dict(action=action) - + action, latents, infer_timing = self._infer(obs, frame_st_id=self.frame_st_id) + out = dict(action=action, timing=infer_timing) + # 可选:返回当前 chunk 解码后的预测视频,供 eval 对比可视化(解码耗时计入 other) + save_vis = obs.get('save_visualization', False) or obs.get(b'save_visualization', False) + if save_vis and latents is not None: + if getattr(self, 'video_processor', None) is None: + self.video_processor = VideoProcessor(vae_scale_factor=1) + if self.enable_offload: + self.vae = self.vae.to(self.device) + try: + t0_decode = time.perf_counter() + video = self.decode_one_video(latents, 'np')[0] + out['video'] = video.cpu().numpy() if hasattr(video, 'cpu') else np.asarray(video) + infer_timing['other'] = infer_timing.get('other', 0) + (time.perf_counter() - t0_decode) + logger.info(f"Returning predicted video chunk, shape {out['video'].shape}") + finally: + if self.enable_offload: + self.vae = self.vae.to('cpu') + return out + def decode_one_video(self, latents, output_type): latents = latents.to(self.vae.dtype) latents_mean = ( @@ -651,7 +746,7 @@ def generate(self): pred_latent_lst = [] pred_action_lst = [] for chunk_id in range(self.job_config.num_chunks_to_infer): - actions, latents = self._infer(init_obs, frame_st_id=(chunk_id * self.job_config.frame_chunk_size)) + actions, latents, _ = self._infer(init_obs, frame_st_id=(chunk_id * self.job_config.frame_chunk_size)) actions = torch.from_numpy(actions) pred_latent_lst.append(latents) pred_action_lst.append(actions) @@ -671,11 +766,28 @@ def generate(self): self.vae = self.vae.to(self.device).to(self.dtype) decoded_video = self.decode_one_video(pred_latent, 'np')[0] + # Optional 2x upscale for viewing: model outputs at 256x320 (robotwin), upscale so demo is less pixelated + export_scale = getattr(self.job_config, 'export_scale', 2) + if export_scale != 1 and export_scale > 1: + t, h, w, c = decoded_video.shape + vid = torch.from_numpy(decoded_video).float().permute(0, 3, 1, 2) # (T, C, H, W) + vid = F.interpolate( + vid, + size=(h * export_scale, w * export_scale), + mode='bilinear', + align_corners=False, + ) + decoded_video = vid.permute(0, 2, 3, 1).numpy().astype(np.uint8) export_to_video(decoded_video, os.path.join(self.save_root, "demo.mp4"), fps=10) def run(args): config = VA_CONFIGS[args.config_name] + # 通过环境变量强制控制 offload:0/false 表示关闭 offload(VAE 常驻 GPU,通常更快) + env_enable_offload = os.environ.get("ENABLE_OFFLOAD") + if env_enable_offload is not None: + config.enable_offload = str(env_enable_offload).lower() not in ("0", "false", "no", "off") + logger.info(f"[Config Override] enable_offload <- {config.enable_offload} (from ENABLE_OFFLOAD={env_enable_offload})") port = config.port if args.port is None else args.port if args.save_root is not None: config.save_root = args.save_root