Skip to content

ci(e2e): run Qwen and DS4 end to end on lucebox3 - #756

Draft
Graffioh wants to merge 3 commits into
Luce-Org:mainfrom
Graffioh:ci/lucebox3-model-e2e
Draft

Graffioh wants to merge 3 commits into
Luce-Org:mainfrom
Graffioh:ci/lucebox3-model-e2e

Conversation

@Graffioh

@Graffioh Graffioh commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

What

A new Model e2e workflow (.github/workflows/model-e2e.yml) runs a real model on lucebox3 and checks whether a change crashes it or changes its behaviour.

  • Qwen3.8-27B (IQ4_XS + dflash2 draft) runs on the R9700 (gfx1201).
  • DeepSeek V4 Flash (STRIX mix, sparse prefill) runs on the Strix Halo (gfx1151).

Each job builds luce_server for its GPU, loads the model, sends 16 fixed prompts with greedy decoding (.github/ci/e2e/prompts.json), stops the server, and compares with the last good run on main (the baseline). The prompts cover arithmetic, lists, code, JSON, translation, unicode, multi-turn, a tool call, a 2.5K-token needle, streaming, thinking, a longer story, and a repeated request.

Result When
❌ fail crash · hang · server won't load or stop · GPU errors in the kernel log · a request fails or times out · the suite runs out of time · a model or the draft is missing (a PR skips instead) · 2+ checks that passed on the baseline now fail · under half the checks pass
⚠️ warn output text differs from the baseline (with the character where it first differs) · a single regressed check · a repeated request answers differently · decode speed −15% · load time +50% · the kernel log can't be read, or lost messages during the run

When it runs

  • Only when a maintainer adds the e2e label, like compact-commits in ci(commits): check conventional commit style and dispatch the compaction bot #755. It runs the models the PR touches (select_models.py), or both when the PR touches neither DS4, Qwen nor shared server code. To test new pushes, remove the label and add it again.
  • Nightly on main: refreshes the baseline. Manual dispatch refreshes the baseline when update_baseline is ticked on main.

