Module Sarek_hip.Hip_rtc

Types

type hiprtc_program

hiprtcProgram is a pointer typedef (struct _hiprtcProgram*); we model the pointee as an opaque named structure, so a hiprtc_program_ptr mirrors NVRTC's nvrtc_program_ptr.

val hiprtc_program : hiprtc_program Ctypes.structure Ctypes.typ
val hiprtc_program_ptr : hiprtc_program Ctypes.structure Ctypes.ptr Ctypes.typ
type hiprtc_result =
  1. | HIPRTC_SUCCESS
  2. | HIPRTC_ERROR_OUT_OF_MEMORY
  3. | HIPRTC_ERROR_PROGRAM_CREATION_FAILURE
  4. | HIPRTC_ERROR_INVALID_INPUT
  5. | HIPRTC_ERROR_INVALID_PROGRAM
  6. | HIPRTC_ERROR_INVALID_OPTION
  7. | HIPRTC_ERROR_COMPILATION
  8. | HIPRTC_ERROR_BUILTIN_OPERATION_FAILURE
  9. | HIPRTC_ERROR_NO_NAME_EXPRESSIONS_AFTER_COMPILATION
  10. | HIPRTC_ERROR_NO_LOWERED_NAMES_BEFORE_COMPILATION
  11. | HIPRTC_ERROR_NAME_EXPRESSION_NOT_VALID
  12. | HIPRTC_ERROR_INTERNAL_ERROR
  13. | HIPRTC_ERROR_LINKING
  14. | HIPRTC_ERROR_UNKNOWN of int
val hiprtc_result_of_int : int -> hiprtc_result
val int_of_hiprtc_result : hiprtc_result -> int
val hiprtc_result : hiprtc_result Ctypes.typ
val string_of_hiprtc_result : hiprtc_result -> string

Library Loading

val hiprtc_lib : Dl.library option Stdlib.Lazy.t
val is_available : unit -> bool
val get_hiprtc_lib : unit -> Dl.library
val foreign_hiprtc_lazy : string -> ('a -> 'b) Ctypes.fn -> ('a -> 'b) lazy_t

Bindings

val hiprtcCreateProgram_lazy : (hiprtc_program Ctypes.structure Ctypes.ptr Ctypes_static.ptr -> string -> string option -> int -> string option Ctypes_static.ptr -> string option Ctypes_static.ptr -> hiprtc_result) lazy_t
val hiprtcCreateProgram : hiprtc_program Ctypes.structure Ctypes.ptr Ctypes_static.ptr -> string -> string option -> int -> string option Ctypes_static.ptr -> string option Ctypes_static.ptr -> hiprtc_result
val hiprtcDestroyProgram_lazy : (hiprtc_program Ctypes.structure Ctypes.ptr Ctypes_static.ptr -> hiprtc_result) lazy_t
val hiprtcDestroyProgram : hiprtc_program Ctypes.structure Ctypes.ptr Ctypes_static.ptr -> hiprtc_result
val hiprtcCompileProgram_lazy : (hiprtc_program Ctypes.structure Ctypes.ptr -> int -> string Ctypes_static.ptr -> hiprtc_result) lazy_t
val hiprtcCompileProgram : hiprtc_program Ctypes.structure Ctypes.ptr -> int -> string Ctypes_static.ptr -> hiprtc_result
val hiprtcGetCodeSize_lazy : (hiprtc_program Ctypes.structure Ctypes.ptr -> Unsigned.size_t Ctypes_static.ptr -> hiprtc_result) lazy_t
val hiprtcGetCodeSize : hiprtc_program Ctypes.structure Ctypes.ptr -> Unsigned.size_t Ctypes_static.ptr -> hiprtc_result
val hiprtcGetCode_lazy : (hiprtc_program Ctypes.structure Ctypes.ptr -> char Ctypes_static.ptr -> hiprtc_result) lazy_t
val hiprtcGetCode : hiprtc_program Ctypes.structure Ctypes.ptr -> char Ctypes_static.ptr -> hiprtc_result
val hiprtcGetProgramLogSize_lazy : (hiprtc_program Ctypes.structure Ctypes.ptr -> Unsigned.size_t Ctypes_static.ptr -> hiprtc_result) lazy_t
val hiprtcGetProgramLogSize : hiprtc_program Ctypes.structure Ctypes.ptr -> Unsigned.size_t Ctypes_static.ptr -> hiprtc_result
val hiprtcGetProgramLog_lazy : (hiprtc_program Ctypes.structure Ctypes.ptr -> char Ctypes_static.ptr -> hiprtc_result) lazy_t
val hiprtcGetProgramLog : hiprtc_program Ctypes.structure Ctypes.ptr -> char Ctypes_static.ptr -> hiprtc_result

