fix(jit): isolate AITER extensions from HIP interposers - #4566
JohnQinAMD wants to merge 2 commits into
Conversation
🏷️ CI GuideRuns automatically on every PR:
Extended tests (opt-in via labels):
|
There was a problem hiding this comment.
Pull request overview
This PR hardens AITER’s JIT extension loading by ensuring the HIP runtime (libamdhip64.so) is promoted into the process-global dynamic symbol scope before any AITER pybind extension is imported, avoiding import-order–dependent initialization failures.
Changes:
- Add a one-time, process-lifetime loader
_load_hip_runtime_global()inaiter/jit/core.pythatdlopenslibamdhip64.sowithRTLD_GLOBAL. - Call the loader immediately before the first JIT extension import in
get_module_custom_op. - Add unit tests validating (1) global load occurs once and doesn’t mutate Python’s
dlopenflags, and (2) HIP promotion happens before extension import.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| aiter/jit/core.py | Promotes HIP runtime to RTLD_GLOBAL once before importing the first JIT extension module. |
| op_tests/test_jit_loader.py | Adds regression tests for one-time global HIP load and correct loader/import ordering. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
f495153 to
df629a6
Compare
df629a6 to
c013f44
Compare
Load AITER-owned native objects with RTLD_DEEPBIND so an earlier RTLD_GLOBAL compatibility stub cannot preempt their HIP dependencies. Keep the policy local to each loader and provide AITER_DISABLE_DEEPBIND=1 for sanitizer and intentional-interposition workflows. Add a regression that recreates TileLang's versioned-to-legacy hipGetDeviceProperties forwarding and exercises the pybind, ctypes, and FMHA paths. Signed-off-by: Yanyuan Qin <yanyuan.qin@amd.com>
c013f44 to
af234f0
Compare
|
Q: does RTLD_DEEPBIND affect HIP API tracing? Nit: RTLD_NOW here is an unrelated behavior change |
Signed-off-by: Yanyuan Qin <yanyuan.qin@amd.com>
@zufayu Thanks for raising this. I tested both the pybind and ctypes loader paths with Test environment:
From a container where this PR checkout has been built, the test can be reproduced with: export HIP_VISIBLE_DEVICES=0
export PYTHONPATH="$PWD${PYTHONPATH:+:$PYTHONPATH}"
TRACE_ROOT=$(mktemp -d /tmp/aiter-rocprof.XXXXXX)
profile_op() {
local name="$1"
local code="$2"
for mode in 0 1; do
AITER_DISABLE_DEEPBIND="$mode" \
rocprofv3 \
--log-level error \
--hip-trace \
--stats \
--output-format csv \
--output-directory \
"$TRACE_ROOT/$name/deepbind-$mode" \
--output-file "$name" \
-- \
python -c "$code"
done
}
profile_op topk_pybind '
import torch
import aiter
gate = torch.randn((128, 128), device="cuda", dtype=torch.float32)
weights = torch.empty((128, 8), device="cuda", dtype=torch.float32)
indices = torch.empty((128, 8), device="cuda", dtype=torch.int32)
tokens = torch.empty((128, 8), device="cuda", dtype=torch.int32)
aiter.topk_softmax(weights, indices, tokens, gate, False)
torch.cuda.synchronize()
'
profile_op topk_ctypes '
import torch
from aiter.ops.moe_op import topk_softmax_asm
gate = torch.randn((128, 128), device="cuda", dtype=torch.float32)
weights = torch.empty((128, 8), device="cuda", dtype=torch.float32)
indices = torch.empty((128, 8), device="cuda", dtype=torch.int32)
tokens = torch.empty((128, 8), device="cuda", dtype=torch.int32)
topk_softmax_asm(weights, indices, tokens, gate, False)
torch.cuda.synchronize()
'
python - "$TRACE_ROOT" <<'PY'
import csv
import sys
from pathlib import Path
root = Path(sys.argv[1])
for op in ("topk_pybind", "topk_ctypes"):
results = {}
for mode in (0, 1):
path = (
root
/ op
/ f"deepbind-{mode}"
/ f"{op}_hip_api_stats.csv"
)
with path.open() as f:
results[mode] = {
row["Name"]: int(row["Calls"])
for row in csv.DictReader(f)
}
assert results[0] == results[1], {
"deepbind_enabled": results[0],
"deepbind_disabled": results[1],
}
print(f"PASS {op}: {len(results[0])} HIP APIs, counts match")
for name in (
"hipLaunchKernel",
"hipModuleLaunchKernel",
"hipGetDevicePropertiesR0600",
"hipDeviceSynchronize",
):
if name in results[0]:
print(f" {name}: {results[0][name]}")
PY
The result was: The complete API-name/count dictionaries matched in both comparisons; only durations varied between runs. Therefore, rocprofv3's callback-based HIP tracing is unaffected by |
@zufayu Good catch — agreed. RTLD_NOW was an unrelated behavior change and could make an otherwise unused unresolved symbol fail eagerly at library load time.
This preserves the previous lazy symbol-resolution behavior while adding only the intended RTLD_DEEPBIND isolation, consistent with the other loader sites. |
|
The fix is technically sound, but it changes dynamic-linking semantics for every AITER user, on by default, to work around a third-party bug. Two things follow.
|
Summary
Load AITER-owned native objects with per-object
RTLD_DEEPBINDwhere it is available. This prevents a HIP compatibility stub that was loaded earlier into the ELF global scope from interposing incompatible HIP symbols in AITER JIT extensions.Related to AITER issue #4585, which contains the standalone real-library reproducer, controlled loader results, and the full TileLang/vLLM root-cause analysis.
The loader covers all native-library paths currently used by AITER:
aiter.jit.core;ctypes;SharedLibraryhelper.The implementation does not change the process-wide
sys.setdlopenflags()state.AITER_DISABLE_DEEPBIND=1provides a process-start opt-out for ASan, intentionalLD_PRELOADinterposition, or tools that are incompatible with deep binding.This PR is intended as a reference implementation and regression test, depends on how necessary of the protection is needed.
Problem
This is an ELF symbol-interposition failure caused by the interaction of a TileLang 0.1.10 HIP stub, TVM's global loading policy, and AITER extensions that are loaded later. It is not an invalid FMHA input. The vLLM regression, import chain, CI failures, and original AITER MoE reproducer are tracked in vLLM issue #51151.
What TileLang's HIP stub is intended to do
TileLang #1867 added
libhip_stub.soso TileLang/TVM can be imported without eagerly linking the real HIP runtime. The stub exports drop-in HIP wrapper functions. On first use, it locateslibamdhip64.soand resolves the real functions withdlopen/dlsym.That implementation creates two separate cross-library problems in TileLang 0.1.10.
1. The private stub enters the process-global symbol scope
Importing TileLang imports its bundled TVM Python package. TVM deliberately opens
libtvm_runtime.sowithRTLD_GLOBAL:The packaged
libtvm_runtime.sohas aDT_NEEDEDdependency onlibhip_stub.so:The global loading mode applies to that dependency group. Consequently, the stub's standard
hip*wrapper names become eligible to satisfy symbol references from unrelated libraries loaded later in the process. In the pinned image, the installed stub exports 30 HIP functions, includinghipMalloc,hipFree,hipGetDevice,hipModuleLaunchKernel, and:An AITER JIT extension normally loaded afterward can therefore resolve a HIP reference to TileLang's stub even though the extension itself has a dependency on the real
libamdhip64.so.2.
hipGetDevicePropertiescrosses an incompatible ABI boundaryWith current ROCm headers, the source-level API and type are macros:
The wrapper in TileLang v0.1.10
hip.ccis written with those source-level names:The preprocessor therefore compiles this as a wrapper exported under
hipGetDevicePropertiesR0600and taking anhipDeviceProp_tR0600*. However, the dispatch table resolves a hard-coded string:Macro expansion does not happen inside a string literal. The R0600 wrapper therefore calls the legacy, unversioned function. The callee writes a different device-properties layout into memory that the caller interprets as
hipDeviceProp_tR0600. The call can return without an obvious loader error, but fields used for device selection and kernel launch sizing are invalid.How this reaches AITER and Kimi-K3
The failing order in vLLM is:
LD_DEBUG=bindingsconfirms the critical binding in the Kimi-K3 process:A valid BF16/head-size-128 invocation then fails with the misleading error:
The same mechanism also explains the
invalid configuration argumentfailures seen in AITER MoE paths: corrupted device properties are cached and later used to calculate a kernel launch grid. This was the initially reported failure in vLLM issue #51151, which shows the TileLang import chain and a minimaltopk_softmax/moe_sortingreproducer.The following controls use the same image, tensors, AITER operation, and real TileLang library. They isolate global visibility as the condition that changes the result:
libhip_stub.soRTLD_LOCALRTLD_GLOBALinvalid argumentlibamdhip64.sogloballyWhy vLLM #50879 exposed it
vLLM #50879 changed optional expert-parallel imports from eager to selected-branch imports. Before that change, the guarded Mori import happened early and opened the real HIP runtime with
RTLD_GLOBALbefore TileLang. That favorable load order accidentally masked the TileLang interposer. The resulting ROCm regression was reported and analyzed in vLLM issue #51151.After Mori became lazy, Kimi-K3—which does not select Mori—could import TileLang first. The PR exposed an existing load-order dependency; it did not create the TileLang ABI bug. vLLM #51110 restores the guarded eager Mori import as a compatibility hotfix, but it only restores the masking order. vLLM #51159 instead defers the TileLang import on ROCm and fixes the reported vLLM startup path, but it also avoids the trigger by changing import timing rather than correcting TileLang's exported symbols or protecting AITER's loader.
Permanent TileLang correction
The originating corrections belong in TileLang:
hipGetDevicePropertiesmacro rather than using an unversioned literal; andThis AITER change is independent defensive hardening. Even after TileLang fixes this specific stub, an AITER extension can encounter other global interposers. AITER's native objects should not accidentally bind their HIP dependencies to an unrelated optional backend solely because that backend was imported first.
Why promoting libamdhip64 with RTLD_GLOBAL is insufficient
The previous version of this PR promoted the real
libamdhip64.sohandle into the global scope. That does not fix a process in which the incompatible stub is already earlier in ELF global lookup order; reopening the real HIP runtime does not reorder that scope. The controlled reproduction still fails in this order:Likewise, eagerly importing AITER before TileLang only changes load order. It can mask the problem, but it does not protect later or lazily compiled AITER extensions.
Fix
For a pybind extension,
_deep_import()first resolves the extension path and opens that exact object with:The normal Python import then reuses the already loaded object. AITER retains the
ctypes.CDLLhandle for process lifetime. This is scoped to the AITER object and avoids a temporary process-global flag change, which would be racy with unrelated imports in other threads.The same policy is applied directly to AITER's other
ctypesand C++dlopenpaths. On platforms whereRTLD_DEEPBINDis unavailable, the added flag is zero and the existing loader behavior is preserved.Set the following before importing AITER to disable the policy:
export AITER_DISABLE_DEEPBIND=1This opt-out is necessary for AddressSanitizer because ASan explicitly rejects objects opened with
RTLD_DEEPBIND. It is also useful when HIP interposition is deliberate. Disabling it restores the original loader behavior and therefore also re-exposes AITER to this TileLang failure.Reproduction
This deterministic reproducer explicitly places TileLang's real HIP stub in the
global ELF scope before AITER loads its FMHA extension. It does not depend on
vLLM's current optional-kernel import order.
The existing loader fails with:
With this PR, the same invocation prints
PASS. SettingAITER_DISABLE_DEEPBIND=1restores the failure, confirming that the resultcomes from loader isolation. Full controls and loader evidence are in
AITER issue #4585.
Validation
The regression test covers pybind, standalone
ctypes, generated-library,MoE sorting, and Kimi-K3 FMHA loading paths.
The C++ loader change also passes a syntax-only compile in the same container.
Kimi-K3 model A/B
The real Kimi-K3 MXFP4 checkpoint was tested on 8x MI355X with TP8 and a 1M
maximum context. The test used the immutable image above, vLLM source at
d31de3c421c0281a959ac1a3cbe1a7f354bae179, and a controlledTorch -> global TileLang stub -> AITER load order.
The pinned nightly has an unrelated Gluon API mismatch: Triton expects
block_bases, while the tested AITER MLA source passescga_layout. Theend-to-end run therefore used
VLLM_ROCM_USE_AITER_MLA=0. The affected ViTFMHA path remained AITER-backed and exercised the loader changed by this PR.
No documentation file is included in this PR.
Related
Prepared with OpenAI Codex assistance.