lucebox3 only

  • Routing: jobs use runs-on: [self-hosted, lucebox3], next to gpu-tests-amd. The runner runs one job at a time, so e2e jobs never share the GPUs with each other or with gpu-tests-amd. Spreading jobs over more luceboxes can come later.
  • One table per model: select_models.py holds each model's GPU, HIP index (R9700 = 0, Strix Halo = 1, as in gpu-tests-amd), model files and server flags. The job reads them from its matrix. Models live in vars.LUCEBOX_MODELS_DIR, default /opt/models.
  • Baseline: baseline runs on main upload their result as the artifact model-e2e-baseline-<model>-<device> (kept 90 days). find_baseline.py gives each job the newest one, counting only scheduled or dispatched model-e2e runs on this repo's main, so PR code can't replace it. Results record the ROCm version, and the report warns when the baseline ran on a different one.
  • Someone using the GPU by hand: the job waits up to 4 minutes, then skips with a warning. To keep jobs off while you benchmark, stop lucebox3's runner service.
  • Seeing every user's GPU processes: the job reads the kernel's KFD process list (/sys/class/kfd/kfd/proc, readable by any user; it's what rocm-smi --showpids uses) and fails if it can't.

Staying bounded

  • 35-minute job cap, plus caps per step: build 12 min, suite 15 min, model load 480 s (DS4 takes ~185 s from a cold page cache, ~30 s warm), 120 s per request, 300 s for the whole suite.
  • Skips repeats: a clean pass (not a warning, which must show again) is remembered by tree hash, configuration, model files, ROCm version and the baseline's digest. The suite is judged against a snapshot of the baseline taken with the key, so a new baseline always re-runs it. A skipped run shows the report of the run it reused.
  • Builds incrementally: the build folder persists on lucebox3 and uses ccache if installed.

Queue fix (separate commit)

By default a concurrency group keeps only one waiting job, so a second PR's waiting GPU job cancelled the first one's. queue: max (GitHub, May 2026) keeps up to 100 waiting, first in first out, on gpu-tests, gpu-tests-amd and speed-profile. actionlint doesn't know the key yet, but GitHub's docs confirm it at job level with cancel-in-progress: false.

Measured on lucebox5 (same R9700 + Strix Halo hardware)

cold build suite incl. load load decode
Qwen3.8-27B IQ4_XS + dflash2 100 s ~30 s 6–18 s ~105 tok/s
DS4 STRIX mix, sparse prefill 95 s ~80 s 28–31 s warm, 185 s cold ~19 tok/s
  • Deterministic: both models gave identical text on all 16 prompts across two runs.
  • Workflow steps replayed: I ran the job's shell steps on lucebox5 with Qwen: persistent build, suite (16/16), pass marker, a cached skip, a re-run once a baseline appeared (same text on all prompts), and writing the new baseline. That was before the switch to lucebox3 only, which dropped the GPU-index lookup and model search from those steps. gpu_wait.sh reports busy when another process holds /dev/kfd. find_baseline.py ran against the live API; the artifact download itself can only run once a baseline exists on main.
  • Why DS4 uses sparse prefill: exact prefill runs at about 21 tok/s on the Strix Halo, and the 2.5K-token needle alone took 120 s.
  • DS4 arithmetic, for reviewers: exact prefill says 60/1.5 = 10, sparse says 17×23 = 340. Both are deterministic, and Qwen gets both right. That's why a single regressed check only warns. It may be a real accuracy issue worth checking separately.

Before enabling

  • Put the three models in /opt/models on lucebox3 (or set LUCEBOX_MODELS_DIR): Qwen3.8-27B-UD-IQ4_XS.gguf, qwen38-dflash2-q8_0.gguf, DeepSeek-V4-Flash-0731-ROCMFPX-MIX-STRIX.gguf.
  • Make the kernel log readable for the runner user on lucebox3: passwordless sudo dmesg or kernel.dmesg_restrict=0. Without it every run warns that GPU errors weren't checked.
  • After merging, dispatch the workflow on main with update_baseline ticked to create the first baseline (or wait for the nightly run).
  • Create the e2e label.
  • New kernel-log lines are matched by their boot timestamp, not by line count, so ring-buffer rollover can't hide a fault. When the log after the run no longer reaches back to the messages from before it, the report says so and warns.

🤖 Generated with Claude Code

@Graffioh
Graffioh force-pushed the ci/lucebox3-model-e2e branch from 5d73da5 to 437f06c Compare September 23, 2026 12:46
@Graffioh Graffioh changed the title ci(e2e): run Qwen and DS4 end to end on lucebox3 ci(e2e): run Qwen and DS4 end to end on any free lucebox Sep 23, 2026
@Graffioh Graffioh added compact-commits Set when you want to run LLM to compact commit history. and removed compact-commits Set when you want to run LLM to compact commit history. labels Sep 24, 2026
@davide221

Copy link
Copy Markdown
Contributor

The commit compaction bot stopped.

The pull request is still a draft.

Maintainers: see the lucebox_bots run log.

@davide221 davide221 removed the compact-commits Set when you want to run LLM to compact commit history. label Sep 24, 2026
@Graffioh
Graffioh marked this pull request as ready for review September 24, 2026 06:40
@Graffioh Graffioh added the compact-commits Set when you want to run LLM to compact commit history. label Sep 24, 2026
@davide221

Copy link
Copy Markdown
Contributor

The commit compaction bot stopped.

Pushing to Graffioh/lucebox-hub failed (the branch moved, the fork blocks maintainer edits, or the token lacks the workflow scope). The branch is unchanged.

Maintainers: see the lucebox_bots run log.

@davide221 davide221 removed the compact-commits Set when you want to run LLM to compact commit history. label Sep 24, 2026
@Graffioh
Graffioh marked this pull request as draft September 24, 2026 06:43
@Graffioh Graffioh added the compact-commits Set when you want to run LLM to compact commit history. label Sep 24, 2026
@davide221

Copy link
Copy Markdown
Contributor

Folded 2 commits into 2 (same final code; the tree hash was checked before pushing).

New commit From
158ac7ab ci(workflows): queue GPU jobs instead of replacing waiting ones 341c98c
7ae026fb ci(e2e): run Qwen and DS4 end to end on any free lucebox 437f06c

Previous head: 437f06c2ce7c5c7b036de33f0e0d43782cf7495c.

@davide221
davide221 force-pushed the ci/lucebox3-model-e2e branch from 437f06c to 7ae026f Compare September 24, 2026 12:08
@davide221 davide221 removed the compact-commits Set when you want to run LLM to compact commit history. label Sep 24, 2026
@Graffioh
Graffioh force-pushed the ci/lucebox3-model-e2e branch from 7ae026f to 1121a5e Compare September 24, 2026 13:51
@Graffioh Graffioh added the compact-commits Set when you want to run LLM to compact commit history. label Sep 24, 2026
@davide221

Copy link
Copy Markdown
Contributor

Folded 3 commits into 2 (same final code; the tree hash was checked before pushing).

New commit From
2b7e938d ci(workflows): queue GPU jobs instead of replacing waiting ones 875bd28
4eff4ad8 ci(e2e): run Qwen and DS4 end to end on any free lucebox 09f3658, 1121a5e

Previous head: 1121a5e57bd44975931025e6812e143b505e5347.

@davide221
davide221 force-pushed the ci/lucebox3-model-e2e branch from 1121a5e to 4eff4ad Compare September 24, 2026 13:53
@davide221 davide221 removed the compact-commits Set when you want to run LLM to compact commit history. label Sep 24, 2026
@Graffioh
Graffioh marked this pull request as ready for review September 24, 2026 14:12
@Graffioh
Graffioh marked this pull request as draft September 24, 2026 14:21

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 12 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread .github/ci/e2e/gpu_wait.sh Outdated
Comment thread .github/ci/kfd_health.sh Outdated
Comment thread .github/workflows/model-e2e.yml
Comment thread .github/workflows/model-e2e.yml Outdated
Comment thread .github/ci/e2e/config.sh Outdated
Comment thread .github/ci/e2e/prompts.json Outdated
Comment thread .github/ci/e2e/run_model_e2e.py
Comment thread .github/ci/e2e/check_host.sh Outdated
Comment thread .github/ci/e2e/test_model_e2e.py
Comment thread .github/ci/e2e/run_model_e2e.py Outdated
Graffioh and others added 2 commits September 24, 2026 14:43
A concurrency group keeps one waiting job by default, so when a second
PR's GPU job arrives GitHub cancels the first PR's waiting job. Add
`queue: max` (up to 100 waiting, first in first out) to the per-device
groups of gpu-tests, gpu-tests-amd and speed-profile.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add a model-backed e2e job that runs when a maintainer adds the `e2e`
label to a PR, and nightly on main. It builds luce_server for one GPU,
loads the model, sends a fixed prompt suite with greedy decoding, and
compares with the last good run on main. Qwen3.8-27B (with its dflash2
draft) runs on the R9700 and DS4 on the Strix Halo.

Jobs go to any idle runner labelled `lucebox-e2e`, so a busy lucebox3
hands the job to the next machine. Every e2e machine has the same GPUs,
ROCm and models, so they share one baseline: baseline runs on main
upload their result as the artifact model-e2e-baseline-<model>-<device>,
and find_baseline.py gives each job the newest one, accepting only
scheduled or dispatched runs on this repository's main. Adding a machine
is: models in /opt/models or ~/models (found by name), check_host.sh,
and a runner with the labels it prints. The KFD health probe moves to
.github/ci/kfd_health.sh so both ROCm jobs share it.

The job fails on a crash, hang, GPU error in the kernel log, a server
that will not load or stop, a failed request, a suite that runs out of
time, or two or more checks that passed on the baseline and now fail.
Changed text, a single regressed check, slower decode and a baseline
from another ROCm only warn. A missing model or draft skips a PR and
fails a baseline run.

gpu_wait.sh sees every user's GPU processes through the kernel's KFD
process list, or passwordless `sudo fuser`, and waits up to 4 minutes
for them before skipping; when it cannot see them a PR skips and a
baseline run fails. New kernel log lines are found by timestamp, so
ring-buffer rollover cannot hide a fault. A clean pass is remembered by
tree, configuration, model files, ROCm and baseline digest. Jobs are
capped at 35 minutes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Graffioh
Graffioh force-pushed the ci/lucebox3-model-e2e branch from 1c9f715 to 9bc0f99 Compare September 24, 2026 14:51
Keep lucebox3 as the only e2e machine for now, and drop what existed only
to spread jobs over several machines: the `lucebox-e2e` runner label and
the dispatch `host` input, the rocminfo HIP-index lookup, the machine
.env / repository variable / model search cascade (config.sh), and the
per-machine setup check (check_host.sh).

Each model's GPU, HIP index, model files and server flags now sit in one
table in select_models.py, and the job reads them from its matrix, with
the models under vars.LUCEBOX_MODELS_DIR (default /opt/models) like the
rest of CI. gpu_wait.sh reads only the kernel's KFD process list, which
any user can read, and fails when it cannot; the `sudo fuser` fallback
and the `unavailable` state are gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Graffioh Graffioh changed the title ci(e2e): run Qwen and DS4 end to end on any free lucebox ci(e2e): run Qwen and DS4 end to end on lucebox3 Sep 25, 2026
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.

2 participants