High-Level Helpers

exception Hiprtc_error of hiprtc_result * string
val check : string -> hiprtc_result -> unit
val base_options : string list

Options forced onto EVERY hiprtc compilation.

The cross-backend rule this implements — and what every OTHER backend does or fails to do about contraction — is docs/fp-contraction-policy.md. Read it before changing anything here.

-ffp-contract=off is a CONFORMANCE requirement, not a tuning choice. HIP (clang) defaults to -ffp-contract=fast, which lets the backend fuse a multiply into the operation that consumes it — including an f32 multiply feeding an f32->f16 narrowing, which RDNA3 can do in one v_fma_mix-class instruction. The fused form rounds the EXACT product straight to binary16 and skips the intermediate f32 rounding the Sarek DSL promises, so the device result stops matching the interpreter.

Measured on gfx1100 with float16_of_float32 (float32_of_float16 (float16_of_float32 (float32_of_float16 x *. 1.1)) +. 1000.0) at x = 5.68359375: contracted gives 1006.5, unfused (and the interpreter, the native path and the host reference) give 1006.0. Isolated, both halves are correct — the f32 product is bit-identical to the host's 0x40c81000 and the device's f32->f16 narrowing is verified round-to-nearest-even on exact ties in both directions — so the defect is specifically the FUSION, not the arithmetic or the conversion.

An exhaustive sweep of all finite binary16 inputs found values on which contraction changed the result; with contraction off, zero. (The count that used to be quoted here was 373. Do not restore it: docs/fp-contraction-policy.md §2 records that 373 appears in-tree for two DIFFERENT populations whose uses contradict each other, and that 620 is the barrier/ISel-combine figure. Neither is the number for THIS sentence, and the right count for it has not been established, so it is not asserted.)

