Skip to content

build(vllm): upgrade to 0.25.1 and make CSCS images reproducible - #12

Merged
Alvorecer721 merged 29 commits into
mainfrom
build/vllm-0251-cscs-image
Aug 14, 2026
Merged

build(vllm): upgrade to 0.25.1 and make CSCS images reproducible#12
Alvorecer721 merged 29 commits into
mainfrom
build/vllm-0251-cscs-image

Conversation

@Alvorecer721

@Alvorecer721 Alvorecer721 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • upgrade the Apertus generation stack from vLLM 0.20.0 to 0.25.1, including the renderer, fused-MoE/refit, RayExecutorV2, and environment-propagation API moves
  • make worker environments dependency-aware and fail closed on interrupted or stale builds, while avoiding redundant rebuilds across checkout aliases
  • add a resumable CSCS Podman -> persistent registry -> Enroot/SquashFS release pipeline with bounded actor layers, pinned inputs, clean-tree source contexts, and baked-image probes
  • preserve FP8 tensor storage across refits and backport vLLM's post-0.25.1 invalid-MNNVL-workspace fix for compiled TP engines on GH200
  • document the resulting image/cache lifecycle, failure recovery, and CSCS launch constraints

Upstream provenance

Validation

  • compiled vLLM TP2 generation/refit unit test on two GH200s (test_vllm_policy_generation_async[2-1]), including CUDA graph capture and TRT-LLM workspace fallback
  • real Apertus-8B Async-GRPO on four GH200s: one TP2 vLLM engine + one TP2 Megatron policy, two complete training steps, repeated refit, exit 0 (Slurm allocation 3076785)
  • observed final-step group throughput: 2,442.66 tokens/s end-to-end; 3,006.32 training tokens/s; 2,906.17 generation tokens/s
  • focused regression suite: 24 passed (test_vllm_patches.py, test_venvs.py)
  • Ruff check and format check on changed Python files
  • branch is 0 commits behind current main; full diff passes git diff --check
  • clean final image build and SquashFS export from code-bearing revision 7c68228e4f09 (Slurm job 3077164, 2:03:51)
  • baked-image TP2 Async-GRPO certification on four GH200s: compiled CUDA graphs, invalid-MNNVL guard/fallback, successful refit, two complete training steps, exit 0 (Slurm job 3077837, 4:48)
  • baked-image compiled TP4 generation on one quad-GH200 node: invalid MNNVL workspace rejected, TRT-LLM FlashInfer fallback initialized, CUDA graphs captured, 4 x 64 tokens generated, exit 0 (Slurm job 3078831, 3:27)

The single-node TP4 result is the maximum relevant tensor-parallel certification for Clariden's quad-GH200 nodes. Two-node TP8 remains optional, non-blocking scalability evidence; the current Apertus-8B Async-GRPO placement uses TP2 inference plus TP2 training on one node and does not require TP8.

CSCS image notes

The certified SquashFS is /iopsstor/scratch/cscs/xyixuan/ce-images/nemo-rl/nemo-rl-apertus-vllm-0.25.1-7c68228e4f09-38c6b702948c.sqsh (48,752,754,688 bytes), SHA-256 d50f39e45f6104d13e12b9323dbe28cc91b0f13e3a250d029ce6cc7e7646742a, OCI image ID 18c36e6a31df01fc0370f65f9446c373d741198abf821b35a89be8214c11e79e. The later PR commits add reusable certification probes and record their evidence; they do not change image contents.

The persistent OCI cache lives under /iopsstor/scratch/cscs/xyixuan/podman-cache/nemo-rl; the local registry process is allocation-scoped, while its blobs and exported SquashFS survive node changes. Release builds reject dirty source/submodules and encode the source commit, recursive submodule SHAs, base manifest, and build arguments in the artifact identity.

