Run Posttrain in a container¶
This guide starts one standard NVIDIA PyTorch container, installs Cosmos Framework plus the locked Posttrain runtime, and runs the public recipes without Slurm-specific launchers. Commands run from the repository root inside the container.
The commands use one four-GPU host as a concrete, runtime-qualified showcase. Four GPUs are not an algorithm or API requirement: users can adapt the recipe topology, model parallelism, and batch sizes to their available hardware while preserving the validation constraints described below.
Reference host used for qualification¶
The commands in this guide were qualified on one Linux NVIDIA GB200 node with four B200 GPUs. For a standalone host, the equivalent reference envelope is:
- a Docker-compatible NVIDIA container runtime configured so
--gpus allexposes all four GPUs; with Docker, this means Docker Engine plus NVIDIA Container Toolkit; - a host NVIDIA driver compatible with the container's CUDA 13.x user-space stack;
nvcr.io/nvidia/pytorch:26.06-py3as the base container;- 64 GiB of container shared memory and unlimited locked memory, as set by the command below;
- plan for about 150 GiB of free disk for the repository, Python environments, Hugging Face cache, model checkpoints, simulator assets, and initial outputs; longer runs need additional checkpoint and output capacity.
This is a tested reference configuration, not a minimum hardware or host version requirement. Other GPU hosts, driver versions, container engines, and storage layouts may work; adapt the image and architecture-specific dependency group where necessary. Before continuing, verify that the chosen runtime can see the expected GPUs, for example:
nvidia-smi
docker run --rm --gpus all nvcr.io/nvidia/pytorch:26.06-py3 nvidia-smi
1. Start the container¶
On the host:
export COSMOS_REPO=/absolute/path/to/cosmos-framework
export COSMOS_RUNS=/absolute/path/to/cosmos-runs
export COSMOS_DATA=/absolute/path/to/posttrain-data
mkdir -p "$COSMOS_RUNS" "$COSMOS_DATA"
docker run --rm -it \
--gpus all \
--ipc=host \
--shm-size=64g \
--ulimit memlock=-1 \
--ulimit stack=67108864 \
-v "$COSMOS_REPO:/workspace/cosmos-framework" \
-v "$COSMOS_RUNS:/outputs" \
-v "$COSMOS_DATA:/data:ro" \
-v "$HOME/.cache/huggingface:/root/.cache/huggingface" \
-w /workspace/cosmos-framework \
nvcr.io/nvidia/pytorch:26.06-py3 bash
2. Install the environment¶
Inside the container, first install the Framework base, then add the locked Posttrain runtime to the same active environment. Posttrain intentionally owns its Torch 2.11, vLLM 0.23, CUDA, and NATTEN compatibility matrix.
apt-get update
apt-get install -y --no-install-recommends curl ffmpeg git git-lfs
python3 -m pip install --quiet uv==0.11.29
export UV_PROJECT_ENVIRONMENT="$PWD/.venv-posttrain"
uv sync --frozen --no-default-groups
source .venv-posttrain/bin/activate
uv sync --project docker/posttrain/environment \
--frozen --group cu130-runtime --active --inexact
export POSTTRAIN_PY="$PWD/.venv-posttrain/bin/python"
export PYTHONPATH="$PWD"
export IMAGINAIRE_OUTPUT_ROOT=/outputs
export WANDB_MODE=disabled
export LD_LIBRARY_PATH=
export RAY_TMPDIR=/tmp/posttrain-ray
mkdir -p "$RAY_TMPDIR"
Verify the runtime:
"$POSTTRAIN_PY" - <<'PY'
import natten, ray, torch, vllm
import cosmos_framework.posttrain
assert torch.cuda.device_count() == 4
print(torch.__version__, torch.version.cuda)
print(vllm.__version__, ray.__version__, natten.__version__)
PY
The GPU-count assertion above checks the showcased setup. When adapting the examples, update that assertion and keep these coupled settings consistent:
- DPO
torchrun --nproc-per-node, model data-parallel width, and visible GPUs; grid.ranks/role shapes, rollout tensor parallelism, and model parallelism;- environment count, global minibatch, microbatch, and rank divisibility for Joint PPO.
Run --validate-manifest after every topology change and before model loading.
Use the configuration reference and
batching guide when changing the showcased resource shape.
Qwen models and public datasets below are read directly from Hugging Face.
Set HF_TOKEN only when the selected repository is gated. Cosmos generator
checkpoints follow the download/conversion flow in
Framework training.
Run length¶
The launch commands below run each recipe to its full rl.max_steps. On the reference host that is
about four minutes for OPD, 30–45 minutes for NFT and DPO, two to three hours for GRPO and PPO, and
longer for Joint PPO. To check that a recipe works end to end, append rl.max_steps=5 to the
overrides after --. For Joint PPO, also append rl.validation.enabled=false, which skips the
policy evaluation that otherwise runs before the first step and after the last one. The
run guide Q&A lists the measured time per recipe, the checkpoint intervals,
and fixes for the errors first runs hit.
3. GRPO¶
This uses Qwen2.5-3B and GSM8K directly from Hugging Face. Policy FSDP and rollout TP are colocated across all four GPUs.
"$POSTTRAIN_PY" -m cosmos_framework.posttrain.entrypoints.grpo \
--config cosmos_framework/posttrain/configs/grpo_qwen3b_gsm8k_public_4gpu.py \
--validate-manifest
"$POSTTRAIN_PY" -m cosmos_framework.posttrain.entrypoints.grpo \
--config cosmos_framework/posttrain/configs/grpo_qwen3b_gsm8k_public_4gpu.py -- \
job.name=grpo_public_4gpu
4. DPO¶
The public recipe creates deterministic image preference pairs in memory. It
needs the public Wan2.2 VAE and starts from Cosmos3-Nano: convert the Nano
checkpoint to DCP with the Framework converter and point DPO_BASE_CHECKPOINT at
it. Section 7 uses the same DCP, so this conversion runs once.
mkdir -p artifacts/wan22_vae
uvx hf@latest download Wan-AI/Wan2.2-TI2V-5B Wan2.2_VAE.pth \
--local-dir artifacts/wan22_vae
export DPO_TOKENIZER_PATH="$PWD/artifacts/wan22_vae/Wan2.2_VAE.pth"
mkdir -p artifacts/checkpoints
"$POSTTRAIN_PY" -m cosmos_framework.scripts.convert_model_to_dcp \
--checkpoint-path Cosmos3-Nano \
-o artifacts/checkpoints/Cosmos3-Nano
export DPO_BASE_CHECKPOINT="$PWD/artifacts/checkpoints/Cosmos3-Nano"
"$POSTTRAIN_PY" -m torch.distributed.run \
--standalone --nproc-per-node=4 \
-m cosmos_framework.posttrain.entrypoints.dpo \
--config cosmos_framework/posttrain/configs/dpo_t2i_public.py -- \
job.name=dpo_public_4gpu
For real preference data, keep the same model and entrypoint but replace the synthetic source with a JSONL dataset:
"$POSTTRAIN_PY" -m torch.distributed.run \
--standalone --nproc-per-node=4 \
-m cosmos_framework.posttrain.entrypoints.dpo \
--config cosmos_framework/posttrain/configs/dpo_t2i_public.py -- \
rl.data.synthetic_pairs=0 \
rl.data.jsonl_path=/data/preferences/train.jsonl \
rl.data.shuffle=true \
job.name=dpo_public_4gpu
The JSONL and tar-backed preference contracts are described in the DPO data contract.
5. PPO¶
The public profile uses Qwen3-0.6B from Hugging Face: two colocated policy/rollout GPUs, one frozen-reference GPU, and one critic GPU.
"$POSTTRAIN_PY" -m cosmos_framework.posttrain.entrypoints.ppo \
--config cosmos_framework/posttrain/configs/ppo_qwen06b_public_4gpu.py \
--validate-manifest
"$POSTTRAIN_PY" -m cosmos_framework.posttrain.entrypoints.ppo \
--config cosmos_framework/posttrain/configs/ppo_qwen06b_public_4gpu.py -- \
job.name=ppo_public_4gpu
6. OPD¶
The public profile uses a two-GPU Qwen2.5-1.5B policy, a one-GPU rollout, and
a one-GPU Qwen2.5-3B teacher. Cross-device policy publication uses NIXL. The
CUDA 13 NIXL wheels below are available for both x86_64 and aarch64; uv
uses the qualified dependency group on aarch64 and selects the matching
standalone wheels on x86_64.
case "$(uname -m)" in
aarch64)
uv sync --project docker/posttrain/environment \
--frozen --group nixl-cu130-aarch64 --active --inexact
;;
x86_64)
uv pip install --python "$POSTTRAIN_PY" --no-deps \
"nixl-cu13==1.3.2" "nixl==1.3.2"
;;
*)
echo "Unsupported architecture: $(uname -m)" >&2
exit 1
;;
esac
export POSTTRAIN_NIXL_CUDA_FLAVOR=cu130
export UCX_TLS=cuda_copy,cuda_ipc,sm,tcp,self
export UCX_NET_DEVICES=lo
export NCCL_NET_PLUGIN=none
export UCX_MODULE_DIR="$($POSTTRAIN_PY -c \
'import sysconfig; print(sysconfig.get_path("purelib") + "/nixl_cu13.libs/ucx")')"
"$POSTTRAIN_PY" -m cosmos_framework.posttrain.utils.nixl_preflight
"$POSTTRAIN_PY" -m cosmos_framework.posttrain.entrypoints.opd \
--config cosmos_framework/posttrain/configs/opd_qwen1p5b_countdown_public_4gpu.py -- \
job.name=opd_public_4gpu
For multi-node NIXL, replace UCX_NET_DEVICES=lo with the qualified NICs and
run an actor-to-actor transfer probe before training.
7. NFT¶
The public NFT profile uses Cosmos3-Nano, four-rank FSDP, four prompt rows, and an HTTP image-reward service. It uses the VAE and the Nano DCP from section 4; skip the converter command below if you already ran it there.
mkdir -p artifacts/checkpoints
"$POSTTRAIN_PY" -m cosmos_framework.scripts.convert_model_to_dcp \
--checkpoint-path Cosmos3-Nano \
-o artifacts/checkpoints/Cosmos3-Nano
mkdir -p artifacts/nft
printf '%s\n' \
'{"prompt":"A red cube on a white table."}' \
'{"prompt":"A blue sphere beside a wooden block."}' \
'{"prompt":"A robot gripper above a green bowl."}' \
'{"prompt":"A yellow mug on a clean workbench."}' \
> artifacts/nft/prompts.jsonl
export NFT_BASE_CHECKPOINT="$PWD/artifacts/checkpoints/Cosmos3-Nano"
export NFT_TOKENIZER_PATH="$PWD/artifacts/wan22_vae/Wan2.2_VAE.pth"
export NFT_T2I_PROMPTS_JSONL="$PWD/artifacts/nft/prompts.jsonl"
export NFT_T2I_PROMPTS_SHA256="$(sha256sum "$NFT_T2I_PROMPTS_JSONL" | cut -d' ' -f1)"
export NFT_REWARD_REVISION=toy-image-hash-v1
export REMOTE_REWARD_ENQUEUE_URL=http://127.0.0.1:8765/enqueue
export REMOTE_REWARD_FETCH_URL=http://127.0.0.1:8765/fetch
export REMOTE_REWARD_TOKEN=local-toy-token
The enqueue endpoint accepts an octet stream containing one JSON metadata line
followed by an NPY image and returns {"uuid":"..."}. The fetch endpoint
accepts form fields uuid and type and returns
{"scores":{"toy_image_hash":[<score>]}}. A production run should use an
immutable, authenticated reward deployment; a local deterministic toy service
is sufficient only for connectivity testing. Create and start that local toy
service in the same container:
cat > artifacts/nft/toy_reward_server.py <<'PY'
from __future__ import annotations
import hashlib
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Lock
from urllib.parse import parse_qs
class RewardHandler(BaseHTTPRequestHandler):
"""Serve the minimal synchronous NFT image-reward protocol."""
protocol_version: str = "HTTP/1.1"
token: str = os.environ["REMOTE_REWARD_TOKEN"]
submitted: set[str] = set()
submitted_lock: Lock = Lock()
def _send_json(self, status: int, value: dict[str, object]) -> None:
body = json.dumps(value, separators=(",", ":")).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _authorized(self) -> bool:
if self.headers.get("Authorization") == f"Bearer {self.token}":
return True
self._send_json(401, {"error": "unauthorized"})
return False
def _body(self) -> bytes:
length = int(self.headers.get("Content-Length", "0"))
return self.rfile.read(length)
def do_GET(self) -> None:
if self.path == "/healthz":
self._send_json(200, {"status": "ok"})
return
self._send_json(404, {"error": "not found"})
def do_POST(self) -> None:
if not self._authorized():
return
body = self._body()
if self.path == "/enqueue":
metadata_raw, separator, array_raw = body.partition(b"\n")
try:
metadata = json.loads(metadata_raw)
except (UnicodeDecodeError, json.JSONDecodeError):
self._send_json(400, {"error": "invalid metadata"})
return
if (
not separator
or not array_raw
or not isinstance(metadata, dict)
or metadata.get("media_type") != "image"
):
self._send_json(400, {"error": "invalid image payload"})
return
service_id = hashlib.sha256(body).hexdigest()
with self.submitted_lock:
self.submitted.add(service_id)
self._send_json(200, {"uuid": service_id})
return
if self.path == "/fetch":
fields = parse_qs(body.decode("utf-8"))
service_id = fields.get("uuid", [""])[0]
provider = fields.get("type", [""])[0]
with self.submitted_lock:
known = service_id in self.submitted
if not known or provider != "toy_image_hash":
self._send_json(404, {"error": "unknown request"})
return
score = int(service_id[:8], 16) / 0xFFFFFFFF
self._send_json(200, {"scores": {"toy_image_hash": [score]}})
return
self._send_json(404, {"error": "not found"})
def log_message(self, format: str, *args: object) -> None:
return
server: ThreadingHTTPServer = ThreadingHTTPServer(("127.0.0.1", 8765), RewardHandler)
print("Toy NFT reward service listening on http://127.0.0.1:8765", flush=True)
server.serve_forever()
PY
"$POSTTRAIN_PY" artifacts/nft/toy_reward_server.py \
> artifacts/nft/toy_reward_server.log 2>&1 &
export NFT_REWARD_SERVER_PID=$!
trap 'kill "$NFT_REWARD_SERVER_PID" 2>/dev/null || true' EXIT
until curl --silent --fail http://127.0.0.1:8765/healthz >/dev/null; do
sleep 1
done
Keep that process running while executing the NFT commands below. The toy score is a deterministic hash of the submitted payload and is not a quality metric.
"$POSTTRAIN_PY" -m cosmos_framework.posttrain.entrypoints.nft \
--validate-manifest -- experiment=cosmos3_nft_t2i_public_4gpu
"$POSTTRAIN_PY" -m cosmos_framework.posttrain.entrypoints.nft -- \
experiment=cosmos3_nft_t2i_public_4gpu \
job.name=nft_public_4gpu
8. Embodied PPO¶
The implementation uses the name Joint PPO because the policy/action backbone and value head form one shared actor-value model and are optimized together in one learner, rather than using a separate critic model. “Joint” describes this model and update topology, not a different PPO objective.
Joint PPO needs an independently pinned LIBERO simulator interpreter and extra model assets. Follow the complete public recipes:
9. Outputs and resume¶
Runs write their resolved configuration, logs, metrics, and checkpoints under:
$IMAGINAIRE_OUTPUT_ROOT/<project>/<group>/<name>/<run_ts>/
Inspect config_resolved.yaml first: it is the fully merged configuration the
entrypoint actually used. When checkpointing is enabled, complete checkpoints
appear under checkpoints/iter_*, and latest_checkpoint.txt advances only
after a complete commit. rl.resume=auto resumes the latest complete checkpoint
for the same job identity; rl.resume=off starts a fresh attempt. Check the
selected recipe's resolved value before relaunching. See the
checkpoint reference and
metrics reference for the complete contracts.