Skip to content

Fold or erase the intrinsics that no SPIR-V producer accepts - #1477

Draft
pvelesko wants to merge 2 commits into
mainfrom
2026-08-25-lower-hint-intrinsics
Draft

Fold or erase the intrinsics that no SPIR-V producer accepts#1477
pvelesko wants to merge 2 commits into
mainfrom
2026-08-25-lower-hint-intrinsics

Conversation

@pvelesko

Copy link
Copy Markdown
Collaborator

Nine intrinsics that clang emits for ordinary __builtin_* calls survive hip-post-link-passes and then fail at SPIR-V emission. hipcc -c on a kernel that calls __builtin_prefetch fails today at -O0 and at -O3, on LLVM 21, 22 and 23, through both producers chipStar supports.

Through the llvm-spirv translator:

InvalidFunctionCall: Unexpected llvm intrinsic:
 llvm.prefetch.p4 [Src: .../SPIRV-LLVM-Translator/lib/SPIRV/SPIRVWriter.cpp:5382 false ]
clang++: error: hipspv-link command failed with exit code 8 (use -v to see invocation)

Through the in-tree SPIR-V backend:

LLVM ERROR: unable to legalize instruction: G_PREFETCH %22:pid(p4), 0, 3, 1 :: (load unknown-size from %ir.add.ptr4, align 1, addrspace 4) (in function: _Z1kPKfPf)

chipStar puts all device code of a binary into a single module, so one __builtin_prefetch anywhere in the program makes every kernel in it fail to build, and neither diagnostic carries a source location. __builtin_prefetch is common in the CPU code people port to HIP.

Measured per intrinsic on minimal spirv64 modules with llvm-spirv and llc from each installed toolchain. The translator rejects all nine on all three LLVM versions with InvalidFunctionCall: Unexpected llvm intrinsic. The backend:

                          LLVM 21   LLVM 22   LLVM 23
llvm.prefetch             reject    reject    reject
llvm.readcyclecounter     reject    reject    reject
llvm.readsteadycounter    reject    reject    reject
llvm.get.rounding         reject    reject    reject
llvm.memcpy.inline        reject    reject    ok
llvm.returnaddress        reject    reject    ok
llvm.frameaddress         reject    reject    ok
llvm.allow.runtime.check  ok        ok        ok
llvm.objectsize           ok        ok        ok

The fix is a new HipLowerHintIntrinsicsPass in llvm_passes/, registered in the post link pipeline next to the other lowering passes and standalone as hip-lower-hint-intrinsics so it is directly testable with opt. It handles eight of the nine, and each rewrite is one the LLVM Language Reference explicitly authorises, so no defined behaviour changes. Quotations below are from llvm/docs/LangRef.md at llvmorg-23.1.0-rc2.

llvm.prefetch is erased. Overview: "The 'llvm.prefetch' intrinsic is a hint to the code generator to insert a prefetch instruction if supported; otherwise, it is a noop." Semantics: "This intrinsic does not modify the behavior of the program."

llvm.readcyclecounter and llvm.readsteadycounter become i64 0. Both Semantics sections say "On backends without support, this is lowered to a constant 0."

llvm.get.rounding becomes i32 1. LangRef fixes the encoding of the result, with 1 meaning "to nearest, ties to even". That is the only mode an OpenCL device offers, per the OpenCL C specification, Rounding Modes: "The only default floating-point rounding mode supported is round to nearest even i.e the default rounding mode will be _rte for floating-point types." The fold is additionally guarded on the module containing no llvm.set.rounding, so a folded getter can never contradict a setter. That guard is not expected to fire, since clang rejects the setter outright on this target with builtin is not supported on this target.

llvm.allow.runtime.check becomes i1 false. Semantics: "For each evaluation of a call to this intrinsic, the program must be valid and correct both if it returns true and if it returns false." false elides the guarded check, which is the better choice on a GPU.

llvm.returnaddress and llvm.frameaddress become a null pointer. Both Semantics sections say the intrinsic "either returns a pointer indicating the [return|frame] address of the specified call frame, or zero if it cannot be identified", and a SPIR-V kernel has no addressable call frame.

llvm.objectsize is resolved with llvm::lowerObjectSizeCall, the same entry point LLVM's own LowerConstantIntrinsics pass uses. Semantics: "The llvm.objectsize intrinsic is lowered to a value representing the size of the object concerned. If the size cannot be determined, llvm.objectsize returns i32/i64 -1 or 0 (depending on the min argument)." That is why the backend already copes and only the translator lane needs this.

The ninth, llvm.memcpy.inline, has a real effect and is expanded to a loop instead. HipLowerMemset becomes HipLowerMemIntrinsics and picks it up next to the llvm.memset expansion it already did. The memset form needed no separate case because MemSetInst::classof already covers Intrinsic::memset_inline, whereas MemCpyInst::classof covers plain llvm.memcpy too, which keeps its direct OpCopyMemorySized translation and is filtered out. A loop preserves what LangRef guarantees: "The behavior of 'llvm.memcpy.inline.' is equivalent to the behavior of 'llvm.memcpy.', but the generated code is guaranteed not to call any external functions."

