Training a MicroDuck walking policy with reinforcement learning#
Authors: Alex He
Knowledge level: Beginner
Publication date: November 6, 2026
This tutorial teaches omnidirectional velocity tracking for the MicroDuck robot with proximal policy optimization (PPO) on AMD GPUs.
This tutorial does not assume prior reinforcement learning or robotics background. Major concepts are explained in the Background knowledge section and in the context of MicroDuck training.
What is the problem to be solved in this tutorial?#
How do you give MicroDuck a desired velocity and direction and let it decide how all 14 joints should move at each moment? For example, given “0.3 m/s forward, 0.1 m/s left, while turning counterclockwise,” it should:
Track forward/back, left/right, and yaw commands as accurately as possible.
Stay upright, with as few falls and as little slipping as possible.
Produce continuous, natural gaits without frequent self-collisions or joint-limit hits.
Keep walking despite sensor noise, actuation delay, mass variation, and external pushes.
Hand-writing control rules that cover every velocity combination for 14 joints is hard. Reinforcement learning (RL) lets the robot try actions in simulation, judge outcomes from reward, and gradually learn a control policy from observations to actions.
The following is a preview video for a successful solution to our problem. It shows MicroDucks that have finished training and walk as instructed: upright, steady, and tracking their commanded velocity with a clean, continuous gait. You will produce something similar in Phase 2, step 2.1 below.
Prerequisites#
Hardware#
An AMD GPU: This tutorial was tested on the following AMD GPUs.
Example GPU |
Architecture |
GPUs required |
|---|---|---|
AMD Instinct™ MI210 |
|
1 |
AMD Instinct™ MI300X |
|
1 |
AMD Radeon™ R9700 |
|
1 |
AMD Ryzen™ AI Max (Strix Halo APU) |
|
1 |
The tutorial can also be run on any other AMD Instinct GPUs. Ensure your system meets the official requirements.
Software#
ROCm with PyTorch: Prebuilt ROCm Docker images are provided, including a ROCm PyTorch image. You can use these prebuilt Docker images to save the effort of manually setting up a ROCm environment.
Docker: Ensure Docker is installed and configured correctly. Follow the Docker installation guide for your operating system. Skip this if you are running on a system that already has the required ROCm + Python + PyTorch stack installed.
Note: Ensure Docker permissions are correctly configured. To configure permissions for non-root access, run:
sudo usermod -aG docker $USER newgrp docker
Verify Docker is working correctly:
docker run hello-world
Scripts and example files: They are fetched from the tutorial repository in a cell in Environment setup.
File |
Contents |
Why it is needed |
Where it is used |
|---|---|---|---|
|
Python, PyTorch/ROCm, package versions, GPU name |
Confirm the stack before training |
Environment-check cell |
|
Wrapper around |
One training entry point |
Phase 1 and the optional continuation |
|
Pretrained PPO checkpoint |
A finished policy to compare with the short student run |
Phase 2 evaluation and replay |
Learning path#
You are encouraged to go through the Background knowledge section first, and then embark on your learning path:
Phase |
Config |
What you do |
|---|---|---|
ROCm environment and lab workspace |
Set up and check |
|
4 env × 2 iter |
Inspect the full log and confirm the stack runs |
|
500 env × 300 iter |
Train from scratch and render a 10-second replay |
|
Provided reference checkpoint |
Replay |
|
Compare |
Compare reward, episode length, curves, and video |
⚠️ Do not use the Run All button. Run cells in order. A long training cell can sit at
[*]while the GPU or CPU is busy. This means the cell is still running. Wait for it to finish before running the next cell.
Background knowledge#
Before starting the tutorial, it is helpful to understand the following concepts.
Reinforcement learning and MDPs#
What is reinforcement learning?#
Reinforcement learning (RL) is a machine learning approach in which an agent takes actions in an environment and uses feedback to improve long-term behavior. Unlike supervised learning, the robot in the problem to be solved by this tutorial is not told the correct 14-joint action to take in each state. It receives rewards for its actions and learns better action sequences through trial and error.
Why train robots with RL in simulation?#
Joints are strongly coupled; one action continues to affect later states.
There is more than one goal: speed, balance, smoothness, energy, and safety must be traded off against each other.
Simulation can generate millions of interactions in parallel, far more cheaply than hardware trial-and-error.
A neural policy can learn nonlinear feedback that is hard to write by hand.
Note: RL is not a magic bullet: a poorly designed reward can be exploited, simulated behavior might not transfer perfectly to real hardware, and training is stochastic. That is why replay, metrics, domain randomization, and eventual on-robot safety checks are used alongside RL.
What is an MDP?#
RL is usually formulated as a Markov Decision Process (MDP). An MDP is often written (S, A, P, R, γ):
Symbol |
Meaning |
MicroDuck example |
|---|---|---|
|
True situation of the world |
Pose, joints, velocities, contacts, … |
|
Decision the agent can make |
14-D joint targets |
|
How an action leads to the next state |
MuJoCo (physics engine) dynamics, contacts, actuator model |
|
How good the current outcome is |
Sum of tracking, upright, gait, … terms |
|
How much future reward is valued |
Closer to 1 means more long-horizon stability |
Note: Given the current state and action, predicting the next step does not require the full history. A real robot usually sees noisy, delayed, incomplete observations
oₜ, so code distinguishes “state” from “observation.”
Mapping MDP onto the RL task#
Command: periodically sample
twist = (vx, vy, yaw_rate); ranges are about ±0.4 m/s forward/back, ±0.3 m/s lateral, ±1.0 rad/s yaw.Observation: body angular velocity, gravity direction, joint position/velocity, last action, velocity command, head command, and other terms. The actor uses a 61-D deployable observation.
Action: every 20 ms the policy outputs 14-D joint targets; a better actuator models (BAM) voltage servo model turns them into actuator effort.
Rates: physics at 200 Hz (
sim_dt=0.005 s), policy at 50 Hz (ctrl_dt=0.02 s).Episode: at most 20 s, that is, 1000 control steps; tilt beyond ~70°, NaNs, or timeout terminate.
Policy: a neural net
πθ(a|o)with parametersθ. A finishedmodel_*.ptstores the learned parameters.
From common RL algorithms to PPO#
Typical RL algorithms#
Family |
Examples |
Idea |
Typical traits |
|---|---|---|---|
Value-based |
Deep Q-Network (DQN) |
Learn action values, then pick the highest |
Classic DQN fits discrete actions, not 14-D continuous joints |
Policy gradient |
REINFORCE |
Directly raise the probability of high-return actions |
Simple, but high-variance and sample-inefficient |
Actor-Critic |
A2C and A3C |
Actor chooses actions; critic evaluates states/actions |
Learns policy and value together; lowers policy-gradient variance |
On-policy actor-critic |
PPO |
Train on data just collected by the current policy, with limited update size |
Stable, mature implementations, scales to many parallel envs |
Off-policy actor-critic |
Soft Actor-Critic (SAC), Twin Delayed DDPG (TD3) |
Reuse experience from a replay buffer |
High sample reuse for continuous control; different tuning/system cost at large scale |
“On-policy” means that training mainly uses data from the current policy. “Off-policy” algorithms can reuse data collected by an older policy.
No algorithm is best for every task.
Why use PPO for MicroDuck training?#
MicroDuck has 14-D continuous actions, and simulation can run hundreds to thousands of environments (envs) at once. The reasons to use PPO for this task are:
Fits continuous control: the policy can output a continuous distribution per joint.
More stable training: PPO clip limits how far the new policy may move, so one update is less likely to destroy a learned gait.
Parallel sampling at scale: 500 or 2048 envs make a large on-policy batch.
Mature locomotion recipes: many verified PPO setups exist for robot walking.
Actor-critic structure: the actor learns actions; the critic estimates future return so you can tell whether an action was better than expected.
PPO does not simply “always pick the max-reward action.” It loops:
PPO involves the following key elements:
Return: discounted reward accumulated from the current time.
Value: the critic’s prediction of future return.
Advantage: how much better the outcome was than the critic expected.
PPO clip: if the new policy changes too much vs the old one, truncate that optimization gain, “don’t step too far at once.”
Entropy: keep some exploration so the policy does not become fully deterministic too early.
This training task uses an asymmetric actor-critic: the actor sees only the deployable 61-D noisy observation; during training the critic also sees foot contacts, foot height, and body linear velocity (76-D total). Extra information helps the critic evaluate training data more accurately, but deployment does not need those privileged observations.
Advanced: PPO hyperparameters used here
Actor/Critic multilayer perceptron (MLP):
[512, 256, 128], exponential linear unit (ELU)PPO clip:
0.2; discountgamma=0.99; GAElambda=0.955 epochs per batch, split into 4 mini-batches
Initial action-noise std
1.0, entropy coefficient0.01Adaptive learning rate, target KL divergence
0.01Left-right symmetric mirror loss, coefficient
0.5
Together these affect update stability, exploration, and compute. Do not casually change many of them in one experiment.
What does one iteration do?#
Each parallel env runs the current policy for 24 control steps.
Record
(observation, action, reward, terminated, value estimate).Gather all transitions, e.g.
500 × 24 = 12,000per round in Phase 1.Compute advantage and update Actor/Critic on the GPU.
Write logs, save a checkpoint if needed, then start the next round.
This tutorial uses Hydra parameters to configure training and evaluation. The key parameters you will use are:
Hydra parameter |
Meaning |
|---|---|
|
Independent simulations running at once |
|
How many sample-and-update loops |
|
Save a checkpoint every N iterations |
|
Do not auto-replay after training |
|
How many 50 Hz control steps to run at eval |
|
Root directory for logs and checkpoints |
Reward, domain randomization, and curriculum#
What is a reward function?#
A reward function turns “how the robot should behave” into a scalar at every step. The policy maximizes long-horizon return:
total reward = task reward + posture/gait reward − cost of unsafe or jerky behavior
Reward is not a human demonstration and not a success rate. A value of 94 does not mean 94% correct. Values are comparable only under the same task and same reward definition.
Omnidirectional MicroDuck walking cannot reward forward speed alone, or the robot could score by falling and sliding. The real task uses multiple objectives:
Design goal |
Representative term |
Weight direction |
What it prevents |
|---|---|---|---|
Track planar velocity |
|
|
Ignoring |
Track yaw rate |
|
|
Not turning with yaw-rate |
Stay upright |
|
|
Tilting or lying down to “farm speed” |
Track head pose |
|
|
Body walks while the head is uncontrolled |
Reasonable leg pose |
|
|
Bizarre joint configurations |
Reasonable air time |
|
|
Dragging feet or no real gait |
Foot height / slip |
|
penalty |
Too high or low swing, sliding on contact |
Safety and smoothness |
self-collision |
penalty |
Hitting itself, slamming limits, jitter |
Body stability |
body angular velocity |
penalty |
Violent upper-body motion |
Reward design is a multi-objective trade-off: too much tracking can yield a brutal gait, or too much smoothness can make the robot refuse to move. Look at total reward, per-term rewards, episode length, and replay video, not a single number.
What is domain randomization?#
Mass, friction, sensors, and actuators in simulation are more ideal than on hardware. Domain randomization randomly changes those conditions per environment during training so the policy works for a family of possible worlds, not just one exact parameter set.
Examples in this training task:
Observation noise and inertial measurement unit (IMU) bias
~20 ms joint-velocity delay
Battery voltage, load sag, command delay, and actuator friction variation
Robot mass/inertia, center of mass (CoM), joint initialization, and foot friction
Random pushes every 3-6 s to train disturbance recovery
The main goal is robustness and a smaller sim-to-real gap. It does not erase model error and does not replace hardware tests, action limits, or emergency stop (e-stop).
Advanced: Domain randomization vs. curriculum learning
Domain randomization is not curriculum learning:
Domain randomization answers “what random world is each env?”
Curriculum learning answers “when does difficulty increase?”
This task gradually strengthens action-change penalties, widens head-command/CoM randomization, and raises the fraction of stand-still commands. Curriculum advances by env control steps. Because each PPO iteration rolls out 24 steps, 12000 steps in the configuration is about 500 iterations. Raising num_envs increases samples per round; it does not by itself advance one environment’s curriculum.
Environment setup#
Follow these steps to set up the environment, launch the Jupyter Notebook, and install the required software.
Provision the ROCm/JupyterLab container#
Note: This tutorial runs in a ROCm PyTorch Docker container with access to a ROCm-capable AMD GPU. If this container and JupyterLab environment have already been provided for you, skip ahead to Check the active ROCm environment. Otherwise, complete the following steps to create and start the container.
Launch the Docker container from your host machine:
On the host, save this notebook (available at AI Developer Hub GitHub repository) in a directory and run:
mkdir -p runs
docker pull rocm/pytorch:rocm7.2.4_ubuntu24.04_py3.12_pytorch_release_2.10.0
docker run --rm -it --name microduck-lab \
--device=/dev/kfd --device=/dev/dri --group-add video \
--ipc=host --shm-size=8g \
-p 127.0.0.1:8888:8888 \
-v "$PWD:/workspace/lab" -v "$PWD/runs:/workspace/runs" \
-w /workspace/lab \
rocm/pytorch:rocm7.2.4_ubuntu24.04_py3.12_pytorch_release_2.10.0 bash
Inside the container, install the system libraries and JupyterLab, then start the server. The notebook’s workspace cell installs the remaining Python training packages and checks out the two pinned repositories.
apt-get update && apt-get install -y --no-install-recommends \
git curl ca-certificates ffmpeg \
libosmesa6 libgl1 libgl1-mesa-dri libglfw3 libxcursor1 libxi6 libxinerama1
python3 -m pip install --no-cache-dir jupyterlab 'jupyter_server>=2.21.1'
python3 -c 'import torch; assert torch.version.hip, "ROCm PyTorch is required"'
jupyter lab --ip=0.0.0.0 --port=8888 --no-browser --allow-root \
--notebook-dir=/workspace/lab
JupyterLab prints a URL with an access token. Open http://127.0.0.1:8888/lab?token=... on the host and select this notebook. For a remote GPU host, forward port 8888 over SSH and open the forwarded local URL. docker exec microduck-lab jupyter server list displays the tokenized URL again.
Check the active ROCm environment#
Start this tutorial (available at AI Developer Hub GitHub repository) in JupyterLab in the provisioned container. The cell below shows the environment used by this notebook’s Python kernel. Confirm HIP is available and at least one GPU is visible before continuing.
import os
import platform
import sys
import torch
from IPython.display import Markdown, display
def _os_name():
values = {}
with open('/etc/os-release', encoding='utf-8') as release:
for line in release:
if '=' in line:
key, value = line.strip().split('=', 1)
values[key] = value.strip('"')
return values.get('PRETTY_NAME', platform.platform())
gpu_count = torch.cuda.device_count() # ROCm PyTorch uses the torch.cuda API.
rows = [
('Operating system', _os_name()),
('Python', sys.version.split()[0]),
('Kernel Python', sys.executable),
('PyTorch', torch.__version__),
('ROCm / HIP', torch.version.hip or 'unavailable'),
('Visible AMD GPUs', str(gpu_count)),
]
rows += [(f'GPU {index}', torch.cuda.get_device_name(index)) for index in range(gpu_count)]
display(Markdown('| Component | Active environment |\n|---|---|\n' +
'\n'.join(f'| {name} | `{value}` |' for name, value in rows)))
if not torch.version.hip or gpu_count < 1:
raise RuntimeError('This notebook needs HIP-enabled PyTorch and a visible AMD GPU. Check the Jupyter kernel and container GPU access.')
Prepare the tutorial workspace#
Run the next cell once before training. It creates microduck_workspace/ beside this notebook, checks out the MicroDuck task and tutorial support files at pinned commits, and installs the Python training packages into the current Jupyter kernel’s virtual environment. The official container already supplies ROCm PyTorch; the cell checks that it remains available after installation. Downloading and installing the packages can take several minutes.
import importlib
import os
import subprocess
import sys
from pathlib import Path
import torch
WORKSPACE = Path.cwd() / 'microduck_workspace'
TASK_ROOT = WORKSPACE / 'microduck_rl_unilab'
LAB_ROOT = WORKSPACE / 'microduck_rl_tutorial'
LOG_ROOT = Path('/workspace/runs')
TASK_SHA = '20fb528fa6c4a9559155b33e5304568668c7b7e2'
LAB_SHA = '0e515b775273f96af4b45b5eed0db886880d6ccc'
def _run(*args):
subprocess.run(args, check=True)
def _checkout(repo, dest, sha, sparse=None):
if not dest.exists():
command = ['git', 'clone', '--filter=blob:none', '--no-checkout']
if sparse:
command.append('--sparse')
_run(*command, repo, str(dest))
_run('git', '-C', str(dest), 'fetch', '--depth', '1', 'origin', sha)
if sparse:
_run('git', '-C', str(dest), 'sparse-checkout', 'set', *sparse)
_run('git', '-C', str(dest), 'checkout', '--detach', sha)
if not (dest / '.git').exists():
raise RuntimeError(f'Expected a Git checkout at {dest}')
actual = subprocess.check_output(['git', '-C', str(dest), 'rev-parse', 'HEAD'], text=True).strip()
if actual != sha:
raise RuntimeError(f'{dest} is at {actual}; expected {sha}')
WORKSPACE.mkdir(parents=True, exist_ok=True)
LOG_ROOT.mkdir(parents=True, exist_ok=True)
_checkout('https://github.com/rocPAI-Forge/microduck_rl_unilab.git', TASK_ROOT, TASK_SHA)
_checkout('https://github.com/rocPAI-Forge/microduck_rl_tutorial.git', LAB_ROOT, LAB_SHA,
sparse=['scripts', 'examples/velocity_flat_demo'])
if not (LAB_ROOT / 'scripts' / 'run_train.sh').is_file():
raise RuntimeError('Lab training script is missing from the pinned checkout')
if not (LAB_ROOT / 'examples' / 'velocity_flat_demo' / 'model_950.pt').is_file():
raise RuntimeError('Reference checkpoint is missing from the pinned checkout')
requirements = [
'matplotlib', 'pandas', 'tensorboard',
'unilab[mujoco]==1.0.0', 'unilab-rl==1.0.0', 'unisim-core==1.0.0',
'torch==2.10.0', 'tensordict==0.14.3',
]
_run(sys.executable, '-m', 'pip', 'install', '--no-cache-dir', *requirements)
_run(sys.executable, '-m', 'pip', 'install', '--no-cache-dir', '--no-deps', '-e', str(TASK_ROOT))
importlib.invalidate_caches()
if not torch.version.hip or torch.cuda.device_count() < 1:
raise RuntimeError('ROCm PyTorch or GPU access is unavailable after package installation')
os.environ.update({
'LAB_ROOT': str(LAB_ROOT),
'MICRODUCK_ROOT': str(TASK_ROOT),
'LAB_LOG_ROOT': str(LOG_ROOT),
'UNILAB_EXTRA_REGISTRY_PACKAGES': 'microduck_rl_unilab.tasks',
'MUJOCO_GL': 'osmesa',
'PYOPENGL_PLATFORM': 'osmesa',
})
scripts_dir = str(LAB_ROOT / 'scripts')
if scripts_dir not in sys.path:
sys.path.insert(0, scripts_dir)
for script in (LAB_ROOT / 'scripts').glob('*.sh'):
script.chmod(0o755)
print('Workspace:', WORKSPACE)
print('Task commit:', TASK_SHA)
print('Lab support commit:', LAB_SHA)
print('ROCm PyTorch:', torch.__version__, 'HIP:', torch.version.hip)
print('Ready for the environment check and training cells below.')
Software stack and environment check#
How do the pieces fit together?#
The following diagram shows how the components work together during training.
Open-source projects this tutorial depends on#
The following table lists a number of open-source projects this tutorial uses.
Project |
Repo |
Role |
How this tutorial uses it |
|---|---|---|---|
UniLab |
General robot RL framework: unified env APIs, parallel sampling, algorithms, logging, checkpoints, visualization, and multiple sim/compute backends |
Creates MuJoCo envs, runs PPO rollouts/updates, saves training artifacts, and provides evaluation and playback |
|
microduck_rl_unilab |
MicroDuck task pack on UniLab: robot assets, better actuator models (BAM), obs/action defs, rewards, commands, domain randomization, curriculum, and task configs |
Supplies |
Both projects use the Apache-2.0 license. Think of them as: UniLab is the generic training base; microduck_rl_unilab implements MicroDuck’s concrete learning task on that base. This repository sits on top and organizes the teaching flow and pretrained assets.
Here are some common mix-ups regarding these projects:
ROCm is not an RL framework: it lets PyTorch run on AMD GPUs.
PyTorch does not contain the MicroDuck task: it handles networks and optimization.
UniLab is not the robot model: it provides generic training and simulation management.
microduck_rl_unilabdefines this tutorial’s robot, rewards, and task config.microduck_rl_tutorialis the teaching layer: it does not reimplement PPO; it organizes experiments and notebooks.This tutorial uses
--sim mujoco. MuJoCo computes robot dynamics; PyTorch/ROCm run the policy network and PPO updates. Those jobs are different.
The tutorial uses HIP-enabled PyTorch from the official ROCm image and MuJoCo for simulation.
Component locations inside the container#
Component |
Location |
|---|---|
ROCm PyTorch and the Jupyter kernel |
|
Notebook |
|
Tutorial workspace |
|
MicroDuck task |
|
Tutorial scripts and reference model |
|
Training and evaluation outputs |
|
The workspace cell sets UNILAB_EXTRA_REGISTRY_PACKAGES and MICRODUCK_ROOT for task registration. Run the check below and confirm HIP, the task, and the reference checkpoint are available.
Comparing the two policies in this tutorial#
Phase 1 trains a student policy from scratch; Phase 2 loads a pretrained reference policy. Compare their evaluation videos and training budgets. Their scores use different measures.
Phase 1: your student run |
Phase 2: provided reference |
|
|---|---|---|
Checkpoint |
|
|
Training budget |
500 env × 300 updates |
2048 env × 951 updates, including directional fine-tuning |
Samples / iteration |
12,000 |
49,152 |
Env steps at checkpoint |
~3.6M |
46,743,552 |
What is measured |
PPO reward, episode length, and replay video |
Fixed-command six-direction selection score 81.9 / 100 (seed 123) and replay video |
The 950 in model_950.pt is a zero-based checkpoint index: updates 0 through 950 make 951 updates. Each update gathers 24 steps in 2048 environments, so 2048 × 24 × 951 = 46,743,552 environment steps.
The tutorial includes model_950.pt to show a more capable reference PPO policy produced with more parallel environments, more training updates, and directional fine-tuning. You can explore this yourself by increasing algo.num_envs and algo.max_iterations in the Phase 1 training cell, then evaluating the resulting checkpoint; expect more GPU time and memory use. Changing those two settings alone does not reproduce the exact reference training recipe. Compare the replay videos and behavior: your run’s PPO reward and the reference’s six-direction selection score are different metrics.
2048 env means more experience is collected in parallel per iteration. It is a sample-budget choice. The policy interface is: actor observation 61-D, critic observation 76-D, action 14-D.
Environment check#
The following cell runs the tutorial’s check_env.sh. Confirm it reports HIP available, that the microduck_velocity_flat task registers, and that the reference checkpoint is found. If any of those fail, the training and eval cells further down do not work.
!bash "$LAB_ROOT/scripts/check_env.sh"
Smoke test (4 env × 2 iter)#
The smoke test runs only 2 iterations to verify ROCm/PyTorch, task registration, MuJoCo XML, sampling, PPO updates, and checkpoint writes.
Run the following cell to perform the smoke test. Unlike later phases, this cell prints the full, unfiltered output directly below the cell instead of writing it to a log file. Watch for:
Learning iteration 0/2: current training roundMean reward: average cumulative reward of recent episodes; low early on is normal.Mean episode length: average surviving control steps; very short usually means frequent falls.Collection time / Learning time: sampling vs network-update timeETA: remaining time at the current pace.
%%bash
set -euo pipefail
cd "${MICRODUCK_ROOT}"
# Only 2 iter: keep the full raw microduck-train output so log fields are visible.
microduck-train --algo ppo --task microduck_velocity_flat --sim mujoco \
algo.num_envs=4 algo.max_iterations=2 training.no_play=true \
training.logger=tensorboard training.log_root="${LAB_LOG_ROOT}"
Phase 1 Train from scratch (500 env × 300 iter)#
Goal: complete a medium-scale PPO run yourself and observe a policy that is “learning, but not fully trained.”
Parameter |
Value |
Beginner reading |
|---|---|---|
|
500 |
500 independent MicroDuck simulations at once |
|
300 |
Repeat 300 times: “sample 24 steps → update the net” |
|
50 |
Save every 50 iter so a crash does not wipe everything |
|
10 |
Notebook refreshes a summary every 10 iter |
Full training output goes to /workspace/runs/logs/phase1_500x300.log. The notebook keeps a single concise status refresh. Watch the overall trend of reward and episode length (these metrics do not rise monotonically).
A cell that remains at [*] means training is running. Training is finished when the log reaches the planned iteration and ETA is 0:00:00.
%%bash
set -euo pipefail
export LAB_TRAIN_LOG="${LAB_LOG_ROOT}/logs/phase1_500x300.log"
export LAB_LOG_EVERY=10
bash "${LAB_ROOT}/scripts/run_train.sh" \
--algo ppo --task microduck_velocity_flat --sim mujoco \
algo.num_envs=500 algo.max_iterations=300 algo.save_interval=50 \
training.no_play=true training.logger=tensorboard training.log_root="${LAB_LOG_ROOT}"
1.1 How to read the one-line log after training#
A sample line in the log:
[train] iter 299/300 | reward 4.59 | ep 78.59 | 0:03:59 | ETA 0:00:00 | 0.80s/iter
The following table explains each field in the line:
Field |
Meaning |
How to read this run |
|---|---|---|
|
PPO iterations are 0-indexed; |
Planned 300 iter finished |
|
Mean cumulative reward of recently finished episodes; tracking, stability, smoothness, energy, etc., combined, not a 0-100 score |
Compare only with the same task and reward |
|
Mean episode length: control steps before fall or timeout |
At 50 Hz this is |
|
Elapsed time printed by the trainer |
Progress of this run, not a hardware result |
|
Remaining time at the recent pace |
Almost done |
|
Average time per PPO iteration printed by the trainer |
A trainer counter, not a policy-quality score |
If reward keeps going up, is training necessarily getting better?#
A rising long-term trend is a good sign, but a single bump or short-term noise does not prove the policy improved. PPO samples randomly, so reward is not monotonic. It can also dip when the training curriculum gets harder, or look high because of reward hacking.
Judge training by all of the following:
Reward trend: tens of iterations, not the last point
Episode length: whether it approaches 1000 steps (~20 s cap on this task)
Eval video: upright, tracking commands, without obvious slip, jitter, or heading error
In this example, reward=4.59, ep=78.59 means the 300-iteration policy learned something, but episodes end after ~1.57 s. Use your own reward, episode length, and replay video to judge your run. The reference policy provides a stronger gait to inspect next.
Quick exercise: if the next run gets
ep=750, convert that to seconds first, then use the reward curve and video to decide whether it really improved.
1.2 Eval: render a 10-second replay#
Training vs eval: training explores and updates parameters; eval loads a checkpoint, runs the current policy, and renders video without learning.
Note: Eval rendering is CPU-bound because OSMesa renders on the CPU rather than the GPU. The cell might remain at [*] for an extended period. Wait for it to finish before proceeding.
Parameter |
Meaning |
|---|---|
|
Load the latest training run for this task |
|
Control rate is 50 Hz, so |
|
Render one robot in a close-up replay |
|
Keep the robot large and centered in the frame |
This replay shows one sampled environment, while the training summary covers the full run. Use the close view to inspect posture, gait, foot slip, and falls.
# Phase 1 eval: show the 10-second robot replay in this cell when it finishes
import os
import subprocess
from pathlib import Path
from IPython.display import Video, display
microduck_root = os.environ["MICRODUCK_ROOT"]
log_root = Path(os.environ["LAB_LOG_ROOT"]) / "MicroduckVelocityFlat"
subprocess.run(
[
"microduck-eval", "--algo", "ppo",
"--task", "microduck_velocity_flat", "--sim", "mujoco",
"--load-run", "-1",
"training.play_steps=500", "training.play_env_num=1",
"training.cam_distance=0.9", "training.cam_tracking=true",
"training.log_root=" + os.environ["LAB_LOG_ROOT"],
],
cwd=microduck_root,
check=True,
)
videos = sorted(log_root.glob("*_mujoco/play_video.mp4"), key=lambda p: p.stat().st_mtime)
if not videos:
raise FileNotFoundError("Eval finished, but play_video.mp4 was not found. Check the logs above.")
video = videos[-1]
print(f"\nRobot playback: {video} ({video.stat().st_size / 1024:.0f} KiB)")
display(Video(
data=video.read_bytes(),
embed=True,
mimetype="video/mp4",
html_attributes='controls autoplay loop muted playsinline style="width:100%;max-width:960px;height:auto"',
))
The cell below reads the training summary from your Phase 1 run and prints the key metrics: checkpoint path, completed iterations, mean reward, and mean episode length. Cross-reference these numbers with the replay in step 1.3.
# Phase 1: read the training summary (robot video was shown in the previous eval cell)
import json
import os
from pathlib import Path
LOG = Path(os.environ["LAB_LOG_ROOT"]) / "MicroduckVelocityFlat"
runs = sorted(LOG.glob("*_mujoco"), key=lambda p: p.stat().st_mtime)
if not runs:
raise FileNotFoundError("Please finish phase 1 training first.")
PHASE1_RUN = runs[-1]
s1 = json.loads((PHASE1_RUN / "run_summary.json").read_text())
print(f"Phase 1 run: {PHASE1_RUN.name}")
print(f" reward={s1['final_mean_reward']:.2f} ep_len={s1['mean_episode_length']:.1f} "
f"iters={s1['completed_iterations']}")
1.3 How to read the Phase 1 replay: why might the robot barely move?#
The Phase 1 video follows one environment using your 300-iteration policy. Its initial state and velocity command are sampled by the task. The robot may fall, reset, and receive another command during the replay.
Separate two cases while watching:
If the target speed is near 0, standing or balancing in place can be the correct response.
If there is a clear motion command but the robot stays still, only steps in place, or falls quickly, the policy has not yet formed a reliable command-tracking gait.
In the example run, reward≈4.59 and episode length≈78.59 steps (~1.57 s at 50 Hz), plus a video where the robot barely moves, show an early policy that is still learning. Your results may differ; interpret your own metrics and replay together. Ten seconds is the length of the eval clip, not each episode’s lifetime; robots may terminate and reset several times in the video.
A 300-iteration policy is not always weak. Use the reward, episode length, and replay from your own run to judge how far it trained.
Phase 2 The pretrained reference demo#
Goal: Load the selected reference checkpoint and inspect the robot’s walking behavior.
A few words on the reference checkpoint:
A checkpoint is model parameters saved at some training moment. It is at
examples/velocity_flat_demo/model_950.pt.The selected checkpoint used 2048 parallel envs and 46,743,552 environment steps (951 updates;
950is a zero-based checkpoint index). Its training included directional fine-tuning.Selection is not based on final reward, but on 32 trials × 10 s of fixed commands: forward, back, left, and right all passed; yaw improved but remains weaker than translation.
Full recipe and six-direction results:
examples/velocity_flat_demo/run_summary.jsonandcardinal_eval*.json.
The following table explains the metrics for the reference demo:
Reference demo metric |
Value |
How to read it |
|---|---|---|
Linear directions |
4 / 4 passed |
Stability, sign, error, and cross-axis drift together |
seed-123 mean score |
~81.9 / 100 |
Six-direction composite, not PPO reward |
seed-123 worst direction |
~59.4 / 100 |
Current bottleneck is still yaw |
Yaw survival |
100% |
No fall in 10 s; does not mean yaw magnitude is fully accurate |
Checkpoint |
|
Chosen by an automatic checkpoint sweep, not the last saved file |
2.1 Eval: render the reference demo on this machine#
The next cell loads examples/velocity_flat_demo/model_950.pt and renders 16 parallel simulations on this machine. Each robot uses the same policy, but receives its own randomly sampled forward/back, lateral, and yaw command. The clip shows a variety of behavior; it does not guarantee every direction appears.
Parameter |
Meaning |
|---|---|
|
Load the provided reference checkpoint |
|
Control rate is 50 Hz, so |
|
Render 16 parallel environments with independently sampled velocity commands |
Default overview camera |
Show the full group; individual robots appear smaller than in Phase 1 |
Frames are rendered on the CPU. The cell stays on [*] for a few minutes. GPU usage is 0 during that time, but it is still running.
# Phase 2 demo eval: show robot playback in this cell when it finishes
import os
import subprocess
from pathlib import Path
from IPython.display import Video, display
microduck_root = os.environ["MICRODUCK_ROOT"]
demo = Path(os.environ["LAB_ROOT"]) / "examples" / "velocity_flat_demo"
subprocess.run(
[
"microduck-eval", "--algo", "ppo",
"--task", "microduck_velocity_flat", "--sim", "mujoco",
"--load-run", str(demo),
"training.play_steps=200", "training.play_env_num=16",
"training.log_root=" + os.environ["LAB_LOG_ROOT"],
],
cwd=microduck_root,
check=True,
)
video = demo / "play_video.mp4"
if not video.is_file():
raise FileNotFoundError("Demo eval finished, but play_video.mp4 was not found. Check the logs above.")
print(f"\nRobot playback: {video} ({video.stat().st_size / 1024:.0f} KiB)")
display(Video(
data=video.read_bytes(),
embed=True,
mimetype="video/mp4",
html_attributes='controls autoplay loop muted playsinline style="width:100%;max-width:1200px;height:auto"',
))
2.2 How to read the Phase 2 reference demo summary#
The cell below reads run_summary.json from the provided reference checkpoint and prints its metrics. The score here is a six-direction selection score out of 100, not the PPO reward used in Phase 1. The two numbers are not directly comparable. Focus on the direction pass rates to understand how well the reference policy handles different desired velocities and directions.
# Phase 2: read the reference-demo selection summary (video was shown in the previous eval cell)
import json
import os
from pathlib import Path
DEMO = Path(os.environ["LAB_ROOT"]) / "examples" / "velocity_flat_demo"
s2 = json.loads((DEMO / "run_summary.json").read_text())
print("=== Phase 2 reference demo run_summary ===")
print(json.dumps({k: s2[k] for k in (
"checkpoint", "completed_iterations", "num_envs",
"selection_eval_yaw_rate", "selection_eval_mean_score",
"selection_eval_worst_direction_score", "selection_reason",
)}, indent=2, ensure_ascii=False))
Phase 3 How do you tell if the policy got better?#
Do not conclude from a single reward number. Compare Phase 1 and Phase 2 in this order:
Episode length first: Check whether episodes end early because of frequent falls. The maximum episode length is 1000 steps, or about 20 s.
Replay video: Check whether the robot stays upright, follows the desired velocity and direction, and avoids obvious slipping, jitter, or an uneven gait.
Reward curve: Check whether the curve shows a long-term improvement, large oscillations, or regression. Noise in RL curves is normal.
Training scale: Phase 1 uses ~3.6M env steps, while the provided
model_950.ptcheckpoint uses 46.7M over 951 updates. Keep in mind that env count and iteration count both changed, so this shows overall scale, not a clean isolation of either factor.
Expected picture#
Dimension |
Phase 1 student |
Phase 2 reference |
|---|---|---|
Source |
Trained from scratch in this notebook |
Provided by the pinned upstream repository |
Parallel environments |
500 |
2048 |
Training updates |
300 |
951, including directional fine-tuning |
Env steps at checkpoint |
~3.6M |
46,743,552 |
Behavior |
Early policy; inspect your eval video |
Four linear directions pass the fixed-command gate |
Metric shown |
PPO reward and episode length |
Six-direction selection score ~81.9/100, not PPO reward |
The reference model has a larger sample budget and a different training recipe. The comparison illustrates the two policies’ observed behavior; it does not isolate training duration as the cause of any difference.
Reflection questions#
Does a longer episode length always mean more accurate velocity tracking? Why?
If you only increase survival reward, what “cheating” behavior might the robot learn?
Phase 2 uses more parallel environments than Phase 1. Why is a higher
num_envsnot, by itself, evidence that the gait is better?To compare
num_envsrigorously, how would you set controls and multiple random seeds?Which video symptoms might expose problems in the reward or domain-randomization config?
Answers to reflection questions (think first, then expand to check)
1. Does longer episode length always mean more accurate velocity tracking?
No. It only means the robot lasted longer before termination. It might stand in place or step slowly to avoid falling without tracking the target speed. Check velocity-tracking reward, episode length, and eval video together.
2. What “cheating” behavior can a survival-only reward produce?
The policy may freeze, crouch, stand stiffly, or keep tiny steps, because those behaviors survive without completing the walking task. That is reward hacking: optimizing the specified score rather than the intended behavior.
3. Why is a higher num_envs not, by itself, evidence that the gait is better?
A larger num_envs only changes how many transitions each update sees. Gait quality still has to be read from the reward terms, episode length, and the evaluation video. Do not treat environment count as a score.
4. How to compare num_envs rigorously?
Fix task config, network, PPO hyperparameters, hardware, and total env steps. When you change num_envs, invert iteration count so groups see the same sample budget. Run each group on the same seed set, report mean and std, and compare learning quality (reward, episode length, and video). Otherwise you cannot tell num_envs from data volume or randomness.
5. Which video symptoms may expose config issues?
Persistent lean to one side: left-right symmetry, command tracking, or randomization may be weak.
Obvious foot slip: foot-slip penalty, friction, or contact modeling may be off.
High-frequency jitter: weak action-smoothness penalty, or insufficient delay/noise coverage.
Only standing, refusing to walk: survival reward too strong vs. task reward.
Frequent falls after command changes: curriculum ramped too fast, or command coverage too narrow.
Tiny simulation changes break it: domain randomization too narrow. If training does not converge, the randomization range might be too wide.
These only generate hypotheses. Check per-term rewards and repeated evals to confirm.
The cell below plots reward and episode length across Phase 1 iterations and overlays the Phase 2 reference demo metrics as horizontal reference lines. Use this to judge whether your Phase 1 run is trending toward the reference or plateauing early. A flat reward curve that never approaches the reference line suggests the policy stopped improving: check domain-randomization settings and training budget.
# Phase 3: phase 1 training metrics + reference-demo behavior gate
import json
import os
from pathlib import Path
import matplotlib.pyplot as plt
from tensorboard.backend.event_processing.event_accumulator import EventAccumulator
LOG = Path(os.environ["LAB_LOG_ROOT"]) / "MicroduckVelocityFlat"
DEMO = Path(os.environ["LAB_ROOT"]) / "examples" / "velocity_flat_demo"
phase1 = sorted(LOG.glob("*_mujoco"), key=lambda p: p.stat().st_mtime)[-1]
s1 = json.loads((phase1 / "run_summary.json").read_text())
s2 = json.loads((DEMO / "run_summary.json").read_text())
print("=== Phase 1: this training run ===")
for name, value in (
("num_envs", s1["global_num_envs"]),
("iterations", s1["completed_iterations"]),
("total_env_steps", s1["total_env_steps"]),
("final_reward", f"{s1['final_mean_reward']:.1f}"),
("episode_length", f"{s1['mean_episode_length']:.0f}"),
("train_wall_sec", f"{s1['training_wall_time_sec']:.0f}"),
):
print(f"{name:24} {value}")
print("\n=== Phase 2: reference demo fixed-command gate ===")
for name, value in (
("checkpoint", s2["checkpoint"]),
("training_iterations", s2["completed_iterations"]),
("selection_mean_score", f"{s2['selection_eval_mean_score']:.1f}"),
("worst_direction_score", f"{s2['selection_eval_worst_direction_score']:.1f}"),
("yaw_rate", s2["selection_eval_yaw_rate"]),
):
print(f"{name:24} {value}")
print("\nNote: PPO reward and the six-direction gate score are different metrics; do not compare them by raw magnitude.")
def plot_scalars(run_dir: Path, tags: list[str]):
ev = list(run_dir.glob("events.out.tfevents.*"))
if not ev:
return None
ea = EventAccumulator(str(ev[0]))
ea.Reload()
out = {}
for tag in tags:
if tag in ea.Tags()["scalars"]:
scalars = ea.Scalars(tag)
out[tag] = ([x.step for x in scalars], [x.value for x in scalars])
return out
tags = ["Train/mean_reward", "Train/mean_episode_length"]
data = plot_scalars(phase1, tags)
if data:
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
for ax, tag in zip(axes, tags):
if tag in data:
xs, ys = data[tag]
ax.plot(xs, ys)
ax.set_title(tag)
ax.set_xlabel("iteration")
fig.suptitle(f"Phase 1 training curves · {phase1.name}")
plt.tight_layout()
plt.show()
Summary#
In this tutorial, you trained a MicroDuck walking policy from scratch with PPO, evaluated its behavior using training metrics and replay video, and compared it with a more extensively trained reference policy. Along the way, you learned how reward, episode length, training scale, domain randomization, and curriculum affect how you interpret an RL training run.
Key takeaways#
What you have learned:
RL learns a control policy from simulated trial-and-error and reward. It does not need a correct joint action per state.
PPO is actor-critic with limited policy-update size. It is stable and fits continuous actions plus large parallel sampling, but not every task.
The reward defines “what good means.” It must cover task, stability, gait, safety, and smoothness, and you must watch for reward hacking.
num_envssets parallel data per round, andmax_iterationssets how many sample-update loops. Training scale is not either number alone.Domain randomization improves robustness under uncertainty, and curriculum changes difficulty over time. They are different ideas.
Good simulation performance does not guarantee good hardware deployment. You still need model checks, latency tests, action limits, fall protection, and e-stop.
Check your understanding#
Use these questions to check your understanding on the key takeaways:
What does the 4-environment, 2-iteration smoke test verify?
What do
num_envs=500andmax_iterations=300control in Phase 1?At 50 Hz, how long is an evaluation with
play_steps=500?Why does Phase 2 load the provided
model_950.ptinstead of training it during this tutorial?
Answers
It checks ROCm, task registration, MuJoCo, PPO updates, and checkpoint writing. It is too short to learn a stable gait.
They set 500 parallel simulations and 300 rollout/update rounds.
500 × 0.02 s = 10 sof replay. An individual episode may end and reset sooner.The reference model was trained with a larger sample budget and directional fine-tuning. Loading it lets you compare its behavior with your shorter training run within the tutorial time.
Next step: try another gait in microduck_rl_unilab#
This tutorial performs omnidirectional velocity tracking with microduck_velocity_flat. The same container already includes the microduck_rl_unilab task pack at /workspace/lab/microduck_workspace/microduck_rl_unilab. Change the task name to keep practicing other gaits and behaviors.
Task |
What it trains |
|---|---|
|
This tutorial: forward, lateral, and yaw velocity tracking |
|
Mostly forward walking, with narrower lateral and yaw commands |
|
Walking plus recovery after a fall |
|
Rising from sit, prone, and supine poses, then body-pose tracking |
The repository README lists the remaining tasks and which simulators they use. Velocity commands, rewards, and curriculum for each task live in:
/workspace/lab/microduck_workspace/microduck_rl_unilab/src/microduck_rl_unilab/conf/ppo/task/<task>/base.yaml
Change one thing at a time, then compare with the run from this tutorial:
lin_vel_x,lin_vel_y, andang_vel_zundercommands.twist.ranges. For example, raise the forward limit above0.4 m/s, or narrow the yaw range.A
weightunderreward. Watch whether upright posture, velocity tracking, and the gait move together.One range in domain randomization or the curriculum.
Confirm the new config with the same smoke test used in this tutorial before raising num_envs and max_iterations:
cd /workspace/lab/microduck_workspace/microduck_rl_unilab
microduck-train --algo ppo --task microduck_sprint_flat --sim mujoco \
algo.num_envs=4 algo.max_iterations=2 training.no_play=true \
training.logger=tensorboard training.log_root=/workspace/runs
Compare reward, episode length, and the replay video, and repeat with more than one seed. When one experiment changes both env count and iteration count, it shows a change in overall training scale. A stable simulation gait still needs action limits, fall protection, and an e-stop before a real robot.
Troubleshooting#
Symptom |
What to do |
|---|---|
Training cell stuck at |
This can be normal. Check for a new directory under |
Eval has no |
Confirm |
No robot picture |
Eval is off-screen. Wait for the cell to finish and the embedded video plays under the log. |
Browser blocks autoplay |
Click the center of the video to start playback. The video is muted and set to loop. |
|
Confirm that the |
Glossary#
The following terms are used throughout this tutorial.
Term |
One-line meaning |
|---|---|
Agent |
The learner or decision maker. In this tutorial, the policy network |
Environment / env |
The simulated world that takes actions and returns the next obs and reward |
Observation |
What the policy can actually read. Can be noisy, delayed, incomplete |
Action |
Commands the policy sends to the controller. In this tutorial, the 14-D joint targets |
Policy / Actor |
Model that maps observations to actions |
Critic / Value |
Predicts future return to help the actor learn |
Reward |
Immediate feedback each step; not a success rate |
Return |
Discounted cumulative reward from a given step |
Episode |
One interaction from reset until fall or timeout |
Rollout |
A stretch of transitions collected with the current policy |
Transition |
One |
Iteration |
One “rollout sampling + PPO update” round |
Checkpoint |
Saved model parameters (and related state) from training |
Domain randomization |
Randomize simulation conditions to improve robustness |
Curriculum |
Gradually change task or randomization difficulty |
Sim-to-real |
Transfer a policy learned in simulation onto a real robot |
More ideas to explore
GAE: Estimate advantage from multi-step rewards and the critic, trading bias vs variance.
PPO clip: Limit optimization gain from the new/old policy probability ratio so one update cannot jump too far.
Mirror loss: Constrain the policy with biped left-right symmetry so gaits do not favor one side.
Curriculum: Change reward weights, command ranges, or randomization over training, rather than enabling all domain-randomization options at once.