The node-spanning TP8 probe deliberately routes collectives through PyTorch NCCL over the CSCS AWS OFI plugin because vLLM 0.25.1's direct PyNccl and Hopper symmetric-memory paths are incompatible with that topology. On the tested single-node TP2 and TP4 topologies, vLLM rejects the unusable FlashInfer MNNVL multicast workspace and safely initializes the compiled TRT-LLM FlashInfer all-reduce backend.

Tracks upstream's own bump (NVIDIA-NeMo/RL#3280) so later ports diff directly
against it. The 0.25 API moves this rebase covers: the serving renderer
(OpenAIServingRender -> OnlineRenderer, with tool_parser/reasoning_parser
folded into a single `parser`), the fused-MoE split (FusedMoE -> MoERunner +
RoutedExperts) and fp8's weight_scale -> weight_scale_inv, and Ray env
propagation through the official VLLM_RAY_EXTRA_ENV_VARS_TO_COPY hook. Two
source patches retire: upstream fixed the Hermes tool-parser race, and the v1
executor patch is inert under 0.25's RayExecutorV2 default.

Also carries the follow-ups upstream shipped in that commit which our
v0.7.0-era tree lacked:

- packed_broadcast's producer and consumer join their side streams before
  returning. 0.25's PyNcclCommunicator enqueues without blocking, so refit
  broadcasts could still be in flight while the caller mutated the source
  weights or generation read them.
- RayExecutorV2 TCPStore port offset and MessageQueue bind retry, which fix
  deterministic EADDRINUSE startup failures for engines spanning nodes.
- _init_workers_ray reports whether its anchor actually landed instead of
  logging success unconditionally, and the caller distinguishes applied,
  inert-under-v2, and broken-and-load-bearing.

Upstream's tests for those come with them (test_vllm_patches,
test_vllm_tcpstore_port, test_vllm_message_queue_port).

The MoE-refit hunks of #3280 are deliberately not ported: they patch the
checkpoint-engine loader (refit_loader/refit_layout), which post-dates v0.7.0
and does not exist here. Our refit goes through vLLM's own load_weights, which
owns the .routed_experts. rename internally.

build_nemo_rl_image.slurm builds the arm64 image with Podman on a compute node
and exports SquashFS for the Container Engine; the machine-local EDF it
produces is gitignored. The README documents the image, its build and cache
lifecycle, and the environment traps certification surfaced: run one
containerized step per allocation, and pin UV, since a personal uv predating
the new [tool.uv] fields hard-fails worker-venv builds.

Certified on that image: GRPO sync gen-KL 0.0003/0.0003/0.0004 on two separate
nodes (job 3065064 and an in-allocation rerun), NeMo-Gym async
0.0008/0.0007/0.0008 in its established band, the xIELU CUDA training kernel
forward+backward within one bf16 ulp, and generation kernel-free in every run.

Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
`uv run --locked` re-syncs the driver environment only. Worker venvs are
reused whenever they carry the readiness marker, so the vLLM 0.20 -> 0.25.1
bump left generation workers importing 0.20 until 0.25-only code failed at
refit with a ModuleNotFoundError on fused_moe.routed_experts. The venvs looked
healthy the whole time; nothing in the run said which resolution they served.

The marker now holds a digest of uv.lock and pyproject.toml instead of being
an empty flag, and a venv counts as ready only when that digest matches. A
mismatch flows into the rebuild-in-place path that already handles unmarked
venvs, since `uv sync` is convergent. pyproject is digested alongside the lock
because [tool.uv] build settings change the installed environment without
changing the resolution. A waiter that sees a completed build with a different
digest now fails with that reason rather than timing out.

venvs.py also honors a pinned UV for its uv invocations, and the fixgate
launcher exports it into the container: the bumped pyproject uses [tool.uv]
fields that uv < 0.11 rejects, so a personal uv earlier on PATH turned worker
venv builds into hard failures.

