Skip to content

Bring up vLLM on ROCm 10 and fix scheduler stalls found in the 6-leg run - #1605

Merged
xiaofei-zheng merged 23 commits into
mainfrom
fix/rocm10-vllm-image-in-skill
Sep 23, 2026
Merged

xiaofei-zheng merged 23 commits into
mainfrom
fix/rocm10-vllm-image-in-skill

Conversation

@lishuoshuo-amd

@lishuoshuo-amd lishuoshuo-amd commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator
  • Description: Fix ROCm 10 validation gaps and the orchestrator stalls that made the 6-leg run hard to trust. The branch adds ROCm 10 vLLM source-install routing, makes the Docker vLLM image patchable, fixes trace discovery and unsafe SGLang lever filtering, records enough stack/profiler identity to explain ROCm10 profiler failures, stops repeated no-op analysis/profile retries, and fixes several Hyperloom scheduler/reporting issues found during the run.

  • Linked issue(s): none

  • Tests: added/updated? commands run? (what to test; if a test was replaced, where its assertions live now):

    • python3 -m pytest src/hyperloom/inference_optimizer/tests/test_setup_cli.py -q -p no:randomly — 85 passed
    • python3 -m pytest src/hyperloom/inference_optimizer/tests/test_install*.py src/hyperloom/inference_optimizer/tests/test_anthropic_credential_registry.py src/hyperloom/inference_optimizer/tests/test_llm_config.py src/hyperloom/inference_optimizer/tests/test_baremetal_doc_version_consistency.py -q -n auto — 290 passed
    • python3 -m pytest src/hyperloom/inference_optimizer/tests/test_roofline_executor.py src/hyperloom/inference_optimizer/tests/test_common_provenance.py src/hyperloom/inference_optimizer/tests/test_prelude_roofline.py src/hyperloom/inference_optimizer/tests/test_kernel_attempt_summary.py -q -p no:randomly — 189 passed, 1 skipped
    • python3 -m pytest src/hyperloom/inference_optimizer/tests/test_explore_executor.py -q -p no:randomly — 86 passed
    • python3 -m pytest src/hyperloom/inference_optimizer/tests/test_kernel_idle_streak.py src/hyperloom/inference_optimizer/tests/test_close_phase_sequencer.py src/hyperloom/inference_optimizer/tests/test_conc_sweep.py src/hyperloom/inference_optimizer/tests/test_conc_sweep_session_deadline.py src/hyperloom/inference_optimizer/tests/test_stop_reason_outcomes.py src/hyperloom/inference_optimizer/tests/test_ray_backend_unit.py::test_specialist_close_forces_actor_kill_after_stop_attempt -q -p no:randomly — 224 passed, 2 skipped
    • python3 -m pytest src/hyperloom/inference_optimizer/tests/test_pre_release_stall_liveness.py src/hyperloom/inference_optimizer/tests/test_delegate_denial_loop.py src/hyperloom/inference_optimizer/tests/test_specialist_lifecycle.py ... -q -p no:randomly — 292 passed, 2 skipped
    • Full python3 -m pytest src/hyperloom -q -n auto still has pre-existing failures on this workstation; the failure set was reproduced byte-identically with the branch changes stashed. One collection error was missing local hypothesis, which is already declared in the test extra.
  • Size/complexity triggers crossed: yes. This is intentionally a consolidation branch for the ROCm10 bring-up and the scheduler bugs discovered while validating it. It is split into small commits by concern so it can still be reviewed by subsystem: vLLM source install, vLLM image source tree, trace discovery, SGLang compatibility filter, AITER/provenance, trace capability gating, scheduler/reporting fixes, codex credential setup, and follow-up scheduler/log fixes.

  • If this simplifies or refactors: which mechanism was removed, which contract was preserved, and the tests that show it:

    • No major mechanism was removed. The branch narrows several behaviours: source-patch specialists are pruned before enqueue when no framework Git root exists; KERNEL phase-entry hooks no longer block the coordinator tick, but the phase machine holds every transition out of KERNEL (and the idle streak) until the entry hook -- the GEAK run on the default route -- returns, and Coordinator.run waits for it before teardown; conc_sweep boot is no longer counted as a measured point.
  • Observable effect: operators get a ROCm10 bare-metal vLLM install path instead of a dead wheel route, Docker vLLM patches affect the served source tree, vLLM roofline picks the real rank trace instead of capture sidecars, known-bad SGLang unified-attention variants are skipped before benchmark, zero-GPU traces suppress repeat roofline scheduling with a recorded reason, and final reports keep GEAK/GEMM outcomes even with long attempt keys. Also:

    • The recommended Docker vLLM image moves from vllm/vllm-openai-rocm:v0.29.0 (vLLM 0.29.0, ROCm 7.2.3) to rocm/vllm:rocm10.0.0_ubuntu24.04_py3.14_pytorch_2.12.0_vllm_0.27.0 (vLLM 0.27.0, ROCm 10.0); docs/compatibility.rst now documents both vLLM routes and the VLLM_SOURCE_REF / VLLM_IMAGE_SOURCE_COMMIT pins.
    • Bare-metal vLLM setup writes VLLM_ROCM_USE_AITER=1 to .env when aiter imports, and on the ROCm 10 source route also persists VLLM_ROOT, FRAMEWORK_REPO_PATH and VLLM_TARGET_DEVICE.
    • reports/kernel_optimization_summary.json moves to schema_version 2 with a new lane_totals block (source_level, geak, gemm_tuning). kernel_opt_outcome now rolls up all lanes and can be unvalidated; totals.attempted counts attempts across all lanes, while totals.integrated / totals.rejected stay source-level only.
    • The kernelforge.gemm_tune run command now exits non-zero when the AITER tune/serve alignment preflight reports a hard mismatch (previously a warning).
  • Breaking changes: no intended public API break. Bare-metal setup now accepts codex-only OpenAI credentials as a valid runtime backend; conc_sweep now treats a run with zero successful pairs as sweep_failed instead of a clean sweep_done. Bare-metal vLLM setup now refuses any ROCm stack other than ROCm 7.2.x (wheel) or ROCm 10 with torch.version.hip 7.15 (source build), and VLLM_INSTALL_METHOD=wheel|source must match the detected route; the wheel route now fails when the ROCm torch version cannot be resolved from VLLM_ROCM_INDEX (previously a warning that fell back to vLLM's own torch pin).

  • PR addresses single concern: no. It addresses the ROCm10 validation campaign and the Hyperloom runtime blockers found by that campaign. The commit stack is separated by subsystem to make review tractable.

  • Root cause is upstream (Magpie/TraceLens/GEAK/IntelliKit/AgentKernelArena), ticket filed: ROCm10 SGLang CPU-only traces are rooted in upstream PyTorch/Kineto rocprofiler-sdk flush behaviour (see [ROCm] Fix teardown memory corruption and truncated traces on the rocprofiler-sdk backend pytorch/kineto#1564). This PR does not patch Kineto; it records/suppresses unsupported GPU traces and leaves image selection/custom torch build as the runtime workaround.

The SGLang docker stack moved to ROCm 10 in #1593, but vLLM stayed on
vllm/vllm-openai-rocm:v0.29.0, so a single example told the reader to pair a
ROCm 10 SGLang image with a ROCm 7.2.3 vLLM one, and the compatibility matrix
still claimed every bare-metal patch level matched its container image.

Move the docker route to rocm/vllm, the only repository publishing a ROCm 10
vLLM build, and say plainly that the bare-metal wheel stays on rocm723 rather
than tracking it.
The ROCm wheel index publishes vLLM for ROCm 7.2 only, so a ROCm 10 host had
no installable vLLM at all and setup failed with no usable route. Add a
VLLM_INSTALL_METHOD that resolves to a source build from VLLM_REPO at
VLLM_SOURCE_REF when the detected ROCm is 10 or newer, and keep the wheel
path for 7.2 where it still resolves. The resolved VLLM_ROOT and its build
target are persisted so the later run launches the interpreter that was
actually built rather than rediscovering one.
The framework agent optimizes by patching vLLM source, but the image serves an
installed wheel, so a patch applied to the image checkout changed nothing that
ran. Create a git worktree from a synthetic baseline commit over the image
checkout and put it ahead of site-packages on the server's PYTHONPATH, so a
patch reaches the process being benchmarked and can be reverted cleanly.
vLLM writes its rank traces directly into the benchmark workspace rather than a
torch_trace subdirectory, which was not among the probed directories. Discovery
fell through to the graph-capture sidecars and roofline then failed with
capture_only_fallback while a complete trace sat in the root: two runs of the
ROCm 10 leg lost kernel attribution that way.
On one exact ROCm 10 Qwen3-14B-FP8 stack this lever raises throughput while
GSM8K collapses to zero, so benchmarking it can only promote a configuration
that is faster and wrong. Drop those variants before they run, keyed on the
stack fingerprint so every other stack keeps the lever.

The aiter component is matched through the git-describe dist version as well as
the plain commit: the fingerprint degrades to that form when AITER_COMMIT is
unset, and an unmatched pin would silently let the lever back in.
AITER root resolution only understood split aiter_meta wheels, so on a source
checkout it returned nothing, AITER_ROOT_DIR went unset, and the GEMM tuner
tuned a different AITER than the one serving. Resolve the source layout too,
and make the tune/serve alignment preflight fail the run instead of warning:
tuning results attributed to the wrong library are worse than no tuning.

The fingerprint degraded to "unknown" for rocm wherever ROCm comes from wheels
rather than /opt/rocm, which made any pin naming rocm structurally unmatchable
on bare metal. Read it from rocm-sdk-core, and record torch as well, since the
torch build selects Kineto's GPU backend and the ROCm 10 legs differ in nothing
else. The backend itself is read from the trace metadata that names it.
An open Kineto bug on the rocprofiler-sdk backend loses every GPU event, so
trace analysis fails identically on each attempt. Only the watermark path was
bounded, at three retries; cycle_start, the KERNEL re-profile and CLOSE kept
arming fresh work that could not succeed, spending the profile budget of an
unattended run on it.

Condemn the stack once, from a parsed trace that carries host ops beside zero
kernels, and suppress the remaining enqueue paths with the reason recorded. The
verdict demands evidence on both sides: an empty or unreadable capture is a
transient failure, and treating it as permanent would give up kernel
attribution for the rest of the session.
A serving round waited on its Ray actor with no wall-clock bound, so a lease
that never acquired resources held the round open indefinitely. Probe the actor
and cap that wait.

A stalled source-patch specialist was re-dispatched every round even when its
source root made the attempt impossible. Prune that family after the first
deterministic preflight failure and skip a key already running or failed.

A long attempt key produced a report filename past the filesystem limit, so the
kernel lane's GEAK and tuning outcomes were never written. Shorten it with a
digest and record the unvalidated bucket and per-lane totals the final report
reads.
Hyperloom drives both the claude and the codex backend, and the CLI preflight
already treats an OpenAI-only environment as a valid single provider, but
bare-metal setup rejected it: the OpenAI side counted only as a rider on an
Anthropic credential from the same host. A gateway configured for codex alone
therefore failed setup with no usable endpoint.

Accept the OpenAI pair as a credential in its own right and persist it with
CODEX_MODEL. A base URL without a key stays an error, so a half-configured
gateway still fails at setup rather than at the first agent call.
KERNEL phase entry could spend minutes inside synchronous reprofile and tuning
work while the coordinator tick loop was frozen, so the idle guard never had a
chance to advance. Run phase-entry hooks out of band and record hook failures
from the background task instead.

Conc-sweep boot rounds were treated as measured variants even though their only
job is to get the persistent server ready. Run the boot as a server-only phase,
then measure the same concurrency through the reuse path; a sweep that ran but
produced no comparable pair now terminates as sweep_failed rather than
sweep_done.

Also stop deterministic source-patch failures from re-entering the scheduler:
forced stalled specialists skip any existing idempotency row, and delegated
source-patch specialists prune immediately when no framework Git root exists.
Several tests asserted provider selection and credential precedence while
inheriting the operator shell's Anthropic/OpenAI credentials, so they answered
for the machine that ran them rather than the fixture they constructed. Clear
only the variables each test owns before asserting on those branches.

Subprocess-based tests also inherited an ambient PYTHONPATH that could resolve a
stale installed Hyperloom instead of this checkout. Put this tree's src first for
child processes during pytest, matching CI's editable install behaviour.
@lishuoshuo-amd
lishuoshuo-amd force-pushed the fix/rocm10-vllm-image-in-skill branch 2 times, most recently from 19a222b to a7eb8ab Compare September 22, 2026 09:19
Comment thread src/hyperloom/inference_optimizer/tests/test_ray_backend_unit.py Fixed
Comment thread src/hyperloom/inference_optimizer/tests/test_ray_backend_unit.py Fixed


def _functions(*names: str) -> str:
text = _INSTALL_SH.read_text()
assert lease._actor is None

next_actor = _FakeActor((0, "next", ""))
monkeypatch.setattr(rb, "get_ray_backend", lambda: _StubBackendP2())
Comment thread src/hyperloom/orchestrator/phases/machine.py Fixed
Comment thread src/hyperloom/inference_optimizer/tests/test_ray_backend_unit.py Fixed
Comment thread src/hyperloom/inference_optimizer/tests/test_ray_backend_unit.py Fixed


def _functions(*names: str) -> str:
text = _INSTALL_SH.read_text()
Comment thread src/hyperloom/orchestrator/phases/machine.py Fixed
assert lease._actor is None

next_actor = _FakeActor((0, "next", ""))
monkeypatch.setattr(rb, "get_ray_backend", lambda: _StubBackendP2())
lishuoshuo-amd and others added 9 commits September 22, 2026 19:04
The detached-launch snippet puts $(dirname "$PYTHON") at the front of PATH, but
PYTHON is never set in that shell: the launch is its own process and
kernel-agent.env.sh does not export it. The unset variable collapsed the entry
to ".", leaving /usr/local/bin ahead of the virtualenv, so python3 resolved to
an interpreter without the optimizer's dependencies and the CLI died on
ModuleNotFoundError before a session dir existed.
On a ROCm 10 stack the installer routes vLLM to a source build, which never
touches the wheel index that carries aiter, and the only AITER installation
lived inside install_sglang_framework. A bare-metal vLLM host therefore ended
up with no aiter at all, so kernelforge's fusion validation - which runs vLLM
with VLLM_ROCM_USE_AITER=1 - had nothing to enable.

Three gaps close together:

- AITER installation moves into ensure_aiter_for_python so the vLLM source
  route installs it into the isolated overlay, on both the fresh-build and the
  reuse path.
- VLLM_ROCM_USE_AITER lands in .env whenever the detected framework is vLLM and
  aiter actually imports. The SGLang images ship SGLANG_USE_AITER pre-set; vLLM
  defaults its gate off, so nothing turned it on. It stays unset when aiter is
  missing, because an enabled gate without a matching module fails at serve
  time rather than falling back.
- The source checkout is a depth-1 fetch of a commit SHA and carries no tags,
  so setuptools_scm stamped builds 0.1.dev1 instead of the release. The build
  now declares its version the same way the SGLang source install does.
The per-distribution SETUPTOOLS_SCM_PRETEND_VERSION_FOR_VLLM never applied: the
source route builds with setup.py develop, which leaves the distribution name
unknown, and vcs-versioning only consults the per-distribution form once it
knows that name. A bare-metal vLLM leg therefore still reported 0.1.dev1 on a
build that had been told its version. Set the generic variable alongside it;
this invocation builds vllm alone, so nothing else can pick it up.

Extracting the AITER install into ensure_aiter_for_python also broke the
bare-metal SGLang test, which sources install_sglang_framework on its own and
so lost the helper that decides whether to install. It now sources both.
Magpie prepends its own profiler flags before EXTRA_VLLM_ARGS, so the
torch_profiler_dir this layer injects is the one vLLM keeps, not the one the
launcher computes. The injected value is the run's output dir, while discovery
probed only the Magpie workspace and its children, so a complete rank trace sat
one level above every probed path and roofline failed three attempts with
no_trace_files before giving up.

Pin Magpie's own trace dir to the same directory through the environment
variable its launcher already reads, so both sides agree whichever flag wins,
and probe the output dir as well. The comment claiming the injected path does
not matter is what allowed the two to drift, so it goes too.
The model-level ceiling and the bypass roofline both resolve achievable peaks
first and vendor theoretical peaks when a GPU has no measured entry. The
TraceLens arch spec was left out of that rule, so on MI355X it returned nothing
at all: with no bundled arch data and a microbenchmark that cannot run while the
serving process holds the only visible GPU, the whole trace_analyze step failed
and every leg continued without a roofline.

Resolve it the same way the other two do. Measured entries still win, so nothing
changes for a GPU that has one.
Profile executors record the workspace they profiled into, not the trace file,
so opening that path raised IsADirectoryError, which the OSError guard swallowed
into an empty result. The backend was therefore never recorded on any run, and
the condemn path that cites it in its reason had nothing to cite.

Resolve a directory to the trace inside it. This is the field that distinguishes
a roctracer build from a rocprofiler-sdk one, which is exactly what a ROCm trace
carrying no GPU kernels has to be judged against.
Moving every phase-entry hook onto a background task freed the tick loop, but it
also detached the effects callers read straight after the transition returns: a
caller advancing into CLOSE sees the sequencer settle as part of that call, and
a hook that had not run yet left it looking at an unsettled phase.

Keep the background task for KERNEL, whose reprofile and tuning work is the
minutes-long case the idle guard needed protecting from, and await the rest.
A hook that raises is still logged and recorded rather than failing the
transition, since that hook also closes the phase it left.

The affected tests stubbed _on_phase_entered with the two-argument signature it
had before reason and evidence were added; awaiting the hook surfaced that as a
TypeError the background task used to swallow.
Both lease.close() calls sat inside assert expressions, so running the suite
under -O would drop the close along with the check and leave the second call
asserting idempotency over a teardown that never happened.
The matrix still listed vllm/vllm-openai-rocm:v0.29.0 as the validated image
and described bare-metal vLLM as the rocm723 wheel only, while the installer
now builds vLLM from source on ROCm 10 and the docs elsewhere point at the
rocm/vllm image. Record the source route, its prerequisites and the two new
pins, and guard VLLM_SOURCE_REF the same way SGLANG_REF is guarded.

Co-authored-by: Cursor <cursoragent@cursor.com>
@lishuoshuo-amd lishuoshuo-amd changed the title Fix/rocm10 vllm image in skill Bring up vLLM on ROCm 10 and fix scheduler stalls found in the 6-leg run Sep 23, 2026
Running the KERNEL entry hook as a background task freed the tick loop, but
the phase machine kept evaluating KERNEL exits while GEAK was still running
inside that hook. GEAK is not a queued KERNEL-lane task and stamps no inline
heartbeat, so the idle guard fired about ten minutes in: on the 10h ROCm 10
run every GEAK leg left KERNEL 40-75 minutes into a 2-3 hour GEAK run, the
conc_sweep then booted servers on the same GPUs, later KERNEL entries started
further GEAK runs alongside the first, and GEAK was still writing after the
session had closed.

Keep the hook out of band, but hold every transition out of KERNEL and the
idle streak while it runs, and let run() wait for it before teardown.

Co-authored-by: Cursor <cursoragent@cursor.com>

@xiaofei-zheng xiaofei-zheng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No blocking issues — LGTM.

Paths I checked:

  • Every _enqueue_internal_analysis_task call site handles the new None return (close.py:98, kernel.py:304, kernel.py:4253, macro_cycle.py:318, prelude.py:2644).
  • sweep_failed is a registered stop reason with a terminal CLOSE handler, and the new branch cannot fire on a sweep that produced pairs: was_skipped + budget_exhausted only co-occur when successful_pairs == 0.
  • Out-of-band KERNEL entry hook: the phase machine holds transitions out of KERNEL and the idle streak while it is in flight, and Coordinator.run awaits it on teardown; a failed or cancelled hook is logged through _record_phase_entry_task_result.
  • GpuSpecialistLease.close forcing the kill after an unconfirmed stop is safe: COOPERATIVE_REAP_BUDGET_SEC is 8.5s (0.5 + 5.0 + 1.0 + 2.0) against CLOSE_STOP_TIMEOUT_SEC 10.0, so a cooperative reap fits inside the bounded stop. Same shape as ServingLease.close.
  • RESOURCE_ACQUIRE_TIMEOUT_SEC bounds only the actor-scheduling phase; the round's own ray.get(ref) still carries no timeout, so the invariant the deleted test_serving_lease_coordinator_no_timeout protected is preserved.
  • gpu_trace_unsupported_reason is an additive defaulted field, so no SCHEMA_VERSION bump is needed; it is also added to the LLM-forbidden allowlist in policy/gate.py.
  • The source-patch preflight cannot prune a specialist the runner would have accepted: the router's root set is a superset of SpecialistSubprocessConfig.framework_source_roots, and both resolve preferred from the same session framework.
  • conc_sweep boot-only rung: excluded from measured points and re-measured as the first reuse point via grid[boot_idx:]; kernel_opt_outcome has no programmatic in-tree consumer beyond the JSON and report.py:1160, which only re-reads ints.
  • Image/pin move is consistent across docs/compatibility.rst, docs/install/install.md, the four example SKILL.md files, install.sh (VLLM_IMAGE_SOURCE_COMMIT) and install_baremetal.sh (VLLM_SOURCE_REF).
  • Title and description match the current diff, including the declared behaviour changes (schema v2 + lane_totals, sweep_failed, non-zero exit on a hard aiter preflight mismatch).

Tests: no local suite run (CI is green at e29247d). I only ran a standalone repro against Recorder._stable_item_filename to check the long-fragment-name path; it behaves as intended for names over the filesystem limit.

@xiaofei-zheng
xiaofei-zheng merged commit 44d8731 into main Sep 23, 2026
33 checks passed
@xiaofei-zheng
xiaofei-zheng deleted the fix/rocm10-vllm-image-in-skill branch September 23, 2026 05:38
@lishuoshuo-amd lishuoshuo-amd mentioned this pull request Sep 23, 2026
6 tasks
lishuoshuo-amd added a commit that referenced this pull request Sep 23, 2026
KERNEL entry runs out of band since #1605 and the phase is held until the hook
returns. In this walkthrough the hook launched geak_runner.py as a real
subprocess, and KERNEL only reached a terminal geak_result once that process
failed, so whether eleven ticks reached CLOSE depended on how fast the runner
exited on the CI host.

The runner path is made unresolvable, which takes the existing
runner_not_found branch: GEAK records a terminal result in-process and the
phase winds down to SWEEP without starting anything. The test now also pins
that branch, so a routing change cannot quietly bring the subprocess back.

Co-authored-by: Cursor <cursoragent@cursor.com>
zoroyihan7 added a commit that referenced this pull request Sep 23, 2026
#1605 landed a bound on the same wait this branch bounds, and the two guard
different windows rather than the same one.

Its deadline covers the window before Ray places the actor, and kills the actor
when it expires. That is safe there and only there: an actor that never acquired
resources has spawned no server, so nothing survives the kill still mapping
GPUs. Once it has been placed, #1605 returns to an unbounded wait, which is the
window this branch's ceiling covers -- and where killing the actor is exactly
what must not happen, because the round may still be serving.

Keep both, in one loop: the acquire probe and its kill, then the ceiling and its
quarantine. Dropped from #1605's version is the bare ``ray.get(ref)`` taken for
an uncancellable round once placed; it carries no deadline, so it reopens the
parked-thread failure for precisely the rounds no cancel will ever reach. Those
rounds keep polling instead, which costs one wakeup per 0.25s and puts them
under the ceiling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

3 participants