MrBalance / inference.py
Harley-ml's picture
Upload 2 files
e217950 verified
Raw
History Blame Contribute Delete
10.9 kB
#!/usr/bin/env python3
"""
Test MrBalance directly from Hugging Face Hub.
Requirements:
pip install -U torch transformers safetensors mujoco
Files:
balance_plate_rl.py
test_mrbalance_hf.py
Usage:
python test_mrbalance_hf.py
python test_mrbalance_hf.py --object sphere
python test_mrbalance_hf.py --object egg
python test_mrbalance_hf.py --object heavy_ball
The script:
1. Downloads MrBalance from Hugging Face.
2. Loads it through AutoModel with trust_remote_code=True.
3. Creates the original MuJoCo environment.
4. Uses the Hugging Face policy to control the plate.
5. Prints episode statistics.
6. Optionally launches the MuJoCo viewer.
"""
from __future__ import annotations
import argparse
import time
import numpy as np
import torch
from transformers import AutoModel
from balance_plate_rl import (
DEVICE,
MAX_EPISODE_STEPS,
OBJECT_TYPES,
PlateBalanceEnv,
)
# ============================================================================
# CONFIG
# ============================================================================
MODEL_ID = "fromziro/MrBalance"
DEFAULT_EPISODES = 5
DEFAULT_OBJECT = "sphere"
DEVICE_TO_USE = torch.device(
DEVICE
)
# ============================================================================
# MODEL LOADING
# ============================================================================
def load_model():
print("=" * 72)
print("Loading MrBalance from Hugging Face")
print("=" * 72)
print(f"Model: {MODEL_ID}")
print(f"Device: {DEVICE_TO_USE}")
print()
model = AutoModel.from_pretrained(
MODEL_ID,
trust_remote_code=True,
)
model = model.to(DEVICE_TO_USE)
model.eval()
print("[load] Model loaded successfully.")
print()
# Print basic architecture information.
print(
f"[model] Observation size: "
f"{model.config.observation_size}"
)
print(
f"[model] Hidden size: "
f"{model.config.hidden_size}"
)
print(
f"[model] Intermediate size: "
f"{model.config.intermediate_size}"
)
print(
f"[model] Bottleneck size: "
f"{model.config.bottleneck_size}"
)
print(
f"[model] Action size: "
f"{model.config.action_size}"
)
print()
return model
# ============================================================================
# SINGLE EPISODE
# ============================================================================
def run_episode(
model,
object_name: str,
seed: int,
render: bool = False,
):
env = PlateBalanceEnv(
seed=seed,
render=render,
active_objects=[object_name],
)
obs = env.reset(
specific_object=object_name
)
total_reward = 0.0
episode_length = 0
if render:
try:
import mujoco.viewer
viewer_context = (
mujoco.viewer.launch_passive(
env.model,
env.data,
)
)
except Exception as exc:
env.close()
raise RuntimeError(
f"Could not launch MuJoCo viewer: {exc}"
) from exc
else:
viewer_context = None
try:
if viewer_context is not None:
with viewer_context as viewer:
while viewer.is_running():
step_start = time.time()
obs_tensor = torch.as_tensor(
obs,
dtype=torch.float32,
device=DEVICE_TO_USE,
).unsqueeze(0)
with torch.no_grad():
output = model(
obs_tensor,
deterministic=True,
)
action = (
output.action[0]
.detach()
.cpu()
.numpy()
.astype(np.float64)
)
obs, reward, terminated, truncated, info = (
env.step(action)
)
total_reward += reward
episode_length += 1
viewer.sync()
if terminated or truncated:
break
# Match roughly the environment's 100 Hz control rate.
target_dt = 0.01
elapsed = time.time() - step_start
if elapsed < target_dt:
time.sleep(
target_dt - elapsed
)
else:
while True:
obs_tensor = torch.as_tensor(
obs,
dtype=torch.float32,
device=DEVICE_TO_USE,
).unsqueeze(0)
with torch.no_grad():
output = model(
obs_tensor,
deterministic=True,
)
action = (
output.action[0]
.detach()
.cpu()
.numpy()
.astype(np.float64)
)
obs, reward, terminated, truncated, info = (
env.step(action)
)
total_reward += reward
episode_length += 1
if terminated or truncated:
break
finally:
env.close()
success = (
not info["fallen"]
and episode_length >= MAX_EPISODE_STEPS
)
return {
"object": object_name,
"reward": float(total_reward),
"length": int(episode_length),
"success": bool(success),
"fallen": bool(info["fallen"]),
"distance": float(info["distance"]),
"velocity": float(info["object_velocity"]),
}
# ============================================================================
# RANDOM OBSERVATION SANITY CHECK
# ============================================================================
def sanity_check(model):
"""
Verify that the Hugging Face model can actually execute inference
independently of MuJoCo.
"""
print("=" * 72)
print("Running model sanity check")
print("=" * 72)
x = torch.randn(
4,
64,
dtype=torch.float32,
device=DEVICE_TO_USE,
)
with torch.no_grad():
output = model(
x,
deterministic=True,
)
print(
"[sanity] action shape:",
tuple(output.action.shape),
)
print(
"[sanity] value shape:",
tuple(output.value.shape),
)
print(
"[sanity] action range:",
float(output.action.min()),
"to",
float(output.action.max()),
)
print(
"[sanity] example action:",
output.action[0].detach().cpu().numpy(),
)
print("[sanity] PASS")
print()
# ============================================================================
# MAIN
# ============================================================================
def main():
parser = argparse.ArgumentParser(
description="Test MrBalance from Hugging Face."
)
parser.add_argument(
"--object",
type=str,
default=DEFAULT_OBJECT,
choices=OBJECT_TYPES,
help="Object to balance.",
)
parser.add_argument(
"--episodes",
type=int,
default=DEFAULT_EPISODES,
help="Number of evaluation episodes.",
)
parser.add_argument(
"--render",
action="store_true",
help="Launch interactive MuJoCo viewer.",
)
parser.add_argument(
"--seed",
type=int,
default=12345,
help="Evaluation seed.",
)
args = parser.parse_args()
model = load_model()
sanity_check(model)
print("=" * 72)
print(
f"Testing object: {args.object}"
)
print(
f"Episodes: {args.episodes}"
)
print("=" * 72)
print()
results = []
for episode in range(args.episodes):
print(
f"[episode {episode + 1}/{args.episodes}] "
f"Running..."
)
result = run_episode(
model=model,
object_name=args.object,
seed=args.seed + episode,
render=args.render,
)
results.append(result)
status = (
"SUCCESS"
if result["success"]
else (
"FALL"
if result["fallen"]
else "TIMEOUT"
)
)
print(
f" status: {status}"
)
print(
f" reward: {result['reward']:.2f}"
)
print(
f" length: {result['length']}"
)
print(
f" distance: {result['distance']:.4f} m"
)
print(
f" velocity: {result['velocity']:.4f} m/s"
)
print()
# ------------------------------------------------------------------------
# Summary
# ------------------------------------------------------------------------
rewards = [
r["reward"]
for r in results
]
lengths = [
r["length"]
for r in results
]
distances = [
r["distance"]
for r in results
]
successes = sum(
r["success"]
for r in results
)
print("=" * 72)
print("RESULTS")
print("=" * 72)
print(
f"Object: {args.object}"
)
print(
f"Reward mean: {np.mean(rewards):.2f}"
)
print(
f"Reward std: {np.std(rewards):.2f}"
)
print(
f"Length mean: {np.mean(lengths):.1f}"
)
print(
f"Survival rate: "
f"{100.0 * sum(not r['fallen'] for r in results) / len(results):.1f}%"
)
print(
f"Success rate: "
f"{100.0 * successes / len(results):.1f}%"
)
print(
f"Tracking error: "
f"{np.mean(distances):.4f} m"
)
print("=" * 72)
if __name__ == "__main__":
main()