ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

RTX 5090复现Openvla,部署,推理,微调、评估和结果分析全流程记录

2026/9/25 16:59:43 拓冰建站 浏览量
RTX 5090复现Openvla,部署,推理,微调、评估和结果分析全流程记录 精简版项目总结、实验结果和演示视频GitHub记录一、环境配置OpenVLA 官方测试栈Python 3.10、torch 2.2.0、torchvision 0.17.0、transformers 4.40.1、tokenizers 0.19.1、timm 0.9.10、flash-attn 2.5.5。OpenVLA LoRA官方写明至少要27GB显存batch_size16约需72G等正式微调的时候改小即可。我的卡是RTX 5090显存32607MiB显存系统内存31GiB总量部署时可用约13GiB。足够部署和微调但是5090的兼容性不是很好比如flash-attn2.5.5没有为RTX 5090sm_120编译 CUDA kernel所以不能完全按OpenVLA官方的 配置来。1、创建conda环境conda create -n openvla python3.10 -y conda activate openvla2、安装适配 RTX 5090 的 PyTorchRTX 5090 需要较新的 CUDA/PyTorch 栈支持sm_120。官方 OpenVLA 的torch2.2.0太老不适合 5090。安装采用 PyTorch 官方 CUDA 12.8 wheel。最终确认的版本为Python: 3.10 PyTorch: 2.9.1cu128 torchvision: 0.24.1cu128 torchaudio: 2.9.1cu128 CUDA runtime bundled with torch: 12.83、安装openvla基础依赖安装基础依赖时使用阿里云 PyPI 镜像加快下载python -m pip install \ transformers4.40.1 \ tokenizers0.19.1 \ timm0.9.10 \ peft0.11.1 \ accelerate1.14.0 \ -i https://mirrors.aliyun.com/pypi/simpletransformers/tokenizers/timm模块OpenVLA 模型结构和视觉编码相关。peft模块LoRA 微调需要。accelerate模块训练/加载大模型常用4、安装本地openvla仓库之前在配置另一个环境的时候已经拉到过本地现在只需要直接引用进当前的openvla conda环境即可。python -m pip install -e /path/to/openvla_repro/openvla --no-deps--no-deps是防止 pip 自动处理依赖很可能会导致降级当前的torch2.9.1cu128进而破坏 RTX 5090 CUDA 12.8 FlashAttention 后续安装的兼容环境。因此这里主动保留已经验证可用的新版本 PyTorch 栈。缺点是OpenVLA 的其他 Python 依赖需要手动补齐。5、下载openvla-7b权重到本地直接从modelscope下载在openvla_env环境里执行python -m pip install modelscope modelscope download --model zixiaoBios/openvla-7b --local_dir /path/to/openvla_repro/models/openvla-7b大约15GB。6、安装缺失的依赖根据导入openvla时的报错来依次缺什么装什么比如第一次报错ModuleNotFoundError: No module named dlimp就安装python -m pip install dlimp githttps://github.com/moojink/dlimp_openvla --no-deps注意因为dlimp不在 PyPI 上所以直接执行pip install dlimp找不到。--no-deps依旧是为了先只装dlimp本体避免自动拉依赖时改坏现有栈。再次导入报错ModuleNotFoundError: No module named tensorflow ModuleNotFoundError: No module named tensorflow_datasetsOpenVLA 官方依赖声明了tensorflow2.15.0、tensorflow_datasets4.9.3、tensorflow_graphics2021.12.3这些主要用于 RLDS 数据加载/LoRA 微调数据管线。执行以下指令安装tensorflow模块python -m pip install tensorflow2.15.0 python -m pip install tensorflow-datasets4.9.3 tensorflow-graphics2021.12.3 matplotlib -i https://mirrors.aliyun.com/pypi/simple然后报错protobuf / tensorflow-metadata 冲突ImportError: cannot import name runtime_version from google.protobuf修复python -m pip install \ protobuf3.20.3 \ tensorflow-metadata1.14.0 \ absl-py1.4.0 \ --force-reinstall \ -i https://mirrors.aliyun.com/pypi/simple7、安装 RTX 5090 可用的 flash-attn进行训练或者微调还需要安装Flash Attention 2它是一个更快、更省显存的 attention 实现前面折腾了这么多归根结底就是因为要避免和flash-attn依赖冲突官方要求2.5.5版本但该版本面向较早 GPU 架构不能保证含有 RTX 5090 的sm_120CUDA kernel。最终使用已为 CUDA 12.8 与sm_120编译的 wheelflash_attn-2.8.3cu128sm120-cp310-cp310-linux_x86_64.whl执行以下指令安装python -m pip install --no-deps --no-cache-dir \ https://github.com/HenryZ838978/flash-attn-blackwell/releases/download/v2.8.3-sm120/flash_attn-2.8.3cu128sm120-cp310-cp310-linux_x86_64.whl可以写个脚本测试一下目前安装是否都成功了import torch import flash_attn from flash_attn import flash_attn_func q torch.randn(2, 256, 8, 64, dtypetorch.float16, devicecuda) k torch.randn(2, 256, 8, 64, dtypetorch.float16, devicecuda) v torch.randn(2, 256, 8, 64, dtypetorch.float16, devicecuda) out flash_attn_func(q, k, v) print(flash_attn:, flash_attn.__version__) print(torch:, torch.__version__, torch.version.cuda) print(gpu:, torch.cuda.get_device_name(0)) print(capability:, torch.cuda.get_device_capability(0)) print(out:, out.shape, out.dtype) print(finite:, torch.isfinite(out).all().item())最终输出flash_attn: 2.8.3 torch: 2.9.1cu128 12.8 gpu: NVIDIA GeForce RTX 5090 capability: (12, 0) out: torch.Size([2, 256, 8, 64]) torch.float16 finite: True不过终端会有提示版本不一致的error比如pip check显示 OpenVLA 要求torch2.2.0但不影响使用降级了就和别的模块依赖冲突了。8、安装 OpenVLA 的 LIBERO 评估依赖LIBERO 是一个lifelong robot learning benchmark里面同时包含任务套件、演示数据、训练、评测脚本并且是 基于 MuJoCo 的操作任务环境。后续用到的训练数据集、测试数据集都来自这里。8.1 安装 OpenVLA 跑 LIBERO eval 所需的仿真相关依赖在OpenVLA 工作空间目录执行以下指令python -m pip install -r experiments/robot/libero/libero_requirements.txt \ -i https://mirrors.aliyun.com/pypi/simple随即遇到问题pip 自动安装了最新mujoco3.12.0并把numpy 1.26.4升级成了numpy 2.2.6。numpy 2.2.6与tensorflow2.15.0不兼容mujoco 3.12.0与robosuite1.4.1不兼容问题LIBERO reset/step 时触发AssertionError。果然不--no-deps就会出问题。OpenVLA 数据加载链路依赖 TensorFlow/TFDSTensorFlow 2.15 要求numpy 1.23.5, 2.0.0所以执行以下指令修复Numpy版本为numpy1.26.4python -m pip install numpy1.26.4 --force-reinstall \ -i https://mirrors.aliyun.com/pypi/simple再将 MuJoCo 固定到与 robosuite 1.4.1 更匹配的版本python -m pip install mujoco3.1.6 --force-reinstall --no-deps \ -i https://mirrors.aliyun.com/pypi/simple8.2 安装 LIBERO 本体在本地工作空间openvla_repro/LIBERO下执行python -m pip install -e . --no-depspip show libero能看到安装成功但import libero失败。原因是 LIBERO 仓库结构比较特殊OpenVLA 需要from libero.libero import benchmark所以 Python 搜索根目录应该是/path/to/openvla_repro/LIBERO,需要把最外层 LIBERO 仓库加入PYTHONPATH后续跑 LIBERO/OpenVLA eval 前都设置export PYTHONPATH/path/to/openvla_repro/LIBERO8.3 初始化 LIBERO 配置终端显示Do you want to specify a custom path for the dataset folder? (Y/N):选择默认路径printf n\n | python -c import libero; print(libero file:, libero.__file__); print(libero config initialized)这样就会创建/path/to/.libero/config.yaml,里面会记录benchmark_root、datasets等。这些是LIBERO自己的用户配置不是系统配置到这里可以验证LIBERO导入能否成功python - PY import numpy import tensorflow as tf import mujoco import robosuite import bddl from libero.libero import benchmark print(numpy:, numpy.__version__) print(tensorflow:, tf.__version__) print(mujoco:, mujoco.__version__) print(robosuite:, robosuite.__version__) print(LIBERO benchmark OK) print(benchmark suites:, benchmark.get_benchmark_dict().keys()) PY最终输出如下一切正常numpy: 1.26.4 tensorflow: 2.15.0 mujoco: 3.1.6 robosuite: 1.4.1 LIBERO benchmark OK benchmark suites: dict_keys([libero_spatial, libero_object, libero_goal, libero_90, libero_10, libero_100])二、评估验证这里用已经预训练好的开源模型openvla-7b-finetuned-libero-spatial来做测试目的仅仅只是看eval脚本能不能运行数据指标和演示视频能否被记录从而验证evaluation pipeline能否正常工作。不是必须完成比如openvla-7b-finetuned-libero-spatial模型其实可以不用下载但这里遇到的问题早晚得解决。OpenVLA 官方 LIBERO 评估有四个主要 suitelibero_spatial空间关系任务libero_object物体变化任务libero_goal目标变化任务libero_10长程任务也叫 LIBERO-Long每个 suite 通常 10 个任务官方 evaluation 默认每个任务 50 次 rollout总共 500 次。我这里只是做个简单测试所以只部署了最常用的libero_spatial并且每个任务只执行一次。1、下载openvla-7b-finetuned-libero-spatialhuggingface上的openvla-7b-finetuned-libero-spatial一开始直接用hf上的git指令单拉下来的只有1.9mb的模型框架权重没下来git clone githf.co:openvla/openvla-7b-finetuned-libero-spatial故改为HuggingFace CLI部署先在openvla环境里安装pip install -U huggingface_hub然后在要下载models的路径执行HF_ENDPOINThttps://hf-mirror.com hf download openvla/openvla-7b-finetuned-libero-spatial --local-dir ./openvla-7b-finetuned-libero-spatial部分成功..终端有一句显示是Error: Local entry not found. Distant resource does not seem to be on huggingface.co.说明有某一个文件hf 客户端尝试从 Hugging Face 获取但是远端没有找到。执行ls -lh来检查发现缺model-00004-of-00004.safetensors。safetensors是 Hugging Face 推出的模型权重存储格式再次cd到path/to/models/openvla-7b-finetuned-libero-spatial执行unset HF_ENDPOINT然后hf download openvla/openvla-7b-finetuned-libero-spatial --local-dir .它会检查已有文件然后补安装完整模型大约15GB。2、验证evaluation pipeline前面说了LIBERO Spatial10个任务每个任务只跑一次故experiments/robot/libero/run_libero_eval.py里修改num_trials_per_task: int 1 # 原本是50然后cd到/openvla_repro/openvla执行export PYTHONPATH的原因之前已经说了如果不这样就会报错ModuleNotFoundError: No module named libero.liberoexport PYTHONPATH/path/to/openvla_repro/LIBERO python experiments/robot/libero/run_libero_eval.py \ --model_family openvla \ --pretrained_checkpoint /path/to/openvla_repro/models/openvla-7b-finetuned-libero-spatial \ --task_suite_name libero_spatial \ --center_crop True也可以参考这位博主的路径硬编码方法OpenVLA学习记录论文解读与复现微调-CSDN博客2.1 解决wandb 与 protobuf 版本冲突现在的报错是ImportError: cannot import name Imports from wandb.proto.wandb_telemetry_pb2wandb 与 protobuf 版本冲突故尝试降低版本到wandb0.15.12pip uninstall wandb -y pip install wandb0.15.122.2 解决wandb 0.15.12 与当前 Python 环境里的 setuptools 版本冲突问题报错显示ModuleNotFoundError: No module named pkg_resources执行pip show setuptools显示Name: setuptools Version: 83.0.0 Location: /path/to/miniconda3/envs/openvla/lib/python3.10/site-packages Required-by: tensorboard, tensorflow, wandbSetuptools 已经安装而且版本setuptools83.0.0非常新但是问题在于setuptools 新版本已经不再默认提供pkg_resources这个旧接口。所以只调整setuptools版本pip install setuptools75.8.0 --no-deps2.3 解决transformers 和 huggingface-hub版本不兼容又冲突了我释怀地笑目前环境transformers: 4.40.1这个版本要求huggingface-hub 0.19.3huggingface-hub 1.0但是现在huggingface-hub1.29.0所以降级pip install huggingface-hub0.23.5 --no-deps2.4 解决PyTorch2.6 的torch.load()默认安全策略变化导致 LIBERO 初始状态文件加载失败还没完报错WeightsUnpickler error: Unsupported global: GLOBAL numpy.core.multiarray._reconstruct was not an allowed global by default.因为LIBERO 数据集里的初始化状态init_states.pt不是纯权重文件而是保存了 Python 对象numpy array 等。之前torch.load(path)默认weights_onlyFalse可以直接加载。但是 PyTorch 2.6 改成torch.load(path, weights_onlyTrue)只允许加载 tensor 权重。所以遇到numpy.core.multiarray._reconstruct这种对象就拒绝。修改/path/to/openvla_repro/LIBERO/libero/libero/benchmark/init.py把init_states torch.load(init_states_path)改为init_states torch.load( init_states_path, weights_onlyFalse )2.5 解决保存任务视频崩溃的问题OpenVLA eval 已经跑完 9/10 个任务第 10 个任务执行到一半时在保存视频阶段崩溃。报错OSError: [Errno 24] Too many open files说明打开文件太多Linux 中一个运行中的程序不是只能打开普通文件。所谓 file descriptor文件描述符FD包括还可能包括打开的文件.txt、图片文件、视频文件、pipe进程通信管道、socket、GPU/EGL相关资源句柄、ffmpeg进程通信通道等。每个进程都有一个限制执行如下指令让当前终端启动的程序最多可以打开65535个文件描述符。ulimit -n 65535并且在当前 run_libero_eval.py 里加入保护机制替换原本的视频保存函数上游代码原本从libero_utils.py导入from experiments.robot.libero.libero_utils import ( get_libero_dummy_action, get_libero_env, get_libero_image, quat2axisangle, save_rollout_video, )原始视频函数位于openvla_repro/openvla/experiments/robot/libero/libero_utils.pydef save_rollout_video(rollout_images, idx, success, task_description, log_fileNone): video_writer imageio.get_writer(mp4_path, fps30) for img in rollout_images: video_writer.append_data(img) video_writer.close()问题在于video_writer.close()只会在所有帧都正常写完后才执行。如果append_data()、ffmpeg 管道或文件描述符分配中途报错函数会直接异常退出writer 和关联资源未必能及时关闭。故改为run_libero_eval.py不再导入save_rollout_video而是使用自己定义_save_rollout_video()video_writer imageio.get_writer(str(mp4_path), fps30) try: for img in rollout_images: video_writer.append_data(img) finally: video_writer.close()finally的含义是无论try中正常完成、抛出异常甚至中断执行Python 离开该代码块前都会执行video_writer.close()。这降低了 MP4 文件、ffmpeg 子进程管道和相关文件描述符在异常路径中残留的风险。记得额外加入import imageio修改保存逻辑原本逻辑是在每个 episode 结束后直接保存视频save_rollout_video( replay_images, total_episodes, successdone, task_descriptiontask_description, log_filelog_file, )没有try/except。因此视频保存一旦出现之前的报错OSError: [Errno 24] Too many open files整个 evaluation 进程会异常终止后续 task、成功率和日志都无法完成。故在eval脚本加入video_save_enabled True并将视频保存包装为video_saved False if video_save_enabled: try: _save_rollout_video(...) video_saved True except Exception as e: video_save_enabled False print(fWarning: failed to save rollout video; disabling later video writes: {e}) log_file.write( fWarning: failed to save rollout video for episode {total_episodes}: {e}\n )这样如果视频保存失败也不会对模型推理、仿真、日志记录有影响。每个 task 结束后显式关闭 LIBERO/MuJoCo 环境原始 eval 脚本会针对每个 task 创建环境env, task_description get_libero_env(task, cfg.model_family, resolution256)但 task 的全部 episode 完成后没有显式的env.close()。在 多 task或每 task 多 rollout 评估中MuJoCo、EGL/OpenGL 上下文及相关资源可能跨 task 累积。故当前脚本在每个 task 的 episode 循环结束后新增了try: env.close() except Exception as e: print(fWarning: failed to close LIBERO environment for task {task_id}: {e}) log_file.write(fWarning: failed to close LIBERO environment for task {task_id}: {e}\n) finally: gc.collect()env.close()请求 robosuite / MuJoCo 释放当前 task 的仿真资源gc.collect()主动触发 Python 垃圾回收尽快回收不再引用的 Python 对象try/except/finally即使关闭环境本身出现异常也不让评估整体崩溃且仍执行垃圾回收。再次启动评估测试终于一切正常视频都保存在rollouts文件夹下便于自行检查效果。三、LoRA微调LoRA的原理、流程和代码1、下载训练数据集OpenVLA README 里明确说微调用的是 Hugging Face 上的openvla/modified_libero_rlds包含libero_spatial_no_noops等 RLDS 数据集https://huggingface.co/datasets/openvla/modified_libero_rlds微调阶段只读取离线演示数据不启动 MuJoCo也不需要仿真窗口。MuJoCo 是后续 LIBERO 策略评测阶段使用的。2、其他准备工作为了确保能稳定长久地实现机器学习我休息所以在开始训练前还依次做了一些测试。比如在finetune脚本里加断点保存机制这次中断退出后下次还能继续这个进度训练并且每训练500step就保存一次防止因为训练步数过多导致模型过拟合或者动作坍缩。另外也增设了一些安全阈值后来正式训练时显存使用峰值29.26 GiBCPU最高温度72°C最高功耗538.17 W最低可用RAM0.69 GiB都没超过阈值。参数配置如下batch_size4 gradient_accumulation_steps4 effective_batch_size16 LoRA rank32 LoRA dropout:0 LR:0.0005 max_steps8000 save_steps500 FlashAttention 2 BF16OpenVLA官方要求batch_size16约需72G所以我正式训练时改成4gradient_accumulation_steps相应地增大到4。数学上和OpenVLA官方的batch_size16gradient_accumulation_steps1的effective batch一样但是GPU执行方式完全不同。gradient_accumulation_steps是梯度累积步数用来模拟更大的 batch size。相当于每次只把 4条样本放进 GPU连续累积 4次梯度再更新一次参数所以 batch size 等效于 16。数据记录在wandb里正式训练前先在Weights Biases: The AI developer platform注册并拿到5090这里又有兼容性问题了我之前终于不会和别的依赖冲突的wandb0.15.12只能匹配旧版的40位API KEY而现在wandb的API KEY都是86位。所以拿到86位KEY之后直接复制进终端export WANDB_API_KEYYOUR_API_KEY然后在同一终端运行finetune脚本即可。训练指标在GitHub记录四、模型效果评估测试依旧是用的Libero Spatial此时记得把之前验证evaluation pipeline时候的num_trials_per_task参数改回50。一共十类任务每个任务跑50个episode。demos在GitHub记录之前用Lora训练保存的都只是adapter评估时需要再 adapter 挂载到 base model 后调用merge_and_unload()生成完整推理模型。根据train loss曲线我把6k到8k步训练过程中保存的模型都做了测试来看效果。如果运行eval脚本遇到问题可以会看第二节评估验证。最终的测试成功率如下Checkpoint成功率6k8.80%6.5k15.40%7k23.00%7.5k7.80%8k26.60%整体趋势并非单调上升。模型从 6k 到 7k 的闭环表现持续改善7.5k 出现显著退化8k 又恢复并取得当前最高的总体成功率。在之前几个训练步的测试中的失败案例在肉眼看来大多都是机械臂完全静止为区分到底是环境未执行动作还是和模型输出无效动作从 7.5k 开始在 eval 中加入了episode级action diagnostics。典型的 7.5k 失败 episode 输出为abs_mean0.001211 abs_max0.002985 delta_mean0 gripper_raw_mean0.996078 gripper_exec_mean-1abs_mean6D 位姿 action 的平均绝对幅度abs_maxrollout 内 6D 位姿 action 的最大绝对幅度delta_mean相邻两次 action 的平均变化量gripper_raw_mean模型原始 gripper 输出均值gripper_exec_mean归一化、符号转换后送入环境的 gripper 均值这说明模型不仅动作小而且连续时间步之间没有变化。它更像是在生成一个固定的默认动作序列。而且由于 500 个 episode 都完整完成、视频保存正常且其他 checkpoint 可以正常控制机械臂因此可以排除 MuJoCo、EGL 渲染或环境步进整体失效。所以判定异常的原因是7.5k checkpoint 对大量视觉输入输出了重复的近似 No-op 动作从而导致闭环策略退化。不过后续梯度也不是只会让模型越来越差。7.5k 之后继续训练时模型又见到了新的样本、图像增强结果和任务组合参数继续更新。如果 7.5k 的 No-op 模式不是稳定收敛点而只是暂时的坏状态那么后续梯度可以把它拉出来就像8k那样。容易产生困扰的是看loss曲线在模型训练后期也是缓慢平稳下降但是到7.5k时测试成功率出现异常。那是因为train loss 是平均的逐 token 指标闭环成功率则是长时序、二值、极其敏感的指标。一个小小的 token logit 排名变化比如原本最高概率是向目标移动的动作 token而更新后最高概率是接近零位移的动作 token。这在在训练 loss 上可能只产生很小变化但argmax会直接切换输出动作第一步没有靠近目标 → 下一帧仍然看见类似场景 → 再输出近似 No-op → 220 步重复 → timeout失败。因此训练 loss 平稳和 rollout 成功率剧烈波动同时存在这并不矛盾。以效果最好的8k为例不同任务的成功率如下Task成功率Task 126%Task 240%Task 310%Task 474%Task 550%Task 626%Task 74%Task 810%Task 90%Task 1026%其中Task9 的 50 次全失败其中有44个视频近似静止Task5 则没有静止视频但仍有 25 次失败说明该任务的主要瓶颈已是抓取、放置或精细控制。有明显运动但最终失败的 episode里有一些是肉眼检查也能明显发现问题比如Task 1的episode1看上去像是停顿了挺长时间才开始移动最终导致超时。Task1的episode2 连续抓空后几乎静止不再移动同时第一次抓空后机械臂抖动严重。TASK1的episode5 成功抓取动作流畅但是放置偏移。TASK5的episode202 没能成功从抽屉中抓取目标而且似乎卡在抽屉里出不来了这个任务的确有难度。Task5的episode203抓夹角度太大夹到抽屉了。关于 episode 2的分析视频显示它在前约0-90帧持续向目标碗移动并尝试操作之后停在失败状态。其诊断数据也明显更活跃abs_mean0.105071 delta_mean0.043254 gripper_exec_mean-0.127273相较 episode 1动作幅度和相邻动作差都更大夹爪输出也在开闭之间切换而不是固定保持打开。至于“抖动”通常是三件事叠加动作 token 在相邻观测之间切换模型输出的平移/旋转增量可能在相反方向来回变化。接触动力学放大误差夹爪接近或碰撞碗、桌面时小的姿态变化会被碰撞响应放大为视觉上的抖动。抓空后的分布偏移演示数据主要覆盖成功抓取轨迹一旦抓空后续画面进入较少见状态行为克隆策略没有明确的恢复策略可能转而输出低幅度默认动作。因此后期看似“完全静止”应该是模型输出的执行效果已接近零控制器在当前姿态下达到局部平衡。此外一些是肉眼观察成功但是程序判定为false的情况。所有spatial任务的目标都是“on the plate”但是LIBERO 的on并不是图像语义上的“碗在盘子里”。它实际调用check_ontop()同时要求碗和盘子的物理碰撞体仍接触碗与盘子的 XY 中心距离小于0.03 m即 3 cm满足指定的高度关系。渲染画面使用的是视觉网格人眼容易把“压着盘沿、略微偏心、被夹爪悬着、看似落入但碰撞体未接触”理解为成功但仿真只认可严格的几何和接触条件。而且 eval时 每执行一步动作都会检查done一旦成功会立即中止该 episode。因此episode跑到timeout说明在任一控制步结束时都没有满足成功条件不是脚本漏掉了一个已经被判为成功的状态。比如Task 9 / episode 408约 frame 75 已将碗移到盘附近之后大部分时间停在目标区域上方或附近。视觉上碗与盘重叠明显但机械臂仍贴近目标。gripper_exec_mean0.636表明夹爪整体更偏向闭合推测是碗仍被夹持或处于轻微悬空或者盘沿接触状态未满足成功条件。Task 9 / episode 418约 frame 90 到达盘附近之后停住。夹爪整体更偏打开gripper_exec_mean-0.564更像已尝试释放但碗可能偏离盘中心、压在边缘或接触关系不满足。Task 10 / episode 452约 frame 135 完成搬运并到达盘区域后续保持静止。视觉上看着动作流畅完成任务但日志记录是timeout。