The launcher change also picks up the probe recipe rename from the previous
commit.

Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
The suite needs the test dependency group, so running it through the shared
.venv re-syncs that venv to a shape training jobs do not want; a job starting
alongside re-syncs it back and the suite dies mid-collection, which is how one
run failed with a Ray abort while a multinode probe was starting. Point it at
.venv-test instead, and gitignore that directory.

Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Rootless Podman stores its pause process under XDG_RUNTIME_DIR. A batch step
has no logind session, so /run/user/<uid> is absent and the first podman
command fails with `open .../libpod/tmp/pause.pid: no such file or directory`.
Earlier builds only succeeded because they ran inside interactive allocations,
which do have that directory. Point it at the allocation-local Podman storage
base when the environment does not provide one.

Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
.dockerignore excluded `.venv` exactly, so a sibling environment such as
`.venv-test` (the isolated venv the unit suite runs in) was tarred into the
build context and the image build died seconds in. Match the whole family.

Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Three causes were found and two are fixed in code (the runtime directory under
sbatch, and .dockerignore matching every local venv); the stale registry lock
needs an operator step, and Podman is only reachable from the compute node
host. Past that point the build still exits without a message, so note the
state and leave the certified image in place rather than keep retrying on a
node that has served many interrupted builds.

Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
The SquashFS was baked from 336136c-dirty-fd360335e307, before the
packed_broadcast stream joins and the two RayExecutorV2 port patches landed.
Verified against the artifact: its /opt/nemo-rl carries neither. Certification
ran the checkout ahead of the baked tree on PYTHONPATH, so the published image
is only self-contained for single-node engines until it is rebuilt.

Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
A batch step has no logind session, so rootless Podman cannot reach
/run/user/<uid>/bus; the registry container starts and listens, but the
launcher readiness probe never reaches port 5000 and the build stops before
its first stage. XDG_RUNTIME_DIR alone does not cover this, so point the
procedure back at an interactive allocation until the session side is solved.

Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Backport the FP8 refit storage fix and regression coverage from the upstream vLLM 0.25.1 integration commit cf766d5. Stable-shape refits now copy into existing allocations instead of rebinding tensor storage and fragmenting GPU memory.

Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Include the normalized worker launch command in readiness fingerprints so changing extras cannot silently reuse an incompatible environment. Keep dependency-file boundaries explicit and cover rebuild and normalization behavior.

Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Run the persistent registry on the host network so plain sbatch jobs do not depend on a logind session for rootless port publishing. Serialize cache writers with flock, pin the GH200 base manifest, reject dirty sources, and construct the image context from the committed tree plus recursive submodules.

Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Normalize checkout aliases in dependency fingerprints and retain actor extras until the final exact uv sync.
Backport vllm-project/vllm#49043 so compiled TP engines fall back when NVSwitch multicast is unavailable.

Upstream-commit: 81962bb6995eaebd1e49998c2a91c9e01e24da27
Add the missing license header to the xIELU microbenchmark and mark the public hermetic dependency checksums as detect-secrets false positives.
Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
@Alvorecer721
Alvorecer721 marked this pull request as ready for review August 13, 2026 22:36
Upstream TP8 validation assumes 8-GPU nodes where the TP group stays inside
NVLink; on quad-GH200 nodes the same setting becomes node-spanning and pays
Slingshot latency per layer. Record the placement policy: TP <= 4 in-node,
DP across nodes for generation throughput, PP across nodes for capacity,
node-spanning TP8 as certified last resort.

Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
The certified SquashFS now has a group-readable copy under
MLLM/containers with an intuitive name following that directory's
conventions; the builder original on personal scratch remains the
provenance-named artifact. Identity (SHA-256, OCI tag/ID) is unchanged.

Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
The certified SquashFS now lives at a stable group-readable path under
MLLM/containers, so the EDF no longer embeds a user-specific path and can
be checked in: clone-and-run needs no EDF authoring. Personal builds
override only the image value.

Signed-off-by: Alvorecer721 <yixuan.xu@ai.ethz.ch>
@Alvorecer721
Alvorecer721 merged commit e65029d into main Aug 14, 2026
9 checks passed
@Alvorecer721
Alvorecer721 deleted the build/vllm-0251-cscs-image branch August 27, 2026 15:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant