fix(aiter): rebuild the shim link line when the AITER library moves - #283
fix(aiter): rebuild the shim link line when the AITER library moves#283demandal25 wants to merge 4 commits into
Conversation
An AITER-backed module cached under .../gfx950/ can keep loading a gfx942 AITER
library and segfault -- and neither setting FLASHINFER_ROCM_ARCH_LIST correctly
nor unsetting it recovers. Only deleting ~/.cache/flashinfer does.
The AITER shim libs live outside the JIT tree, under
aiter_libs/<arch>__aiter-<version>/, and reach the module only as an -L/-rpath
on the link line. JitSpec.build() writes build.ninja only when it is missing
(core.py:323), so once a module has been built the recorded link line is never
revisited. The resulting .so carries a RUNPATH into the original directory.
Observed on an MI350X, and this is the mechanism behind a crash that first
looked like a broken AITER kernel:
build.ninja 12:51:07 written with aiter_libs/gfx942__aiter-0.1.10
.so 12:53:28 relinked from that stale ninja, RUNPATH -> gfx942
test_activation_aiter_hip.py::test_silu_and_mul_aiter_vs_ref -> SIGSEGV
The second run had FLASHINFER_ROCM_ARCH_LIST=gfx950 set and did build a correct
gfx950 library alongside; it was simply never linked. AITER's silu_and_mul is
fine on gfx950 -- 33/33 pass on a clean cache -- so without this fix the obvious
reading of the crash is that CDNA4 support is broken, which it is not.
CLAUDE.md already documents the build.ninja staleness as a developer gotcha
("changing env vars is a silent no-op"). What is new is that on this path it is
not a no-op but a segfault, reachable by following our own documented workaround
for the wrong-arch build.
Fix: the three AITER-linked generators route their spec through
refresh_aiter_jitspec(), which calls write_ninja() unconditionally. write_ninja
funnels through write_if_different, so it costs nothing when the link line is
unchanged and rewrites exactly when it moves; ninja then relinks on its own.
Changing the spec name to include the AITER tag was considered and rejected: the
name becomes TORCH_EXTENSION_NAME, and an AITER version such as 0.1.10 is not a
valid C++ identifier.
CDNA3 impact: none. On a single-architecture host the resolved tag never
changes, write_if_different makes the extra call a no-op, and no link line is
rewritten. The behaviour only differs when the AITER library path changes
between runs, which on a gfx942-only machine it does not.
Verified on MI350X (gfx950), ROCm 7.2.0, torch 2.9.1, amd-aiter 0.1.10, A/B
against one poisoned cache with FLASHINFER_ROCM_ARCH_LIST=gfx950 in both arms:
control (unmodified amd-integration): exit 139, SIGSEGV
treatment (this branch): 33 passed
build.ninja rpath gfx942__aiter-0.1.10 -> gfx950__aiter-0.1.10
.so RUNPATH gfx942__aiter-0.1.10 -> gfx950__aiter-0.1.10
Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR addresses a ROCm/AITER JIT cache failure mode where a previously generated build.ninja can retain stale AITER link flags (notably -L/-rpath), causing a module cached under one arch directory (e.g. gfx950/) to continue loading a different-arch AITER library (e.g. gfx942/) and potentially segfault. The fix introduces an explicit “refresh” step that regenerates the ninja file so link lines track the resolved AITER library location across runs.
Changes:
- Add
refresh_aiter_jitspec(spec)to force regeneration ofbuild.ninjafor AITER-backed specs. - Route the AITER generators in activation, norm, and RoPE through
refresh_aiter_jitspec(...)so link-line changes take effect without manual cache deletion.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| flashinfer/jit/aiter_source.py | Adds refresh_aiter_jitspec() to rewrite build.ninja when AITER lib paths change. |
| flashinfer/jit/activation.py | Wraps AITER activation JIT spec creation with the refresh helper. |
| flashinfer/jit/norm.py | Wraps AITER RMSNorm JIT spec creation with the refresh helper. |
| flashinfer/jit/rope.py | Wraps AITER RoPE JIT spec creation with the refresh helper. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Address review feedback on refresh_aiter_jitspec(): - Take spec.lock_path around write_ninja(). write_if_different truncates in place, so an unlocked rewrite could empty build.ninja while a concurrent builder's ninja is reading it. This is the same lock JitSpec.build() holds while running ninja, and matches what build_jit_specs() already does before writing a manifest. Reachable in practice: pytest -n auto shares one JIT cache across processes. - Skip the write entirely when the module is AOT-prebuilt or when FLASHINFER_DISABLE_JIT is set. build_and_load() loads straight from aot_path in the first case and build() raises in the second, so ninja never reads the manifest and rewriting it is pure filesystem noise. No nesting risk: callers run gen_*_aiter_module() to completion before build_and_load() acquires the same lock.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (1)
flashinfer/jit/aiter_source.py:185
refresh_aiter_jitspecis introduced without type annotations, while the rest of this module consistently annotates public helpers (e.g.resolve_aiter_build_arch() -> str). Adding aJitSpecinput/return annotation improves readability and static checking for the new helper that's now part of the JIT-generator API surface.
def refresh_aiter_jitspec(spec):
Address a suppressed review comment: refresh_aiter_jitspec() was the only unannotated def in aiter_source.py, while the module annotates its other helpers (resolve_aiter_build_arch() -> str, _aiter_libs_dir() -> Path). JitSpec is imported from .core alongside the existing logger import; core does not import aiter_source, so this adds no cycle.
|
Suppressed comment from review 4964930619 — read and addressed.
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (1)
flashinfer/jit/aiter_source.py:224
refresh_aiter_jitspec()introduces new behavior that is critical for correctness (rewritingbuild.ninjato pick up a moved AITER-L/-rpath), but there is currently no unit test coverage for it. The existing ROCm tests coveraiter_source.resolve_aiter_build_arch()/_aiter_libs_dir()(e.g.tests/rocm_tests/test_aiter_build_arch_hip.py), but nothing exercises the new refresh path (short-circuit forspec.is_aot/FLASHINFER_DISABLE_JIT, and callingspec.write_ninja()under the per-spec lock). Please add a small GPU-free test that monkeypatches a dummy spec’swrite_ninjaandlock_pathto assert the expected call/skip behavior.
if spec.is_aot or os.environ.get("FLASHINFER_DISABLE_JIT"):
return spec
with FileLock(spec.lock_path, thread_local=False):
spec.write_ninja()
return spec
Address a suppressed review comment asking for unit coverage of refresh_aiter_jitspec(), whose behaviour is correctness-critical: without the refresh a module cached under one arch keeps loading another arch's AITER lib and segfaults. GPU-free by construction, following test_aiter_build_arch_hip.py: a StubSpec records write_ninja() calls and carries a real lock file, so nothing compiles or touches a device. Runs in ~1s. Covers the JIT path (writes), the two short-circuits (AOT-prebuilt and FLASHINFER_DISABLE_JIT), and that the write blocks while a concurrent holder owns the lock. Confirmed to be a real regression test, not a vacuous one: reverting refresh_aiter_jitspec() to its pre-fix body fails three of the four (the AOT and DISABLE_JIT skips write anyway, and the lock test measures a 0.000s wait).
|
Suppressed comment from review 4965876749 — read and addressed.
Covers the JIT path (writes), both short-circuits ( Checked as a real regression test rather than a vacuous one — reverting |
Summary
An AITER-backed module cached under
.../gfx950/can keep loading a gfx942 AITER library and segfault — and neither settingFLASHINFER_ROCM_ARCH_LISTcorrectly nor unsetting it recovers. Only deleting~/.cache/flashinferdoes.Observed on an MI350X, and this is the mechanism behind a crash that first looked like a broken AITER kernel:
The second run had
FLASHINFER_ROCM_ARCH_LIST=gfx950set and did build a correct gfx950 library alongside — it was simply never linked.What changed
flashinfer/jit/aiter_source.py— newrefresh_aiter_jitspec(spec): callswrite_ninja()so a changed AITER library path takes effect.flashinfer/jit/{activation,norm,rope}.py— the three AITER-linked generators route their spec through it.Architecture / design notes
The AITER shim libs live outside the JIT tree, under
aiter_libs/<arch>__aiter-<version>/, and reach the module only as an-L/-rpathon the link line.JitSpec.build()writesbuild.ninjaonly when it is missing (core.py:323), so once a module has been built the recorded link line is never revisited, and the resulting.socarries aRUNPATHinto the original directory.CLAUDE.mdalready documents this staleness as a developer gotcha — "changing env vars is a silent no-op". What is new is that on this path it is not a no-op but a segfault, reachable by following our own documented workaround for a wrong-architecture build.write_ninjafunnels throughwrite_if_different, so calling it unconditionally costs nothing when the link line is unchanged and rewrites exactly when it moves; ninja then relinks on its own.Two refinements in the second commit. The refresh is scoped to the JIT path: an AOT-prebuilt module loads straight from
aot_path, and aFLASHINFER_DISABLE_JITrun raises before ninja is consulted, so in both cases the manifest has no reader and rewriting it would be pure filesystem noise. And the write now takesspec.lock_path— the same lockJitSpec.build()holds while ninja runs — becausewrite_if_differenttruncates in place: without it, a concurrent builder (pytest -n autoshares one JIT cache across processes) could have the manifest emptied under it mid-read.Changing the spec name to include the AITER tag was considered and rejected: the name becomes
TORCH_EXTENSION_NAME, and an AITER version such as0.1.10is not a valid C++ identifier.Worth stating plainly, because the failure is misleading: AITER's
silu_and_mulis fine on gfx950 — 33/33 pass on a clean cache. Without this fix the obvious reading of the crash is that CDNA4 support is broken, which it is not.Relationship to #281
They compound and are best landed together. #281 stops the wrong-architecture build at the source; this PR is what lets an already-poisoned cache recover. A user upgrading to #281 alone would still segfault until they deleted
~/.cache/flashinfer.CDNA3 impact
None. On a single-architecture host the resolved tag never changes,
write_if_differentmakes the extra call a no-op, and no link line is rewritten. Behaviour differs only when the AITER library path changes between runs, which on a gfx942-only machine it does not.Test plan
Verified on MI350X (gfx950), ROCm 7.2.0, torch 2.9.1, amd-aiter 0.1.10. A/B against one poisoned cache,
FLASHINFER_ROCM_ARCH_LIST=gfx950in both arms:amd-integrationbuild.ninjarpath:gfx942__aiter-0.1.10→gfx950__aiter-0.1.10.soRUNPATH:gfx942__aiter-0.1.10→gfx950__aiter-0.1.10pre-commit run -a