From fdb70ff07b3bd0d6bc9b875ad00ecadd2a702730 Mon Sep 17 00:00:00 2001 From: droot Date: Thu, 5 Nov 2026 13:44:01 -0800 Subject: [PATCH] k8s/deploy/demo: live lights, an idle fleet view, and the recipe on the Sweep tab Live state - The Fleet chips and the Timeline's current turn come from /api/v1/inspect/now, polled twice a second, instead of replaying the activity history about 8 s behind. A turn, op or batch of sampler requests that just ended is kept as a finished span until the history has it. On an API server without the endpoint (404), the page falls back to the replay, so it works with or without it. - On a shared LoRA runtime, each job's chip is lit only while that job's op or sampler requests run. Fleet - The Fleet tab opens on "no sweep": the pool with no job's counts or chips, any worker holding a GPU drawn as a neutral chip. Starting a sweep switches it to the active sweeps at the click, holding on the empty pool until the new sweep's first Job appears rather than showing the previous sweep; ?sweep=active, ?sweep=all or ?sweep= opens on those instead. - A time-sliced worker is drawn on the GPU only in its turns, even before its first one, so a worker waiting for the GPU no longer shows as holding it. Work that runs at the same time shares the trace's height, one band per job, so a LoRA sampler serving five jobs in one batch shows all five instead of the selected job hiding the rest. The timeline's tick step follows its width, and its legend counts holders that do not fit. - The pool shows only nodes labelled openrl.io/enabled (the ones the scheduler places workers on), or every GPU node when none is; GPU names still resolve on every node, so older sweeps' timelines keep them. - No end card when a sweep finishes: the headline, the done tile and the clock already say so. Sweep - Cards under the spec show the recipe every job runs: GSM8K, GRPO, 8 x 8 answers per step, 512 tokens, learning rates and LoRA rank, and the spec's totals as it is edited. The batch settings are constants in app.py that the Jobs are launched with, so the cards show what runs. - The job list is collapsed until opened, so the page before a launch shows the spec and not earlier sweeps. - "demo" is gone from the header and title, runs are named sweep-HHMMSS, the keynote-20 preset is mixed-20, and the qwen-8 preset is gone. Run on open-rl-dra-use5 (8 x H100) with 16- and 20-job sweeps. --- k8s/deploy/demo/README.md | 41 +++- k8s/deploy/demo/app.py | 33 +-- k8s/deploy/demo/index.html | 467 +++++++++++++++++++++++-------------- 3 files changed, 347 insertions(+), 194 deletions(-) diff --git a/k8s/deploy/demo/README.md b/k8s/deploy/demo/README.md index 9844c464..6e3fc983 100644 --- a/k8s/deploy/demo/README.md +++ b/k8s/deploy/demo/README.md @@ -26,18 +26,28 @@ a Job is matched to its run. presets. Start submits one client Job per row and count, each running the repo's `fft-gsm8k-rl` or `lora-gsm8k-rl` scenario with `TINKER_TAGS=openrl.recipe=rl`. Stop releases the run's workers through - `delete_model` and deletes the Job. Jobs are listed newest first; sweeps get - a letter (A is the newest) so job numbers stay unambiguous across sweeps. -- **Fleet.** Counts, the job table with step progress, and the pool: a box - per node with one row per GPU and a chip per worker holding it. A chip is - lit while its worker is computing (a time-slicer turn, or a shared LoRA - runtime serving one of its runs) and soft while it waits for its turn. - Below the pool: the queue (jobs without a GPU yet; outlined while the pod - is still starting) and the share of the pool's GPU time held by a job since - the first submit, computed from the placements the server recorded. Click - a GPU row or a job row to open it on the Timeline tab. Counts, bars and - states move to each new poll instead of jumping; a worker that gets a GPU - flies from its job's tag in the queue to its row. + `delete_model` and deletes the Job. Under the spec, cards show the recipe + every job runs (GSM8K, GRPO, the batch, sampling and learning rates, from + `RECIPE` in `app.py`, the same settings the Jobs get) and sum up the spec as + it is edited. The job list below is collapsed until opened; jobs are listed + newest first, and sweeps get a letter (A is the newest) so job numbers stay + unambiguous across sweeps. +- **Fleet.** A headline (how many jobs share how many GPUs), counts (running, + the queue while anything waits, done, steps done), the job table, and the + pool: a box per node with one row per GPU and one chip per job a worker + serves, so the jobs on a shared LoRA runtime show one by one like full + fine-tuning workers. A chip is lit while that job's work is on the GPU (a + time-slicer turn, or the job's ops on a shared runtime) and outlined while + it waits for its turn. Below the pool: the queue (only while a job waits) + and the share of the pool's GPU time held by a job since the first submit, + computed from the placements the server recorded. Click a GPU row or a job + row to open it on the Timeline tab. Counts, bars and states move to each + new poll instead of jumping; a worker that gets a GPU flies from its job's + tag in the queue to its row. The sweep selector opens on no sweep: the pool + with no job's counts or chips, any worker holding a GPU drawn as a neutral + chip, the shot before a demo starts. Starting a sweep switches it to the + active sweeps; it also offers every sweep or one sweep, and `?sweep=active`, + `?sweep=all` or `?sweep=` opens on those. - **Timeline.** One occupancy trace per GPU: 1 while a worker is on it, coloured by the holder, 0 when idle; the thin band at the base is a worker that holds the GPU between time-slicer turns. The default window, fit, @@ -55,6 +65,13 @@ a Job is matched to its run. top, or one job alone. Read from the `metrics.jsonl` the client writes to the log directory the app assigned it (`RUNS_ROOT/`). +## Live state + +The lights and the timeline's current turn come from `/api/v1/inspect/now`, +polled twice a second, which serves each worker's state as it wrote it the +moment it changed. On an API server without that endpoint, the page replays +the activity history instead, about 8 s behind. + ## Presenting Press `p` (or open `/?present=1#fleet`) for presentation mode: the canvas diff --git a/k8s/deploy/demo/app.py b/k8s/deploy/demo/app.py index 322bc465..eaf71bef 100644 --- a/k8s/deploy/demo/app.py +++ b/k8s/deploy/demo/app.py @@ -46,8 +46,22 @@ RUN_LABEL = "openrl.io/demo-run" HERE = Path(__file__).parent +# Every client Job runs the e2e script's GSM8K RL scenario (tinker_cookbook's math_rl recipe) with these settings; +# the Sweep tab shows RECIPE under the spec, so what it says is what the Jobs run. +GROUP_SIZE, GROUPS_PER_BATCH, MAX_TOKENS = 8, 8, 512 +RECIPE = { + "dataset": "GSM8K", + "group_size": GROUP_SIZE, + "groups_per_batch": GROUPS_PER_BATCH, + "max_tokens": MAX_TOKENS, + "temperature": 1.0, # run_training_e2e.run_gsm8k_rl + "learning_rate": {"lora": "1e-4", "fft": "1e-5"}, # run_training_e2e.run_gsm8k_rl + "lora_rank": 32, # tinker_cookbook math_rl CLIConfig default + "loss": "importance sampling", # tinker_cookbook math_rl CLIConfig default, with kl_penalty_coef 0 +} + PRESETS = { - "keynote-20": { + "mixed-20": { "steps": 4, "jobs": [ {"kind": "lora", "model": "google/gemma-4-e2b", "count": 5, "steps": 8}, @@ -56,13 +70,6 @@ {"kind": "fft", "model": "Qwen/Qwen3-0.6B", "count": 5, "steps": 4}, ], }, - "qwen-8": { - "steps": 5, - "jobs": [ - {"kind": "lora", "model": "Qwen/Qwen3-0.6B", "count": 4, "steps": 5}, - {"kind": "fft", "model": "Qwen/Qwen3-0.6B", "count": 4, "steps": 5}, - ], - }, "smoke-2": {"steps": 2, "jobs": [{"kind": "fft", "model": "Qwen/Qwen3-0.6B", "count": 2, "steps": 2}]}, } @@ -111,9 +118,9 @@ def job_manifest(run: str, job: str, kind: str, model: str, steps: int, seed: in f"log_dir={RUNS_ROOT}/{job}", f"base_model={model}", f"steps={steps}", - "group_size=8", - "groups_per_batch=8", - "max_tokens=512", + f"group_size={GROUP_SIZE}", + f"groups_per_batch={GROUPS_PER_BATCH}", + f"max_tokens={MAX_TOKENS}", f"extra=seed={seed}", ], # openrl.recipe=rl makes the API server start the sampler with the trainer instead of on the first sample @@ -319,7 +326,7 @@ async def metrics(jobs: str): @app.get("/api/sweep/presets") async def presets(): - return {"presets": PRESETS, "namespace": NAMESPACE, "cluster": CLUSTER_NAME} + return {"presets": PRESETS, "recipe": RECIPE, "namespace": NAMESPACE, "cluster": CLUSTER_NAME} @app.get("/api/sweep/jobs") @@ -329,7 +336,7 @@ async def jobs(): @app.post("/api/sweep/start") async def start(body: dict): - run = re.sub(r"[^a-z0-9-]", "-", str(body.get("run") or time.strftime("demo-%H%M%S")).lower())[:30].strip("-") + run = re.sub(r"[^a-z0-9-]", "-", str(body.get("run") or time.strftime("sweep-%H%M%S")).lower())[:30].strip("-") specs = body.get("jobs") or [] if not specs: raise HTTPException(400, "no jobs") diff --git a/k8s/deploy/demo/index.html b/k8s/deploy/demo/index.html index 8747b2ac..8347d634 100644 --- a/k8s/deploy/demo/index.html +++ b/k8s/deploy/demo/index.html @@ -3,7 +3,7 @@ -OpenRL demo +OpenRL