{"id":22788,"date":"2010-06-26T17:12:06","date_gmt":"2010-06-26T17:12:06","guid":{"rendered":"https:\/\/scannn.com\/github-nvidia-nemo-labs-molt-%c2%b7-github\/"},"modified":"2010-06-26T17:12:06","modified_gmt":"2010-06-26T17:12:06","slug":"github-nvidia-nemo-labs-molt-%c2%b7-github","status":"publish","type":"post","link":"https:\/\/scannn.com\/lv\/github-nvidia-nemo-labs-molt-%c2%b7-github\/","title":{"rendered":"GitHub - NVIDIA-NeMo\/labs-molt \u00b7 GitHub"},"content":{"rendered":"\n<div id=\"\">\n<p dir=\"auto\">Molt is <strong>agentic-first<\/strong> and <strong>PyTorch-native<\/strong>. The agent is the program;<br \/>\nthe trainer is a single actor; reward is any Python you write inside an <code>Env<\/code><br \/>\nor <code>ChatAgent<\/code> \u2014 graders, multi-turn tools, VLM environments, LLM-as-judge.<br \/>\nThree components carry the rest \u2014 <strong>Ray<\/strong> for placement and async queues,<br \/>\n<strong>vLLM<\/strong> for rollout, <strong>NVIDIA AutoModel + FSDP2<\/strong> for training in pure<br \/>\nPyTorch. That is the whole stack: <strong>~9.2K lines of RL code that scale to<br \/>\n1T-class MoE<\/strong> on vLLM with TP \/ EP \/ CP \u2014 think DeepSeek-V3 at<br \/>\n<code>--fsdp.ep_size 256<\/code>, Adam CPU offload for the largest actors. One agent<br \/>\nAPI, one trainable actor, clean enough to read end-to-end.<\/p>\n<p dir=\"auto\">Three boxes. One async loop.<\/p>\n<p align=\"center\" dir=\"auto\">\n  <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https:\/\/github.com\/NVIDIA-NeMo\/labs-molt\/blob\/main\/assets\/molt.jpg\"><\/a>\n<\/p>\n<p dir=\"auto\"><strong>Ray<\/strong> owns placement and the async queue between the three boxes \u2014 that<br \/>\nis the entire runtime. The contract is <strong>token-first<\/strong>: token ids,<br \/>\nlogprobs, action ranges, rewards, and multimodal tensors stay aligned from<br \/>\nrollout to training. Anything you can compute in Python is a valid reward,<br \/>\nincluding LLM-as-judge calls back through the same vLLM engines that drive<br \/>\nrollout.<\/p>\n<p><markdown-accessiblity-table><\/p>\n<table>\n<thead>\n<tr>\n<th\/>\n<th>What you get<\/th>\n<th>Why it matters for research<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td> <strong>Agentic-first<\/strong><\/td>\n<td>One Gymnasium-aligned API \u2014 <code>Env.step()<\/code> or <code>ChatAgent.run()<\/code> \u2014 covers graders, multi-turn tools, VLM environments, and OpenAI\/Anthropic-compatible servers<\/td>\n<td>The agent <em>is<\/em> the program \u2014 iterate on environments in plain Python, the trainer stays untouched<\/td>\n<\/tr>\n<tr>\n<td> <strong>Fully-async runtime<\/strong><\/td>\n<td>Ray placement, async rollout queues, vLLM engines, partial rollout, weight sync<\/td>\n<td>Rollout, training, and weight sync overlap \u2014 a DeepSeek-V3-class actor stays fed without bespoke infra<\/td>\n<\/tr>\n<tr>\n<td> <strong>PyTorch-native, AutoModel-first<\/strong><\/td>\n<td>FSDP2 + NVIDIA AutoModel, pure PyTorch end-to-end<\/td>\n<td>Hack the model in the language you already write; no backend ceremony<\/td>\n<\/tr>\n<tr>\n<td> <strong>Single-actor simplicity<\/strong><\/td>\n<td>One actor, optional KL reference \u2014 the whole RL graph fits on a page<\/td>\n<td>Every gradient is explicit; every loss term is one file away<\/td>\n<\/tr>\n<tr>\n<td> <strong>Frontier-scale MoE<\/strong><\/td>\n<td>AutoModel + FSDP2 + TP \/ EP \/ CP + Adam CPU offload, MoE-native \u2014 e.g. DeepSeek-V3 with <code>--fsdp.ep_size 256<\/code><\/td>\n<td>The same script that trains 8B scales to 1T-class MoE \u2014 no rewrite between scales<\/td>\n<\/tr>\n<tr>\n<td> <strong>Token-first contract<\/strong><\/td>\n<td>Aligned token ids, logprobs, action ranges, rewards, multimodal tensors<\/td>\n<td>Multi-turn, VLM, and tool-call traces share one format end-to-end<\/td>\n<\/tr>\n<tr>\n<td> <strong>Small, hackable surface<\/strong><\/td>\n<td>~9.2K LOC of RL code across 3 thin layers<\/td>\n<td>Fork one layer without touching the others \u2014 read it in an afternoon<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><\/markdown-accessiblity-table><\/p>\n<p dir=\"auto\">The RL ecosystem optimizes for breadth. Molt optimizes for<br \/>\n<strong>agentic research velocity at scale<\/strong> \u2014 the smallest PyTorch-native stack<br \/>\nthat still drives fully-async agentic RL at frontier MoE scale on vLLM.<\/p>\n<p><markdown-accessiblity-table><\/p>\n<table>\n<thead>\n<tr>\n<th\/>\n<th align=\"center\"><strong> Molt<\/strong><\/th>\n<th align=\"center\">OpenRLHF<\/th>\n<th align=\"center\">verl<\/th>\n<th align=\"center\">slime<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Training backend<\/td>\n<td align=\"center\"><strong>PyTorch \/ FSDP2 + NVIDIA AutoModel<\/strong><\/td>\n<td align=\"center\">DeepSpeed ZeRO-3<\/td>\n<td align=\"center\">FSDP \/ FSDP2 \/ Megatron<\/td>\n<td align=\"center\">Megatron (FSDP exp.)<\/td>\n<\/tr>\n<tr>\n<td>Rollout engine<\/td>\n<td align=\"center\">vLLM (Ray)<\/td>\n<td align=\"center\">vLLM (Ray)<\/td>\n<td align=\"center\">vLLM \/ SGLang \/ TRT-LLM<\/td>\n<td align=\"center\">SGLang only<\/td>\n<\/tr>\n<tr>\n<td>RL topology<\/td>\n<td align=\"center\"><strong>actor (+ optional PPO critic)<\/strong><\/td>\n<td align=\"center\">actor + critic + RM<\/td>\n<td align=\"center\">actor + critic + RM<\/td>\n<td align=\"center\">actor + critic + RM<\/td>\n<\/tr>\n<tr>\n<td>Reward source<\/td>\n<td align=\"center\"><strong>agent Python<\/strong><\/td>\n<td align=\"center\">agent \/ endpoint \/ RM<\/td>\n<td align=\"center\">agent \/ RM \/ endpoint<\/td>\n<td align=\"center\">rollout fn \/ RM<\/td>\n<\/tr>\n<tr>\n<td>Parallelism<\/td>\n<td align=\"center\"><strong>TP \/ EP \/ CP<\/strong>, MoE-native<\/td>\n<td align=\"center\">ZeRO-3 \/ FSDP<\/td>\n<td align=\"center\">TP \/ PP \/ EP \/ SP<\/td>\n<td align=\"center\">TP \/ PP \/ DP \/ CP \/ EP<\/td>\n<\/tr>\n<tr>\n<td>Multimodal<\/td>\n<td align=\"center\">VLM RL, multi-turn tool calls<\/td>\n<td align=\"center\">VLM RL (v0.10+)<\/td>\n<td align=\"center\">Qwen2.5-VL, Kimi-VL<\/td>\n<td align=\"center\">geo3k VLM<\/td>\n<\/tr>\n<tr>\n<td>Config surface<\/td>\n<td align=\"center\"><strong>CLI flags only<\/strong><\/td>\n<td align=\"center\">CLI + scripts<\/td>\n<td align=\"center\">Hydra + YAML<\/td>\n<td align=\"center\">CLI + YAML<\/td>\n<\/tr>\n<tr>\n<td>RL code size\u00b9<\/td>\n<td align=\"center\"><strong>~9.2K LOC<\/strong><\/td>\n<td align=\"center\">~7.2K<\/td>\n<td align=\"center\">~62K<\/td>\n<td align=\"center\">~25K<\/td>\n<\/tr>\n<tr>\n<td>Design center<\/td>\n<td align=\"center\"><strong>agentic-first research<\/strong><\/td>\n<td align=\"center\">RLHF coverage<\/td>\n<td align=\"center\">production breadth<\/td>\n<td align=\"center\">Megatron throughput<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><\/markdown-accessiblity-table><\/p>\n<p dir=\"auto\"><strong>One framework, one job.<\/strong> Molt is the smallest PyTorch-native<br \/>\nstack that takes an NVIDIA AutoModel from SFT to frontier-scale agentic<br \/>\nRL on vLLM. Read every line that touches your gradients, in plain PyTorch.<\/p>\n<blockquote>\n<p dir=\"auto\">\u00b9 RL code = every Python file the framework&#8217;s RL path uses \u2014 online<br \/>\ntrainer, rollout, Ray orchestration, experience\/advantage\/reward\/KL\/loss,<br \/>\nactor\/critic\/RM inference, plus shared models, utils, parallelism, and<br \/>\nkernels the RL training command depends on. Excludes pure SFT, DPO\/KTO\/IPO<br \/>\ntrainers, reward-model <strong>training<\/strong>, distillation, vendored third-party<br \/>\ncode, tests, examples, scripts, and docs. Counts code lines only (blank<br \/>\nand comment-only lines excluded). Measured by tracing the import graph<br \/>\nfrom each RL entry point (<code>molt.cli.train_rl_ray<\/code>,<br \/>\n<code>openrlhf.cli.train_ppo_ray<\/code>, <code>verl.trainer.main_ppo<\/code>); slime loads its<br \/>\nMegatron\/SGLang backends lazily, so its core <code>slime\/<\/code> package plus its<br \/>\n<code>slime_plugins\/<\/code> model-zoo (+~4.7K \u2014 the in-repo model code its RL path<br \/>\nuses, counted on the same basis as molt&#8217;s <code>models\/<\/code>) are counted, minus<br \/>\nSFT\/distillation. Molt measured 2026-07-20 on this repo; the others<br \/>\nmeasured 2026-06-16 at each repo&#8217;s then-latest main HEAD<br \/>\n(verl <code>86e8123<\/code>, slime <code>243773c<\/code>, OpenRLHF <code>b3d2927<\/code>).<\/p>\n<\/blockquote>\n<p><markdown-accessiblity-table><\/p>\n<table>\n<thead>\n<tr>\n<th>Area<\/th>\n<th>Support<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>SFT<\/td>\n<td><code>molt.cli.train_sft<\/code><\/td>\n<\/tr>\n<tr>\n<td>RL<\/td>\n<td>vLLM-backed online RL via <code>molt.cli.train_rl_ray<\/code><\/td>\n<\/tr>\n<tr>\n<td>Runtime<\/td>\n<td>Ray placement, async rollout queues, vLLM engines, partial rollout sync<\/td>\n<\/tr>\n<tr>\n<td>Model scale<\/td>\n<td>AutoModel + FSDP2 with TP \/ EP \/ CP, MoE-native \u2014 e.g. DeepSeek-V3 at <code>--fsdp.ep_size 256<\/code><\/td>\n<\/tr>\n<tr>\n<td>Model backend<\/td>\n<td><strong>NVIDIA AutoModel is the primary path<\/strong> \u2014 native CP \/ EP \/ TP, custom MoE+EP parallelizer, TE fused attention; everything model-side aligns with AutoModel&#8217;s own recipes. The HF transformers path is a <strong>non-preferred fallback<\/strong> (AutoModel drops to it only when a model has no native class) supporting <strong>text + flash_attention_2 + packing only \u2014 no CP \/ EP \/ TP<\/strong><\/td>\n<\/tr>\n<tr>\n<td>Optimizer<\/td>\n<td><code>adam<\/code> (default), with CPU offload for the largest actors (<code>--fsdp.offload optimizer<\/code>). <code>muon<\/code> (Newton\u2013Schulz via Dion: Muon for 2D weights and grouped MoE experts, AdamW for embeddings \/ head \/ norms) is <strong>experimental<\/strong> \u2014 runs distributed (FSDP \/ EP) but has shown no consistent win over <code>adam<\/code> yet, which stays the recommended default<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><\/markdown-accessiblity-table><\/p>\n<p><markdown-accessiblity-table><\/p>\n<table>\n<thead>\n<tr>\n<th>Area<\/th>\n<th>Support<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Agent interface<\/td>\n<td><code>--train.agent_path<\/code> with <code>Env<\/code> or <code>ChatAgent<\/code> subclass + an <code>AgentRunner<\/code><\/td>\n<\/tr>\n<tr>\n<td>Reward source<\/td>\n<td><code>Result(reward=...)<\/code> returned from <code>Env.step<\/code> or <code>ChatAgent.run<\/code><\/td>\n<\/tr>\n<tr>\n<td>Modalities<\/td>\n<td>Text and VLM prompts, including image payloads<\/td>\n<\/tr>\n<tr>\n<td>Chat templates<\/td>\n<td>Assistant spans (SFT loss mask + multi-turn rollout stitching) are derived from the model&#8217;s own chat template \u2014 no hard-coded markers. Verified on ChatML (Qwen3.x, Nemotron omni3), Kimi-K2.6, GLM, Gemma and DeepSeek<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><\/markdown-accessiblity-table><\/p>\n<p><markdown-accessiblity-table><\/p>\n<table>\n<thead>\n<tr>\n<th>Area<\/th>\n<th>Support<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Estimators<\/td>\n<td><code>reinforce<\/code>, <code>reinforce_baseline<\/code>, <code>rloo<\/code>, <code>grpo<\/code>, <code>dr_grpo<\/code>, <code>gae<\/code> (PPO), <code>on_policy_distill<\/code><\/td>\n<\/tr>\n<tr>\n<td>PPO critic<\/td>\n<td><code>--algo.advantage.estimator gae<\/code> adds a value model: its own Ray group (<code>CriticModelActor<\/code>), colocated on the actor&#8217;s GPUs by default or disaggregatable, GAE advantages (<code>--algo.advantage.lam<\/code>) + clipped value loss (<code>--critic.value_clip<\/code>), own optimizer\/LR (<code>--critic.adam.lr<\/code>) and resumable <code>_critic<\/code> checkpoint. Built on <code>NeMoAutoModelForCausalLM<\/code> + a scalar value head, so it keeps the native TP \/ EP \/ CP path<\/td>\n<\/tr>\n<tr>\n<td>Distillation<\/td>\n<td>On-policy distillation \u2014 per-token reverse KL to a frozen teacher, via <code>--algo.advantage.estimator on_policy_distill<\/code> + <code>--ref.model_name_or_path<\/code><\/td>\n<\/tr>\n<tr>\n<td>IS correction<\/td>\n<td>Train\/rollout logprob-mismatch correction for off-policy \/ async rollout: <code>is_correction_level {off,token,seq,geo}<\/code> \u00d7 <code>is_correction_mode {mask,clip,trunc}<\/code> (covers TIS, IcePop, seq-mask-tis; see <em>IS correction<\/em> below)<\/td>\n<\/tr>\n<tr>\n<td>KL<\/td>\n<td>Optional reference workers when <code>--algo.kl.init_coef &gt; 0<\/code> (the reference doubles as the distillation teacher)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><\/markdown-accessiblity-table><\/p>\n<p><markdown-accessiblity-table><\/p>\n<table>\n<thead>\n<tr>\n<th>Area<\/th>\n<th>Support<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Router replay (R3)<\/td>\n<td><code>--train.routing_replay<\/code> \u2014 vLLM&#8217;s per-token top-k selection replayed in the training forward; details in the <em>MoE routing stability<\/em> section under Scaling Knobs<\/td>\n<\/tr>\n<tr>\n<td>Router freeze<\/td>\n<td><code>--actor.freeze_moe_router<\/code> holds the gate\/router weights fixed so vLLM and the actor keep routing tokens to the same experts. Stabilizes MoE RL \/ distillation and shrinks the same rollout-vs-train logprob gap the IS-correction filters address \u2014 a router that drifts between refits is a large source of that gap<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><\/markdown-accessiblity-table><\/p>\n<div class=\"markdown-heading\" dir=\"auto\">\n<h3 tabindex=\"-1\" class=\"heading-element\" dir=\"auto\">IS correction (train\/rollout logprob mismatch)<\/h3>\n<p><a id=\"user-content-is-correction-trainrollout-logprob-mismatch\" class=\"anchor\" aria-label=\"Permalink: IS correction (train\/rollout logprob mismatch)\" href=\"#is-correction-trainrollout-logprob-mismatch\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"\/><\/svg><\/a><\/div>\n<p dir=\"auto\">Async and partial rollout make the FSDP actor&#8217;s recomputed <code>pi_train<\/code> diverge from<br \/>\nvLLM&#8217;s gen-time <code>pi_rollout<\/code> (different kernels, plus a mid-request weight swap the<br \/>\nHTTP router can&#8217;t observe). Molt corrects the resulting off-policy update with the<br \/>\nper-token importance ratio <code>pi_train \/ pi_rollout<\/code>, gated by two knobs:<\/p>\n<ul dir=\"auto\">\n<li><code>--algo.advantage.is_correction_level {off, token, seq, geo}<\/code> \u2014 granularity of the<br \/>\ngated ratio. <code>off<\/code> disables correction; <code>token<\/code> gates each token&#8217;s own ratio;<br \/>\n<code>seq<\/code>\/<code>geo<\/code> aggregate a sequence&#8217;s ratios (<code>exp(sum)<\/code> \/ <code>exp(mean)<\/code>) into a<br \/>\nper-sequence <strong>rejection filter<\/strong> (kept sequences still carry their per-token IS<br \/>\nweight), so they require <code>mode mask<\/code>.<\/li>\n<li><code>--algo.advantage.is_correction_mode {mask, clip, trunc}<\/code> \u2014 treatment of a unit<br \/>\noutside the band. <code>mask<\/code> drops it (zero gradient); <code>clip<\/code> clamps its weight into<br \/>\nthe band; <code>trunc<\/code> clamps only the upper tail.<\/li>\n<li><code>--algo.advantage.is_correction_threshold LOW HIGH<\/code> \u2014 the <code>[low, high]<\/code> band on the<br \/>\nratio (recipes use a tight <code>0.99 1.01<\/code>).<\/li>\n<\/ul>\n<p dir=\"auto\">The named schemes and their prior art:<\/p>\n<p><markdown-accessiblity-table><\/p>\n<table>\n<thead>\n<tr>\n<th>Flags<\/th>\n<th>Scheme<\/th>\n<th>Prior art<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>level token mode trunc<\/code><\/td>\n<td>truncated IS<\/td>\n<td>TIS<\/td>\n<\/tr>\n<tr>\n<td><code>level token mode mask<\/code><\/td>\n<td>token masking<\/td>\n<td>IcePop<\/td>\n<\/tr>\n<tr>\n<td><code>level token mode clip<\/code><\/td>\n<td>token clip<\/td>\n<td>per-token weight clamp<\/td>\n<\/tr>\n<tr>\n<td><code>level geo mode mask<\/code><\/td>\n<td>seq-mask-tis (recipe default)<\/td>\n<td>MIS-style sequence masked importance sampling<\/td>\n<\/tr>\n<tr>\n<td><code>level seq mode mask<\/code><\/td>\n<td>product-ratio reject<\/td>\n<td>sequence log-ratio sum<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><\/markdown-accessiblity-table><\/p>\n<p dir=\"auto\">References: <strong>TIS<\/strong> (truncated importance sampling of the train\/infer ratio), <strong>IcePop<\/strong><br \/>\n(token-level masking of out-of-band ratios), and <strong>MIS<\/strong> (masked importance sampling, Yingru Li \u2014<br \/>\nsequence-level masked IS, which motivates the <code>seq<\/code>\/<code>geo<\/code> rejection filter).<\/p>\n<p dir=\"auto\">First clone the repo \u2014 the launch scripts, agents, and recipes live here, and<br \/>\n<code>examples\/scripts\/docker_run.sh<\/code> mounts this checkout into the container. For local<br \/>\n(non-container) development, add the editable install: it pulls the exact git-pinned<br \/>\nAutoModel this repo is validated against, so R3 routing replay and Muon work out of the box:<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"git clone https:\/\/github.com\/NVIDIA-NeMo\/labs-molt.git&#10;cd labs-molt&#10;pip install -e &quot;.[vllm]&quot;          # local development only \u2014 the container bakes everything in\">\n<pre>git clone https:\/\/github.com\/NVIDIA-NeMo\/labs-molt.git\n<span class=\"pl-c1\">cd<\/span> labs-molt\npip install -e <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>.[vllm]<span class=\"pl-pds\">\"<\/span><\/span>          <span class=\"pl-c\"><span class=\"pl-c\">#<\/span> local development only \u2014 the container bakes everything in<\/span><\/pre>\n<\/div>\n<p dir=\"auto\"><strong>The recommended path is the project container<\/strong> (<code>dockerfile\/Dockerfile<\/code>). It bakes the<br \/>\nfull CUDA-13 stack \u2014 torch 2.11 \u00b7 vLLM \u00b7 TransformerEngine \u00b7 flash-attn \u00b7 mamba \u00b7 DeepEP \u00b7<br \/>\nNVIDIA AutoModel \u2014 built for A100 \/ H100 \/ H200 \/ B200\u00b7GB200, so it runs SFT and RL as-is<br \/>\nwith no local dependency wrangling. Pull the prebuilt image from Docker Hub:<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"docker pull hijkzzz\/molt:latest   # or a pinned release: hijkzzz\/molt:0.1.3\">\n<pre>docker pull hijkzzz\/molt:latest   <span class=\"pl-c\"><span class=\"pl-c\">#<\/span> or a pinned release: hijkzzz\/molt:0.1.3<\/span><\/pre>\n<\/div>\n<p dir=\"auto\">&#8230;or build it yourself from the Dockerfile (e.g. to change the CUDA \/ vLLM \/ AutoModel pins):<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"docker build -f dockerfile\/Dockerfile -t hijkzzz\/molt:latest .\">\n<pre>docker build -f dockerfile\/Dockerfile -t hijkzzz\/molt:latest <span class=\"pl-c1\">.<\/span><\/pre>\n<\/div>\n<p dir=\"auto\">The released package is also on PyPI for checkout-free installs:<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"pip install &quot;molt-rl[vllm]&quot;\">\n<pre>pip install <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>molt-rl[vllm]<span class=\"pl-pds\">\"<\/span><\/span><\/pre>\n<\/div>\n<blockquote>\n<p dir=\"auto\"><strong>Note<\/strong>: PyPI forbids git-pinned dependencies, so <code>molt-rl<\/code> depends on AutoModel&#8217;s PyPI<br \/>\nrelease instead \u2014 it can lag the pin in <code>requirements.txt<\/code>, and R3 routing replay needs<br \/>\nthe newer pin (it fails fast with instructions when the installed AutoModel is too old).<\/p>\n<\/blockquote>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"torchrun --standalone --nproc_per_node=8 -m molt.cli.train_sft \\&#10;  --model.model_name_or_path \/path\/to\/automodel \\&#10;  --data.dataset \/path\/to\/sft.jsonl \\&#10;  --data.input_key input \\&#10;  --data.output_key output \\&#10;  --ckpt.output_dir .\/ckpt\/sft \\&#10;  --fsdp.attn_implementation te\">\n<pre>torchrun --standalone --nproc_per_node=8 -m molt.cli.train_sft \\\n  --model.model_name_or_path \/path\/to\/automodel \\\n  --data.dataset \/path\/to\/sft.jsonl \\\n  --data.input_key input \\\n  --data.output_key output \\\n  --ckpt.output_dir .\/ckpt\/sft \\\n  --fsdp.attn_implementation te<\/pre>\n<\/div>\n<p dir=\"auto\">SFT uses the same AutoModel\/FSDP2 model-loading path as RL.<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"python3 -m molt.cli.train_rl_ray \\&#10;  --actor.model_name_or_path \/path\/to\/automodel \\&#10;  --data.prompt_dataset \/path\/to\/prompts.jsonl \\&#10;  --data.input_key input \\&#10;  --train.agent_path examples\/python\/agents\/math.py \\&#10;  --vllm.num_engines 2 \\&#10;  --vllm.tensor_parallel_size 2 \\&#10;  --rollout.batch_size 128 \\&#10;  --train.batch_size 128 \\&#10;  --train.micro_batch_size 1 \\&#10;  --algo.advantage.estimator reinforce \\&#10;  --algo.kl.init_coef 0 \\&#10;  --fsdp.attn_implementation te \\&#10;  --ckpt.output_dir .\/ckpt\/rl\">\n<pre>python3 -m molt.cli.train_rl_ray \\\n  --actor.model_name_or_path \/path\/to\/automodel \\\n  --data.prompt_dataset \/path\/to\/prompts.jsonl \\\n  --data.input_key input \\\n  --train.agent_path examples\/python\/agents\/math.py \\\n  --vllm.num_engines 2 \\\n  --vllm.tensor_parallel_size 2 \\\n  --rollout.batch_size 128 \\\n  --train.batch_size 128 \\\n  --train.micro_batch_size 1 \\\n  --algo.advantage.estimator reinforce \\\n  --algo.kl.init_coef 0 \\\n  --fsdp.attn_implementation te \\\n  --ckpt.output_dir .\/ckpt\/rl<\/pre>\n<\/div>\n<p dir=\"auto\">Common RL switches:<\/p>\n<p><markdown-accessiblity-table><\/p>\n<table>\n<thead>\n<tr>\n<th>Goal<\/th>\n<th>Flags<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Disable reference workers<\/td>\n<td><code>--algo.kl.init_coef 0<\/code><\/td>\n<\/tr>\n<tr>\n<td>Enable KL regularization<\/td>\n<td>Set <code>--algo.kl.init_coef<\/code> above zero and place reference workers with <code>--ref.num_nodes<\/code>, <code>--ref.num_gpus_per_node<\/code>, or <code>--train.colocate_fsdp_models<\/code><\/td>\n<\/tr>\n<tr>\n<td>Compare samples per prompt<\/td>\n<td><code>--rollout.n_samples_per_prompt 8<\/code> plus <code>reinforce_baseline<\/code>, <code>rloo<\/code>, <code>grpo<\/code>, or <code>dr_grpo<\/code><\/td>\n<\/tr>\n<tr>\n<td>Decouple rollout and training<\/td>\n<td><code>--train.async_queue_size 2<\/code><\/td>\n<\/tr>\n<tr>\n<td>Keep rollout alive during sync<\/td>\n<td><code>--train.partial_rollout_enable<\/code><\/td>\n<\/tr>\n<tr>\n<td>Filter by agent scores<\/td>\n<td><code>--algo.dynamic_filtering_enable --algo.dynamic_filtering_range 0.0 1.0<\/code><\/td>\n<\/tr>\n<tr>\n<td>Correct async rollout logprobs<\/td>\n<td><code>--algo.advantage.is_correction_level geo<\/code> (seq-mask-tis; token-level adds <code>--algo.advantage.is_correction_mode clip\/trunc\/mask<\/code>)<\/td>\n<\/tr>\n<tr>\n<td>Freeze MoE routing (stabilize MoE RL)<\/td>\n<td><code>--actor.freeze_moe_router<\/code><\/td>\n<\/tr>\n<tr>\n<td>On-policy distillation<\/td>\n<td><code>--algo.advantage.estimator on_policy_distill --ref.model_name_or_path \/path\/to\/teacher<\/code><\/td>\n<\/tr>\n<tr>\n<td>Independent eval sampling<\/td>\n<td><code>--eval.temperature<\/code>, <code>--eval.top_p<\/code>, <code>--eval.max_new_tokens<\/code>, <code>--eval.n_samples_per_prompt<\/code> (unset ones fall back to rollout)<\/td>\n<\/tr>\n<tr>\n<td>Eval a checkpoint (no training)<\/td>\n<td><code>--eval.eval_only --eval.dataset &lt;path&gt;<\/code> \u2014 score the eval set once and exit; vLLM holds the HF weights, so the policy\/ref\/critic FSDP actors are never built and their GPUs go to the eval<\/td>\n<\/tr>\n<tr>\n<td>Dump \/ replay a rollout batch<\/td>\n<td><code>--train.rollout_dump_dir &lt;dir&gt;<\/code> then <code>--train.rollout_replay_dir &lt;dir&gt;<\/code> re-runs training on it without regenerating<\/td>\n<\/tr>\n<tr>\n<td>Check weight-update coverage<\/td>\n<td><code>--train.check_weight_update_equal<\/code> warns which vLLM params a broadcast left stale<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><\/markdown-accessiblity-table><\/p>\n<p dir=\"auto\">Every RL run points at one Python module:<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"--train.agent_path \/path\/to\/agent.py\">\n<pre>--train.agent_path \/path\/to\/agent.py<\/pre>\n<\/div>\n<p dir=\"auto\">The module must export <code>AgentRunner<\/code>. Choose <strong>one<\/strong> of two paths:<\/p>\n<div class=\"markdown-heading\" dir=\"auto\">\n<h3 tabindex=\"-1\" class=\"heading-element\" dir=\"auto\">1. <code>Env<\/code> \u2014 framework owns the LLM loop <em>(Gymnasium-style step\/reset)<\/em><\/h3>\n<p><a id=\"user-content-1-env--framework-owns-the-llm-loop-gymnasium-style-stepreset\" class=\"anchor\" aria-label=\"Permalink: 1. Env \u2014 framework owns the LLM loop (Gymnasium-style step\/reset)\" href=\"#1-env--framework-owns-the-llm-loop-gymnasium-style-stepreset\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"\/><\/svg><\/a><\/div>\n<div class=\"highlight highlight-source-python notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"from molt.agents import Env, Result, StepEnvRunner&#10;&#10;class MathEnv(Env):&#10;    async def step(self, state) -&gt; Result:&#10;        # state: observation_text, action_text, label, sampling_params&#10;        reward = grade(state[&quot;action_text&quot;], state[&quot;label&quot;])&#10;        return Result(reward=reward, terminated=True)&#10;&#10;class AgentRunner(StepEnvRunner):&#10;    def __init__(self):&#10;        super().__init__(MathEnv)\">\n<pre><span class=\"pl-k\">from<\/span> <span class=\"pl-s1\">molt<\/span>.<span class=\"pl-s1\">agents<\/span> <span class=\"pl-k\">import<\/span> <span class=\"pl-v\">Env<\/span>, <span class=\"pl-v\">Result<\/span>, <span class=\"pl-v\">StepEnvRunner<\/span>\n\n<span class=\"pl-k\">class<\/span> <span class=\"pl-v\">MathEnv<\/span>(<span class=\"pl-v\">Env<\/span>):\n    <span class=\"pl-k\">async<\/span> <span class=\"pl-k\">def<\/span> <span class=\"pl-en\">step<\/span>(<span class=\"pl-s1\">self<\/span>, <span class=\"pl-s1\">state<\/span>) <span class=\"pl-c1\">-&gt;<\/span> <span class=\"pl-smi\">Result<\/span>:\n        <span class=\"pl-c\"># state: observation_text, action_text, label, sampling_params<\/span>\n        <span class=\"pl-s1\">reward<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-en\">grade<\/span>(<span class=\"pl-s1\">state<\/span>[<span class=\"pl-s\">\"action_text\"<\/span>], <span class=\"pl-s1\">state<\/span>[<span class=\"pl-s\">\"label\"<\/span>])\n        <span class=\"pl-k\">return<\/span> <span class=\"pl-en\">Result<\/span>(<span class=\"pl-s1\">reward<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">reward<\/span>, <span class=\"pl-s1\">terminated<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-c1\">True<\/span>)\n\n<span class=\"pl-k\">class<\/span> <span class=\"pl-v\">AgentRunner<\/span>(<span class=\"pl-v\">StepEnvRunner<\/span>):\n    <span class=\"pl-k\">def<\/span> <span class=\"pl-en\">__init__<\/span>(<span class=\"pl-s1\">self<\/span>):\n        <span class=\"pl-en\">super<\/span>().<span class=\"pl-c1\">__init__<\/span>(<span class=\"pl-v\">MathEnv<\/span>)<\/pre>\n<\/div>\n<p dir=\"auto\">The framework drives vLLM, tokenization, multimodal accounting, and<br \/>\nper-turn budgets. Your <code>step()<\/code> returns a <code>Result<\/code>; the framework chains<br \/>\nturns until <code>terminated<\/code> or <code>truncated<\/code>.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\">\n<h3 tabindex=\"-1\" class=\"heading-element\" dir=\"auto\">2. <code>ChatAgent<\/code> \u2014 you own the loop via the OpenAI <strong>or<\/strong> Anthropic SDK<\/h3>\n<p><a id=\"user-content-2-chatagent--you-own-the-loop-via-the-openai-or-anthropic-sdk\" class=\"anchor\" aria-label=\"Permalink: 2. ChatAgent \u2014 you own the loop via the OpenAI or Anthropic SDK\" href=\"#2-chatagent--you-own-the-loop-via-the-openai-or-anthropic-sdk\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"\/><\/svg><\/a><\/div>\n<div class=\"highlight highlight-source-python notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"from openai import AsyncOpenAI&#10;from molt.agents import ChatAgent, ChatAgentRunner, ChatContext, Result&#10;&#10;class MyAgent(ChatAgent):&#10;    async def run(self, ctx: ChatContext) -&gt; Result:&#10;        # ctx.base_url carries the session id and auto-captures the token&#10;        # trace \u2014 no extra_body, no logprobs=True, no session plumbing.&#10;        client = AsyncOpenAI(base_url=ctx.base_url, api_key=ctx.api_key)&#10;        resp = await client.chat.completions.create(&#10;            model=ctx.model_name,&#10;            messages=[{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: ctx.prompt}],&#10;            max_tokens=ctx.sampling_params.max_tokens,&#10;            temperature=ctx.sampling_params.temperature,&#10;        )&#10;        return Result(reward=grade(resp.choices[0].message.content, ctx.label))&#10;&#10;class AgentRunner(ChatAgentRunner):&#10;    def __init__(self):&#10;        super().__init__(MyAgent)\">\n<pre><span class=\"pl-k\">from<\/span> <span class=\"pl-s1\">openai<\/span> <span class=\"pl-k\">import<\/span> <span class=\"pl-v\">AsyncOpenAI<\/span>\n<span class=\"pl-k\">from<\/span> <span class=\"pl-s1\">molt<\/span>.<span class=\"pl-s1\">agents<\/span> <span class=\"pl-k\">import<\/span> <span class=\"pl-v\">ChatAgent<\/span>, <span class=\"pl-v\">ChatAgentRunner<\/span>, <span class=\"pl-v\">ChatContext<\/span>, <span class=\"pl-v\">Result<\/span>\n\n<span class=\"pl-k\">class<\/span> <span class=\"pl-v\">MyAgent<\/span>(<span class=\"pl-v\">ChatAgent<\/span>):\n    <span class=\"pl-k\">async<\/span> <span class=\"pl-k\">def<\/span> <span class=\"pl-en\">run<\/span>(<span class=\"pl-s1\">self<\/span>, <span class=\"pl-s1\">ctx<\/span>: <span class=\"pl-smi\">ChatContext<\/span>) <span class=\"pl-c1\">-&gt;<\/span> <span class=\"pl-smi\">Result<\/span>:\n        <span class=\"pl-c\"># ctx.base_url carries the session id and auto-captures the token<\/span>\n        <span class=\"pl-c\"># trace \u2014 no extra_body, no logprobs=True, no session plumbing.<\/span>\n        <span class=\"pl-s1\">client<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-en\">AsyncOpenAI<\/span>(<span class=\"pl-s1\">base_url<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">ctx<\/span>.<span class=\"pl-c1\">base_url<\/span>, <span class=\"pl-s1\">api_key<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">ctx<\/span>.<span class=\"pl-c1\">api_key<\/span>)\n        <span class=\"pl-s1\">resp<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-k\">await<\/span> <span class=\"pl-s1\">client<\/span>.<span class=\"pl-c1\">chat<\/span>.<span class=\"pl-c1\">completions<\/span>.<span class=\"pl-c1\">create<\/span>(\n            <span class=\"pl-s1\">model<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">ctx<\/span>.<span class=\"pl-c1\">model_name<\/span>,\n            <span class=\"pl-s1\">messages<\/span><span class=\"pl-c1\">=<\/span>[{<span class=\"pl-s\">\"role\"<\/span>: <span class=\"pl-s\">\"user\"<\/span>, <span class=\"pl-s\">\"content\"<\/span>: <span class=\"pl-s1\">ctx<\/span>.<span class=\"pl-c1\">prompt<\/span>}],\n            <span class=\"pl-s1\">max_tokens<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">ctx<\/span>.<span class=\"pl-c1\">sampling_params<\/span>.<span class=\"pl-c1\">max_tokens<\/span>,\n            <span class=\"pl-s1\">temperature<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">ctx<\/span>.<span class=\"pl-c1\">sampling_params<\/span>.<span class=\"pl-c1\">temperature<\/span>,\n        )\n        <span class=\"pl-k\">return<\/span> <span class=\"pl-en\">Result<\/span>(<span class=\"pl-s1\">reward<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-en\">grade<\/span>(<span class=\"pl-s1\">resp<\/span>.<span class=\"pl-c1\">choices<\/span>[<span class=\"pl-c1\">0<\/span>].<span class=\"pl-c1\">message<\/span>.<span class=\"pl-c1\">content<\/span>, <span class=\"pl-s1\">ctx<\/span>.<span class=\"pl-c1\">label<\/span>))\n\n<span class=\"pl-k\">class<\/span> <span class=\"pl-v\">AgentRunner<\/span>(<span class=\"pl-v\">ChatAgentRunner<\/span>):\n    <span class=\"pl-k\">def<\/span> <span class=\"pl-en\">__init__<\/span>(<span class=\"pl-s1\">self<\/span>):\n        <span class=\"pl-en\">super<\/span>().<span class=\"pl-c1\">__init__<\/span>(<span class=\"pl-v\">MyAgent<\/span>)<\/pre>\n<\/div>\n<p dir=\"auto\">The same server speaks the Anthropic wire too \u2014 point <code>AsyncAnthropic<\/code> at<br \/>\n<code>ctx.session_url<\/code> (the session root <em>without<\/em> <code>\/v1<\/code>; the SDK appends<br \/>\n<code>\/v1\/messages<\/code> itself), everything else is identical:<\/p>\n<div class=\"highlight highlight-source-python notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"from anthropic import AsyncAnthropic&#10;&#10;client = AsyncAnthropic(base_url=ctx.session_url, api_key=ctx.api_key)&#10;msg = await client.messages.create(&#10;    model=ctx.model_name,&#10;    messages=[{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: ctx.prompt}],&#10;    max_tokens=ctx.sampling_params.max_tokens,&#10;)&#10;text = msg.content[0].text\">\n<pre><span class=\"pl-k\">from<\/span> <span class=\"pl-s1\">anthropic<\/span> <span class=\"pl-k\">import<\/span> <span class=\"pl-v\">AsyncAnthropic<\/span>\n\n<span class=\"pl-s1\">client<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-en\">AsyncAnthropic<\/span>(<span class=\"pl-s1\">base_url<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">ctx<\/span>.<span class=\"pl-c1\">session_url<\/span>, <span class=\"pl-s1\">api_key<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">ctx<\/span>.<span class=\"pl-c1\">api_key<\/span>)\n<span class=\"pl-s1\">msg<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-k\">await<\/span> <span class=\"pl-s1\">client<\/span>.<span class=\"pl-c1\">messages<\/span>.<span class=\"pl-c1\">create<\/span>(\n    <span class=\"pl-s1\">model<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">ctx<\/span>.<span class=\"pl-c1\">model_name<\/span>,\n    <span class=\"pl-s1\">messages<\/span><span class=\"pl-c1\">=<\/span>[{<span class=\"pl-s\">\"role\"<\/span>: <span class=\"pl-s\">\"user\"<\/span>, <span class=\"pl-s\">\"content\"<\/span>: <span class=\"pl-s1\">ctx<\/span>.<span class=\"pl-c1\">prompt<\/span>}],\n    <span class=\"pl-s1\">max_tokens<\/span><span class=\"pl-c1\">=<\/span><span class=\"pl-s1\">ctx<\/span>.<span class=\"pl-c1\">sampling_params<\/span>.<span class=\"pl-c1\">max_tokens<\/span>,\n)\n<span class=\"pl-s1\">text<\/span> <span class=\"pl-c1\">=<\/span> <span class=\"pl-s1\">msg<\/span>.<span class=\"pl-c1\">content<\/span>[<span class=\"pl-c1\">0<\/span>].<span class=\"pl-c1\">text<\/span><\/pre>\n<\/div>\n<p dir=\"auto\">A FastAPI vLLM server is auto-launched on loopback under the session URL.<br \/>\nExternal HTTP callers (browser automation, eval harnesses, OSWorld, \u2026) hit the<br \/>\nsame engine through either <code>\/v1\/chat\/completions<\/code> (OpenAI) or <code>\/v1\/messages<\/code><br \/>\n(Anthropic) \u2014 both decode to one token-exact accumulation.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\">\n<h4 tabindex=\"-1\" class=\"heading-element\" dir=\"auto\">Context compaction \u2192 multiple step-samples per rollout<\/h4>\n<p><a id=\"user-content-context-compaction--multiple-step-samples-per-rollout\" class=\"anchor\" aria-label=\"Permalink: Context compaction \u2192 multiple step-samples per rollout\" href=\"#context-compaction--multiple-step-samples-per-rollout\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"\/><\/svg><\/a><\/div>\n<p dir=\"auto\">Each chat call carries the prior turn&#8217;s <strong>exact<\/strong> tokens forward and appends only<br \/>\nthe new delta, so a multi-turn episode stitches into one monotonic token-exact<br \/>\ntrajectory. But a long-horizon agent often <strong>compacts<\/strong> its context \u2014 summarizing<br \/>\nor dropping old turns to stay under the window (e.g. a <code>\/compact<\/code> step) \u2014 which<br \/>\n<em>rewrites<\/em> the prefix, so it&#8217;s no longer a clean extension of what was tokenized.<br \/>\nThe model&#8217;s own chat template can rewrite the prefix too: Qwen3-style templates<br \/>\nre-render a prior assistant turn without its <code>&lt;think&gt;<\/code> block once a newer user<br \/>\nquery follows.<\/p>\n<p dir=\"auto\">The server detects this automatically: when an incoming request rewrites the<br \/>\nprefix instead of extending it, it <strong>seals the current segment and starts a fresh<br \/>\ntoken-exact segment<\/strong> from the re-templated conversation. One<br \/>\nrollout therefore emits several segment trajectories \u2014 they share the rollout&#8217;s<br \/>\nreward and <code>rollout_id<\/code>, so group baselines (GRPO\/RLOO\/\u2026) dedup them to <em>one<br \/>\nreward per rollout<\/em> while each segment still contributes its own generated tokens<br \/>\nto the policy gradient (the same step-sample contract multi-turn agents use). No<br \/>\nagent-side change is needed \u2014 it works on both wires, including external harnesses<br \/>\n(Claude Code, opencode, AgentScope, \u2026) whose compaction is opaque to us.<\/p>\n<p><markdown-accessiblity-table><\/p>\n<table>\n<thead>\n<tr>\n<th>Field<\/th>\n<th>Meaning<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>reward<\/code><\/td>\n<td>Scalar reward consumed by the trainer (required)<\/td>\n<\/tr>\n<tr>\n<td><code>observation<\/code><\/td>\n<td>Next-turn observation text (multi-turn only)<\/td>\n<\/tr>\n<tr>\n<td><code>terminated<\/code><\/td>\n<td>Episode finished naturally; defaults to <code>True<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>truncated<\/code><\/td>\n<td>Cut off externally (max turns, length, etc.)<\/td>\n<\/tr>\n<tr>\n<td><code>info<\/code><\/td>\n<td>Optional dict of scalar diagnostics for logging<\/td>\n<\/tr>\n<tr>\n<td><code>score<\/code><\/td>\n<td>Optional dynamic-filtering \/ dashboard score (defaults to <code>reward<\/code>)<\/td>\n<\/tr>\n<tr>\n<td><code>images<\/code><\/td>\n<td>Optional list of next-turn images<\/td>\n<\/tr>\n<tr>\n<td><code>sampling_params<\/code><\/td>\n<td>Optional per-turn override<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><\/markdown-accessiblity-table><\/p>\n<p dir=\"auto\">Four reference agents ship under <code>examples\/python\/agents\/<\/code>:<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"--train.agent_path examples\/python\/agents\/math.py          # Env: single-turn boxed grader&#10;--train.agent_path examples\/python\/agents\/geo3k.py         # Env: VLM multi-turn + Python tool&#10;--train.agent_path examples\/python\/agents\/chat_minimal.py  # ChatAgent: hello-world chat loop&#10;--train.agent_path examples\/python\/agents\/chat_geo3k.py    # ChatAgent: VLM multi-turn + Python tool\">\n<pre>--train.agent_path examples\/python\/agents\/math.py          <span class=\"pl-c\"><span class=\"pl-c\">#<\/span> Env: single-turn boxed grader<\/span>\n--train.agent_path examples\/python\/agents\/geo3k.py         <span class=\"pl-c\"><span class=\"pl-c\">#<\/span> Env: VLM multi-turn + Python tool<\/span>\n--train.agent_path examples\/python\/agents\/chat_minimal.py  <span class=\"pl-c\"><span class=\"pl-c\">#<\/span> ChatAgent: hello-world chat loop<\/span>\n--train.agent_path examples\/python\/agents\/chat_geo3k.py    <span class=\"pl-c\"><span class=\"pl-c\">#<\/span> ChatAgent: VLM multi-turn + Python tool<\/span><\/pre>\n<\/div>\n<p dir=\"auto\">Reference launch scripts live under <code>examples\/scripts\/<\/code>. Two end-to-end<br \/>\nfamilies ship today, both on the AutoModel + FSDP2 backend:<\/p>\n<p><markdown-accessiblity-table><\/p>\n<table>\n<thead>\n<tr>\n<th>Workflow<\/th>\n<th>quick_start<\/th>\n<th>slurm<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Qwen3.6-35B-A3B VLM SFT on geo3k<\/td>\n<td><code>quick_start\/sft_qwen3_6_35b.sh<\/code><\/td>\n<td><code>slurm\/sft_qwen3_6_35b.sh<\/code><\/td>\n<\/tr>\n<tr>\n<td>Qwen3.6-35B-A3B VLM RL on geo3k (multi-turn Python tool)<\/td>\n<td><code>quick_start\/rl_qwen3_6_35b.sh<\/code><\/td>\n<td><code>slurm\/rl_qwen3_6_35b.sh<\/code><\/td>\n<\/tr>\n<tr>\n<td>Qwen3-4B dense SFT on text math<\/td>\n<td><code>quick_start\/sft_qwen3_4b.sh<\/code><\/td>\n<td><code>slurm\/sft_qwen3_4b.sh<\/code><\/td>\n<\/tr>\n<tr>\n<td>Qwen3-4B dense RL on text math<\/td>\n<td><code>quick_start\/rl_qwen3_4b.sh<\/code><\/td>\n<td><code>slurm\/rl_qwen3_4b.sh<\/code><\/td>\n<\/tr>\n<tr>\n<td>Nemotron-Omni-30B-A3B VLM RL on geo3k (hybrid SSM MoE, CP8+EP8)<\/td>\n<td>\u2014<\/td>\n<td><code>slurm\/rl_omni3_30b.sh<\/code><\/td>\n<\/tr>\n<tr>\n<td>Nemotron-Omni-30B-A3B on-policy distillation<\/td>\n<td>\u2014<\/td>\n<td><code>slurm\/rl_distill_omni3_30b.sh<\/code><\/td>\n<\/tr>\n<tr>\n<td>GLM-5.2 ~750B RL on text math (MLA + DSA sparse attention, EP256)<\/td>\n<td>\u2014<\/td>\n<td><code>slurm\/rl_glm5_2_753b.sh<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><\/markdown-accessiblity-table><\/p>\n<p dir=\"auto\">Quick-start single-node usage:<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"MODEL_PATH=\/path\/to\/Qwen3-4B bash examples\/scripts\/quick_start\/rl_qwen3_4b.sh\">\n<pre>MODEL_PATH=\/path\/to\/Qwen3-4B bash examples\/scripts\/quick_start\/rl_qwen3_4b.sh<\/pre>\n<\/div>\n<p dir=\"auto\">The geo3k VLM scripts (<code>rl_qwen3_6_35b.sh<\/code> \/ <code>sft_qwen3_6_35b.sh<\/code>) auto-prepare the<br \/>\ndataset on first run via <code>examples\/python\/utils\/prepare_geo3k.py<\/code>. To pre-stage it<br \/>\nmanually (or refresh it), run:<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"python3 examples\/python\/utils\/prepare_geo3k.py --num-proc 8 --out-dir .tmp\/geo3k\">\n<pre>python3 examples\/python\/utils\/prepare_geo3k.py --num-proc 8 --out-dir .tmp\/geo3k<\/pre>\n<\/div>\n<p dir=\"auto\">Or point <code>PROMPT_DATASET<\/code> \/ <code>EVAL_DATASET<\/code> at your own data.<\/p>\n<p dir=\"auto\">Slurm usage:<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"# 1) SFT smoke on interactive 2 nodes&#10;sbatch examples\/scripts\/slurm\/sft_qwen3_6_35b.sh&#10;&#10;# 2) RL smoke on interactive 2 nodes (auto-preps geo3k on first run)&#10;sbatch examples\/scripts\/slurm\/rl_qwen3_6_35b.sh&#10;&#10;# 3) Scale RL to 4 nodes for convergence&#10;sbatch --nodes=4 examples\/scripts\/slurm\/rl_qwen3_6_35b.sh\">\n<pre><span class=\"pl-c\"><span class=\"pl-c\">#<\/span> 1) SFT smoke on interactive 2 nodes<\/span>\nsbatch examples\/scripts\/slurm\/sft_qwen3_6_35b.sh\n\n<span class=\"pl-c\"><span class=\"pl-c\">#<\/span> 2) RL smoke on interactive 2 nodes (auto-preps geo3k on first run)<\/span>\nsbatch examples\/scripts\/slurm\/rl_qwen3_6_35b.sh\n\n<span class=\"pl-c\"><span class=\"pl-c\">#<\/span> 3) Scale RL to 4 nodes for convergence<\/span>\nsbatch --nodes=4 examples\/scripts\/slurm\/rl_qwen3_6_35b.sh<\/pre>\n<\/div>\n<div class=\"markdown-heading\" dir=\"auto\">\n<h3 tabindex=\"-1\" class=\"heading-element\" dir=\"auto\">Multi-turn Python tool env<\/h3>\n<p><a id=\"user-content-multi-turn-python-tool-env\" class=\"anchor\" aria-label=\"Permalink: Multi-turn Python tool env\" href=\"#multi-turn-python-tool-env\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"\/><\/svg><\/a><\/div>\n<p dir=\"auto\"><code>examples\/python\/agents\/geo3k.py<\/code> is the VLM multi-turn recipe used by the<br \/>\nQwen3.6 RL script. The model emits a <code>&lt;tool_call&gt;<\/code> invoking<br \/>\n<code>python_executor(code=...)<\/code>; the env runs the snippet in a sandboxed<br \/>\nsubprocess and feeds the captured stdout back as a <code>&lt;tool_response&gt;<\/code> turn.<br \/>\nThe loop runs up to <code>MAX_AGENT_TURNS<\/code> (agent default 5; the shipped Qwen3.6<br \/>\nrecipe raises it to 10); the final <code>&lt;answer&gt;ANSWER&lt;\/answer&gt;<\/code> (or<br \/>\n<code>\\boxed{ANSWER}<\/code> for legacy distributions) is graded against the ground truth<br \/>\nand becomes the reward.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\">\n<h3 tabindex=\"-1\" class=\"heading-element\" dir=\"auto\">OpenAI- \/ Anthropic-compatible server agent<\/h3>\n<p><a id=\"user-content-openai---anthropic-compatible-server-agent\" class=\"anchor\" aria-label=\"Permalink: OpenAI- \/ Anthropic-compatible server agent\" href=\"#openai---anthropic-compatible-server-agent\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"\/><\/svg><\/a><\/div>\n<p dir=\"auto\">For agents that already speak OpenAI Chat Completions or the Anthropic Messages<br \/>\nAPI, subclass <code>ChatAgent<\/code> (see <code>examples\/python\/agents\/chat_minimal.py<\/code>). The<br \/>\nauto-launched server exposes both <code>\/v1\/chat\/completions<\/code> and <code>\/v1\/messages<\/code><br \/>\nagainst the rolling vLLM engines, so any external loop (browser automation, eval<br \/>\nharness, OSWorld \u2026) can drive the policy through a stock OpenAI or Anthropic SDK<br \/>\n\u2014 both wires decode to the same token-exact trajectory capture.<\/p>\n<p dir=\"auto\">Distill a student onto a frozen teacher on the student&#8217;s <em>own<\/em> on-policy<br \/>\nsamples. A single switch \u2014<br \/>\n<code>--algo.advantage.estimator on_policy_distill<\/code> \u2014 turns the reference model into<br \/>\nthe teacher and makes the per-token <strong>reverse KL<\/strong> to it the entire training<br \/>\nsignal: the advantage becomes <code>-kl_coef \u00b7 (log \u03c0_student \u2212 log \u03c0_teacher)<\/code> with<br \/>\nno scalar reward, no group baseline, and no whitening, so the policy loss is the<br \/>\npolicy-gradient estimator of the reverse-KL gradient that pulls the student onto<br \/>\nthe teacher.<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"python3 -m molt.cli.train_rl_ray \\&#10;  --actor.model_name_or_path \/path\/to\/student \\&#10;  --ref.model_name_or_path \/path\/to\/teacher \\&#10;  --algo.advantage.estimator on_policy_distill \\&#10;  --data.prompt_dataset \/path\/to\/prompts.jsonl \\&#10;  --data.input_key input \\&#10;  # vllm \/ fsdp \/ batch flags as in the RL quick start\">\n<pre>python3 -m molt.cli.train_rl_ray \\\n  --actor.model_name_or_path \/path\/to\/student \\\n  --ref.model_name_or_path \/path\/to\/teacher \\\n  --algo.advantage.estimator on_policy_distill \\\n  --data.prompt_dataset \/path\/to\/prompts.jsonl \\\n  --data.input_key input \\\n  <span class=\"pl-c\"><span class=\"pl-c\">#<\/span> vllm \/ fsdp \/ batch flags as in the RL quick start<\/span><\/pre>\n<\/div>\n<p dir=\"auto\">Everything else is derived from the one switch, so pure distillation needs no<br \/>\nreward function and no task agent \u2014 only the teacher checkpoint. Selecting the<br \/>\nestimator forces <code>--algo.kl.estimator k1<\/code>, turns <code>--algo.kl.use_loss<\/code> off (the<br \/>\nKL flows through the advantage, not a separate loss term), defaults<br \/>\n<code>--algo.kl.init_coef<\/code> to <code>1.0<\/code>, and \u2014 when no <code>--train.agent_path<\/code> is given \u2014<br \/>\nauto-selects a built-in single-turn, VLM-aware generator<br \/>\n(<code>molt\/agents\/distill_agent.py<\/code>) that samples one on-policy completion per<br \/>\nprompt and returns a dummy <code>0.0<\/code> reward the estimator ignores.<\/p>\n<p dir=\"auto\">The teacher <strong>must share the student&#8217;s processor\/tokenizer<\/strong> so the per-token<br \/>\nlogprobs align over the same (vision-expanded) sequence \u2014 typically a larger or<br \/>\nmore-trained checkpoint from the same family. It loads inference-only and can be<br \/>\ncolocated on the actor nodes (<code>--train.colocate_fsdp_models<\/code>) or given its own<br \/>\n<code>--ref.num_nodes<\/code>. Watch <code>kl<\/code> \/ <code>logprobs_diff<\/code> fall toward 0 as the student<br \/>\nmatches the teacher; task accuracy is not the objective, so eval is off.<\/p>\n<p dir=\"auto\">To distill a <strong>multi-turn tool-use distribution<\/strong> (matching how the student is<br \/>\nactually deployed), point <code>--train.agent_path<\/code> at the task&#8217;s real agent (e.g.<br \/>\n<code>chat_geo3k.py<\/code>) \u2014 its reward is simply ignored by the estimator.<br \/>\n<code>examples\/scripts\/slurm\/rl_distill_omni3_30b.sh<\/code> is a ready VLM example off the omni3<br \/>\nEP8 \/ CP8 \/ TE \/ DeepEP recipe.<\/p>\n<p dir=\"auto\">Molt targets AutoModel custom models with FSDP2:<\/p>\n<p><markdown-accessiblity-table><\/p>\n<table>\n<thead>\n<tr>\n<th\/>\n<th>Mode<\/th>\n<th>Flag<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Actor<\/strong><\/td>\n<td>Tensor parallel<\/td>\n<td><code>--fsdp.tp_size 2<\/code><\/td>\n<\/tr>\n<tr>\n<td\/>\n<td>Expert parallel<\/td>\n<td><code>--fsdp.ep_size 8<\/code> (e.g. <code>256<\/code> for DeepSeek-V3-class MoE)<\/td>\n<\/tr>\n<tr>\n<td\/>\n<td>Context parallel<\/td>\n<td><code>--fsdp.cp_size 8<\/code> (32K+ sequences), incl. VLMs and MoE routing replay<\/td>\n<\/tr>\n<tr>\n<td\/>\n<td>Optimizer CPU offload<\/td>\n<td><code>--fsdp.offload optimizer<\/code> (frees VRAM for the largest actors)<\/td>\n<\/tr>\n<tr>\n<td><strong>vLLM rollout<\/strong><\/td>\n<td>Tensor parallel<\/td>\n<td><code>--vllm.tensor_parallel_size 2<\/code><\/td>\n<\/tr>\n<tr>\n<td\/>\n<td>Expert parallel<\/td>\n<td><code>--vllm.enable_expert_parallel<\/code> (EP = TP \u00d7 DP)<\/td>\n<\/tr>\n<tr>\n<td\/>\n<td>Data parallel<\/td>\n<td><code>--vllm.data_parallel_size 4<\/code> (single-node mp; raises EP past TP \u2014 DeepSeek-V3-style TP8+DP4 \u2192 EP32)<\/td>\n<\/tr>\n<tr>\n<td\/>\n<td>Scheduler token budget<\/td>\n<td><code>--vllm.max_num_batched_tokens 32768<\/code><\/td>\n<\/tr>\n<tr>\n<td\/>\n<td>MTP spec-decode<\/td>\n<td><code>--vllm.mtp_num_speculative_tokens 1<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>MoE stability<\/strong><\/td>\n<td>Router replay (R3)<\/td>\n<td><code>--train.routing_replay<\/code><\/td>\n<\/tr>\n<tr>\n<td\/>\n<td>Router freeze<\/td>\n<td><code>--actor.freeze_moe_router<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><\/markdown-accessiblity-table><\/p>\n<p dir=\"auto\">Context parallelism is delegated to AutoModel&#8217;s <code>ContextParallelSharder<\/code>, so each<br \/>\nmodel gets the sharding its attention backend needs \u2014 round-robin for hybrid<br \/>\nSSM \/ linear-attention models (Nemotron Omni, Qwen3.5-MoE), flat THD streams for<br \/>\nsparse-attention models (GLM-5.2 DSA). VLM vision towers and routing replay shard<br \/>\nwith the sequence, so <code>--fsdp.cp_size<\/code> composes with <code>--data.image_key<\/code> and<br \/>\n<code>--train.routing_replay<\/code>. Sample packing (<code>--fsdp.packing_samples<\/code>) is text-only<br \/>\nand off by default; under CP it takes the THD path.<\/p>\n<div class=\"markdown-heading\" dir=\"auto\">\n<h3 tabindex=\"-1\" class=\"heading-element\" dir=\"auto\"> MTP rollout (speculative decoding)<\/h3>\n<p><a id=\"user-content--mtp-rollout-speculative-decoding\" class=\"anchor\" aria-label=\"Permalink: &#x26a1; MTP rollout (speculative decoding)\" href=\"#-mtp-rollout-speculative-decoding\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"\/><\/svg><\/a><\/div>\n<p dir=\"auto\">Checkpoints that ship a multi-token-prediction (MTP) head \u2014 e.g. <strong>Qwen3.6-MoE<\/strong><br \/>\n(<code>mtp_num_hidden_layers: 1<\/code>) \u2014 can use it to <strong>speed up generation<\/strong> via vLLM<br \/>\nspeculative decoding. One flag turns it on:<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"--vllm.mtp_num_speculative_tokens 1   # 0 = off (default); 1 is a good default\">\n<pre>--vllm.mtp_num_speculative_tokens 1   <span class=\"pl-c\"><span class=\"pl-c\">#<\/span> 0 = off (default); 1 is a good default<\/span><\/pre>\n<\/div>\n<p dir=\"auto\">vLLM auto-detects the per-architecture MTP draft from the served checkpoint<br \/>\n(<code>qwen3_5_moe \u2192 qwen3_5_mtp<\/code>), drafts <em>N<\/em> tokens, and the target model verifies<br \/>\neach by rejection sampling. This is <strong>lossless<\/strong>: accepted tokens follow the<br \/>\ntarget policy&#8217;s distribution, so the rollout log-probs stay unbiased for the RL<br \/>\nobjective \u2014 it only changes throughput, never the learned policy.<\/p>\n<p dir=\"auto\">Notes:<\/p>\n<ul dir=\"auto\">\n<li><strong>Rollout-only.<\/strong> Like verl and NeMo-RL, the RL\/SFT loss is <strong>main-head-only<\/strong>;<br \/>\nMolt does not train the MTP head. The draft shares <code>embed_tokens<\/code>\/<code>lm_head<\/code> with<br \/>\nthe target (refreshed every weight broadcast), so it tracks the updating policy;<br \/>\nonly the small MTP block stays at its checkpoint weights, so acceptance degrades<br \/>\ngracefully rather than off a cliff.<\/li>\n<li><strong>Model support is vLLM-side.<\/strong> Qwen3.6-MoE works out of the box. The official<br \/>\nNemotron-Nano-Omni HF checkpoint ships <strong>no<\/strong> MTP head, and vLLM 0.24 only<br \/>\nauto-detects MTP for the <em>Super<\/em>-Omni arch (not Nano) \u2014 so omni3 rollout-MTP is<br \/>\nunavailable until upstream adds it; vLLM errors at engine init if enabled on an<br \/>\nunsupported checkpoint.<\/li>\n<\/ul>\n<div class=\"markdown-heading\" dir=\"auto\">\n<h3 tabindex=\"-1\" class=\"heading-element\" dir=\"auto\"> MoE routing stability \u2014 Router Replay (R3) &amp; router freeze<\/h3>\n<p><a id=\"user-content--moe-routing-stability--router-replay-r3--router-freeze\" class=\"anchor\" aria-label=\"Permalink: &#x1f3af; MoE routing stability \u2014 Router Replay (R3) &amp; router freeze\" href=\"#-moe-routing-stability--router-replay-r3--router-freeze\"><svg data-component=\"Octicon\" class=\"octicon octicon-link\" viewbox=\"0 0 16 16\" version=\"1.1\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z\"\/><\/svg><\/a><\/div>\n<p dir=\"auto\">MoE RL is unstable because the rollout (vLLM) and training (FSDP) routers pick<br \/>\nexperts <strong>independently<\/strong> \u2014 even at identical weights, numerical differences<br \/>\nflip a fraction of the top-k per layer, compounding until most tokens route to<br \/>\ndifferent experts than they did during rollout. That breaks the importance-<br \/>\nsampling assumption behind GRPO\/GSPO. Molt closes the gap at three levels \u2014 the<br \/>\nfirst two are on by default in the qwen3.5-moe recipes, the third is an optional<br \/>\nheavier alternative to R3:<\/p>\n<p dir=\"auto\"><strong>fp32 router precision (default).<\/strong> The gate linear + expert-output combine run in<br \/>\nfp32 (matching vLLM&#8217;s fp32 router) so the two sides agree on the gate <em>weights<\/em> to<br \/>\nbegin with. A bf16 router silently drifts from vLLM and makes <code>vllm_kl<\/code> climb with<br \/>\ntraining. Override with <code>MOLT_GATE_PRECISION=bfloat16<\/code>.<\/p>\n<p dir=\"auto\"><strong>Rollout Routing Replay (R3, default)<\/strong> \u2014 fix the top-k <em>selection<\/em> at the source<br \/>\n(<a href=\"https:\/\/arxiv.org\/abs\/2510.11370\" rel=\"nofollow\">arXiv:2510.11370<\/a>): vLLM returns the per-token<br \/>\nexpert ids it chose, and the training forward replays that exact selection.<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"--train.routing_replay   # default in the qwen3.5-moe recipes\">\n<pre>--train.routing_replay   <span class=\"pl-c\"><span class=\"pl-c\">#<\/span> default in the qwen3.5-moe recipes<\/span><\/pre>\n<\/div>\n<ul dir=\"auto\">\n<li><strong>Freezes the routing, not the router.<\/strong> Only the discrete top-k <em>selection<\/em> is<br \/>\nreplayed; the router logits are still recomputed from the live weights, so the<br \/>\ngradient keeps flowing into the router (it keeps learning).<\/li>\n<li><strong>Full-sequence, absolute-position aligned<\/strong>: routing is laid down by token<br \/>\nposition; positions the engine returns no routing for keep their natural selection.<\/li>\n<li>Needs AutoModel <code>RouterReplay<\/code> (<code>nemo_automodel.components.moe.router_replay<\/code>,<br \/>\nPR #2797). Incompatible with <code>--train.partial_rollout_enable<\/code> (vLLM frees routing<br \/>\non preemption).<\/li>\n<\/ul>\n<p dir=\"auto\"><strong>Router freeze (optional, blunter).<\/strong> Hold the gate\/router weights fixed so the<br \/>\nrouting can&#8217;t drift at all \u2014 excludes the router from the optimizer <em>and<\/em> the refit,<br \/>\nso vLLM and the actor route to the same experts <strong>by construction<\/strong>, no engine<br \/>\nsupport needed. The trade-off: the router stops learning, so it&#8217;s <strong>redundant with<br \/>\nR3<\/strong> and off by default; reach for it only if routing drift still dominates and a<br \/>\nfixed router is acceptable.<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"--actor.freeze_moe_router   # off by default; redundant with R3\">\n<pre>--actor.freeze_moe_router   <span class=\"pl-c\"><span class=\"pl-c\">#<\/span> off by default; redundant with R3<\/span><\/pre>\n<\/div>\n<p dir=\"auto\">Fast local checks:<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"python -m compileall -q molt examples\/python tests&#10;pytest -q\">\n<pre>python -m compileall -q molt examples\/python tests\npytest -q<\/pre>\n<\/div>\n<p dir=\"auto\">Container checks:<\/p>\n<div class=\"highlight highlight-source-shell notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"SKIP_BUILD=1 DOCKER_GPUS=all DOCKER_SHM_SIZE=32g \\&#10;  bash examples\/scripts\/docker_run.sh &quot;pytest -q&quot;\">\n<pre>SKIP_BUILD=1 DOCKER_GPUS=all DOCKER_SHM_SIZE=32g \\\n  bash examples\/scripts\/docker_run.sh <span class=\"pl-s\"><span class=\"pl-pds\">\"<\/span>pytest -q<span class=\"pl-pds\">\"<\/span><\/span><\/pre>\n<\/div>\n<p dir=\"auto\">Molt is based on OpenRLHF and keeps its Python package layout where<br \/>\npractical. The active architecture is intentionally minimal: a<br \/>\nGymnasium-aligned agent (<code>Env<\/code> \/ <code>ChatAgent<\/code>), a single trainable actor,<br \/>\noptional KL reference workers, vLLM generation, and online policy<br \/>\noptimization \u2014 all on PyTorch + AutoModel.<\/p>\n<p dir=\"auto\">If you use Molt in your research, please cite:<\/p>\n<div class=\"highlight highlight-text-bibtex notranslate position-relative overflow-auto\" dir=\"auto\" data-snippet-clipboard-copy-content=\"@article{hu2026molt,&#10;  title        = {Molt: A Scalable PyTorch-Native Training Framework for Agentic Reinforcement Learning},&#10;  author       = {Jian Hu and Molt Contributors},&#10;  year         = {2026},&#10;  howpublished = {\\url{https:\/\/github.com\/NVIDIA-NeMo\/labs-molt}},&#10;  doi          = {10.13140\/RG.2.2.23375.65447}&#10;}\">\n<pre><span class=\"pl-k\">@article<\/span>{<span class=\"pl-en\">hu2026molt<\/span>,\n  <span class=\"pl-s\">title<\/span>        = <span class=\"pl-s\"><span class=\"pl-pds\">{<\/span>Molt: A Scalable PyTorch-Native Training Framework for Agentic Reinforcement Learning<span class=\"pl-pds\">}<\/span><\/span>,\n  <span class=\"pl-s\">author<\/span>       = <span class=\"pl-s\"><span class=\"pl-pds\">{<\/span>Jian Hu and Molt Contributors<span class=\"pl-pds\">}<\/span><\/span>,\n  <span class=\"pl-s\">year<\/span>         = <span class=\"pl-s\"><span class=\"pl-pds\">{<\/span>2026<span class=\"pl-pds\">}<\/span><\/span>,\n  <span class=\"pl-s\">howpublished<\/span> = <span class=\"pl-s\"><span class=\"pl-pds\">{<\/span>\\url{https:\/\/github.com\/NVIDIA-NeMo\/labs-molt}<span class=\"pl-pds\">}<\/span><\/span>,\n  <span class=\"pl-s\">doi<\/span>          = <span class=\"pl-s\"><span class=\"pl-pds\">{<\/span>10.13140\/RG.2.2.23375.65447<span class=\"pl-pds\">}<\/span><\/span>\n}<\/pre>\n<\/div>\n<p dir=\"auto\">External contributions are welcome \u2014 see <a href=\"https:\/\/github.com\/NVIDIA-NeMo\/labs-molt\/blob\/main\/CONTRIBUTING.md\">CONTRIBUTING.md<\/a>.<br \/>\nAll commits must be signed off (<code>git commit -s<\/code>) per the<br \/>\n<a href=\"https:\/\/developercertificate.org\/\" rel=\"nofollow\">Developer Certificate of Origin (DCO)<\/a>.<\/p>\n<p dir=\"auto\"><a href=\"https:\/\/github.com\/NVIDIA-NeMo\/labs-molt\/blob\/main\/LICENSE\">Apache License 2.0<\/a>. Copyright and third-party attributions:<br \/>\n<a href=\"https:\/\/github.com\/NVIDIA-NeMo\/labs-molt\/blob\/main\/NOTICE\">NOTICE<\/a> and <a href=\"https:\/\/github.com\/NVIDIA-NeMo\/labs-molt\/blob\/main\/THIRD_PARTY_NOTICES.md\">THIRD_PARTY_NOTICES.md<\/a>.<\/p>\n<\/div>\n<p><a href=\"https:\/\/github.com\/NVIDIA-NeMo\/labs-molt?utm_source=tldrai\">Source link <\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Molt is agentic-first and PyTorch-native. The agent is the program; the trainer is a single actor; reward is any Python you write inside an Env or ChatAgent \u2014 graders, multi-turn tools, VLM environments, LLM-as-judge. Three components carry the rest \u2014 Ray for placement and async queues, vLLM for rollout, NVIDIA AutoModel + FSDP2 for training [&hellip;]<\/p>\n","protected":false},"author":16,"featured_media":22789,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[143],"tags":[],"class_list":["post-22788","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai"],"_links":{"self":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts\/22788","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/users\/16"}],"replies":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/comments?post=22788"}],"version-history":[{"count":0,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts\/22788\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/media\/22789"}],"wp:attachment":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/media?parent=22788"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/categories?post=22788"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/tags?post=22788"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}