The new pass runs before the pipeline's DCE so the address computations that fed an erased llvm.prefetch are removed with it.

Tests are tests/compiler sources driven by hipcc -c through add_hipcc_test, one per intrinsic, at -O0 and -O3, since the intrinsics do not all survive to the device link at the same optimization level. llvm.objectsize is checked at -O0 only, because from -O1 up LLVM's LowerConstantIntrinsics folds it away before the device link and the test would pass regardless. All 17 fail on the tests only commit on the translator lane, and 8 of the 17 fail there on the LLVM 23 in-tree backend lane, which already accepts allow.runtime.check, objectsize, memcpy.inline, returnaddress and frameaddress. All 17 pass on the fix commit on both lanes.

Verified on two builds of this branch, one against LLVM 22.1.0 and one against LLVM 23.1.0-rc2 with the in-tree SPIR-V backend, both of which build the whole tree and pass all 17 new tests. In tests/compiler the only failures are the shell tests that execute the binary they build, which cannot run on the GPU used here; the exact count is environment specific rather than a property of the branch.

Caveats. No device was available on the machine this was developed on, so the six executing tests fail at hipErrorNotInitialized (OpenCL failed to initialize any devices) and every result above is compile level only. CI needs to confirm the runtime behaviour, in particular that __builtin_memcpy_inline still copies the right bytes and that __builtin_flt_rounds returning 1 matches the device. LLVM 21 coverage here is a syntax check of the two pass sources against the LLVM 21.1.7 headers plus the standalone llvm-spirv and llc matrix, not a full chipStar build.

Nine intrinsics that clang emits for ordinary __builtin_* calls reach the
device link and fail there. Seven of the nine break both SPIR-V producers
chipStar supports.

Through the llvm-spirv translator every one of them aborts translation:

  InvalidFunctionCall: Unexpected llvm intrinsic:
   llvm.prefetch.p4

Through the in-tree SPIR-V backend seven of them abort code generation, for
example:

  LLVM ERROR: unable to legalize instruction: G_PREFETCH %22:pid(p4), 0, 3, 1

chipStar puts all device code of a binary in one module, so any one of these
takes down every kernel in the program, and the diagnostic carries no source
location.

The tests are compile-only, driven by hipcc -c through add_hipcc_test, at -O0
and -O3 since the intrinsics do not all survive to the device link at the same
optimization level. llvm.objectsize is checked at -O0 only because LLVM's own
LowerConstantIntrinsics pass folds it away from -O1 up.
@pvelesko
pvelesko force-pushed the 2026-08-25-lower-hint-intrinsics branch from 65ed5c3 to 7eaf31b Compare September 1, 2026 06:37
Nine intrinsics that clang emits for ordinary __builtin_* calls reach SPIR-V
emission and fail there. The llvm-spirv translator rejects all nine with
"InvalidFunctionCall: Unexpected llvm intrinsic", and the in-tree SPIR-V
backend fails to legalize seven of them. chipStar puts all device code of a
binary in one module, so one __builtin_prefetch anywhere in it makes every
kernel in the program fail to build, with a diagnostic that carries no source
location.

New HipLowerHintIntrinsicsPass handles eight of them, each with a rewrite the
LLVM Language Reference explicitly authorises, so no defined behavior changes:

  llvm.prefetch             erased ("otherwise, it is a noop")
  llvm.readcyclecounter     0 ("On backends without support, this is
  llvm.readsteadycounter    lowered to a constant 0.")
  llvm.get.rounding         1, round to nearest ties to even, the only mode an
                            OpenCL device offers, and guarded by the absence of
                            llvm.set.rounding, which clang rejects on spirv64
  llvm.allow.runtime.check  false ("the program must be valid and correct both
                            if it returns true and if it returns false")
  llvm.returnaddress        null ("or zero if it cannot be identified")
  llvm.frameaddress         null (same sentence)
  llvm.objectsize           lowerObjectSizeCall, the same entry point LLVM's
                            own LowerConstantIntrinsics pass uses

The ninth, llvm.memcpy.inline, has a real effect and is expanded to a loop
instead. HipLowerMemset becomes HipLowerMemIntrinsics and picks it up next to
the memset expansion it already did. The memset form needed no separate case
because MemSetInst::classof covers Intrinsic::memset_inline, whereas
MemCpyInst::classof covers plain llvm.memcpy too, which keeps its direct
OpCopyMemorySized translation and is filtered out.

The pass runs before the pipeline's DCE so the address computations feeding an
erased llvm.prefetch are removed with it.
@pvelesko
pvelesko force-pushed the 2026-08-25-lower-hint-intrinsics branch from 7eaf31b to 7f6792b Compare September 1, 2026 06:44
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.

1 participant