Skip to content

HRX: libhip_intercept wraps only 331 of 540 HIP symbols (rest silently unobservable) #410

Closed

Description

@bong-water-water-bong

HRX: libhip_intercept wraps only 331 of 540 HIP symbols, so the rest are silently unobservable

libhip_intercept.so is the drop-in proxy that forwards HIP calls to the real backend and
optionally routes them through a logging/tracing interceptor. It exports 331 of the
540 symbols that HRX's libamdhip64.so exports, so 215 calls are never intercepted.

This does not break those calls — the proxy loads the backend with
RTLD_NOW | RTLD_GLOBAL, so unwrapped functions resolve directly against the backend. The
effect is that any tool relying on the proxy for visibility (logging, tracing, buffer
inspection) sees nothing at all for those 215 functions.

Repro

Compare the two export tables:

nm -D --defined-only libamdhip64.so.7 | awk '{print $3}' | sed 's/@.*//' | sort -u > A   # 540
nm -D --defined-only libhip_intercept.so | awk '{print $3}' | sed 's/@.*//' | sort -u > B # 331
comm -23 A B | wc -l    # 215

215 symbols present in the backend and absent from the proxy. 166 of those 215 have real
implementations in HRX
(the remainder are the hipErrorNotSupported stubs).

Confirmed at runtime that the calls still work

