Lab 3 · First Training

What you do in this lab

You start one real training run on a cloud GPU, and you read it while it runs. Before the button, this page walks you through every piece the run is made of: the body, what it sees, how it is scored, when a try ends, the recipe, and the knobs you may turn.

Read the cards in order. Each one asks you to do one thing. The last card is the button.

A training scene: the camera rides one car, neighboring courses' waypoint chains stretch to the horizon
// A real training scene, 64 worlds packed close for the shot. Each chain of dots is one world's course. Your run gets 1024.

Isaac Lab makes 1024 copies of the twin, each on its own course of 10 waypoints about 5 m apart. All 1024 cars live in one tensor. One sim step moves every car at once. Headless: numbers come back, not pictures.

The body you train

Do this: drive the car below. This is the twin as the physics engine sees it: a box for the chassis, a nose, four cylinders for the wheels, with the real masses and sizes. That is all training ever touches. Grip, the steering lock, what happens in a collision: all from these shapes. Full steer is 26.37 degrees, the angle the real car's servo reaches at the end of its calibrated travel: the same number in the sim, in SIL, and on the floor.

loading the simulator
racer.drive(steer=+0.00, throttle=+0.00) waypoints 0/6

Then do this: switch the body to the scan mesh. Nothing about the driving changes. The mesh is a skin: we filmed the real car, built a splat from the video (a 3D Gaussian splat: a cloud of coloured blobs that renders like a photo from any side), turned the splat into a mesh (a surface of triangles), and laid the mesh over the same joints: the chassis mesh on the chassis body, a wheel mesh on each wheel.

Training never sees the mesh. Physics wants few, simple shapes: fast, stable, thousands of copies. Eyes want the real car. Keeping the two apart means the pretty one can change any time without training again.

What the car sees

Not a camera. Not lidar. Each step, the policy gets three numbers: the distance to the next waypoint, and the cos and sin of the heading error. You watched them in The Twin.

// one angle as cos and sin: no jump at +/-180 degrees. No perception, on purpose.

The reward: how a step is scored

Every step, every car gets a number. Training pushes the policy toward actions that got bigger numbers. Four terms, with the weights from the env's cfg:

  • progress x 1.0 - how much closer to the waypoint than last step. Going backwards costs.
  • heading x 0.05 - a small bonus for facing the waypoint; it fades fast with the angle (coefficient 0.25).
  • goal reached + 10.0 - once, when the car is within 0.4 m. The next waypoint lights up.
  • steer rate - 0.05 x (change in steer)^2 - a tax on jerking the wheel. Without it the policy bang-bangs the steering.

Do this: read the lines below top to bottom and find the four terms in the reward = (...) sum. This is the file the GPU runs, not a copy.

sim/isaaclab/GoatRacer/GoatRacer/tasks/goatracer_fiesta_3obs_env.py

    def _get_rewards(self) -> torch.Tensor:
        c = self.cfg
        position_progress = self._previous_position_error - self._position_error
        heading_rew = torch.exp(-self._target_heading_error.abs() / c.heading_coefficient)
        goal_reached = self._position_error < c.position_tolerance
        self._target_index = self._target_index + goal_reached.long()
        self.task_completed = self._target_index > (c.num_goals - 1)
        self._target_index = self._target_index % c.num_goals

        # squared steer change — _actions2/_last_actions2 are maintained by the
        # chassis' _pre_physics_step
        dsteer = self._actions2[:, 0] - self._last_actions2[:, 0]
        self._ep3_dsteer += dsteer.abs()
        self._ep3_steps += 1.0
        reward = (
            position_progress * c.position_progress_weight
            + heading_rew * c.heading_progress_weight
            + goal_reached.float() * c.goal_reached_bonus
            - dsteer.pow(2) * c.steer_rate_weight
        )

        one_hot = torch.nn.functional.one_hot(self._target_index, num_classes=c.num_goals)
        self.waypoints.visualize(marker_indices=one_hot.view(-1).tolist())
        return torch.nan_to_num(reward, nan=0.0)

When a try ends

A try (an episode) ends three ways: the car finished all 10 waypoints (success), the clock ran out (30.0 s), or the car flipped. Then that copy resets to the start of a fresh course and goes again. 1024 copies, each on its own clock: at any moment some are starting and some are finishing.

sim/isaaclab/GoatRacer/GoatRacer/tasks/goatracer_fiesta_3obs_env.py

    def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]:
        # original contract (+ physics reality: a flipped car has failed).
        # NOTE: timeout deliberately lands in the *terminated* slot — the v3
        # chassis fixed that order (see its _get_dones); this env keeps the
        # original layout on purpose. Don't "fix" either one.
        flipped = self.robot.data.projected_gravity_b[:, 2] > -0.5
        task_failed = (self.episode_length_buf > self.max_episode_length) | flipped
        if hasattr(self, "_nan_envs") and self._nan_envs.any():
            task_failed = task_failed | self._nan_envs
            self.extras["nan_resets"] = self._nan_envs.sum()
        # eval_v3_speed reads these: "laps" = completed courses; no crash concept here
        self.extras["lap_completed"] = self.task_completed.clone()
        self.extras["crashed"] = torch.zeros_like(self.task_completed)
        return task_failed, self.task_completed

Do this: find flipped. It reads the gravity vector in the car's own frame: when "down" is no longer below the car, the car is on its roof.

The loop and the network

The trained policy driving toward its next waypoint, the rest of the course ahead
// Rollout (the trained policy driving on its own, recorded), final reward 73.59. Green is the next waypoint.

One iteration: every car drives 32 steps with the current policy, the rewards are summed up, and PPO (the training algorithm) pulls the weights toward what scored well. Then again. Your run is 60 iterations.

The network it pulls:

obs … 32 dense 32 · ELU … 32 dense 32 · ELU action 2 1,250 parameters

// 3 obs -> dense 32 -> ELU -> dense 32 -> ELU -> dense 2 · 1,250 params

  • Input - the three numbers.
  • Layers - each dense layer multiplies by learned weights, adds a bias, and bends through ELU.
  • Output - steer and drive.
  • Parameters - 1,250 weights. Training moves these toward a higher reward.

The recipe: every hyperparameter

Everything PPO is told, from the recipe file the GPU reads. Nothing here is typed into this page; it is the file. The ones you may turn in the Train card are marked.

sim/isaaclab/GoatRacer/GoatRacer/tasks/agents.py - class GoatRacerFiesta3ObsPPORunnerCfg

namevaluein words
num_steps_per_env32steps every car drives before one update
save_interval50a checkpoint every N iterations
max_iterations300the recipe default; your run sets its own
empirical_normalizationFalsescale the observations by what was seen? off: the three numbers are already small
experiment_name"goatracer_fiesta_3obs"the folder the run is saved in
init_noise_std1.0how wild the first actions are
actor_hidden_dims[32, 32]the actor layers, the picture above
critic_hidden_dims[32, 32]the critic layers: the coach
activation"elu"the bend between layers
value_loss_coef2.0how much the coach's error counts next to the actor's
use_clipped_value_lossTruethe coach moves in small steps too
clip_param0.2how far one update may move the policy
entropy_coef0.01pays for exploring; zero locks onto one habit
num_learning_epochs8how many times one batch is squeezed
num_mini_batches8the batch is cut in N pieces per epoch
learning_rate5e-4the step size
schedule"adaptive"adaptive: the step size follows desired_kl
gamma0.99how far ahead reward counts
lam0.95how much the coach's guess smooths the credit (GAE lambda)
desired_kl0.008how much the policy may change per update before the step size backs off
max_grad_norm1.0a cap on one update's size, for safety

Do this: pick one knob and decide what you expect if you double it. Then, after your first run, do a second with that one change and the same seed, and compare the curves.

How to read the curve

A real run of this task: reward starts near zero, passes 10 by iteration 10, 70 by iteration 20, and levels off near 80. That climb and flatten means it learned.

Where it flattens, more iterations stop buying skill. Remember that spot. The next lab abuses it.

Two reward curves over 60 iterations: 256 worlds and 4096 worlds, same seed, landing on the same plateau
// Lab 4090, 2026-09-05: seed 42, 60 iterations, at 256 and at 4096 worlds. Same plateau: 63 and 64.

More worlds learns faster per iteration, same plateau. Past a full GPU, more worlds buys smoother, not better.

What you get back, and the button

The trained policy arriving on its waypoint
// The same rollout, arriving: the goal bonus pays out.

The run ends with the curve, a replay of the policy driving, and policy.onnx, the exported policy file in the format the real car loads. The Jetson on the car runs it with no PyTorch and no GPU.

Do this: in the Train card below, leave every knob where it is, press the button, and watch the curve. Your first run is the baseline every later run is compared to.

// You are reading this with no account. The Train card shows a run that is already done; an account starts your own.

Follow the White Rabbit

Train

A run that is already done

// 256 worlds · 20 iterations · seed 42 · final reward 55.34 - the run this page describes, trained on the GPU and kept. Read the curve: near zero, then the climb, then flat.

Reward curve (replayed from the stored series)
Episode length
-steps / s
The policy driving (replayed in your browser)

Your own run needs an account

A run here starts 1024 copies of the car on a cloud GPU. You see the reward curve live. About two minutes later you have a replay of your policy driving and the policy file the real car loads.

Create an account to train Log in

// no account yet? Race a trained policy in your browser now. It is the policy that the last lab of this band trains.

Code

// the command the GPU box runs for your settings - the task, the worlds, the iterations, the seed. Every piece above is read by this one line.

isaaclab.sh -p scripts/rsl_rl/train.py --task GoatRacer-Fiesta-3obs-v1 --num_envs 1024 --max_iterations 60 --seed 42 --headless

// the reward is sim/isaaclab/GoatRacer/GoatRacer/tasks/goatracer_fiesta_3obs_env.py; the recipe is sim/isaaclab/GoatRacer/GoatRacer/tasks/agents.py. Both are in the cards above.