{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-05-06-vllm-v0-to-v1-correctness-before-corrections-in-rl/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "[PipelineRL](https://huggingface.co/blog/ServiceNow-AI/github.com/ServiceNow/PipelineRL/) uses vLLM as the inference engine for rollout generation. The inference engine samples tokens and returns token logprobs; the trainer uses those logprobs to compute policy ratios, KL, clip rate, entropy, and reward. Any discrepancy in how those logprobs are computed can change the training dynamics. This is the train-inference mismatch we needed to eliminate during the vLLM V0 to V1 migration.\n\n**TL;DR.** vLLM V1 matched our vLLM V0 reference after we fixed four things: processed rollout logprobs, V1-specific runtime defaults, the inflight weight-update path, and the fp32 `lm_head` used for the final projection. We fixed the backend behavior before changing the RL objective.\n\nThe reference run used vLLM `0.8.5`; the V1 runs used vLLM `0.18.1`. Figure 1 shows the final result. The red run is the initial V1 attempt, and the green run is the final V1 run after the fixes described below.\n\n![](https://cdn-uploads.huggingface.co/production/uploads/61f750055596aa721ce68efe/2Q2pkNtiYPrlLVLoH51dO.png)\\\nFigure 1. Trainer-side metrics for the vLLM V0 reference (blue), the initial vLLM V1 attempt (red), and the final vLLM V1 run after our fixes (green), including the fp32 `lm_head`. The final V1 run returns close to the V0 trajectory across clip rate, KL, entropy, and reward.\n\n## Migration Objective\n\nvLLM V1 is a substantial rewrite of the V0 engine. Our migration target was therefore deliberately narrow:\n\n1. verify that V1 returned rollout logprobs in the form the trainer expected\n2. rerun the same workload against the V0 reference\n3. evaluate objective-level changes only after backend parity was restored\n\nThe first visible symptoms appeared in:\n\n- `clamp_log_ratio_new_old_indicator`\n- `kl_new_old`\n- `entropy`\n- `reward`\n\nThose metrics came from a GSPO training run, the objective used for this experiment. The same class of mismatch can surface in PPO, GRPO, or any online RL system that treats rollout-side logprobs as part of the optimization target.\n\nThe initial V1 run showed the problem clearly. The trainer-side logprobs and reward moved away from the V0 reference early in training.\n\n![](https://cdn-uploads.huggingface.co/production/uploads/61f750055596aa721ce68efe/YhRfKIEKndf4LGG9g7Ck9.png)\\\nFigure 2. Current-policy logprobs computed by the trainer during updates (left) and reward (right). The initial vLLM V1 run (red) separates from the vLLM V0 reference (blue).\n\nThe same pattern appears in the trainer metrics. Clip rate is the easiest signal to read in the initial comparison.\n\n![](https://cdn-uploads.huggingface.co/production/uploads/61f750055596aa721ce68efe/6fQ4PVHFb_bbYMzducj1o.png)\\\nFigure 3. Trainer-side metrics for the vLLM V0 reference (blue) and the initial vLLM V1 attempt (red). Clip rate tracks the rollout/trainer policy gap; entropy and reward show how that gap propagates into training.\n\n## Failure Modes\n\nWe separated the possible causes into three layers:\n\n1. **Semantic mismatch**: the backend returns logprobs with different meaning relative to what the trainer expects.\n2. **Inference-path mismatch**: the backend uses different runtime defaults for caching, scheduling, or request handling, so the same prompts follow a different execution path.\n3. **Objective mismatch**: the RL objective needs correction for the amount of staleness or backend mismatch that remains.\n\nWe initially suspected the third category too early. The useful diagnosis came from treating the first two as backend behavior problems and ruling them out first.\n\n## V1 Backend Fixes\n\n### Logprob Semantics\n\nThe first issue was semantic. vLLM V1 returns logprobs from the raw model outputs by default, before logits post-processing such as temperature scaling, penalties, and top-k/top-p filtering. PipelineRL expected logprobs from the processed distribution used by the sampler.\n\nThe required setting was:\n\n- `logprobs-mode=processed_logprobs`\n\nThis removed the obvious mean offset in rollout logprobs. The training curves still showed a gap relative to the known-good reference, so the next issue had to be in the inference path.\n\nThe policy-ratio plot shows this directly. Once `processed_logprobs` is on for V1, the mean policy ratio stays centered extremely close to `1.0` across all three runs. That establishes the mean-bias fix. The remaining mismatch shows up in clip rate, KL, entropy, and downstream training behavior.\n\n![](https://cdn-uploads.huggingface.co/production/uploads/61f750055596aa721ce68efe/K0PQyxwunQUipAXv9Iy1n.png)\\\nFigure 4. Per-step deviation of the rollout/trainer policy ratio from 1.0, scaled by 10,000, for the vLLM V0 reference (blue), the initial vLLM V1 run (red), and the corrected vLLM V1 run (green).\n\n### Runtime Defaults\n\nThe early V1 run mixed the engine version with V1 runtime defaults:\n\n- prefix caching, left unset in the early run so the vLLM `0.18.1` default applied\n- async scheduling, left unset in the early run so the vLLM `0.18.1` default applied\n- an ad-hoc `disable-cascade-attn` override that was set through launch-time kwarg passthrough and sits outside the parity recipe in committed config\n\nFor the parity run, we made these choices explicit:\n\n```yaml\nvllm_config:\n  use_v1: true\n  vllm_kwargs:\n    logprobs-mode: processed_logprobs\n    enable-prefix-caching: false\n    async-scheduling: false\n```\n\nPrefix caching deserves a separate note. It is normally a correctness-preserving inference optimization for a fixed model state. In this online RL setup, it was a V1-only difference in cache lifetime and reuse relative to the V0 reference path. The actor was also handling repeated prefixes, concurrent requests, async scheduling, and inflight weight updates.\n\nA prefix-cache hit can reuse state computed before a weight update when the cache policy ignores the weight-update boundary. Disabling prefix caching removed one V1-only degree of freedom from the parity comparison.\n\n### Inflight Weight Updates\n\nWeight synchronization also had to match the online-RL update model. One option was to make V1 stricter than V0 by draining requests and clearing caches at every update. That would answer a separate question. We first needed to verify that V1 could match the existing V0 behavior.\n\nWhat V0 effectively did was closer to:\n\n- block execution at an engine boundary\n- load the new weights\n- resume without an explicit cached-state invalidation\n\nThe nearest V1 analogue was:\n\n```python\nawait engine.pause_generation(mode=\"keep\", clear_cache=False)\nawait engine_client.collective_rpc_async(\n    \"receive_weight_update\",\n    args=(request.model_dump_json(),),\n)\nawait engine.resume_generation()\n```\n\nTwo details matter:\n\n- `mode=\"keep\"` matches the old inflight update model more closely than `wait` or `abort`\n- `clear_cache=False` matches the V0 wrapper behavior, which left cached state intact on update\n\nLag was a useful runtime diagnostic. The initial V1 path carries more persistent lag later in training than the corrected V1 run.\n\n![](https://cdn-uploads.huggingface.co/production/uploads/680ba1729f7688275d2ce0f4/XWXONkgJtznG09PVLFCAl.png)\\\nFigure 5. Number of steps the weights in the rollout server are behind the trainer policy, for the vLLM V0 reference (blue), the initial vLLM V1 run (red), and the corrected vLLM V1 run (green).\n\n## The Remaining Gap: fp32 lm\\_head\n\nThe V1 backend fixes above removed the obvious migration issues, but final parity still required matching the numerical path used to compute logits. The trainer used an fp32 `lm_head` for the final projection. The rollout backend had to match that behavior.\n\nA closely related issue appears in the [MiniMax-M1 technical report](https://arxiv.org/abs/2506.13585): their RL run showed a training/inference token-probability mismatch that they traced to the LM output head and fixed by computing the head in fp32.\n\nThis matters because the RL update consumes token logprobs directly. Small changes in logits can become visible in policy ratios, KL, and clipping. The final projection precision is therefore part of the correctness surface for online RL. The [ScaleRL paper](https://arxiv.org/abs/2510.13786) later includes fp32 logits/head computation as part of its RL recipe and ablates it as a useful design choice for large-scale RL.\n\nWith the fp32 `lm_head` path included, reward gives a compact view of the final parity result. In Figure 6, the final V1 run tracks the V0 reference; the initial V1 attempt produces a clearly different reward curve.\n\n![](https://cdn-uploads.huggingface.co/production/uploads/680ba1729f7688275d2ce0f4/oCuXYiTLSl6-jPJLBzWoU.png)\\\nFigure 6. Reward for the vLLM V0 reference (blue), the initial vLLM V1 attempt (red), and the final vLLM V1 run with the fp32 `lm_head` path (green). With the fp32 head included, the final V1 run tracks the V0 reference.\n\n## Ablations\n\nThe negative results are important because they rule out common explanations.\n\n- **`processed_logprobs` alone**: fixed the semantic logprob bug; the training mismatch remained.\n- **Batch invariance**: the mismatch remained in a separate test, with higher lag, higher clip rate, and NCCL complications.\n- **Treating the first V1 run as a fair baseline**: the first V1 run had multiple V1-only defaults enabled, so it was a confounded migration comparison.\n\n## Why We Fixed Backend Correctness First\n\nObjective-side corrections such as truncated importance sampling, importance-ratio reweighting, and related methods are useful tools. If rollouts are intentionally stale, generated asynchronously, or produced by a backend where equivalence to the trainer-side policy is unavailable, then some form of correction is often the right thing to add.\n\nThe first problem here was inference correctness. After moving to V1, the rollout backend returned logprobs and runtime behavior that broke the trainer assumption. Adding an objective-side correction at that point would have mixed two questions:\n\n- is the inference backend producing the right logprobs?\n- given correct logprobs, does the objective still need an off-policy or async correction?\n\nThose questions need to be separated. Otherwise an objective-side correction can compensate for broken inference-backend behavior, which makes the training curve harder to interpret.\n\nThe current objective can still improve. After inference parity is restored, the next improvement is the usual async/off-policy cleanup:\n\n- keep explicit behavior-policy logprobs from rollout time\n- recompute trainer-side old-policy logprobs at optimization time\n- separate backend mismatch correction from the policy-update ratio\n- track diagnostics like ESS for the correction term alongside aggregate trainer metrics\n\nThe main lesson from this migration is narrower: fix backend correctness first, then add corrections for the mismatch that remains.\n",
  "authors": [],
  "captured_at": "2026-10-19T01:26:53.998049939+00:00",
  "category": "ai",
  "content_html": "<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/blog/ServiceNow-AI/github.com/ServiceNow/PipelineRL/\">PipelineRL</a> uses vLLM as the inference engine for rollout generation. The inference engine samples tokens and returns token logprobs; the trainer uses those logprobs to compute policy ratios, KL, clip rate, entropy, and reward. Any discrepancy in how those logprobs are computed can change the training dynamics. This is the train-inference mismatch we needed to eliminate during the vLLM V0 to V1 migration.</p>\n<p><strong>TL;DR.</strong> vLLM V1 matched our vLLM V0 reference after we fixed four things: processed rollout logprobs, V1-specific runtime defaults, the inflight weight-update path, and the fp32 <code>lm_head</code> used for the final projection. We fixed the backend behavior before changing the RL objective.</p>\n<p>The reference run used vLLM <code>0.8.5</code>; the V1 runs used vLLM <code>0.18.1</code>. Figure 1 shows the final result. The red run is the initial V1 attempt, and the green run is the final V1 run after the fixes described below.</p>\n<figure class=\"article-figure\"><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/61f750055596aa721ce68efe/2Q2pkNtiYPrlLVLoH51dO.png\" alt=\"\" loading=\"eager\" decoding=\"async\" fetchpriority=\"high\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture><figcaption>Figure 1. Trainer-side metrics for the vLLM V0 reference (blue), the initial vLLM V1 attempt (red), and the final vLLM V1 run after our fixes (green), including the fp32 <code>lm_head</code>. The final V1 run returns close to the V0 trajectory across clip rate, KL, entropy, and reward.</figcaption></figure>\n<h2>Migration Objective</h2>\n<p>vLLM V1 is a substantial rewrite of the V0 engine. Our migration target was therefore deliberately narrow:</p>\n<ol>\n<li>verify that V1 returned rollout logprobs in the form the trainer expected</li>\n<li>rerun the same workload against the V0 reference</li>\n<li>evaluate objective-level changes only after backend parity was restored</li>\n</ol>\n<p>The first visible symptoms appeared in:</p>\n<ul>\n<li><code>clamp_log_ratio_new_old_indicator</code></li>\n<li><code>kl_new_old</code></li>\n<li><code>entropy</code></li>\n<li><code>reward</code></li>\n</ul>\n<p>Those metrics came from a GSPO training run, the objective used for this experiment. The same class of mismatch can surface in PPO, GRPO, or any online RL system that treats rollout-side logprobs as part of the optimization target.</p>\n<p>The initial V1 run showed the problem clearly. The trainer-side logprobs and reward moved away from the V0 reference early in training.</p>\n<figure class=\"article-figure\"><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/61f750055596aa721ce68efe/YhRfKIEKndf4LGG9g7Ck9.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture><figcaption>Figure 2. Current-policy logprobs computed by the trainer during updates (left) and reward (right). The initial vLLM V1 run (red) separates from the vLLM V0 reference (blue).</figcaption></figure>\n<p>The same pattern appears in the trainer metrics. Clip rate is the easiest signal to read in the initial comparison.</p>\n<figure class=\"article-figure\"><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/61f750055596aa721ce68efe/6fQ4PVHFb_bbYMzducj1o.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture><figcaption>Figure 3. Trainer-side metrics for the vLLM V0 reference (blue) and the initial vLLM V1 attempt (red). Clip rate tracks the rollout/trainer policy gap; entropy and reward show how that gap propagates into training.</figcaption></figure>\n<h2>Failure Modes</h2>\n<p>We separated the possible causes into three layers:</p>\n<ol>\n<li><strong>Semantic mismatch</strong>: the backend returns logprobs with different meaning relative to what the trainer expects.</li>\n<li><strong>Inference-path mismatch</strong>: the backend uses different runtime defaults for caching, scheduling, or request handling, so the same prompts follow a different execution path.</li>\n<li><strong>Objective mismatch</strong>: the RL objective needs correction for the amount of staleness or backend mismatch that remains.</li>\n</ol>\n<p>We initially suspected the third category too early. The useful diagnosis came from treating the first two as backend behavior problems and ruling them out first.</p>\n<h2>V1 Backend Fixes</h2>\n<h3>Logprob Semantics</h3>\n<p>The first issue was semantic. vLLM V1 returns logprobs from the raw model outputs by default, before logits post-processing such as temperature scaling, penalties, and top-k/top-p filtering. PipelineRL expected logprobs from the processed distribution used by the sampler.</p>\n<p>The required setting was:</p>\n<ul>\n<li><code>logprobs-mode=processed_logprobs</code></li>\n</ul>\n<p>This removed the obvious mean offset in rollout logprobs. The training curves still showed a gap relative to the known-good reference, so the next issue had to be in the inference path.</p>\n<p>The policy-ratio plot shows this directly. Once <code>processed_logprobs</code> is on for V1, the mean policy ratio stays centered extremely close to <code>1.0</code> across all three runs. That establishes the mean-bias fix. The remaining mismatch shows up in clip rate, KL, entropy, and downstream training behavior.</p>\n<figure class=\"article-figure\"><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/61f750055596aa721ce68efe/K0PQyxwunQUipAXv9Iy1n.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture><figcaption>Figure 4. Per-step deviation of the rollout/trainer policy ratio from 1.0, scaled by 10,000, for the vLLM V0 reference (blue), the initial vLLM V1 run (red), and the corrected vLLM V1 run (green).</figcaption></figure>\n<h3>Runtime Defaults</h3>\n<p>The early V1 run mixed the engine version with V1 runtime defaults:</p>\n<ul>\n<li>prefix caching, left unset in the early run so the vLLM <code>0.18.1</code> default applied</li>\n<li>async scheduling, left unset in the early run so the vLLM <code>0.18.1</code> default applied</li>\n<li>an ad-hoc <code>disable-cascade-attn</code> override that was set through launch-time kwarg passthrough and sits outside the parity recipe in committed config</li>\n</ul>\n<p>For the parity run, we made these choices explicit:</p>\n<pre><code class=\"language-yaml\"><span class=\"code-snippet\" data-language=\"YAML\"><span class=\"syntax-source syntax-yaml\"><span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">vllm_config</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">use_v1</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-constant syntax-language syntax-boolean syntax-yaml\">true</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">vllm_kwargs</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n    <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">logprobs-mode</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">processed_logprobs</span>\n    <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">enable-prefix-caching</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-constant syntax-language syntax-boolean syntax-yaml\">false</span>\n    <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">async-scheduling</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-constant syntax-language syntax-boolean syntax-yaml\">false</span>\n</span></span></code></pre>\n<p>Prefix caching deserves a separate note. It is normally a correctness-preserving inference optimization for a fixed model state. In this online RL setup, it was a V1-only difference in cache lifetime and reuse relative to the V0 reference path. The actor was also handling repeated prefixes, concurrent requests, async scheduling, and inflight weight updates.</p>\n<p>A prefix-cache hit can reuse state computed before a weight update when the cache policy ignores the weight-update boundary. Disabling prefix caching removed one V1-only degree of freedom from the parity comparison.</p>\n<h3>Inflight Weight Updates</h3>\n<p>Weight synchronization also had to match the online-RL update model. One option was to make V1 stricter than V0 by draining requests and clearing caches at every update. That would answer a separate question. We first needed to verify that V1 could match the existing V0 behavior.</p>\n<p>What V0 effectively did was closer to:</p>\n<ul>\n<li>block execution at an engine boundary</li>\n<li>load the new weights</li>\n<li>resume without an explicit cached-state invalidation</li>\n</ul>\n<p>The nearest V1 analogue was:</p>\n<pre><code class=\"language-python\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-keyword syntax-other syntax-await syntax-python\">await</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">engine</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">pause_generation</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-variable syntax-parameter syntax-python\">mode</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">keep<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-variable syntax-parameter syntax-python\">clear_cache</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-language syntax-python\">False</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n<span class=\"syntax-keyword syntax-other syntax-await syntax-python\">await</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">engine_client</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">collective_rpc_async</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\">\n    <span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">receive_weight_update<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>\n    <span class=\"syntax-variable syntax-parameter syntax-python\">args</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-group syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">request</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">model_dump_json</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span><span class=\"syntax-punctuation syntax-separator syntax-tuple syntax-python\">,</span><span class=\"syntax-punctuation syntax-section syntax-group syntax-end syntax-python\">)</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>\n</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n<span class=\"syntax-keyword syntax-other syntax-await syntax-python\">await</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">engine</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">resume_generation</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n</span></span></code></pre>\n<p>Two details matter:</p>\n<ul>\n<li><code>mode=&quot;keep&quot;</code> matches the old inflight update model more closely than <code>wait</code> or <code>abort</code></li>\n<li><code>clear_cache=False</code> matches the V0 wrapper behavior, which left cached state intact on update</li>\n</ul>\n<p>Lag was a useful runtime diagnostic. The initial V1 path carries more persistent lag later in training than the corrected V1 run.</p>\n<figure class=\"article-figure\"><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/680ba1729f7688275d2ce0f4/XWXONkgJtznG09PVLFCAl.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture><figcaption>Figure 5. Number of steps the weights in the rollout server are behind the trainer policy, for the vLLM V0 reference (blue), the initial vLLM V1 run (red), and the corrected vLLM V1 run (green).</figcaption></figure>\n<h2>The Remaining Gap: fp32 lm_head</h2>\n<p>The V1 backend fixes above removed the obvious migration issues, but final parity still required matching the numerical path used to compute logits. The trainer used an fp32 <code>lm_head</code> for the final projection. The rollout backend had to match that behavior.</p>\n<p>A closely related issue appears in the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2506.13585\">MiniMax-M1 technical report</a>: their RL run showed a training/inference token-probability mismatch that they traced to the LM output head and fixed by computing the head in fp32.</p>\n<p>This matters because the RL update consumes token logprobs directly. Small changes in logits can become visible in policy ratios, KL, and clipping. The final projection precision is therefore part of the correctness surface for online RL. The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://arxiv.org/abs/2510.13786\">ScaleRL paper</a> later includes fp32 logits/head computation as part of its RL recipe and ablates it as a useful design choice for large-scale RL.</p>\n<p>With the fp32 <code>lm_head</code> path included, reward gives a compact view of the final parity result. In Figure 6, the final V1 run tracks the V0 reference; the initial V1 attempt produces a clearly different reward curve.</p>\n<figure class=\"article-figure\"><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://cdn-uploads.huggingface.co/production/uploads/680ba1729f7688275d2ce0f4/oCuXYiTLSl6-jPJLBzWoU.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture><figcaption>Figure 6. Reward for the vLLM V0 reference (blue), the initial vLLM V1 attempt (red), and the final vLLM V1 run with the fp32 <code>lm_head</code> path (green). With the fp32 head included, the final V1 run tracks the V0 reference.</figcaption></figure>\n<h2>Ablations</h2>\n<p>The negative results are important because they rule out common explanations.</p>\n<ul>\n<li><strong><code>processed_logprobs</code> alone</strong>: fixed the semantic logprob bug; the training mismatch remained.</li>\n<li><strong>Batch invariance</strong>: the mismatch remained in a separate test, with higher lag, higher clip rate, and NCCL complications.</li>\n<li><strong>Treating the first V1 run as a fair baseline</strong>: the first V1 run had multiple V1-only defaults enabled, so it was a confounded migration comparison.</li>\n</ul>\n<h2>Why We Fixed Backend Correctness First</h2>\n<p>Objective-side corrections such as truncated importance sampling, importance-ratio reweighting, and related methods are useful tools. If rollouts are intentionally stale, generated asynchronously, or produced by a backend where equivalence to the trainer-side policy is unavailable, then some form of correction is often the right thing to add.</p>\n<p>The first problem here was inference correctness. After moving to V1, the rollout backend returned logprobs and runtime behavior that broke the trainer assumption. Adding an objective-side correction at that point would have mixed two questions:</p>\n<ul>\n<li>is the inference backend producing the right logprobs?</li>\n<li>given correct logprobs, does the objective still need an off-policy or async correction?</li>\n</ul>\n<p>Those questions need to be separated. Otherwise an objective-side correction can compensate for broken inference-backend behavior, which makes the training curve harder to interpret.</p>\n<p>The current objective can still improve. After inference parity is restored, the next improvement is the usual async/off-policy cleanup:</p>\n<ul>\n<li>keep explicit behavior-policy logprobs from rollout time</li>\n<li>recompute trainer-side old-policy logprobs at optimization time</li>\n<li>separate backend mismatch correction from the policy-update ratio</li>\n<li>track diagnostics like ESS for the correction term alongside aggregate trainer metrics</li>\n</ul>\n<p>The main lesson from this migration is narrower: fix backend correctness first, then add corrections for the mismatch that remains.</p>\n",
  "content_markdown": "[PipelineRL](https://huggingface.co/blog/ServiceNow-AI/github.com/ServiceNow/PipelineRL/) uses vLLM as the inference engine for rollout generation. The inference engine samples tokens and returns token logprobs; the trainer uses those logprobs to compute policy ratios, KL, clip rate, entropy, and reward. Any discrepancy in how those logprobs are computed can change the training dynamics. This is the train-inference mismatch we needed to eliminate during the vLLM V0 to V1 migration.\n\n**TL;DR.** vLLM V1 matched our vLLM V0 reference after we fixed four things: processed rollout logprobs, V1-specific runtime defaults, the inflight weight-update path, and the fp32 `lm_head` used for the final projection. We fixed the backend behavior before changing the RL objective.\n\nThe reference run used vLLM `0.8.5`; the V1 runs used vLLM `0.18.1`. Figure 1 shows the final result. The red run is the initial V1 attempt, and the green run is the final V1 run after the fixes described below.\n\n![](https://cdn-uploads.huggingface.co/production/uploads/61f750055596aa721ce68efe/2Q2pkNtiYPrlLVLoH51dO.png)\\\nFigure 1. Trainer-side metrics for the vLLM V0 reference (blue), the initial vLLM V1 attempt (red), and the final vLLM V1 run after our fixes (green), including the fp32 `lm_head`. The final V1 run returns close to the V0 trajectory across clip rate, KL, entropy, and reward.\n\n## Migration Objective\n\nvLLM V1 is a substantial rewrite of the V0 engine. Our migration target was therefore deliberately narrow:\n\n1. verify that V1 returned rollout logprobs in the form the trainer expected\n2. rerun the same workload against the V0 reference\n3. evaluate objective-level changes only after backend parity was restored\n\nThe first visible symptoms appeared in:\n\n- `clamp_log_ratio_new_old_indicator`\n- `kl_new_old`\n- `entropy`\n- `reward`\n\nThose metrics came from a GSPO training run, the objective used for this experiment. The same class of mismatch can surface in PPO, GRPO, or any online RL system that treats rollout-side logprobs as part of the optimization target.\n\nThe initial V1 run showed the problem clearly. The trainer-side logprobs and reward moved away from the V0 reference early in training.\n\n![](https://cdn-uploads.huggingface.co/production/uploads/61f750055596aa721ce68efe/YhRfKIEKndf4LGG9g7Ck9.png)\\\nFigure 2. Current-policy logprobs computed by the trainer during updates (left) and reward (right). The initial vLLM V1 run (red) separates from the vLLM V0 reference (blue).\n\nThe same pattern appears in the trainer metrics. Clip rate is the easiest signal to read in the initial comparison.\n\n![](https://cdn-uploads.huggingface.co/production/uploads/61f750055596aa721ce68efe/6fQ4PVHFb_bbYMzducj1o.png)\\\nFigure 3. Trainer-side metrics for the vLLM V0 reference (blue) and the initial vLLM V1 attempt (red). Clip rate tracks the rollout/trainer policy gap; entropy and reward show how that gap propagates into training.\n\n## Failure Modes\n\nWe separated the possible causes into three layers:\n\n1. **Semantic mismatch**: the backend returns logprobs with different meaning relative to what the trainer expects.\n2. **Inference-path mismatch**: the backend uses different runtime defaults for caching, scheduling, or request handling, so the same prompts follow a different execution path.\n3. **Objective mismatch**: the RL objective needs correction for the amount of staleness or backend mismatch that remains.\n\nWe initially suspected the third category too early. The useful diagnosis came from treating the first two as backend behavior problems and ruling them out first.\n\n## V1 Backend Fixes\n\n### Logprob Semantics\n\nThe first issue was semantic. vLLM V1 returns logprobs from the raw model outputs by default, before logits post-processing such as temperature scaling, penalties, and top-k/top-p filtering. PipelineRL expected logprobs from the processed distribution used by the sampler.\n\nThe required setting was:\n\n- `logprobs-mode=processed_logprobs`\n\nThis removed the obvious mean offset in rollout logprobs. The training curves still showed a gap relative to the known-good reference, so the next issue had to be in the inference path.\n\nThe policy-ratio plot shows this directly. Once `processed_logprobs` is on for V1, the mean policy ratio stays centered extremely close to `1.0` across all three runs. That establishes the mean-bias fix. The remaining mismatch shows up in clip rate, KL, entropy, and downstream training behavior.\n\n![](https://cdn-uploads.huggingface.co/production/uploads/61f750055596aa721ce68efe/K0PQyxwunQUipAXv9Iy1n.png)\\\nFigure 4. Per-step deviation of the rollout/trainer policy ratio from 1.0, scaled by 10,000, for the vLLM V0 reference (blue), the initial vLLM V1 run (red), and the corrected vLLM V1 run (green).\n\n### Runtime Defaults\n\nThe early V1 run mixed the engine version with V1 runtime defaults:\n\n- prefix caching, left unset in the early run so the vLLM `0.18.1` default applied\n- async scheduling, left unset in the early run so the vLLM `0.18.1` default applied\n- an ad-hoc `disable-cascade-attn` override that was set through launch-time kwarg passthrough and sits outside the parity recipe in committed config\n\nFor the parity run, we made these choices explicit:\n\n```yaml\nvllm_config:\n  use_v1: true\n  vllm_kwargs:\n    logprobs-mode: processed_logprobs\n    enable-prefix-caching: false\n    async-scheduling: false\n```\n\nPrefix caching deserves a separate note. It is normally a correctness-preserving inference optimization for a fixed model state. In this online RL setup, it was a V1-only difference in cache lifetime and reuse relative to the V0 reference path. The actor was also handling repeated prefixes, concurrent requests, async scheduling, and inflight weight updates.\n\nA prefix-cache hit can reuse state computed before a weight update when the cache policy ignores the weight-update boundary. Disabling prefix caching removed one V1-only degree of freedom from the parity comparison.\n\n### Inflight Weight Updates\n\nWeight synchronization also had to match the online-RL update model. One option was to make V1 stricter than V0 by draining requests and clearing caches at every update. That would answer a separate question. We first needed to verify that V1 could match the existing V0 behavior.\n\nWhat V0 effectively did was closer to:\n\n- block execution at an engine boundary\n- load the new weights\n- resume without an explicit cached-state invalidation\n\nThe nearest V1 analogue was:\n\n```python\nawait engine.pause_generation(mode=\"keep\", clear_cache=False)\nawait engine_client.collective_rpc_async(\n    \"receive_weight_update\",\n    args=(request.model_dump_json(),),\n)\nawait engine.resume_generation()\n```\n\nTwo details matter:\n\n- `mode=\"keep\"` matches the old inflight update model more closely than `wait` or `abort`\n- `clear_cache=False` matches the V0 wrapper behavior, which left cached state intact on update\n\nLag was a useful runtime diagnostic. The initial V1 path carries more persistent lag later in training than the corrected V1 run.\n\n![](https://cdn-uploads.huggingface.co/production/uploads/680ba1729f7688275d2ce0f4/XWXONkgJtznG09PVLFCAl.png)\\\nFigure 5. Number of steps the weights in the rollout server are behind the trainer policy, for the vLLM V0 reference (blue), the initial vLLM V1 run (red), and the corrected vLLM V1 run (green).\n\n## The Remaining Gap: fp32 lm\\_head\n\nThe V1 backend fixes above removed the obvious migration issues, but final parity still required matching the numerical path used to compute logits. The trainer used an fp32 `lm_head` for the final projection. The rollout backend had to match that behavior.\n\nA closely related issue appears in the [MiniMax-M1 technical report](https://arxiv.org/abs/2506.13585): their RL run showed a training/inference token-probability mismatch that they traced to the LM output head and fixed by computing the head in fp32.\n\nThis matters because the RL update consumes token logprobs directly. Small changes in logits can become visible in policy ratios, KL, and clipping. The final projection precision is therefore part of the correctness surface for online RL. The [ScaleRL paper](https://arxiv.org/abs/2510.13786) later includes fp32 logits/head computation as part of its RL recipe and ablates it as a useful design choice for large-scale RL.\n\nWith the fp32 `lm_head` path included, reward gives a compact view of the final parity result. In Figure 6, the final V1 run tracks the V0 reference; the initial V1 attempt produces a clearly different reward curve.\n\n![](https://cdn-uploads.huggingface.co/production/uploads/680ba1729f7688275d2ce0f4/oCuXYiTLSl6-jPJLBzWoU.png)\\\nFigure 6. Reward for the vLLM V0 reference (blue), the initial vLLM V1 attempt (red), and the final vLLM V1 run with the fp32 `lm_head` path (green). With the fp32 head included, the final V1 run tracks the V0 reference.\n\n## Ablations\n\nThe negative results are important because they rule out common explanations.\n\n- **`processed_logprobs` alone**: fixed the semantic logprob bug; the training mismatch remained.\n- **Batch invariance**: the mismatch remained in a separate test, with higher lag, higher clip rate, and NCCL complications.\n- **Treating the first V1 run as a fair baseline**: the first V1 run had multiple V1-only defaults enabled, so it was a confounded migration comparison.\n\n## Why We Fixed Backend Correctness First\n\nObjective-side corrections such as truncated importance sampling, importance-ratio reweighting, and related methods are useful tools. If rollouts are intentionally stale, generated asynchronously, or produced by a backend where equivalence to the trainer-side policy is unavailable, then some form of correction is often the right thing to add.\n\nThe first problem here was inference correctness. After moving to V1, the rollout backend returned logprobs and runtime behavior that broke the trainer assumption. Adding an objective-side correction at that point would have mixed two questions:\n\n- is the inference backend producing the right logprobs?\n- given correct logprobs, does the objective still need an off-policy or async correction?\n\nThose questions need to be separated. Otherwise an objective-side correction can compensate for broken inference-backend behavior, which makes the training curve harder to interpret.\n\nThe current objective can still improve. After inference parity is restored, the next improvement is the usual async/off-policy cleanup:\n\n- keep explicit behavior-policy logprobs from rollout time\n- recompute trainer-side old-policy logprobs at optimization time\n- separate backend mismatch correction from the policy-update ratio\n- track diagnostics like ESS for the correction term alongside aggregate trainer metrics\n\nThe main lesson from this migration is narrower: fix backend correctness first, then add corrections for the mismatch that remains.\n",
  "dateCreated": "2026-10-19T01:26:53.998049939+00:00",
  "date_created": "2026-05-06T19:06:55+00:00",
  "date_modified": "2026-05-06T19:06:55+00:00",
  "date_published": "2026-05-06T19:06:55+00:00",
  "description": "PipelineRL uses vLLM as the inference engine for rollout generation. The inference engine samples tokens and returns token logprobs; the trainer uses those logprobs to compute policy ratios, KL, clip rate, entropy, and reward. Any…",
  "external_url": "https://huggingface.co/blog/ServiceNow-AI/correctness-before-corrections",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/huggingface-co/2026/05/2026-05-06-vllm-v0-to-v1-correctness-before-corrections-in-rl.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/huggingface-co/2026/05/2026-05-06-vllm-v0-to-v1-correctness-before-corrections-in-rl.md"
  },
  "id": "urn:aggr:item:26c8daa909bffb463d8397a5d53ac347e5788fb8",
  "isBasedOn": {
    "@id": "https://huggingface.co/blog/ServiceNow-AI/correctness-before-corrections"
  },
  "mainEntity": {
    "@id": "https://huggingface.co/blog/ServiceNow-AI/correctness-before-corrections",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-05-06-vllm-v0-to-v1-correctness-before-corrections-in-rl/#webpage"
    },
    "dateModified": "2026-05-06T19:06:55+00:00",
    "datePublished": "2026-05-06T19:06:55+00:00",
    "headline": "vLLM V0 to V1: Correctness Before Corrections in RL",
    "image": {
      "@type": "ImageObject",
      "caption": null,
      "height": 90,
      "url": "https://cdn-thumbnails.huggingface.co/social-thumbnails/blog/ServiceNow-AI/correctness-before-corrections.png",
      "width": 160
    },
    "timeRequired": "PT7M",
    "url": "https://huggingface.co/blog/ServiceNow-AI/correctness-before-corrections",
    "wordCount": 1536
  },
  "name": "Archived snapshot: vLLM V0 to V1: Correctness Before Corrections in RL",
  "preview": {
    "alt": null,
    "color": null,
    "height": 90,
    "placeholder": {
      "data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
      "hash": ""
    },
    "url": "https://cdn-thumbnails.huggingface.co/social-thumbnails/blog/ServiceNow-AI/correctness-before-corrections.png",
    "width": 160
  },
  "reading_minutes": 7,
  "replicated_at": null,
  "source": "huggingface-co",
  "source_name": "Hugging Face - Blog",
  "summary": null,
  "tags": [],
  "temporalCoverage": "2026-10-19T01:26:53.998049939+00:00",
  "title": "vLLM V0 to V1: Correctness Before Corrections in RL",
  "url": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-05-06-vllm-v0-to-v1-correctness-before-corrections-in-rl/",
  "word_count": 1536
}