VLA · Phase 1 · Introduction to VLA
1.4 — VLA Architecture and the Perception-to-Action Pipeline
The main components, from observation encoding through to the action output.
Follow information through a VLA in order: image to patch tokens, tokens joined with the instruction, the backbone, and the action output. Each stage has a small NumPy version so you can see the shapes.
Claims about papers, models and textbooks carry numbered references, listed with what each one is cited for in the References. Code marked “Toy model” is our own illustration, not the cited system.
Learning objectives
After this lesson, you should be able to:
- Name the main stages of a VLA pipeline.
- Compute how many patch tokens a 224 by 224 image gives with 16 by 16 patches.
- Explain what the projector does.
- Decode action tokens to a bounded robot command.
- Camera images are cut into patches and encoded
- A projector maps features into the language model’s space
- The instruction is tokenised and joins the sequence
- The backbone processes the sequence
- The action output is decoded into a robot command
The one-sentence idea
A VLA is a chain: encode the image, project it, join it with the instruction, run the backbone, decode an action.
Every stage has a cost in time and in data.
The pipeline in one picture
Different VLAs differ in detail, but many follow the same chain: an image encoder turns camera frames into feature vectors, a projector maps them into the input space of a language-model backbone, the instruction is tokenised and joins the sequence, and an action output turns the backbone’s result into robot commands. OpenVLA states its structure in these terms: a visual encoder that turns the image into patch embeddings, a projector that maps them into the language model’s input space, and the language-model backbone.[1] Its encoder fuses pretrained DINOv2 and SigLIP features, and its backbone is a Llama 2 language model.[1] π0 uses a different output stage: a pretrained vision-language model paired with a separate set of weights, the action expert, that generates action chunks with flow matching.[2]
Block diagrams. Read each diagram from top to bottom. The left column of the first one is the image path, the right column is the instruction path, and they meet in the language-model backbone.
The rest of the lesson takes the stages in order and runs a small version of each in NumPy. The sizes are smaller than in any real model and the weights are random, so the examples show how shapes and information flow, not what a trained model does.
Stage 1: images become patch tokens
Vision Transformers apply a standard Transformer directly to a sequence of image patches.[3] The title of that paper, “An image is worth 16x16 words”, names the patch size it uses. Cutting an image into patches is a reshape:
import numpy as np
# A fake colour image, 224 pixels tall and 224 wide, with 3 colour values (red, green, blue) per pixel.
# np.arange fills it with 0, 1, 2, ... so every number is different and easy to follow.
image = np.arange(224 * 224 * 3, dtype=np.float32).reshape(224, 224, 3)
patch_size = 16 # each patch is a square of 16 x 16 pixels
grid = 224 // patch_size # 224 / 16 = 14 patches along each side
# Step 1: split the rows into 14 groups of 16 and the columns into 14 groups of 16.
# The shape becomes (14, 16, 14, 16, 3) = (patch row, row inside patch, patch column, column inside patch, colour).
blocks = image.reshape(grid, patch_size, grid, patch_size, 3)
# Step 2: move the two "which patch" axes to the front.
# The shape becomes (14, 14, 16, 16, 3): the first two numbers now pick one patch.
blocks = blocks.transpose(0, 2, 1, 3, 4)
# Step 3: flatten every patch into one list of numbers (16 x 16 x 3 = 768),
# and list all 14 x 14 = 196 patches one after another.
patches = blocks.reshape(grid * grid, patch_size * patch_size * 3)
print("image:", image.shape)
print("patches:", patches.shape, "=", grid, "x", grid, "patches, each", patch_size * patch_size * 3, "numbers")
# Check: the first patch should be exactly the top-left 16 x 16 corner of the image.
top_left_corner = image[:patch_size, :patch_size].reshape(-1)
print("first patch equals the top-left 16x16 block:", bool(np.array_equal(patches[0], top_left_corner)))Output:
image: (224, 224, 3) patches: (196, 768) = 14 x 14 patches, each 768 numbers first patch equals the top-left 16x16 block: True
Each flattened patch is then mapped to a feature vector by learned weights, and the Transformer processes the sequence of those vectors. The Transformer itself is the architecture introduced in “Attention is all you need”, which is based entirely on attention, without recurrence or convolutions.[4]
Stage 2: join the image and the instruction
The language model expects vectors of its own width, so a projector maps the image features to that width. The instruction is tokenised and embedded to the same width, and the two are placed in one sequence. In the example below the weights are random, which is enough to show the shapes:
import numpy as np
rng = np.random.default_rng(0) # fixed seed, so the "random" numbers are the same every run
# --- The image side ---------------------------------------------------------------
n_patches = 196 # from the previous example: 14 x 14 patches
vision_dim = 64 # width of each feature vector the image encoder produces
lm_dim = 128 # width of the vectors the language model works with
# Pretend output of the image encoder: one feature vector per patch (random numbers here).
image_features = rng.normal(size=(n_patches, vision_dim))
# The projector is a matrix that converts vectors of width 64 into vectors of width 128.
# (Real projectors are trained; this one is random, so it only shows the shapes.)
projector = rng.normal(size=(vision_dim, lm_dim)) / np.sqrt(vision_dim)
image_tokens = image_features @ projector # (196, 64) x (64, 128) -> (196, 128)
# --- The instruction side ---------------------------------------------------------
# A tiny vocabulary: each word gets a number, and each number gets a vector of width 128.
vocab = {"pick": 0, "up": 1, "the": 2, "red": 3, "block": 4}
embedding_table = rng.normal(size=(len(vocab), lm_dim))
words = "pick up the red block".split() # ["pick", "up", "the", "red", "block"]
word_numbers = [vocab[word] for word in words] # [0, 1, 2, 3, 4]
text_tokens = embedding_table[word_numbers] # look up one vector per word -> (5, 128)
# --- Join them ----------------------------------------------------------------------
# Both now have width 128, so they can be stacked into one sequence: image tokens first, then text.
sequence = np.concatenate([image_tokens, text_tokens])
print("image tokens:", image_tokens.shape)
print("text tokens:", text_tokens.shape)
print("sequence into the backbone:", sequence.shape)Output:
image tokens: (196, 128) text tokens: (5, 128) sequence into the backbone: (201, 128)
Toy model. A real projector, vocabulary and sizes are different (OpenVLA’s backbone is a 7-billion-parameter Llama 2). Random weights make this a shape demonstration only.
In this layout most of the sequence is image: 196 of 201 positions. In a layout like this, the number of image tokens largely sets the sequence length the backbone has to process.
Stage 3: the backbone’s result becomes an action
With the token approach, the backbone predicts action tokens one after another, as it would words. OpenVLA splits each action dimension into 256 bins and trains with next-token prediction on the action tokens, and at run time the predicted tokens are decoded to continuous values and de-normalised to the robot’s physical scale.[1][1] The example below takes a made-up set of logits for seven action dimensions, picks the most likely bin in each, and decodes it, using the same bin scheme as lesson 1.2:
import numpy as np
rng = np.random.default_rng(1) # fixed seed, so the "random" numbers are the same every run
n_bins = 256 # each action number is described by one of 256 possible bins
# The robot command has 7 numbers: x, y, z movement, 3 rotations, and the gripper (open to closed).
# These are the smallest and largest allowed values for each of the 7 (made-up limits).
low = np.array([-0.05, -0.05, -0.05, -0.5, -0.5, -0.5, 0.0])
high = np.array([ 0.05, 0.05, 0.05, 0.5, 0.5, 0.5, 1.0])
# Pretend model output: for each of the 7 numbers, a score for each of the 256 bins (random here).
scores = rng.normal(size=(7, n_bins))
# Step 1: for each of the 7 numbers, pick the bin with the highest score.
# These bin numbers (0 to 255) are the "action tokens" the model writes.
tokens = scores.argmax(axis=1)
# Step 2: turn each bin number back into a real value.
# Bin k covers a small slice of [low, high]; we use the middle of that slice.
fraction = (tokens + 0.5) / n_bins # a number between 0 and 1: how far along the range
action = low + fraction * (high - low) # scale to the real range of each number
print("tokens:", tokens.tolist())
print("action:", np.round(action, 3).tolist())
# Check: every decoded value must lie between its low and high limit.
inside = np.all((action >= low) & (action <= high))
print("every value inside its limits:", bool(inside))Output:
tokens: [183, 17, 115, 92, 156, 128, 28] action: [0.022, -0.043, -0.005, -0.139, 0.111, 0.002, 0.111] every value inside its limits: True
Toy model. Random logits give a meaningless action. The example shows decoding, not prediction.
Notice the guarantee the decoding step gives for free: every bin lies inside the limits, so the decoded command is always in range. A model that generates continuous values instead, like π0’s action expert, has to produce valid values by training, and a safe system checks them anyway.
Where the time and the data go
Each stage costs time, and the loop in lesson 1.1 does not wait. The RT-1 system, an earlier robot Transformer, combined a FiLM-conditioned EfficientNet, a TokenLearner and a Transformer, and ran at 3 Hz with about 35 million parameters.[5][5] OpenVLA, at 7 billion parameters,[1] is far larger. Sizes this different show that “VLA” covers a family of designs, and model speed is one reason the execution method (lesson 1.5) matters.
Common misconceptions
Misconception 1 “All VLAs have the same architecture.”
Correction OpenVLA writes actions as tokens; π0 uses a separate action expert with flow matching; RT-1 used a different combination of components.
Misconception 2 “The language model sees the picture directly.”
Correction It sees feature vectors that an image encoder produced and a projector mapped into its input space.
Misconception 3 “The decoded action is always right if it is in range.”
Correction In range only means valid. Whether it is correct depends on training.
Engineering takeaways
- Patches, then projection, then a sequence with the instruction, then the backbone, then an action output.
- Image tokens dominate the sequence length.
- Token outputs are decoded to bounded values.
- Architectures differ between models.
Follow the shapes and you can follow the model.
Knowledge check
Try each question first, then reveal the explanation.
Question 1
How many 16 by 16 patches does a 224 by 224 image give?
Explanation
196. 224 / 16 = 14, and 14 × 14 = 196.
Question 2
What does the projector do?
Explanation
It maps image features to the width and space the language model expects.
Question 3
Why is decoding action bins always in range?
Explanation
Every bin lies between the limits, so its centre does too.
References
Numbered notes in the text, like [1], point here. Each entry says what it is cited for and how that was checked. Papers and books can be revised or misread, so follow the links before relying on a detail. Code marked “Toy model” is our own illustration of an idea, not the code of the cited system.
(2024). OpenVLA: An open-source vision-language-action model. Conference on Robot Learning (CoRL) 2024. arXiv:2406.09246
Cited for:
- The model has three parts: a visual encoder that turns the image into patch embeddings, a projector that maps them into the language model’s input space, and the language-model backbone. Checked against the paper, via summaries of its text.
- A 7-billion-parameter open-source VLA trained on 970,000 real-world robot demonstrations, built on a Llama 2 language model with a visual encoder that fuses pretrained DINOv2 and SigLIP features. Checked against the paper’s abstract.
- Each action dimension is split into 256 bins between the 1st and 99th quantile of the training actions; the 256 bins are written over the 256 least-used tokens of the Llama tokenizer, and the model is trained with next-token prediction on the action tokens. Checked against the paper, via summaries of its text.
- At run time the predicted action tokens are decoded to continuous values and de-normalised to the robot’s physical scale. Checked against the paper, via summaries of its text.
(2024). π0: A vision-language-action flow model for general robot control. arXiv:2410.24164
Cited for:
- Pairs the pretrained vision-language model with a separate set of weights for the action outputs (the “action expert”) that generates action chunks with flow matching. Checked against the paper, via summaries of its text.
(2021). An image is worth 16x16 words: Transformers for image recognition at scale. International Conference on Learning Representations (ICLR) 2021. arXiv:2010.11929
Cited for:
- Applies a standard Transformer directly to a sequence of image patches. Checked against the paper’s abstract.
(2017). Attention is all you need. Advances in Neural Information Processing Systems 30 (NIPS 2017). arXiv:1706.03762
Cited for:
- Introduces the Transformer, an architecture based entirely on attention, without recurrence or convolutions. Checked against the paper’s abstract.
(2022). RT-1: Robotics Transformer for real-world control at scale. arXiv:2212.06817
Cited for:
- Combines a FiLM-conditioned EfficientNet, a TokenLearner and a Transformer. Checked against the authors’ own blog post.
- A model of about 35 million parameters that runs at 3 Hz. Checked against the authors’ own blog post.
What’s next
Next, how the robot executes what the model outputs: open loop or closed loop.