hipDeviceGetExecutionCtx (HIP 7.14, a real implementation, absent from the proxy's table):

direct backend        hipDeviceGetExecutionCtx -> 0 (valid ctx handle)
via libhip_intercept  hipDeviceGetExecutionCtx -> 0 (valid ctx handle)

So the gap costs observability, not correctness. Filed as a visibility defect.

Examples of the 166 real functions that are not intercepted

  • Execution / green context (HIP 7.14): hipDeviceGetExecutionCtx,
    hipExecutionCtxStreamCreate, hipExecutionCtxRecordEvent,
    hipExecutionCtxSynchronize, hipExecutionCtxGetDevice, ...
  • SM resource partitioning: hipDeviceGetDevResource, hipDevSmResourceSplit,
    hipDevSmResourceSplitByCount, hipDevResourceGenerateDesc, hipStreamGetDevResource
  • Driver graph API: hipDrvGraphAddMemcpyNode, hipDrvGraphAddMemsetNode,
    hipDrvGraphAddMemFreeNode, hipDrvGraphExec*NodeSetParams, hipDrvGraphMemcpyNode*Params
  • _spt variants: hipEventRecord_spt, hipMemcpy_spt, hipMemset_spt, ...
  • Legacy aliases: hipGetDevicePropertiesR0000 / R0600, hipChooseDeviceR0000 / R0600
  • Array / texture: hipArrayDestroy, hipCreateChannelDesc, hipBindTextureToMipmappedArray

Likely cause

The proxy also exports six symbols that HRX's libamdhip64.so does not export at all:
__hipRegisterSurface, hipGLGetDevices, hipGraphicsGLRegisterBuffer,
hipGraphicsGLRegisterImage, plus its own hip_passthrough_get_active_table /
hip_passthrough_get_real_table.

That strongly suggests the proxy's symbol list was generated against an older or upstream HIP
export set
and has not been regenerated against the current HRX ABI. Regenerating it from
nm -D of the built libamdhip64.so should close the gap.

Activity

  1. bong-water-water-bong commented on Oct 13, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    Follow-up: two additional defects found while re-verifying this report

    Re-testing this issue with the proxy correctly preloaded (LD_PRELOAD=<...>/libhip_intercept.so
    — my original test had it only on LD_LIBRARY_PATH, so it was never loaded) confirmed the
    coverage gap above and surfaced two more problems.

    Confirmed: the coverage gap is real

    hipGetDeviceCount (present in the proxy's export table) is logged:
    hipGetDeviceCount() -> count=1, ret=0

    hipDeviceGetExecutionCtx (absent from the proxy's export table) is not logged at all, yet
    the call succeeds. Consistent with the original analysis: missing wrapper ⇒ not intercepted, not
    broken.

    New defect 1 — the documented interceptor variable does not exist in the code

    Variable
    libhrx/src/passthrough/hip_intercept.c reads HIP_INTERCEPTION_LIBRARY
    libhrx/src/passthrough/README.md documents HIP_PASSTHROUGH_INTERCEPTOR

    The README's name is never read. A user following the documentation gets a silently
    un-instrumented run with no warning
    — which is exactly how I first misdiagnosed the logging
    interceptor as broken. Suggest reading both names (documented one taking precedence) or fixing
    the README.

    Full env surface read by hip_intercept.c: HIP_PASSTHROUGH_BACKEND_LIB,
    HIP_INTERCEPTION_LIBRARY, HIP_LOG_FILE, HIP_LOG_LEVEL.

    New defect 2 — the proxy initializes twice

    // hip_intercept.c:167 — guarded
    static void ensure_init(void) { pthread_once(&g_init_once, intercept_init); }
    
    // hip_intercept.c ~255 — UNGUARDED
    __attribute__((constructor(101))) static void early_init(void) {
      intercept_init();          // bypasses pthread_once
    }

    early_init calls intercept_init() directly from an early constructor while every public
    entry point goes through the pthread_once-guarded ensure_init(). Initialisation therefore
    runs from the constructor and through the guard.

    Observed consequence: HIP Logging initialized printed three times in one short run, with a
    matching HIP Logging shutting down each time. Because hip_interceptor_init opens the log with
    fopen(path, "w"), a re-init truncates the log earlier calls are still writing to — my
    captured log contained the mid-line fragment nt=0, ret=3, the tail of a line whose start had
    been truncated away.

    Fix: have early_init call ensure_init() so both routes share the guard, and make
    hip_interceptor_init idempotent.

    Correction to my earlier comment

    The hip_logging "writes no output" report I filed separately was my error and has been
    closed. It works — including HIP_LOG_FILE — when the proxy is actually preloaded and the
    process is not run under an environment where the target file is unwritable. Only the two
    defects above are genuine.

  2. bong-water-water-bong commented on Oct 13, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    Root cause found: the proxy was generated before HIP 7.14

    The coverage gap is not arbitrary — it has a precise cause.

    libhrx/src/passthrough/stubs_generated.c states its own provenance:

    // Auto-generated passthrough stubs for all HIP symbols
    // Generated from /opt/rocm/lib/libamdhip64.so
    

    Every forwarded symbol carries an explicit versioned-symbol directive:

    __asm__(".symver __hipGetPCH, __hipGetPCH@hip_4.2");

    Counting those directives in the checked-in file:

    Symbols the proxy covers 387
    Version nodes present 17: hip_4.2 4.3 4.4 4.5 5.0 5.1 5.2 5.3 5.5 5.6 6.0 6.1 6.2 6.4 6.5 7.1 7.2
    Version nodes absent hip_7.14
    hip_7.14 directives in the file 0

    Meanwhile the HRX library it is meant to proxy exports 18 nodes — the same 17 plus
    hip_7.14
    .

    The gap is exactly the 7.14 boundary

    The symbols this issue lists as unreachable are predominantly the HIP 7.14 additions:

    hipExecutionCtxCreate, hipExecutionCtxDestroy, hipExecutionCtxGetDevice,
    hipExecutionCtxGetId, hipExecutionCtxRecordEvent, hipExecutionCtxStreamCreate,
    hipExecutionCtxSynchronize, hipExecutionCtxWaitEvent, hipDeviceGetDevResource,
    hipDevSmResourceSplit, hipDevSmResourceSplitByCount, hipDevResourceGenerateDesc,
    hipStreamGetDevResource, hipGreenCtxCreate

    Measured directly: of the 10 hipExecutionCtx* / hipDevResource* symbols HRX exports,
    the proxy covers 0.

    So the 215-symbol gap is a generation-version boundary, not a random omission: the stub file
    was produced against a ROCm libamdhip64.so that predates HIP 7.14, and has not been regenerated
    since.

    Fix

    Regenerate stubs_generated.c against the current HRX libamdhip64.so (or a current ROCm
    build), so the node set includes hip_7.14. The generator's own convention already does the
    right thing — .symver per symbol with the node taken from the source library — so this is a
    regeneration, not a redesign.

    A cheap guard would be to assert during generation that the produced node set equals the node set
    in HRX's own amdhip64.map, which fails loudly the next time a version node is added upstream
    rather than silently leaving a gap.

  3. bong-water-water-bong commented on Oct 16, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    All three defects fixed (fork), and the fix is bigger than the report implies

    Fixed on 1bit-MONSTER/hrx-system 1bit/main — PR 1bit-MONSTER/hrx-system#3, branch 410-libhip-intercept-coverage.

    Coverage: 331 → 544 exports; comm -23 against the backend's table is now 0. Version nodes now match amdhip64.map exactly, hip_7.14 included. hipDeviceGetExecutionCtx is intercepted and logged through LD_PRELOAD of the rebuilt proxy over a stock ROCm 7.16 backend (no HRX device needed for that check).

    Correction 1 — regenerating stubs_generated.c alone would have changed nothing

    The file was not in the build graph: libhrx/src/passthrough/CMakeLists.txt listed only hip_intercept.c, and the stubs it defines (__hipGetPCH, …) are absent from the shipped .so. So the reported fix ("regenerate the stub file") was necessary but not sufficient — the regenerated file had to be regenerated and linked in. That is why the gap could sit there looking generated-and-correct.

    Correction 2 — there was no usable generator, and now there is

    add_logging.py is not the stub generator (it rewrites hip_intercept.c, and its __main__ hardcodes a personal path); the map/stubs were produced ad hoc. The PR commits libhrx/src/passthrough/generate_stubs.py with a node-set assertion against amdhip64.map (tested: a stray hip_9.9 node exits 1) and a --check mode, documented in the passthrough README. This is the difference between fixing the gap once and leaving it to reopen at the next HIP version node.

    Correction 3 — the double init prints twice, not three times

    early_init() now goes through ensure_init(), and hip_interceptor_init() is idempotent. Measured on stderr, a marker written between two init calls survives; before, it was truncated. The report's "three times" reading came from the log file itself being re-fopened with "w" mid-run — the truncation is the defect that hid its own count.

    What this does not do

    Same shipping caveat as the rest of the fork line: the engine pins third_party/hrx-system at 98d05d94 (held by config/hrx-system-hold.config), so this is not yet in engine builds.

    Evidence: team/evidence/410-evidence.md plus 410-symbols.txt, 410-runtime-{before,after}.txt, 410-reinit.txt, probe sources and build script.

  4. bong-water-water-bong commented on Oct 16, 2026

    @bong-water-water-bong
    CollaboratorAuthor

    Shipped: the engine now pins the fix

    All three defects are fixed and in engine builds: PR #423 moved third_party/hrx-system to 1bit/held-98d05d94-3fixes (3319e5361, merged as 7d44c99).

    Route, for the record: the fix landed on 1bit/main (PR #3) but the engine holds hrx-system at 98d05d94, so the owner chose a backport branch off that same commit. The only hand-written code in it is the +61/−0 api.h launch-config port that the regenerated stubs need.

    Verified at the pinned line (gfx1151, -DONEBIT_HRX=ON build exit 0):

    • comm -23 between the backend's 540 HIP exports and libhip_intercept's 544 defined symbols = 0 (the issue measured 331 of 540 covered).
    • Op gate test-backend-ops -b HRX0: 1141/1141 passed, 0 failures.
    • tests/hrx_long_prompt.py passes against a build at this pin, which is what the pin's validated-pin record rests on.

    From the fix comment, the three corrections that matter: the stub file was never in the build graph (regenerating it alone would have changed nothing, so it is regenerated and linked), there was no usable generator (there now is, committed, with a node-set assertion), and the double init prints twice, not three times.

    One loose end is tracked as a follow-up task rather than left implicit: the committed generator's --check mode reports the checked-in stubs "out of date" at this pin even though coverage is correct, so it is being reproduced and either fixed or scoped down before anything wires it into CI.

    Closing as completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions