Skip to content

llvm_passes: expand llvm.llround and llvm.llrint - #1406

Merged
pvelesko merged 2 commits into
mainfrom
2026-07-29-github-1404-llround-clean
Aug 27, 2026
Merged

llvm_passes: expand llvm.llround and llvm.llrint#1406
pvelesko merged 2 commits into
mainfrom
2026-07-29-github-1404-llround-clean

Conversation

@pvelesko

@pvelesko pvelesko commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1404

libstdc++ declares std::llround and std::llrint for float as constexpr, which HIP turns into implicitly host device functions. They win overload resolution in device code over anything chipStar can declare (a plain device overload with the same signature is rejected outright, see the analysis in #1404) and expand to __builtin_llroundf, so the module ends up with an llvm.llround intrinsic that llvm-spirv has no translation for. The failure surfaces at hipspv-link with no source location.

The new pass rounds in floating point with llvm.round or llvm.rint, which the translator maps to the OpenCL round and rint ExtInsts, then converts.

Calling the devicelib entry points (__chip_llround_f32 and friends) was the first attempt and does not work: devicelib is linked in before the post-link passes run, so a symbol first referenced by a post-link pass is never pulled in and the program build fails with unresolved external symbol __chip_llround_f32.

New test tests/devicelib/TestStdLlroundFloat.cpp, verified on Intel Arc B570 (OpenCL): does not link before, PASSED after. It pins down both rounding modes (llround(2.5f) is 3, llrint(2.5f) is 2) for float and double.

Found while getting the Kokkos HIP backend running on chipStar; this is what blocks Kokkos_CoreUnitTest_HIP from linking.

This pass is a bridge, not the real fix. The real fix belongs in the SPIR-V producers, and both are open upstream: llvm/llvm-project#217618 llvm/llvm-project#217618 for the in tree backend, and KhronosGroup/SPIRV-LLVM-Translator#3987 KhronosGroup/SPIRV-LLVM-Translator#3987 for the translator. Neither reaches chipStar automatically, and the LLVM 21 lanes never receive either, so the pass is still required. Removal is tracked in #1476.

@pvelesko

Copy link
Copy Markdown
Collaborator Author

/run-aurora-ci

@pvelesko
pvelesko force-pushed the 2026-07-29-github-1404-llround-clean branch 2 times, most recently from b4e9a23 to ba3357b Compare August 24, 2026 09:55
@pvelesko
pvelesko force-pushed the 2026-07-29-github-1404-llround-clean branch from ba3357b to 9fbf37b Compare August 24, 2026 12:23
std::llround and std::llrint on a float expand to llvm.llround and
llvm.llrint, and std::lround on a float expands to llvm.lround. None of
these has a 1:1 OpenCL.std ExtInst, because every OpenCL.std rounding
instruction is float to float, so a producer that does not expand them
itself rejects the module.

llvm.lrint is not reachable through std::lrint: chipStar declares
lrint(float) in sp_math.hh and pulls it into namespace std, so that
overload wins and routes to the devicelib entry point. The test reaches
the intrinsic through __builtin_lrintf instead.

llvm.llround, llvm.llrint and llvm.lrint have no translation today, so
the test does not link. llvm.lround does lower natively and is covered
here to pin that it keeps producing the same values.
llvm.lround, llvm.llround, llvm.lrint and llvm.llrint take a floating
point value and return an integer. OpenCL.std has no instruction of that
shape, since ceil, floor, rint, round and trunc are all float to float,
so these four need a two instruction expansion rather than a 1:1 ExtInst
and a producer that does not do it rejects the module:

  InvalidFunctionCall: Unexpected llvm intrinsic: llvm.llround.i64.f32

or, with the in-tree SPIR-V backend:

  LLVM ERROR: unable to legalize instruction: %8:iid(s64) = G_INTRINSIC_LRINT

The failure surfaces at hipspv-link with no source location at all, and
it cannot be headed off in the headers: libstdc++ declares these for
float as constexpr, which HIP turns into implicitly __host__ __device__
functions, so they win overload resolution over anything chipStar can
declare (a plain __device__ overload with the same signature is rejected
outright).

So expand them here: round in floating point with llvm.round or
llvm.rint, which both producers map to the OpenCL round and rint
ExtInsts, then convert. Calling the devicelib entry points instead does
not work, because devicelib is linked in before the post-link passes run
and only the symbols referenced at that point are pulled in.

Measured against llvm-spirv and the in-tree SPIR-V backend of LLVM
22.1.0:

  intrinsic   translator   backend
  lround      ok           ok
  llround     fails        ok
  lrint       fails        fails
  llrint      fails        fails

All four are handled rather than only the three that fail today, so the
set is defined by the shape of the intrinsic instead of by which producer
version happens to be in use, and the pass can be removed in one go once
the producers cover them. lround already lowers natively to exactly this
expansion (OpExtInst round, then OpConvertFToS) on both producers, so
covering it changes no emitted code.

Fixes #1404
@pvelesko
pvelesko force-pushed the 2026-07-29-github-1404-llround-clean branch from 9fbf37b to 8f7adfb Compare August 25, 2026 21:58
@pvelesko
pvelesko merged commit 5cd301d into main Aug 27, 2026
18 checks passed
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.

std::llround and std::llrint on a float emit llvm.llround / llvm.llrint, which llvm-spirv cannot translate

1 participant