|
| 1 | +# Time-Slicing Integration Guide for verl Workloads |
| 2 | + |
| 3 | +This guide covers integrating **verl** (Volcano Engine Reinforcement Learning framework) with the **llm-d-rl-time-slicing** platform using the pre-packaged `timeslice-verl` integration (`pkg/integrations/verl/`). |
| 4 | + |
| 5 | +### Motivation: Maximizing GPU Utilization |
| 6 | + |
| 7 | +In verl's **fully-async** recipe (`fully_async_policy`), rollout generation runs continuously while the trainer sits idle whenever it waits for the next batch of samples — in our reference runs that idle fraction was ~55–70% of wall-clock. Cooperative time-slicing backfills those valleys: multiple jobs' trainers share one physical GPU pool, and the platform checkpoints the idle trainer's GPU state to host RAM (`cuda-checkpoint`) at every handoff. Rollout engines keep dedicated GPUs and never stop generating: while a job's trainer is checkpointed off the shared GPU, its rollout engine keeps generating samples into the staleness queue. |
| 8 | + |
| 9 | +### Supported Modes |
| 10 | + |
| 11 | +| verl mode | Status | Notes | |
| 12 | +|---|---|---| |
| 13 | +| **Fully-async** (`fully_async_policy`) | **Supported** | Via the `TimesliceFullyAsyncTrainer` subclass + fork lifecycle hooks. See the runnable example below. | |
| 14 | +| v1 sync modes (e.g. `main_ppo` PPO/GRPO) | Not shipped in this PR | The v1 trainers expose the same `on_*` hook convention; integration is future work. | |
| 15 | + |
| 16 | +For a runnable example, see: |
| 17 | + |
| 18 | +* **[Fully-Async Example](examples/fully-async/README.md)** — two fully-async RL jobs (math RLVR + code RLVR) sharing one trainer GPU, each with a dedicated rollout GPU |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## Table of Contents |
| 23 | +1. [Cluster Prerequisites](#1-cluster-prerequisites) |
| 24 | +2. [Deploying the Time-Slicing Platform](#2-deploying-the-time-slicing-platform) |
| 25 | +3. [Integrating with verl](#3-integrating-with-verl) |
| 26 | +4. [General Troubleshooting](#4-general-troubleshooting) |
| 27 | + |
| 28 | +--- |
| 29 | + |
| 30 | +## 1. Cluster Prerequisites |
| 31 | + |
| 32 | +Before deploying cooperative time-slicing for verl, ensure your environment meets the following requirements: |
| 33 | + |
| 34 | +* A Kubernetes cluster (we validated on GKE, COS nodes) with 1-GPU nodes (H100-class recommended). The GPUs used for time-slicing must NOT be in use by anything else. |
| 35 | +* Cluster-admin access; `kubectl`, `helm` (v3), `git`, and `envsubst` (from gettext; `brew install gettext` on macOS) on your workstation. |
| 36 | +* Nodes can pull from Docker Hub (`verlai/verl` image, ~20 GB) and reach GitHub + HuggingFace from pods (model + dataset download at startup; the model is fetched by HF id per node, no shared volume). |
| 37 | + |
| 38 | +### Node Labeling and Time-Slice Groups |
| 39 | + |
| 40 | +The orchestrator discovers resource pools (*groups*) from node labels. Jobs in the same group take turns holding the group's accelerator lock. Label ONLY the nodes whose GPUs should be time-sliced into the group — the orchestrator discovers groups from this label — **without it, lock requests hang forever**: |
| 41 | + |
| 42 | +```bash |
| 43 | +kubectl label node <trainer-node> group.timeslice.io/trainers=true --overwrite |
| 44 | +``` |
| 45 | + |
| 46 | +Nodes hosting rollout engines must NOT carry the group label, and rollout pods must not carry `timeslice.io/*` pod labels: only labeled pods on labeled nodes are snapshot candidates, so unlabeled rollout processes are never touched by the platform. |
| 47 | + |
| 48 | +Verify the orchestrator synced the group after labeling: |
| 49 | + |
| 50 | +```bash |
| 51 | +kubectl -n timeslice-system logs deploy/timeslice-timesliceorchestrator --tail=50 \ |
| 52 | + | grep -i trainers | tail -3 |
| 53 | +``` |
| 54 | + |
| 55 | +### Host-RAM Sizing Rule |
| 56 | + |
| 57 | +A head pod's memory limit must fit its normal RSS **plus its entire GPU allocation** — the cuda-checkpoint dump of a trainer's full GPU state lands in pod memory. Size trainer-node host RAM to hold the GPU memory footprint of every job whose trainer can be checkpointed there. |
| 58 | + |
| 59 | +--- |
| 60 | + |
| 61 | +## 2. Deploying the Time-Slicing Platform |
| 62 | + |
| 63 | +We deploy the core platform components — **TimeSlice Orchestrator** (Deployment: the gRPC lock service) and **Snapshot Agent** (DaemonSet, one per GPU node: performs `cuda-checkpoint` snapshot/restore of a job's GPU state when the orchestrator hands the lock over) — using the parent Helm chart. |
| 64 | + |
| 65 | +> If you previously installed the platform, do a clean |
| 66 | +> `helm uninstall timeslice -n timeslice-system` first — `helm install` fails |
| 67 | +> on an existing release name, and the orchestrator keeps lock state in |
| 68 | +> memory; a stale instance will confuse a fresh run. (Note: uninstalling also |
| 69 | +> removes the bundled NVIDIA DRA driver DaemonSet until you reinstall.) |
| 70 | +
|
| 71 | +```bash |
| 72 | +git clone https://github.com/llm-d-incubation/llm-d-rl-time-slicing.git |
| 73 | +cd llm-d-rl-time-slicing |
| 74 | + |
| 75 | +helm dependency update ./deploy |
| 76 | +helm install timeslice ./deploy -n timeslice-system --create-namespace \ |
| 77 | + -f ./deploy/values-gke.yaml \ |
| 78 | + -f /path/to/your/values-overrides.yaml |
| 79 | +``` |
| 80 | + |
| 81 | +> **Release pin:** use the official `ghcr.io/llm-d-incubation/llm-d-rl-time-slicing/*` images containing the IDLE cold-start fix. The runnable example pins them in its [`values-timeslice.yaml`](examples/fully-async/values-timeslice.yaml) override file (images only). |
| 82 | +
|
| 83 | +Verify (orchestrator Running; one agent pod per GPU node): |
| 84 | + |
| 85 | +```bash |
| 86 | +kubectl -n timeslice-system get pods -o wide |
| 87 | +``` |
| 88 | + |
| 89 | +--- |
| 90 | + |
| 91 | +## 3. Integrating with verl |
| 92 | + |
| 93 | +### How It Works |
| 94 | + |
| 95 | +The `timeslice-verl` package ships `TimesliceFullyAsyncTrainer`, a subclass of verl's `fully_async_policy` trainer that overrides the trainer's empty `on_*` lifecycle hooks to acquire/release the orchestrator lock at the trainer's natural wait points. It registers under the trainer name `timeslice` (verl's `register_trainer` registry; the package's `verl.plugins` entry point makes `import verl` load the registering module) and is selected with a single hydra override: `async_training.trainer_name=timeslice`. No monkey-patching and no verl source edits in your job — it requires a verl build with the fully-async lifecycle hooks + trainer registry and per-pool placement-group bundle resources (currently the `feat/fully-async-lifecycle-hooks` fork branch, until the commits land upstream). |
| 96 | + |
| 97 | +Lock protocol: |
| 98 | + |
| 99 | +| Trainer lifecycle point | Lock action | |
| 100 | +|---|---| |
| 101 | +| `init_workers` (model load) | ACQUIRE → load → YIELD | |
| 102 | +| initial weight sync (pre-fit) | ACQUIRE → sync → YIELD | |
| 103 | +| a batch of samples is ready | ACQUIRE (this is the resume point) | |
| 104 | +| weight update + param sync done | YIELD | |
| 105 | + |
| 106 | +### Placement-Group Pinning |
| 107 | + |
| 108 | +Trainer/rollout placement is pinned with Ray **custom resources** — the head starts ray with `--resources='{"trainer_node": 100}'`, the worker with `--resources='{"rollout_node": 100}'`, and verl's per-pool placement-group bundle resources (hydra override `ray_pg_extra_resources={trainer_pool: {trainer_node: 1}, rollout_pool: {rollout_node: 1}}`) make verl's trainer placement group request `{trainer_node: 1}` and the rollout PG `{rollout_node: 1}`. The timeslice trainer verifies at startup that the pinning config actually reached the trainer (`TIMESLICE_REQUIRE_PG_PINNING=1` — refuse to train unpinned), and a per-pod **role watchdog** enforces placement at runtime (dies loudly if a process of the wrong role ever touches a node's GPU). |
| 109 | + |
| 110 | +### Installing verl (Pinned Fork) |
| 111 | + |
| 112 | +Each job pod clones the [`feat/fully-async-lifecycle-hooks`](https://github.com/aishukamal/verl/tree/feat/fully-async-lifecycle-hooks) branch of `github.com/aishukamal/verl` — upstream `983cb0f24443f87b3d161fad318445130a620b07` + two feature commits (fully-async lifecycle hooks + trainer registry; per-pool PG bundle resources) — and installs it with `pip install -e ".[gpu]"`. This is a temporary fork until the commits land upstream; once they do, point the `VERL_REPO`/`VERL_REF` pod envs at mainline verl instead. |
| 113 | + |
| 114 | +### Installing the `timeslice-verl` Package (ConfigMap) |
| 115 | + |
| 116 | +Publish the integration package (`pkg/integrations/verl/`) into the cluster as ConfigMaps — the job pods install it from these, no GitHub fetch: |
| 117 | + |
| 118 | +```bash |
| 119 | +# Package source: the repo you cloned in §2 |
| 120 | +PKG=llm-d-rl-time-slicing/pkg/integrations/verl |
| 121 | +kubectl create configmap timeslice-verl-root --from-file=$PKG/pyproject.toml |
| 122 | +kubectl create configmap timeslice-verl-src --from-file=$PKG/timeslice_verl/ |
| 123 | +``` |
| 124 | + |
| 125 | +ConfigMap keys cannot contain `/`, so the package tree is split into two maps (project root + module sources) and reassembled inside the pod; this also works in air-gapped clusters. |
| 126 | + |
| 127 | +### Required NCCL Environment |
| 128 | + |
| 129 | +Set `NCCL_CUMEM_ENABLE=0` and `NCCL_NVLS_ENABLE=0` on every job pod — the NVLS/cuMem NCCL transports don't survive cuda-checkpoint, and a trainer will die right after a restore without them (the example manifests already set both). |
| 130 | + |
| 131 | +--- |
| 132 | + |
| 133 | +## 4. General Troubleshooting |
| 134 | + |
| 135 | +Every entry below is a failure we actually hit while building the integration. For failures specific to the runnable demo's topology and manifests, see the [example's troubleshooting section](examples/fully-async/README.md#8-troubleshooting). |
| 136 | + |
| 137 | +| Symptom | Cause | Fix | |
| 138 | +|---|---|---| |
| 139 | +| First `acquire()` hangs forever; orchestrator logs show no group | Trainer node not labeled | §1 label; a node scaled up from 0 has no label — relabel after every node replacement | |
| 140 | +| Acquire hangs; orchestrator logs `waiting for job ... to transition from IDLE` | Platform image without the PR #152 fix | Use the official pinned platform images (§2 release pin) | |
| 141 | +| Trainer dies right after a restore: `NCCL ... unhandled cuda error` in `transport/nvls.cc` | `NCCL_CUMEM_ENABLE`/`NCCL_NVLS_ENABLE` not set to `0` — NVLS/cuMem NCCL transports don't survive cuda-checkpoint | Set both to `0` on all pods (§3; the example manifests do) — keep them if you edit | |
| 142 | +| `ImportError` from `timeslice` client at startup | grpcio/protobuf too old in the base image | Upgrade to `grpcio>=1.81 protobuf>=7.35` in the job pods (the example manifests do) — keep that step | |
| 143 | +| Startup aborts with `TIMESLICE_REQUIRE_PG_PINNING=1 but no extra bundle resources resolve` | The `ray_pg_extra_resources` hydra override is missing/typo'd, or the verl build lacks the feature | Keep the `ray_pg_extra_resources={trainer_pool: ...}` override in the head launch script intact and use the pinned verl branch (§3) | |
| 144 | +| Trainer placement group lands on the rollout node (role watchdog FATAL within ~1 min of training start) | PG pinning not active — custom resources missing on ray start, or the pinning check was disabled | Verify both `ray start` lines carry `--resources` and both pods set `TIMESLICE_REQUIRE_PG_PINNING=1` | |
| 145 | +| `ROLE-FATAL` in a pod log | A process of the wrong role got a compute context on that node's GPU | The watchdog killed the run precisely so nothing got frozen mid-generation; check `ray status` custom resources and that the `timeslice-verl-root`/`timeslice-verl-src` ConfigMaps were created from the correct package checkout (§3), then relaunch | |
| 146 | +| Rollout throughput collapses when the other job trains; agent log shows cuda-checkpoint activity on a ROLLOUT node | A rollout pod carries `timeslice.io/*` labels, or a rollout node is labeled into the group | Rollout pods/nodes must stay unlabeled — only head pods and the trainer node (§1). A rollout node must never show cuda-checkpoint activity | |
| 147 | +| One job crashed (even after a clean yield — holding the lock is not required); orchestrator logs `Active job is already running, exiting early` forever | Known platform gap: the orchestrator keeps the last active job sticky in memory; crashed jobs need manual cleanup | Delete BOTH jobs' pods, `helm uninstall` + reinstall the platform (orchestrator state is in-memory), relaunch. The plugin releases the lock on clean crashes; the group can still wedge | |
| 148 | +| `Found multiple active Ray instances` warning on a shared node | With `hostPID`, Ray's `address='auto'` discovery scans `/proc` and sees the OTHER job's GCS — a driver can join the wrong ray cluster | `export RAY_ADDRESS="$(hostname -i):6379"` right after `ray start --head` (the example manifests do) — keep that line if you edit | |
| 149 | +| Hydra error `Key 'use_trainer_do_validate' is not in struct trainer` | Wrong config key path | These flags live under `async_training.*`, not `trainer.*` (the example manifests are correct) | |
| 150 | +| `save_freq` / checkpoint-save-skipped warning in log | Checkpoint saving would RPC a possibly-frozen worker | By design: the timeslice trainer vetoes saves when `save_freq > 0`; keep `trainer.save_freq=-1` | |
| 151 | +| Steps complete but one job's staleness queue near `staleness_threshold × batch` | Very asymmetric job speeds | Raise `async_training.staleness_threshold` (we use 8) or budget shorter runs; queue overflow drops samples | |
0 commit comments