BotManifold
Tutorial

Reading the Alpamayo-R1 code

A guided walk through NVIDIA's released Alpamayo-R1 inference code: the prompt, the trajectory tokens, reasoning generation, the action expert reading the reasoning model's cache, flow-matching sampling and the unicycle action space, with small CPU versions you can run.

Why read the code

The concepts page describes Alpamayo-R1 from its paper. A paper says what the authors meant to build; the code says what runs. The released repository, github.com/NVlabs/alpamayo, is small (about 4,000 lines of Python for inference) and readable in an afternoon, and none of what follows needs the gated 22 GB weights or a GPU. It is Apache-2.0 licensed; the weights have their own licence.

This page follows one call of the test script through the code. We read commit 11a0e01 (the repository is in limited maintenance and points to newer releases, so details may move). Where we quote the code we name the file; the three runnable pieces at the end are our own small re-implementations, written to match what the code does and tested on a CPU.

Two rows of boxes. Top: four cameras times four frames plus ego history into a chat prompt with the history as 48 tokens, into VLM generate which samples text with trajectory tokens masked out, into a box that stops after the traj_future_start token and keeps the key-value cache. A green arrow goes from that box down to the second row. Bottom: noise of 64 acceleration and curvature pairs into Euler steps of action_in_proj, the expert reading the cache, and action_out_proj, into 64 acceleration and curvature pairs at 10 hertz, into action_to_traj, a unicycle rollout giving positions and rotations.
One inference in the released code, simplified. The reasoning model writes text and leaves its key-value cache; the action expert reads that cache while it turns noise into controls.

A map of the repository

File (under src/alpamayo_r1/)What it holds
test_inference.pyThe end-to-end example: load a clip, build the prompt, sample, print the reasoning and minADE
load_physical_aiavdataset.pyReads cameras and ego motion for one clip of the Physical AI AV dataset
helper.pyThe chat prompt and the image processor settings
models/base_model.pyThe vision-language backbone, the added trajectory and special tokens
models/alpamayo_r1.pyThe model: the expert, and the sampling function this page follows
models/delta_tokenizer.pyThe ego history as discrete tokens
models/action_in_proj.pyHow a noisy action chunk becomes expert input tokens
diffusion/flow_matching.pyFlow-matching training targets and the Euler sampler
action_space/unicycle_accel_curvature.pyControls to trajectory and back

1. What goes in

load_physical_aiavdataset returns, for one moment in a clip:

  • Images from four cameras (front wide, front telephoto, cross left, cross right), four frames each: 16 images.
  • Ego history: 16 past positions and rotations at 10 Hz, 1.6 s.
  • Ego future: 64 positions at 10 Hz, 6.4 s, used only to score the prediction.

These are the code's defaults. The paper describes a 2 s history; the released loader's default is 1.6 s. Small differences like this are why it is worth reading both.

2. The prompt

helper.create_message builds a chat in the backbone's format. The system message is "You are a driving assistant that generates safe and accurate actions." The user message holds the 16 images, then 48 placeholder tokens between <|traj_history_start|> and <|traj_history_end|>, then the instruction "output the chain-of-thought reasoning of the driving process, then output the future trajectory." The assistant's turn is opened with <|cot_start|>, so generation begins inside the reasoning.

The 48 placeholders are 16 history steps times three coordinates. They are filled in the next step.

3. The ego history as tokens

The backbone reads tokens, so the history has to become tokens. DeltaTrajectoryTokenizer takes the step-to-step motion (each position minus the one before), maps each coordinate's range (by default −4 to 4 m for x and y) onto a fixed number of bins (1,000 by default), and rounds. The token ids are offsets into a block of trajectory tokens added to the vocabulary (<i0>, <i1>, …), and fuse_traj_tokens writes them over the placeholders.

Our version of the same idea:

libs/tutorial/alpamayo_pieces.py (delta_tokens)
def delta_tokenize(
    positions: torch.Tensor, bins: int = 1000, low: float = -4.0, high: float = 4.0
) -> torch.Tensor:
    """Turns a path into tokens: each step's (dx, dy) quantised into ``bins`` levels.

    The first step is measured from the origin. Steps outside [low, high] metres are
    clipped to the end bins.
    """
    steps = torch.diff(positions, dim=0, prepend=torch.zeros(1, positions.shape[1]))
    scaled = (steps - low) / (high - low) * (bins - 1)
    return scaled.round().long().clamp(0, bins - 1)


def delta_detokenize(
    tokens: torch.Tensor, bins: int = 1000, low: float = -4.0, high: float = 4.0
) -> torch.Tensor:
    """Inverts ``delta_tokenize``: bin centres back to steps, then a running sum."""
    steps = tokens.float() / (bins - 1) * (high - low) + low
    return torch.cumsum(steps, dim=0)

Encoding steps rather than positions keeps every token in a small range wherever the car is. The cost is that decoding is a running sum, so rounding errors add up along the path. For a 6.4 s curve at 10 m/s, 1,000 bins reconstruct the path to within 2.6 cm; 100 bins to within 24 cm. For an input that is plenty, which is why the history can afford it.

The same tokenizer family produced the discrete future trajectory the model is trained to write after its reasoning (the 128 tokens the paper describes). At inference, as the next section shows, those tokens are switched off.

4. Writing the reasoning

sample_trajectories_from_data_with_vlm_rollout in models/alpamayo_r1.py is the function this page follows. Its first half is ordinary language-model generation, with two details that matter.

  • Trajectory tokens are masked. An ExpertLogitsProcessor sets the logits of every trajectory token to minus infinity before sampling, so the backbone can only write text. Its docstring gives the reason: the discrete trajectory tokens are not used by the expert, and masking them gives better Chain-of-Causation text.
  • Generation stops one token late. A custom StopAfterEOS stops after the token that follows <|traj_future_start|>. The comment explains why: the key-value cache is only updated for a token when the next one is generated, and the expert needs the cache to include the end of the reasoning.

The test script samples with temperature=0.6, top_p=0.98 and up to 256 new tokens, and asks for one trajectory sample (num_traj_samples=1) so that it fits in GPU memory. More samples give more trajectories and reasoning traces for the same scene.

What is left after generation is the key-value cache of the prompt plus the reasoning. That cache is the whole interface between the two halves of the model.

5. The action expert reads the cache

The expert is built in the model's constructor with AutoModel.from_config from a copy of the backbone's text configuration (with optional overrides), and its token-embedding table is deleted. It is a transformer with the same layer shapes as the language model, which is what lets its layers attend to keys and values the language model computed. It never sees text tokens; its inputs are action tokens.

Each flow step calls an inner step_fn:

  1. action_in_proj turns the noisy chunk into one token per waypoint (64 tokens). Each coordinate (acceleration, curvature) and the flow time get Fourier features (sines and cosines at logarithmically spaced frequencies), an MLP mixes them, and a layer norm finishes.
  2. The expert runs on those 64 tokens with past_key_values set to the reasoning model's cache. Position ids continue from where the reasoning ended, and an attention mask blocks the padding that shorter samples in a batch have after their reasoning. Attention among the action tokens is not causal (expert_non_causal_attention defaults to true): every waypoint sees every other.
  3. The cache is cropped back to its original length, so the next step starts clean.
  4. action_out_proj maps each output token to a velocity for that waypoint's two controls.

The mask, in our version:

libs/tutorial/alpamayo_pieces.py (expert_mask)
def expert_attention_mask(
    prompt_length: int, valid_until: int, actions: int
) -> torch.Tensor:
    """Which positions each action token may attend to, as an additive mask.

    The reasoning model's cache holds ``prompt_length`` positions: the prompt, the
    reasoning text and, for shorter samples in a batch, padding after the end of the
    text at ``valid_until``. The action tokens come after the cache. Each action token
    may see the cache up to ``valid_until`` and every action token (in both directions,
    since the expert is not causal); the padding is blocked.

    Returns:
        Shape (actions, prompt_length + actions): 0 where allowed, -inf where blocked.
    """
    mask = torch.zeros(actions, prompt_length + actions)
    mask[:, valid_until:prompt_length] = float("-inf")
    return mask

This design is why the paper's latency table can show 5 flow steps costing about 9 ms after 70 ms of reasoning: the expensive part, reading the images and writing the reasoning, happens once, and each flow step is one pass of the expert over 64 tokens against a fixed cache.

6. Flow matching, as in the course

diffusion/flow_matching.py uses the same convention as the Flow matching module: noise at t=0t = 0, data at t=1t = 1, the noisy chunk xt=t x+(1−t) εx_t = t\,x + (1-t)\,\varepsilon, and the network regresses the velocity x−εx - \varepsilon with a squared error. Sampling starts from Gaussian noise and takes Euler steps from 0 to 1.

Two settings are worth knowing:

  • Number of steps. The class default is 10. The paper's latency table reports 5. The checkpoint's own configuration decides which runs; we have not read it, because it sits behind the gated access.
  • Where training spends its effort. Training times are drawn from a Beta(1.5, 1) distribution and flipped, so about two thirds of samples have tt below 0.5: training spends more of its effort near the noisy end than a uniform draw would.

7. From controls to a trajectory

The chunk the expert produces is not positions. It is 64 pairs of acceleration and curvature, normalised by a stored mean and standard deviation. action_to_traj in action_space/unicycle_accel_curvature.py rolls them out from the current state: speed is the running sum of acceleration, heading turns by curvature times distance travelled, and positions integrate the velocity with the trapezoid rule. The starting speed comes from the history.

libs/tutorial/alpamayo_pieces.py (unicycle)
def unicycle_rollout(
    accel: torch.Tensor, curvature: torch.Tensor, v0: float, dt: float = DT
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
    """Integrates (acceleration, curvature) controls into a path from the origin.

    Speed is the running sum of acceleration; heading turns by curvature times the
    distance travelled in each step; position uses the average of the start and end
    velocity of each step (the trapezoid rule). The car starts at the origin facing +x.

    Args:
        accel: Accelerations, shape (n,), in m/s^2.
        curvature: Curvatures, shape (n,), in 1/m.
        v0: Speed at the start, in m/s.
        dt: Time step in seconds.

    Returns:
        Positions (n, 2), headings (n,) and speeds (n,) after each step.
    """
    speed = torch.cat([torch.tensor([v0]), v0 + torch.cumsum(accel * dt, dim=0)])
    turn = curvature * speed[:-1] * dt + curvature * accel * dt**2 / 2
    heading = torch.cat([torch.zeros(1), torch.cumsum(turn, dim=0)])
    vx, vy = speed * torch.cos(heading), speed * torch.sin(heading)
    x = torch.cumsum((vx[:-1] + vx[1:]) * dt / 2, dim=0)
    y = torch.cumsum((vy[:-1] + vy[1:]) * dt / 2, dim=0)
    return torch.stack([x, y], dim=-1), heading[1:], speed[1:]

Our tests check the geometry: constant curvature 0.02 m⁻¹ at 10 m/s traces a circle of radius 50 m and turns 1.28 rad in 6.4 s. They also show something a downstream controller must handle: braking at 2 m/s² from 6 m/s reaches zero speed after 3 s and then keeps going, into negative speed. The released rollout, like ours, has no stop: speed is a running sum with no clamp.

The reverse direction, used to make training targets from logged paths, is a regularised least-squares fit (traj_to_action): find accelerations and curvatures whose rollout matches the log, with a penalty that keeps them smooth. Curvature is set to zero below 0.6 m/s, where heading is too noisy to define it. Bounds of ±9.8 m/s² and ±0.33 m⁻¹ are stored for checking actions.

8. What comes out

The function returns positions and rotations for each sample, shaped [batch, sets, samples, 64, …], and, with return_extra=True, the decoded reasoning text. The test script prints the reasoning and the minADE over the samples against the logged future: with one sample, that is simply its average displacement error.

What the code adds to the paper

Reading both, a few things stand out. Each is a fact about the released code's defaults, not a claim about what NVIDIA trained.

TopicPaperReleased code
History length2 sLoader default 16 steps at 10 Hz, 1.6 s
Flow steps5 in the latency tableClass default 10; the checkpoint config decides
Discrete trajectory tokensTrained after the reasoningMasked out at inference; the expert produces the trajectory
BackboneCosmos-ReasonBuilt with the Qwen3VL classes from transformers; the config class default names Qwen/Qwen3-VL-8B-Instruct
RouteNot in the released model (repository FAQ)Special tokens named route_start, route_pad and route_end exist in the token list, unused by the inference path

None of these is a contradiction; checkpoints carry their own configuration. They are reminders to check the code and the model card before quoting a number from the paper for the model you downloaded.

Reference implementation (PyTorch)

libs/tutorial/alpamayo_pieces.py; tests are in libs/tutorial/tests/test_alpamayo_pieces.py. They check that zero controls drive straight at constant speed, that constant curvature traces a circle of radius one over the curvature, that braking runs through zero speed, that delta tokens round-trip with an error that grows along the path and with fewer bins, that large steps are clipped, and that the expert's mask blocks exactly the padding. Run uv run --group tutorial python -m libs.tutorial.alpamayo_pieces.

Check yourself

  1. Where does the reasoning end and the trajectory begin, in tokens, and what tells generation to stop?
  2. Why does the expert need the same layer shapes as the backbone's language model?
  3. Why are the discrete trajectory tokens masked during reasoning generation?
  4. Why is the history encoded as step-to-step motion, and what does that cost when decoding?
  5. A flow step costs one expert pass. Why is that cheap compared with the reasoning?
  6. What does the released rollout do when a braking command would take the speed below zero?
Answers
  1. The reasoning is generated after <|cot_start|>; generation stops one token after <|traj_future_start|>, through a custom stopping criterion, so that the key-value cache includes the end of the reasoning.
  2. Its layers attend to keys and values the language model computed. To read that cache, each layer must expect keys and values of the same number of heads and size, so the expert is built from the backbone's text configuration.
  3. The trajectory comes from the expert, not from discrete tokens, so the backbone should only write text. The code's docstring says masking them gives better Chain-of-Causation generation.
  4. Steps stay in a small range wherever the car is, so a fixed set of bins covers them. Decoding is a running sum, so each step's rounding error adds up: 2.6 cm at the end of a 6.4 s curve with 1,000 bins, 24 cm with 100.
  5. The images and the reasoning are processed once, into the cache. Each flow step only runs the expert over 64 action tokens against that fixed cache.
  6. It keeps integrating: speed is a running sum of acceleration with no clamp, so it goes negative. A controller or planner downstream has to stop the car instead.

Quiz

Answer each question; feedback appears as soon as you choose.

Question 1 of 5

What connects the reasoning model to the action expert at inference?

Question 2 of 5

Why does the code mask the trajectory tokens during reasoning generation?

Question 3 of 5

What does the expert output at each flow step?

Question 4 of 5

Decoding delta trajectory tokens is a running sum. What follows?

Question 5 of 5

Which of these is a default in the released code that differs from the paper's description?

0 of 5 answered

Sources

On this page