The other two options, and why they are here (backlog #136)

-fhip-fp32-correctly-rounded-divide-sqrt and -fno-gpu-flush-denormals-to-zero are already clang's HIP defaults. They are set anyway, and the reason is the whole subject of backlog #136: an inherited default is a choice nobody made. The OpenCL backend inherited a 3-ulp sqrt this way for years, silently, because it passed no build options at all.

MEASURED on this machine, 2026-07-26, ROCm 7.2.4 / AMD clang 22.0.0git, gfx1100, on out = sqrtf(a) + a/b compiled with clang++ -x hip --offload-arch=gfx1100 --cuda-device-only -O3 -S:

  • Adding both flags to today's option list is a NO-OP: the emitted assembly is IDENTICAL over all 13 floating-point instructions. So this costs nothing.
  • The liveness control confirms the comparison can go non-identical: -fno-hip-fp32-correctly-rounded-divide-sqrt drops the emitted code from the refined form (v_div_scale_f32 / v_div_fmas_f32 / v_div_fixup_f32, and v_sqrt_f32 with v_fma_f32 Newton residuals) to a bare v_rcp_f32 / v_sqrt_f32 with v_frexp/v_ldexp scaling — 10 instructions, no v_div_fixup_f32.
  • -fgpu-flush-denormals-to-zero moves .amdhsa_float_denorm_mode_32 from 3 (IEEE, subnormals preserved) to 0 (flushed).

What that buys, given hiprtc_options appends these LAST: a caller passing -fgpu-flush-denormals-to-zero or -fno-hip-fp32-correctly-rounded-divide-sqrt is now neutralised by last-occurrence, exactly as -ffp-contract=off neutralises -ffp-contract=fast. Both verified: denorm mode returns to 3, and v_div_fixup_f32 returns to the output.

What it does NOT buy, and this is the limit of the whole append-last defence: -ffast-math from a caller still removes v_div_fixup_f32 and is NOT rescued by appending -fhip-fp32-correctly-rounded-divide-sqrt, because it sets the per-instruction afn fast-math flag rather than changing the divide/sqrt lowering default. That class is warned about below and is a candidate for outright rejection — see fp_relaxing_option_prefixes.

val fp_relaxing_option_prefixes : string list

Floating-point option classes a caller can pass that would re-enable contraction, or otherwise relax the f32 evaluation discipline, if they took effect. Matched by prefix so -ffp-contract=fast and -ffp-model=fast are both caught.

EXTENDED FOR backlog #136. The first four entries were the original list. Each of the rest was MEASURED to change the emitted gfx1100 code through this exact option ordering (ROCm 7.2.4 / AMD clang 22.0.0git, 2026-07-26) while passing this warning silently:

  • -Ofast, -cl-fast-relaxed-math, -cl-unsafe-math-optimizations, -fapprox-func — all degrade divide and sqrt to the approximate form, the same result as -ffast-math, under names the original four prefixes could not see. Note -Ofast in particular: a prefix list is the wrong shape for it in general (a -O prefix would wrongly catch -O3), which is why the full token is listed.
  • -fgpu-flush-denormals-to-zero — moves .amdhsa_float_denorm_mode_32 from 3 to 0, flushing binary32 subnormals, which docs/fp-contraction-policy.md §1 forbids.
  • -fno-hip-fp32-correctly-rounded-divide-sqrt — removes v_div_fixup_f32.
  • -munsafe-fp-atomics — swaps the global_atomic_cmpswap_b32 CAS loop for a hardware global_atomic_add_f32 whose rounding and denormal behaviour is not the IEEE path.

THE LAST THREE ARE NOW NEUTRALISED by base_options (verified: denorm mode returns to 3, v_div_fixup_f32 returns), so for those the warning says "your flag was overridden", not "your flag took effect". THE FIRST FOUR ARE NOT NEUTRALISED BY ANYTHING — measured: appending -fhip-fp32-correctly-rounded-divide-sqrt does not restore v_div_fixup_f32 after -ffast-math, and no trailing clang option was found that undoes the -cl-* spellings at all.

That asymmetry is the argument for eventually REJECTING the first group rather than warning, as Cuda_nvrtc.check_fp_conformance and Opencl_fp.check_fp_conformance both do for their unneutralisable options. That is a caller-visible behaviour change and is deliberately NOT made here; this change only stops the options being invisible.

val has_prefix : prefix:string -> string -> bool
val hiprtc_options : string list -> string list

Assemble the final hiprtc option array.

CALLER OPTIONS FIRST, base_options LAST. hiprtc passes its option array straight to clang, and clang resolves conflicting floating-point options by LAST OCCURRENCE — explicitly so for -ffp-contract against -ffast-math and -ffp-model. With the conformance flag placed first (as it was), a caller passing -ffp-contract=fast, -ffast-math or -ffp-model=fast would silently reinstate the very contraction base_options exists to forbid, and the f16 device/interpreter agreement would break on the inputs measured above. Putting it last makes that unreachable by construction rather than by convention. (No count is quoted: see base_options on why "373" must not be restored.)

Contraction is then guaranteed off regardless of the caller. The other effects of a fast-math-class flag (reassociation, finite-math-only) are NOT neutralised by -ffp-contract=off, so those are warned about rather than silently accepted; they are not rejected because compile_with_options exists for legitimate rocWMMA include/define threading and we do not want to break that path.

Compile HIP C++ source to a finalized code object, ready to feed to hipModuleLoadData. Returns the code-object bytes as an OCaml string.

When arch is omitted (the default), hiprtc targets the CURRENTLY-SELECTED device (the caller has already hipSetDevice'd it) - this is both robust and portable across gfx targets (incl. the integrated gfx1036 iGPU) and avoids any lossy arch-string derivation. An explicit arch (e.g. "gfx1100") may be passed for cross-compilation / rocWMMA experiments; if hiprtc rejects the option outright it falls back to the current-device default. NOTE a merely *mismatched* (but syntactically valid) arch compiles here and only fails at hipModuleLoadData, so callers should prefer the default.

val compile_to_code_object : ?name:string -> ?arch:string -> ?options:string list -> string -> string