Download code/extras/vllm_rollout/rollout_worker.py from viai957/align: direct link, hf CLI and curl.
- Browser
- Download file 10.1 kB
-
https://huggingface.co/viai957/align/resolve/main/code/extras/vllm_rollout/rollout_worker.py
- Command line
-
hf download hf://viai957/align/code/extras/vllm_rollout/rollout_worker.py
-
curl -L -o rollout_worker.py https://huggingface.co/viai957/align/resolve/main/code/extras/vllm_rollout/rollout_worker.py
10.1 kB
| """vLLM rollout worker: serves align/rollout/protocol.py requests from a spool dir. | |
| OPTIONAL EXTRA -- NOT part of the `align` package. The default GRPO rollout is | |
| the dependency-free native sampler (align/rollout/native.py); this worker is | |
| the high-throughput alternative, selected with `backend='spool'`. | |
| RUNS UNDER THE ISOLATED ROLLOUT VENV (.venv-rollout: vllm 0.26.0 / cu130), | |
| never the training venv -- the 2026-08-22 sovereignty amendment confines | |
| vLLM behind this process boundary. See extras/vllm_rollout/README.md. Launch it via | |
| `align.rollout.client.spawn_worker`, which sets the REQUIRED environment: | |
| VLLM_USE_FLASHINFER_SAMPLER=0 (flashinfer's JIT ninja build fails on | |
| this box's toolchain -- measured | |
| 2026-08-23, rollout pilot) | |
| TMPDIR / XDG_CACHE_HOME under /apps (the box's no-swap root is 13GB) | |
| CUDA_VISIBLE_DEVICES=<the one dedicated engine GPU> | |
| Weight sync = filesystem: when a request's `weights_dir` differs from the | |
| engine's current one, the engine is torn down and rebuilt from the new | |
| checkpoint (~60s measured). The trainer amortizes that by syncing every | |
| `sync_every` optimizer steps; the rho correction owns the resulting | |
| off-policyness. | |
| Failure contract: any per-request exception is written back as a RESULT | |
| with `error` set (traceback tail in extras) so the trainer fails fast at | |
| collect() instead of sitting at a timeout; the worker keeps serving. | |
| """ | |
| import argparse | |
| import os | |
| import sys | |
| import time | |
| import traceback | |
| def _die(msg: str) -> None: | |
| print(f"rollout_worker refusing to start: {msg}", file=sys.stderr, flush=True) | |
| sys.exit(2) | |
| # ENV hygiene BEFORE importing anything heavy: this venv has no align guard, | |
| # and a cache or TMPDIR off /apps is the exact class of fault that wedged | |
| # this box on 2026-08-12 (root is 13GB, /tmp is a noexec tmpfs). | |
| for _var in ("TMPDIR", "XDG_CACHE_HOME"): | |
| _val = os.environ.get(_var, "") | |
| if not _val.startswith("/apps"): | |
| _die(f"{_var}={_val!r} does not resolve under /apps; set it before launch") | |
| # <repo>/extras/vllm_rollout/rollout_worker.py -> <repo>: three levels up. | |
| _REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| sys.path.insert(0, _REPO_ROOT) | |
| # align.rollout.protocol is stdlib-only BY CONTRACT (see its docstring); it is | |
| # the single align import permitted here, taken from the repo tree directly so | |
| # align never needs installing into the rollout venv. (`import align` and | |
| # `import align.rollout` are both lazy/stdlib-only for exactly this reason.) | |
| from align.rollout.protocol import ( # noqa: E402 | |
| RolloutResult, | |
| read_request, | |
| spool_dirs, | |
| touch_heartbeat, | |
| write_result, | |
| ) | |
| def _extract_sampled_logprobs(token_ids, sample_logprobs, request_id): | |
| """Per-position logprob of the SAMPLED token. | |
| vllm 0.26 API shape (verified against .venv-rollout's vllm/outputs.py and | |
| vllm/logprobs.py): CompletionOutput.logprobs is SampleLogprobs -- a | |
| sequence over positions where each element is dict[int, Logprob] (or the | |
| list-compatible FlatLogprobs view of the same); with | |
| SamplingParams(logprobs=0) each position carries the sampled token's own | |
| entry, so position t resolves as logprobs[t][token_ids[t]].logprob. | |
| """ | |
| if sample_logprobs is None: | |
| raise RuntimeError( | |
| f"request {request_id!r}: engine returned no logprobs despite " | |
| "SamplingParams(logprobs=0); the rho correction cannot run without " | |
| "pi^infer_old, so this result would be unusable." | |
| ) | |
| out = [] | |
| for t, tok in enumerate(token_ids): | |
| pos = sample_logprobs[t] | |
| if pos is None or tok not in pos: | |
| raise RuntimeError( | |
| f"request {request_id!r}: position {t} lacks the sampled token " | |
| f"{tok} in its logprob dict (keys: " | |
| f"{sorted(pos) if pos else pos}); refusing to substitute." | |
| ) | |
| out.append(float(pos[tok].logprob)) | |
| return out | |
| def serve(args) -> int: | |
| # Heartbeat BEFORE the vllm import: the import alone costs ~30s, and the | |
| # first smoke run proved a trainer canary can race it -- the client's | |
| # startup grace plus this early beat make "booting" visibly distinct from | |
| # "never started". | |
| spool_dirs(args.spool) | |
| touch_heartbeat(args.spool) | |
| from vllm import LLM, SamplingParams | |
| req_dir, _ = spool_dirs(args.spool) | |
| engine = None | |
| loaded_weights_dir = None | |
| last_work = time.time() | |
| print(f"rollout_worker serving spool={args.spool} tp={args.tp}", flush=True) | |
| while True: | |
| touch_heartbeat(args.spool) | |
| pending = sorted( | |
| (os.path.join(req_dir, f) for f in os.listdir(req_dir) if f.endswith(".json")), | |
| key=os.path.getmtime, | |
| ) | |
| if not pending: | |
| if args.idle_exit_s and time.time() - last_work > args.idle_exit_s: | |
| print(f"idle {args.idle_exit_s}s; exiting", flush=True) | |
| return 0 | |
| time.sleep(args.poll_s) | |
| continue | |
| for path in pending: | |
| request_id = os.path.basename(path)[: -len(".json")] | |
| reloaded = False | |
| try: | |
| req = read_request(path) | |
| if engine is None or req.weights_dir != loaded_weights_dir: | |
| if engine is not None: | |
| # vllm's own teardown first, then drop the reference; | |
| # the fresh constructor re-measures free VRAM. | |
| del engine | |
| engine = None | |
| import gc | |
| gc.collect() | |
| import torch | |
| torch.cuda.empty_cache() | |
| t0 = time.perf_counter() | |
| engine = LLM( | |
| model=req.weights_dir, | |
| dtype="bfloat16", | |
| tensor_parallel_size=args.tp, | |
| gpu_memory_utilization=args.gpu_mem_util, | |
| ) | |
| loaded_weights_dir = req.weights_dir | |
| reloaded = True | |
| print( | |
| f"engine loaded from {req.weights_dir} in {time.perf_counter() - t0:.1f}s", | |
| flush=True, | |
| ) | |
| sp = SamplingParams( | |
| n=req.n_samples_per_prompt, | |
| temperature=req.temperature, | |
| top_p=req.top_p, | |
| max_tokens=req.max_tokens, | |
| seed=req.seed, | |
| logprobs=0, # sampled token's own logprob per position | |
| ) | |
| t1 = time.perf_counter() | |
| outs = engine.generate(req.prompts, sp) | |
| gen_s = time.perf_counter() - t1 | |
| completions, logprobs, texts, prompt_token_ids = [], [], [], [] | |
| for o in outs: | |
| completions.append([list(map(int, c.token_ids)) for c in o.outputs]) | |
| logprobs.append( | |
| [ | |
| _extract_sampled_logprobs(c.token_ids, c.logprobs, request_id) | |
| for c in o.outputs | |
| ] | |
| ) | |
| texts.append([c.text for c in o.outputs]) | |
| # THE ENGINE'S OWN prompt tokenization, one list per prompt | |
| # in request order. Completion ids continue from exactly | |
| # this sequence; trainer-side assembly must concatenate | |
| # these + completion ids and NEVER re-tokenize the prompt | |
| # text (re-tokenization is not identity -- the alignment | |
| # defect class the 2026-08-23 audit closed). | |
| prompt_token_ids.append(list(map(int, o.prompt_token_ids))) | |
| write_result( | |
| args.spool, | |
| RolloutResult( | |
| request_id=request_id, | |
| completions=completions, | |
| logprobs=logprobs, | |
| texts=texts, | |
| weights_dir=req.weights_dir, | |
| engine_reloaded=reloaded, | |
| generate_seconds=gen_s, | |
| extras={"prompt_token_ids": prompt_token_ids}, | |
| ), | |
| ) | |
| print( | |
| f"served {request_id}: {sum(len(c) for c in completions)} " | |
| f"completions in {gen_s:.1f}s (reloaded={reloaded})", | |
| flush=True, | |
| ) | |
| except Exception as e: # noqa: BLE001 -- the trainer must fail fast, not time out | |
| tb = traceback.format_exc() | |
| print(f"request {request_id} FAILED: {e}\n{tb}", file=sys.stderr, flush=True) | |
| write_result( | |
| args.spool, | |
| RolloutResult( | |
| request_id=request_id, | |
| completions=[], | |
| logprobs=[], | |
| texts=[], | |
| weights_dir=loaded_weights_dir or "", | |
| engine_reloaded=reloaded, | |
| generate_seconds=0.0, | |
| error=str(e), | |
| extras={"traceback_tail": tb[-2000:]}, | |
| ), | |
| ) | |
| finally: | |
| # Consumed = removed; the result file is the record. | |
| if os.path.exists(path): | |
| os.remove(path) | |
| last_work = time.time() | |
| def main() -> int: | |
| p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) | |
| p.add_argument("--spool", required=True) | |
| p.add_argument("--gpu-mem-util", type=float, default=0.85) | |
| p.add_argument("--poll-s", type=float, default=2.0) | |
| p.add_argument("--tp", type=int, default=1) | |
| p.add_argument("--idle-exit-s", type=float, default=0.0, help="0 = serve forever") | |
| return serve(p.parse_args()) | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |