🏆 Selected as an Oral Presentation at ACM Multimedia (ACM MM) 2026
CF-VLA introduces a plug-and-play action expert that replaces the action generation component in flow-based VLA backbones, enabling seamless integration while improving both inference speed and action generation performance without modifying the underlying architecture.
📄 Paper: https://arxiv.org/abs/2604.24622
src/openpi/models_pytorch/cf_vla.pypi0_2stg_pytorchscripts/train_pytorch.pyscripts/run_train_cf_vla.shThe reference two-stage policy lives in src/openpi/models_pytorch/cf_vla.py, with the compatible configuration name pi0_2stg_pytorch.
The implementation is built on top of a $\pi_{0.5}$-style VLA policy, but the coarse-to-fine design is not tied to this specific codebase. It can also be instantiated on other policy frameworks with analogous prefix/suffix action-generation interfaces.
At a high level, a $\pi_{0.5}$-style policy can be understood in three blocks:
embed_prefix converts image and language inputs into prefix tokens and runs a cached VLM forward pass.embed_suffix constructs the action-side tokens consumed by the action expert, injecting timestep information while reusing the prefix context.forward() and sample_actions() repeatedly call the suffix path while reusing the cached prefix states, so most action-generation logic lives on the suffix side.CF_VLA keeps this overall scaffold and modifies the places that matter for coarse-to-fine action generation. Compared with a plain $\pi_{0.5}$ policy, CF_VLA adds a Gaussian prediction head for coarse initialization, inserts an explicit coarse stage before the refinement stage inside forward() and sample_actions(), and exposes phase-specific switches in model_kwargs so the same backbone can realize both the Phase I warm-up path and the Phase II coupled path.
Main components:
Shared $\pi_{0.5}$ prefix path.
embed_prefix builds prefix tokens from images and language. A cached forward through paligemma_with_expert yields past_key_values for the action expert.
Shared $\pi_{0.5}$ suffix path.
embed_suffix builds action and time embeddings. forward_suffix and denoise_step run the expert trunk on suffix tokens while attending to the cached prefix.
New Gaussian posterior head.
Mean and log-variance are predicted by action_out_proj and action_out_proj_logvar, then packed into DiagonalGaussianDistribution, which implements mode(), sample(), and kl().
New two-stage control flow.
Phase I and Phase II appear as branches inside forward() and sample_actions(). Routing is controlled through model_kwargs, including:
1stg_loss_type1stg_as_noise1stg_output_modetimes_list_traintimes_start_testLoss composition.
The Phase I log-variance coefficient is implemented by loss_logvar_weight. The phase-specific coarse-loss weights $\lambda_{\mathrm{I}}$ and $\lambda_{\mathrm{II}}$ are both implemented by loss_1stg_weight in their respective phases.
Sampling schedule.
sample_actions() first runs a coarse pass near $t=1$, then a fine pass from the coarse output. Step scaling follows the configured refinement start time, for example times_start_test.
Once the shared $\pi_{0.5}$ scaffold and the new Gaussian head are fixed, Phase I and Phase II can be read as two straight-line executions of the same backbone. They mainly differ in how the coarse output is supervised and how it seeds the fine stage.
Phase I warm-up.
After building the prefix cache once, the code first performs a coarse pass at $t=1$ from a zero initialization. The posterior mean is supervised with MSE, and the log-variance is supervised with a separate coefficient implemented by loss_logvar_weight. Then the code performs a second suffix forward at $t=0.1$ to regress the usual refinement target. The whole Phase I coarse loss is weighted by loss_1stg_weight.
Phase II coupled training.
The code keeps the same prefix/suffix scaffold, but changes the coarse branch into a Gaussian posterior-matching step. The coarse output is trained against a target Gaussian via KL, and a sample from that posterior is directly used to initialize the fine branch. The Phase II coarse loss is also weighted by loss_1stg_weight, but with the Phase II-specific value.
This is why the implementation is best understood in four logical pieces:
forward() and sample_actions() implementation.The first architectural change beyond a plain $\pi_{0.5}$ action head is that the suffix path predicts both mean and log-variance, instead of only a deterministic action-direction vector.
The helper below packages this output as a diagonal Gaussian posterior. This posterior is reused by both the coarse stage and the refinement stage.
class DiagonalGaussianDistribution(object):
def __init__(self, parameters: torch.Tensor, deterministic: bool = False):
self.mean, self.logvar = torch.chunk(parameters, 2, dim=-1)
self.logvar = torch.clamp(self.logvar, -5.0, 20.0)
# std, var, sample(), kl(other), nll(...), mode() elided
def mode(self) -> torch.Tensor:
return self.mean
# forward_suffix: suffix expert -> linear heads -> Gaussian parameters
suffix_out = hidden_states[:, -self.config.action_horizon :]
v_t = torch.cat(
[
self.action_out_proj(suffix_out),
self.action_out_proj_logvar(suffix_out),
],
dim=-1,
)
return DiagonalGaussianDistribution(v_t)
Phase I training first builds the prefix cache once, then runs a coarse pass at $t=1$ from a zero state.
The coarse branch regresses:
u_t = noise - action
In the default endpoint setting, the coarse input is zero, so this becomes:
u_t = 0 - action
The coarse branch also predicts log-variance. The target log-variance is constructed from noise_var, and the matching term is weighted by loss_logvar_weight.
After that, the fine branch performs a second suffix forward at $t=0.1$ and regresses the usual refinement target.
The total Phase I objective is:
fine-stage MSE + loss_1stg_weight * coarse-stage loss
At inference time, the prefix cache is built once. Each stage calls denoise_step, which runs the action expert with frozen prefix KV cache.
The default sampling process is:
x_t = 0 at t = 1.t = 0.1.Phase II keeps the same overall flow as Phase I, but changes the semantics of the coarse branch.
The coarse pass at $t=1$ is interpreted as a Gaussian posterior. It is matched through KL to the target Gaussian:
N(u_t, noise_var * I)
where:
u_t = 0 - action
Then a sample from the predicted posterior is used to initialize the fine branch. The fine branch regresses:
v_t -> noise - action
where noise is constructed from the sampled coarse state rather than from an independent Gaussian sample.
Phase II uses the same high-level two-step inference schedule as Phase I:
The main difference from Phase I is not the inference structure itself, but the training objective: Phase II couples the fine initializer to the coarse posterior.
# Prefix cache is identical to the Phase I sampling path.
bsize = observation.state.shape[0]
actions_shape = (
bsize,
self.config.action_horizon,
self.config.action_dim,
)
# Coarse step
one_noise = torch.zeros(actions_shape, dtype=torch.float32, device=device)
dt = torch.tensor(-1.0, dtype=torch.float32, device=device)
x_t = one_noise
time = torch.tensor(1.0, dtype=torch.float32, device=device)
expanded_time = time.expand(bsize)
posterior = self.denoise_step(
state,
prefix_pad_masks,
past_key_values,
x_t,
expanded_time,
)
x_t = x_t + dt * posterior.mode()
noise = x_t
# Fine step
dt = torch.tensor(-1.0, dtype=torch.float32, device=device)
x_t = noise
time = torch.tensor(0.1, dtype=torch.float32, device=device)
dt = dt * 0.1
expanded_time = time.expand(bsize)
posterior = self.denoise_step(
state,
prefix_pad_masks,
past_key_values,
x_t,
expanded_time,
)
x_t = x_t + dt * posterior.mode()
return x_t
forward() and sample_actions() MethodsThe production implementation interleaves the above logic with several compatibility branches. The phase-specific paths correspond to the following entries in model_kwargs:
1stg_loss_type: selects the coarse objective.
mse: default Phase I warm-up path.kl: default Phase II path.nll: optional Phase II ablation.1stg_as_noise: whether the coarse output is routed into the fine initializer.
1stg_output_mode: whether to use posterior.mode() or posterior.sample() when forming the fine initializer.
times_t1: optional rescale of the coarse terminal time.
times_start_test: refinement start time and step-size scaling at inference.
times_list_train: candidate fine times sampled during training.
noisy_actions: clean coarse state versus diffusion-style mixing for the fine pass.
flow_num: number of fine samples averaged per optimization step.
In short, the full implementation can be read as a $\pi_{0.5}$-style prefix/suffix policy with three CF_VLA-specific additions:
Use the standalone environment file
environment.ymlto create thecf_vlaconda environment for GPU reproduction.
# 1. Create the environment
conda env create -f environment.yml
# 2. Activate the environment
conda activate cf_vla
# 3. Install the project package
pip install -e .
# 4. Install the openpi client
pip install -e packages/openpi_client
pip install -e ".[bench-libero]"
pip install -e ".[bench-calvin]"
bash scripts/run_train_cf_vla.sh \
configs/pi05_2stg_pytorch_delta_actions/Cf_vla_libero.yaml
Start the policy server, then run the evaluation client:
# Server
python scripts/serve_policy.py --env LIBERO
# Client
python examples/libero/main.py
If you find this work useful, please cite:
Du et al., CF-VLA: Efficient Coarse-to-Fine Action Generation for Vision-Language-Action Policies, arXiv preprint arXiv:2604.24622, 2026. 📄 Paper: https://arxiv.org/abs/2604.24622
@article{du2026cfvla,
title={CF-VLA: Efficient Coarse-to-Fine Action Generation for Vision-Language-Action Policies},
author={Du, Fan and Yan, Feng and Wu, Jianxiong and Xu, Xinrun and Zhang, Weiye and Wang, Weinong and Guo, Yu and Qian, Bin and He, Zhihai and Wang, Fei and Yang, Heng},
journal={arXiv preprint arXiv:2604.24622},
year={2026}
}
## License
- Code license: MIT. See `LICENSE`.
- Third-party dependency notes: `THIRD_PARTY_NOTICES.md`
- Model weights and datasets may have separate licenses. Please verify them separately before release.
5 commits
1 commits
🏆 Selected as an Oral Presentation at ACM Multimedia (ACM MM) 2026
CF-VLA introduces a plug-and-play action expert that replaces the action generation component in flow-based VLA backbones, enabling seamless integration while improving both inference speed and action generation performance without modifying the underlying architecture.
📄 Paper: https://arxiv.org/abs/2604.24622
src/openpi/models_pytorch/cf_vla.pypi0_2stg_pytorchscripts/train_pytorch.pyscripts/run_train_cf_vla.shThe reference two-stage policy lives in src/openpi/models_pytorch/cf_vla.py, with the compatible configuration name pi0_2stg_pytorch.
The implementation is built on top of a $\pi_{0.5}$-style VLA policy, but the coarse-to-fine design is not tied to this specific codebase. It can also be instantiated on other policy frameworks with analogous prefix/suffix action-generation interfaces.
At a high level, a $\pi_{0.5}$-style policy can be understood in three blocks:
embed_prefix converts image and language inputs into prefix tokens and runs a cached VLM forward pass.embed_suffix constructs the action-side tokens consumed by the action expert, injecting timestep information while reusing the prefix context.forward() and sample_actions() repeatedly call the suffix path while reusing the cached prefix states, so most action-generation logic lives on the suffix side.CF_VLA keeps this overall scaffold and modifies the places that matter for coarse-to-fine action generation. Compared with a plain $\pi_{0.5}$ policy, CF_VLA adds a Gaussian prediction head for coarse initialization, inserts an explicit coarse stage before the refinement stage inside forward() and sample_actions(), and exposes phase-specific switches in model_kwargs so the same backbone can realize both the Phase I warm-up path and the Phase II coupled path.
Main components:
Shared $\pi_{0.5}$ prefix path.
embed_prefix builds prefix tokens from images and language. A cached forward through paligemma_with_expert yields past_key_values for the action expert.
Shared $\pi_{0.5}$ suffix path.
embed_suffix builds action and time embeddings. forward_suffix and denoise_step run the expert trunk on suffix tokens while attending to the cached prefix.
New Gaussian posterior head.
Mean and log-variance are predicted by action_out_proj and action_out_proj_logvar, then packed into DiagonalGaussianDistribution, which implements mode(), sample(), and kl().
New two-stage control flow.
Phase I and Phase II appear as branches inside forward() and sample_actions(). Routing is controlled through model_kwargs, including:
1stg_loss_type1stg_as_noise1stg_output_modetimes_list_traintimes_start_testLoss composition.
The Phase I log-variance coefficient is implemented by loss_logvar_weight. The phase-specific coarse-loss weights $\lambda_{\mathrm{I}}$ and $\lambda_{\mathrm{II}}$ are both implemented by loss_1stg_weight in their respective phases.
Sampling schedule.
sample_actions() first runs a coarse pass near $t=1$, then a fine pass from the coarse output. Step scaling follows the configured refinement start time, for example times_start_test.
Once the shared $\pi_{0.5}$ scaffold and the new Gaussian head are fixed, Phase I and Phase II can be read as two straight-line executions of the same backbone. They mainly differ in how the coarse output is supervised and how it seeds the fine stage.
Phase I warm-up.
After building the prefix cache once, the code first performs a coarse pass at $t=1$ from a zero initialization. The posterior mean is supervised with MSE, and the log-variance is supervised with a separate coefficient implemented by loss_logvar_weight. Then the code performs a second suffix forward at $t=0.1$ to regress the usual refinement target. The whole Phase I coarse loss is weighted by loss_1stg_weight.
Phase II coupled training.
The code keeps the same prefix/suffix scaffold, but changes the coarse branch into a Gaussian posterior-matching step. The coarse output is trained against a target Gaussian via KL, and a sample from that posterior is directly used to initialize the fine branch. The Phase II coarse loss is also weighted by loss_1stg_weight, but with the Phase II-specific value.
This is why the implementation is best understood in four logical pieces:
forward() and sample_actions() implementation.The first architectural change beyond a plain $\pi_{0.5}$ action head is that the suffix path predicts both mean and log-variance, instead of only a deterministic action-direction vector.
The helper below packages this output as a diagonal Gaussian posterior. This posterior is reused by both the coarse stage and the refinement stage.
class DiagonalGaussianDistribution(object):
def __init__(self, parameters: torch.Tensor, deterministic: bool = False):
self.mean, self.logvar = torch.chunk(parameters, 2, dim=-1)
self.logvar = torch.clamp(self.logvar, -5.0, 20.0)
# std, var, sample(), kl(other), nll(...), mode() elided
def mode(self) -> torch.Tensor:
return self.mean
# forward_suffix: suffix expert -> linear heads -> Gaussian parameters
suffix_out = hidden_states[:, -self.config.action_horizon :]
v_t = torch.cat(
[
self.action_out_proj(suffix_out),
self.action_out_proj_logvar(suffix_out),
],
dim=-1,
)
return DiagonalGaussianDistribution(v_t)
Phase I training first builds the prefix cache once, then runs a coarse pass at $t=1$ from a zero state.
The coarse branch regresses:
u_t = noise - action
In the default endpoint setting, the coarse input is zero, so this becomes:
u_t = 0 - action
The coarse branch also predicts log-variance. The target log-variance is constructed from noise_var, and the matching term is weighted by loss_logvar_weight.
After that, the fine branch performs a second suffix forward at $t=0.1$ and regresses the usual refinement target.
The total Phase I objective is:
fine-stage MSE + loss_1stg_weight * coarse-stage loss
At inference time, the prefix cache is built once. Each stage calls denoise_step, which runs the action expert with frozen prefix KV cache.
The default sampling process is:
x_t = 0 at t = 1.t = 0.1.Phase II keeps the same overall flow as Phase I, but changes the semantics of the coarse branch.
The coarse pass at $t=1$ is interpreted as a Gaussian posterior. It is matched through KL to the target Gaussian:
N(u_t, noise_var * I)
where:
u_t = 0 - action
Then a sample from the predicted posterior is used to initialize the fine branch. The fine branch regresses:
v_t -> noise - action
where noise is constructed from the sampled coarse state rather than from an independent Gaussian sample.
Phase II uses the same high-level two-step inference schedule as Phase I:
The main difference from Phase I is not the inference structure itself, but the training objective: Phase II couples the fine initializer to the coarse posterior.
# Prefix cache is identical to the Phase I sampling path.
bsize = observation.state.shape[0]
actions_shape = (
bsize,
self.config.action_horizon,
self.config.action_dim,
)
# Coarse step
one_noise = torch.zeros(actions_shape, dtype=torch.float32, device=device)
dt = torch.tensor(-1.0, dtype=torch.float32, device=device)
x_t = one_noise
time = torch.tensor(1.0, dtype=torch.float32, device=device)
expanded_time = time.expand(bsize)
posterior = self.denoise_step(
state,
prefix_pad_masks,
past_key_values,
x_t,
expanded_time,
)
x_t = x_t + dt * posterior.mode()
noise = x_t
# Fine step
dt = torch.tensor(-1.0, dtype=torch.float32, device=device)
x_t = noise
time = torch.tensor(0.1, dtype=torch.float32, device=device)
dt = dt * 0.1
expanded_time = time.expand(bsize)
posterior = self.denoise_step(
state,
prefix_pad_masks,
past_key_values,
x_t,
expanded_time,
)
x_t = x_t + dt * posterior.mode()
return x_t
forward() and sample_actions() MethodsThe production implementation interleaves the above logic with several compatibility branches. The phase-specific paths correspond to the following entries in model_kwargs:
1stg_loss_type: selects the coarse objective.
mse: default Phase I warm-up path.kl: default Phase II path.nll: optional Phase II ablation.1stg_as_noise: whether the coarse output is routed into the fine initializer.
1stg_output_mode: whether to use posterior.mode() or posterior.sample() when forming the fine initializer.
times_t1: optional rescale of the coarse terminal time.
times_start_test: refinement start time and step-size scaling at inference.
times_list_train: candidate fine times sampled during training.
noisy_actions: clean coarse state versus diffusion-style mixing for the fine pass.
flow_num: number of fine samples averaged per optimization step.
In short, the full implementation can be read as a $\pi_{0.5}$-style prefix/suffix policy with three CF_VLA-specific additions:
Use the standalone environment file
environment.ymlto create thecf_vlaconda environment for GPU reproduction.
# 1. Create the environment
conda env create -f environment.yml
# 2. Activate the environment
conda activate cf_vla
# 3. Install the project package
pip install -e .
# 4. Install the openpi client
pip install -e packages/openpi_client
pip install -e ".[bench-libero]"
pip install -e ".[bench-calvin]"
bash scripts/run_train_cf_vla.sh \
configs/pi05_2stg_pytorch_delta_actions/Cf_vla_libero.yaml
Start the policy server, then run the evaluation client:
# Server
python scripts/serve_policy.py --env LIBERO
# Client
python examples/libero/main.py
If you find this work useful, please cite:
Du et al., CF-VLA: Efficient Coarse-to-Fine Action Generation for Vision-Language-Action Policies, arXiv preprint arXiv:2604.24622, 2026. 📄 Paper: https://arxiv.org/abs/2604.24622
@article{du2026cfvla,
title={CF-VLA: Efficient Coarse-to-Fine Action Generation for Vision-Language-Action Policies},
author={Du, Fan and Yan, Feng and Wu, Jianxiong and Xu, Xinrun and Zhang, Weiye and Wang, Weinong and Guo, Yu and Qian, Bin and He, Zhihai and Wang, Fei and Yang, Heng},
journal={arXiv preprint arXiv:2604.24622},
year={2026}
}
## License
- Code license: MIT. See `LICENSE`.
- Third-party dependency notes: `THIRD_PARTY_NOTICES.md`
- Model weights and datasets may have separate licenses. Please verify them separately before release.
5 commits
1 commits
Python
99.5%