Skip to content

HIP (ROCm) backend for LTS-C++17, C++17-only, plus LTS bug fixes - #401

Merged
morousg merged 54 commits into
LTS-C++17from
LTS-C++17-rocm
Oct 29, 2026
Merged

morousg merged 54 commits into
LTS-C++17from
LTS-C++17-rocm

Conversation

@morousg

@morousg morousg commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Adds an AMD GPU backend (HIP/ROCm, ParArch::GPU_AMD) to LTS-C++17. It is written in strict C++17 and modelled on main's HIP support. It also fixes LTS bugs found while porting and in review, most of which affect CPU and CUDA too. Apart from those fixes, CPU and CUDA behaviour is unchanged: the HIP code is behind ENABLE_HIP, the FK_HIP_BACKEND macro and ParArch::GPU_AMD.

What it adds

  • HIP backend. ENABLE_HIP exists when CMake finds a HIP compiler for AMD GPUs (amdclang++) and is then ON by default.
    • HIP sources are built with CMake's HIP language at HIP_STANDARD 17, with -Werror=c++20-extensions.
    • Each test, utest and benchmark gets a _hip target next to _cpp/_cu. A new __ONLY_GPU__ marker limits a test to the CUDA and HIP targets.
  • FK_HIP_BACKEND selects the HIP code paths, not __HIPCC__. The HIP headers also define __HIPCC__ in nvcc compilations that include them, so that macro can't tell the two apart.
  • GPU_AMD streams, Ptr memory, TransformDPP/DivergentBatchTransformDPP and executors. Main selects these with requires; here they are partial specializations. The HIP executors are the CUDA executors with the HIP API names, including the thread fusion alignment fallback of 28101e3/1a9bfe8.
  • fk::float3, fk::uchar3... in every backend (as main's 943895c). On CPU and CUDA they are the existing global types. Under HIP they are FKL's own structs, because HIP's global vector types are not FKL vector types.
  • Stream_ owns its native stream through a shared_ptr, for both GPU_NVIDIA and GPU_AMD (from main's fcfe515/fe3e17c). The LTS Stream_ never destroyed the cudaStream_t it created.
  • Benchmarks: HIP event timers, and a HIP-graphs variant of benchmark_horizontal_fusion.
  • Docs: the README and the build-and-test skill cover the HIP build, the _hip targets and the fk:: vector types. They also say not to link FKL code compiled for different backends into one program.

LTS bug fixes (CPU and CUDA too)

setTo

  • Host memory with a GPU stream: the kernel faulted. It is now filled by the CPU after the stream's pending work.
  • HostPinned memory and both copies of DeviceAndPinned memory are filled by kernels on the stream, so the fill is in stream order and graph captures record it.
  • During a graph capture, setTo on Host memory throws and leaves the capture intact.
  • With a CPU stream, Device and DeviceAndPinned memory throw.

Memory and resource bugs

  • Ptr::crop/getPlane reference counts: backport of main d0588f8 (Ptr::crop and Ptr::getPlane leak the parent allocation #377).
  • CircularTensor::update wrote one plane past the end of the tensor on every update: backport of main 1838493. On the RTX 4090, without it test_circular_tensor fails BATCH 2, 4 and 5 and compute-sanitizer reports 3041 out-of-bounds writes. Its Device-memory check now also covers HIP streams.
  • Ref::operator= kept its old counter (6724ed1).
  • A failed stream destroy no longer leaves its error for the next cudaGetLastError().

Grid sizes

  • Grids and thread fusion thread spaces are now computed with integer ceilDiv instead of ceil(n / float(d)). With the float version, sizes above 2^24 lost their last block, and the last elements were never written.
  • Zero-size launches still throw.

HIP Debug builds

  • DivergentBatchTransformDPP calls each sequence through a lambda, not a function pointer.
  • At -O0 clang kept the pointer as an indirect call, and the kernel's stack size left out the callee. test_circular_tensor_hip then crashed with a memory fault in a Debug build.

Headers

  • raw_image.h is self-contained.
  • C++17 conformance fixes in array.h, ptr_nd.h and fkBenchmarksCommon.h.

Benchmarks

  • benchmark_horizontal_fusion returns non-zero when a comparison fails. The CSV output is unchanged.

Many of these also exist on main: the setTo ordering, grid sizes, __HIPCC__ selection, the CircularTensor check, the stream destroy error, __grid_constant__ and the HIP utest checks.

CI changes

  • The Linux workflows compile the benchmarks (-DENABLE_BENCHMARK=ON). ctest skips them (-E "^benchmark_").
  • The Linux jobs actually build CUDA now. Before, they pointed at CUDA toolkits the runners no longer have, and silently built and tested the CPU backend only.
    • They now use CUDA 13.4 (as main does) next to 12.9.
    • They pass -DCMAKE_CUDA_COMPILER, so a missing toolkit fails the job.
    • They configure with -DENABLE_HIP=OFF.
  • The Linux x64, ARM64 and Windows jobs require the cuda runner label, so they can't run on the ROCm runner.
  • The Windows workflow is unchanged and is to be disabled for LTS-C* in a follow-up. Its CUDA paths are stale: at the final tip only the clang-cl 14.44 + CUDA 12.9 job found a toolkit and built and tested CUDA. The other three jobs build CPU only.
  • No workflow builds HIP. A ROCm job (main's cmake-linux-rocm-amd64.yml) follows in a separate PR.

Verification at the final tip

  • AMD, RX 9060 (gfx1200), ROCm 10.1:
    • Release, g++ host compiler, benchmarks built: all 42 _hip and 39 _cpp ctest entries pass, benchmarks included (benchmark_thread_fusion_cpp is left out as too slow).
    • Release, amdclang++ as host compiler with -pedantic-errors -Werror=c++20-extensions: 38/38 _hip and 36/36 _cpp tests.
    • Debug and RelWithDebInfo, HIP and CPU: 74/74 each. ENABLE_CPU=OFF: 38/38.
    • Debug builds for gfx906, gfx90a, gfx942, gfx950, gfx1030, gfx1100, gfx1151, gfx1200 and gfx1201 compile without warnings, with no indirect calls in device code.
    • ASan + UBSan are clean on the new and changed CPU tests and on the HIP host code.
    • All 46 _cu targets compile with nvcc 13.4.
  • NVIDIA, RTX 4090, CUDA 13.4 (separate agent):
    • 124/124 targets build, with the same warnings as before. ctest 87/87, compute-sanitizer clean, and -G Debug runs identical to the previous tip.
    • Release SASS: the 317 kernels shared with 1a9bfe8 are identical.
    • Against the previous tip, 331 of 332 kernels are identical. The one that differs is test_circular_tensor_cu's DivergentBatchTransformDPP kernel for BATCH 4, after the lambda change above. It has the same 80 instructions and the same loads and stores; only the circular-index wrap computation moved from uniform to per-thread registers. That is a codegen variation, not a behaviour change.
  • CI: all 12 jobs pass.
    • Linux x64 (g++-15, clang++-21, CUDA 13.4), Ubuntu 24.04 (g++-13, clang++-21, CUDA 12.9) and ARM64 (4 jobs, CUDA 12.9 and 13.4): 79/79 tests in each job.
    • Windows: 4 jobs pass, one of them with CUDA (see above).
  • The CUDA side can't be run on the authoring machine. It has no NVIDIA GPU; there, every _cu target was compiled with nvcc 13.4.

Review

  • Eight review rounds, each by two independent reviewers (one for library/CMake/CI, one for tests/benchmarks/docs).
  • An NVIDIA-side review with probes on the 4090.
  • The eighth round found nothing to change.
  • Each fix is its own commit; the commit messages give the evidence.

Known issue (ROCm, not FKL)

  • HIP Debug builds can fault when several GPU processes run at once (ROCm 10.1, gfx1200).
    • With four -O0 HIP processes in parallel, some kernels abort with a memory fault. A small HIP program with no FKL code fails the same way.
    • The faults stop with HIP_FORCE_DEV_KERNARG=0 and in RelWithDebInfo. Run alone, every Debug test passes.
    • Worth remembering if a ROCm Debug job ever runs on a shared runner.

Not ported (intentionally)

🤖 Generated with Claude Code

https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n

morousg and others added 30 commits October 29, 2026 11:21
Add ENABLE_HIP (ON by default, disabled with a status message when no HIP
compiler is found) and cmake/libs/hip/{hip_init,hip,archs,target_generation,
debug}.cmake, modelled on main's HIP support:
- CMake's HIP language (amdclang++). HIP_STANDARD 17, HIP_STANDARD_REQUIRED,
  HIP_EXTENSIONS OFF, so HIP sources compile with -std=c++17 like the CXX and
  CUDA ones. TEMPLATE_DEPTH is applied as for CUDA.
- HIP_ARCHITECTURES follows CMAKE_HIP_ARCHITECTURES (the local GPU by
  default); -DHIP_ARCH seeds it on the first configure.
- Targets link hip::host, not hip::device: hip::device adds
  "-x hip --offload-arch" to the CXX sources of a target, which breaks
  tests/main.cpp when the CXX compiler is not amdclang++.
- find_package(hip) also looks in the ROCm root of the HIP compiler found
  by CMake, so HIP_PATH/ROCM_PATH need not be set.

Test, utest and benchmark discovery generate a _hip target next to _cpp and
_cu (sources marked __ONLY_CU__ or __ONLY_CPU__ are skipped), plus the
utest_saturate HIP libraries. New marker __ONLY_GPU__: CUDA and HIP targets
only, no _cpp target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
Stream_<ParArch::GPU_NVIDIA> never destroyed the cudaStream_t it created.
Its destructor destroyed the stream only when getRefCount() == 0, but the
Ref base class is destroyed after the body of the derived destructor, so
the last copy always saw a count of 1 (a CPU mock of the same logic created
4 streams and destroyed 0). In addition, Ref::operator= took the other
counter without releasing its own, so assigned-over counters leaked and
the counts were wrong. Each fk::Stream leaked about 160 KB of device
memory.

Port main's fix (fcfe515 for Ref, fe3e17c for the stream), which is
already C++17:
- Ref: atomic counter, operator= releases the previous reference.
- Stream_<GPU_NVIDIA>: the cudaStream_t is owned by a shared_ptr shared by
  the copies. The last copy destroys it with cudaStreamDestroy, which
  releases it when its pending work has finished (no synchronization, which
  would invalidate CUDA graph captures running in other threads). A stream
  given by the user is never destroyed. The class is a verbatim copy of
  main's.
- utests/core/execution_model/utest_stream_ownership.h from main: copies
  share the stream, a copy keeps it usable after the original is gone, a
  user stream is not destroyed, and no device memory is lost. The _cpp
  target has nothing to test.

Not compiled with nvcc on this machine (no CUDA toolkit); it must be
verified on a CUDA machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
HIP declares its own vector types (::float3, ::uchar3...) in the global
namespace (hip/hip_runtime.h). They can not be FKL's vector types: they
are not aggregates, they bring their own operators, and some layouts differ
from CUDA's and FKL's (::double4 is 32/32 under HIP). As on main (943895c),
under HIP FKL uses its own types, the fk::Float3... structs the CPU backend
already uses, and leaves the global names to HIP.

So that one source compiles for every backend, the FKL vector types are
now also named fk::float3, fk::uchar3... (and fk::bool1-4):
- with nvcc and on the CPU they are using-declarations of the existing
  global types (the CUDA vector types, or the CPU aliases of fk::Float3...).
  They name the same types, so nothing changes for CPU and CUDA code, and
  code with "using namespace fk;" can keep using the global names there;
- under HIP they are aliases of FKL's structs (fk::float3 = fk::Float3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
The GPU_AMD backend, compiled by amdclang++ (__HIPCC__), modelled on main's
HIP support and written in C++17:
- utils.h: hip_runtime.h, the FK_* host/device macros, gpuAssert(hipError_t)
  and gpuErrchk, __grid_constant__ as clang's attribute.
- parallel_architectures.h: defaultParArch is GPU_AMD under HIP.
- stream.h: Stream_<ParArch::GPU_AMD>, main's class (the hipStream_t is
  owned by a shared_ptr: copies share it, the last one destroys it, a user
  stream is never destroyed).
- ptr_nd.h, ptr_utils.h, image.h: hipMalloc/hipMallocPitch/hipHostMalloc,
  hipFree/hipHostFree, hipMemcpyAsync/hipMemcpy2DAsync, uploadTo/downloadTo
  and upload/download on Stream_<GPU_AMD>; defaultMemType is
  DeviceAndPinned, as with CUDA; setTo runs on the GPU stream as with CUDA.
- data_parallel_patterns.h: TransformDPP and DivergentBatchTransformDPP for
  GPU_AMD, using blockIdx/blockDim/threadIdx instead of cooperative groups.
  DivergentBatchTransformDPPBase gets the GPU ParArch of the TransformDPP it
  runs as a template parameter (GPU_NVIDIA by default, as before).
- executors.h, executor_kernels.h: Executor<TransformDPP<GPU_AMD>> and
  Executor<DivergentBatchTransformDPP<GPU_AMD>>, generated from the CUDA
  executors with the HIP runtime API. Main selects these with
  "requires (ARCH == ParArch::GPU_AMD)" (C++20); here they are partial
  specializations for ParArch::GPU_AMD. The host-only helpers (CtxDim3,
  getDefaultBlockSize) are shared with CUDA.
- interpolation.h, color_conversion.h: the device intrinsics
  (__float2int_rd...) also under __HIP_DEVICE_COMPILE__.

The CUDA code is unchanged: HIP code is in __HIPCC__ branches or
specializations next to the CUDA ones, and only guards were widened to
"defined(__NVCC__) || defined(__HIPCC__)" where code is API independent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
The global names (float3, uchar3...) are HIP's own vector types in the
_hip targets, so tests and utests use the fk:: names, which are the same
types as before in the _cpp and _cu targets. Mechanical change (as main's
943895c), also in the token-pasting launch macros of test_thread_fusion.h.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
- fkBenchmarksCommon.h: TimeMarkerOne/TimeMarkerTwo<ParArch::GPU_AMD>
  with hipEvent_t, generated from the CUDA ones. The CUDA timers were
  guarded with "defined(__CUDACC__) || defined(__HIP__)", which would
  compile them, with the CUDA API, under HIP: they are now __CUDACC__ only
  (same as before with nvcc, which never defines __HIP__).
- benchmark_thread_fusion.h: fk::-qualified vector types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
Clang (-std=c++17) reports C++20 extensions in LTS code, which a strict
C++17 build (-Werror=c++20-extensions -pedantic-errors) rejects:
- array.h: the constexpr ArrayVector(const T& initValue) constructors of
  the unions initialize no member ("constexpr union constructor that does
  not initialize any member is a C++20 extension"). They now value-
  initialize "at" before assigning its elements: same result.
- ptr_nd.h: uninitialized local variables in constexpr functions
  ("uninitialized variable in a constexpr function is a C++20 extension"):
  size_t pitch in PtrImpl<_2D>::d_malloc and int currentDevice in
  Ptr::allocDevice/allocHostPinned/allocDeviceAndPinned are initialized to
  0 before the runtime calls set them.
- fkBenchmarksCommon.h: a stray ';' in BenchmarkResultsNumbersOne
  (rejected by -pedantic-errors).

g++ does not report them, which is why they went unnoticed. No functional
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
- test_ptr_nd.h: the uploadTo/downloadTo tests with Device and HostPinned
  pointers also run under HIP.
- test_divergent_hf_executor.h: __ONLY_GPU__ instead of __ONLY_CU__, and
  defaultParArch instead of GPU_NVIDIA, so it runs on CUDA and HIP (there is
  no CPU executor for DivergentBatchTransformDPP). It now also checks the
  output: plane 0 must be imgA * 2 and plane 1 imgB + 100 (it only checked
  that the call compiled and ran). With nvcc it is the same test as before
  plus that check.
- utest_VBase.h: under HIP, fk::float3 is FKL's aggregate vector type and
  HIP's ::float3 is not an FKL vector type (as main's utest).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
README: CPU, CUDA and HIP backends, how to configure a HIP build, and why
code that compiles for HIP uses fk::float3... The build-and-test skill
lists ENABLE_HIP, HIP_ARCH, the _hip suffix and the __ONLY_GPU__ marker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
The CUDA build compares Horizontal Fusion with the same batch launched as a
CUDA graph (benchmark_Horizontal_Fusion_NO_CPU_OVERHEAD_CUDAGraphs). The HIP
build now runs the same comparison with HIP graphs
(benchmark_Horizontal_Fusion_NO_CPU_OVERHEAD_HIPGraphs, written to
..._HIPGraphs_GPU_AMD.csv): the CUDA function generated with the hipGraph*
API (hipStreamBeginCapture, hipGraphAddChildGraphNode, hipGraphInstantiate,
hipGraphLaunch...). The CUDA version is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
With a GPU stream and a Ptr in MemType::Host or MemType::HostPinned, setTo
launched a GPU kernel that wrote ptr(). For Host memory that pointer comes
from malloc, so the kernel faults (HSA_STATUS_ERROR_MEMORY_FAULT on the
RX 9060). Non-device memory is now filled by the CPU executor, as on main
(fabbb80); the GPU branch is main's text.

test_ptr_nd checks setTo on Host memory in every backend and on HostPinned
memory in the GPU builds (the CPU backend can not allocate pinned memory).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
launch() ignored the results of the fused-vs-iterated comparisons and
always returned 0, so ctest and the paper runs passed with wrong results.
It now ANDs every launch_benchmark_* result, prints a Passed!!!/Failed!!!
line as benchmark_thread_fusion does, and returns -1 on any failure. The
CSV files are unchanged.

benchmark_thread_fusion already returned -1 on a mismatch.
benchmark_latency_hiding and benchmark_numinstructions_x_operation only
time their executions and have no comparison to fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
… data

Executor<TransformDPP<ParArch::GPU_AMD>> gets the alignment guard of the
CUDA executor (28101e3): when the pointers or pitches of a thread fused
read or write are not aligned to the thread fusion vector types, it
launches the TF::DISABLED kernel through launchNonThreadFused. The HIP
executor is again the CUDA one with the HIP runtime API names.

On the RX 9060 (gfx1200) misaligned thread fused accesses do not fault
and give the same results, so this keeps the backends consistent rather
than fixing a crash there. In test_thread_fusion_alignment_hip the guard
moves 147 of 434 launches to the TF::DISABLED kernel. hipMallocPitch
pitches are 256-byte aligned, so FKL-allocated device Ptr2Ds keep thread
fusion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
Like the other tests (dfd946e): under HIP the unqualified uchar2 and
float2 are HIP's own vector types, not FKL's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
- Ptr::crop and Ptr::getPlane incremented the count and then built the
  result with a constructor that increments it again, so the allocation
  of every cropped Ptr (and Image) was never freed.
- freePtr() only cleared ref when it released the last reference. Alloc
  and allocTensor on a Ptr that shared its allocation then threw, and
  the destructor released the same reference again: a use-after-free of
  the other owners' memory.

test_ptr_nd checks the counts of crops, planes, a crop that outlives
its parent, and Alloc/allocTensor on a shared Ptr.

Closes #377

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HaJGJQnKer8xAT5P66U9Dz
(cherry picked from commit d0588f8)

LTS-C++17: test_ptr_nd keeps test_setTo_host and its launch() (no
launch_impl or namespace fk wrapper), adds test_shared_allocation_ref_counts
before them and ANDs its result into the return value; the new test uses
fk::uchar3 so that the _hip target builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
Configure every LTS workflow with -DENABLE_BENCHMARK=ON so that CI
compiles the benchmarks (fkBenchmarksCommon.h, the CUDA graphs variant
of benchmark_horizontal_fusion...) with every host compiler and CUDA
version of the matrix. They were never built in CI before.

The benchmarks register as ctest tests (benchmark_*_cpp and _cu);
benchmark_thread_fusion_cpp alone takes more than 10 minutes, so the
Test step excludes them with -E "^benchmark_".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
The Linux x64, ARM64 and Windows jobs asked for [self-hosted, linux, x64]
(or arm64/windows) only. Those labels also match runners without a CUDA
toolkit: main's CPU job runs on asusb550mlinux, the ROCm machine, which
has the self-hosted, linux, x64 labels and no nvcc. A CUDA job scheduled
there finds no CUDA compiler, disables CUDA and passes without building
or testing a single _cu target (or fails because the host compiler of
the matrix is not installed there).

Ask for the cuda label too, as main's CUDA workflows do
(minisforumwsl, jetsonagxorin and MINISFORUM carry it). The Ubuntu 24.04
job already asks for its own ubuntu2404 label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
0e56a81 (main's fabbb80) sent every non-Device Ptr to a synchronous CPU
fill when the stream is a GPU stream. That fixed MemType::Host, which the
GPU can not write, but HostPinned memory was filled by a kernel on the
stream before, and the CPU fill is not ordered with the work already
enqueued: a download into the Ptr that is still pending overwrites the
values, an upload from it that is still pending copies the new values,
and a graph capture does not record the fill. The pinned copy of
DeviceAndPinned memory had the same problem (also before 0e56a81).

With a GPU stream, setTo now:
- fills Device and HostPinned memory with a kernel on the stream, as
  LTS-C++17 did before 0e56a81;
- fills both copies of DeviceAndPinned memory with kernels on the
  stream;
- fills Host memory from the CPU after stream.sync(), so that the work
  enqueued before setTo has finished.

test_ptr_nd checks the order with copies that wait behind a busy GPU (a
clock64() spin kernel): a download into HostPinned and into Host
memory, an upload from HostPinned memory, and a download of
DeviceAndPinned memory, each followed by setTo. On the RX 9060 the old
code fails the HostPinned download, HostPinned upload and
DeviceAndPinned cases (3/3 runs); the new code passes them.

Reported by the NVIDIA-side review of PR #401 (verified on the RTX 4090)
and by the review of this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
(cherry picked from commit 1838493)

LTS-C++17: CircularTensor::update launches a DivergentBatchTransformDPP
whose plane space is the sum of the planes of its sequences. The copy
sequence declared BATCH planes (CircularTensorRead::num_elems_z returned
BATCH) instead of BATCH - 1, so a last row of blocks wrote plane BATCH,
past the end of the output Tensor, in every update, on CUDA and HIP. On
the RX 9060 that write lands in the update's temporary tensor: with BATCH
4 and 5 the oldest plane ends up holding a newer input.

The library change is main's commit as is. Main's test_circular_tensor.h
does not exist on LTS: tests/data/test_circular_tensor.h is its GPU test
(__ONLY_GPU__, since LTS has no CPU executor for
DivergentBatchTransformDPP) for BATCH 2, 3, 4, 5, 8 and 15. Without the
fix it fails for BATCH 4 and 5 on HIP; with it, all pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
update throws std::runtime_error when the CircularTensor is not in
Device or DeviceAndPinned memory and the stream is a GPU stream, but it
only checked ParArch::GPU_NVIDIA. With the GPU_AMD DivergentBatch
executor a Host-memory CircularTensor was launched on the GPU: on the
RX 9060 it aborts with "an illegal memory access was encountered". The
check now covers GPU_NVIDIA and GPU_AMD, as setTo does.

test_circular_tensor checks that the update of a Host-memory
CircularTensor with a GPU stream throws.

Reported by the NVIDIA-side review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
The HIP code paths (ParArch::GPU_AMD streams, memory, DPPs and
executors, the fk:: vector type aliases, the HIP timers) were selected
with __HIPCC__. ROCm's hip/hip_common.h also defines __HIPCC__ in nvcc
and clang CUDA compilations that include a HIP header (HIP on NVIDIA),
so a .cu file that included one before FKL got both backends:
fk::float3 became fk::Float3 instead of ::float3, ptr_nd.h allocated
with hipMalloc and copied with cudaMemcpyAsync, and Stream_<GPU_AMD> was
defined with the CUDA one. LTS-C++17 before the HIP backend compiled
such files.

compiler_macros.h defines FK_HIP_BACKEND for HIP compilations by clang
(__HIP__, which only clang predefines, in HIP mode) that are not CUDA
ones, and every former use of __HIPCC__ in include/, tests/, utests/ and
benchmarks/ tests FK_HIP_BACKEND instead. The stale "__HIP__" #endif
comments of ptr_nd.h and image.h name the new macro.

No change for real HIP, CUDA or CPU builds: the preprocessed output of
all 190 translation units of the HIP + CPU build (241 host, device and
CPU passes) is byte-identical before and after. A CPU compilation of
test_ptr_nd with -D__HIPCC__ failed before and is now identical to one
without it.

Reported by the NVIDIA-side review of PR #401 and by the review of this
branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
…nd CUDA

b8752fd made fk::float3, fk::uchar3... using-declarations of the global
vector types with nvcc and on the CPU, so that CPU and CUDA code keeps
the same types, but only the HIP side was checked. utest_VBase now also
asserts, outside HIP, that fk::float3, fk::uchar1, fk::uchar3,
fk::int2, fk::double4 and fk::bool3 are the global types.

Suggested by the NVIDIA-side review of PR #401 and by the review of this
branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
… only

- The _cpp target only printed "nothing to test" and passed: the
  source is now marked __ONLY_GPU__, as test_divergent_hf_executor.h,
  and the CPU stub is gone.
- The "no device memory lost" check can not fail under HIP: ROCm maps
  the streams onto a few hardware queues and reuses their memory. On
  the RX 9060, 4000 streams created after a few others and never
  destroyed cost 0.00 MB of free device memory, so the check passed
  with a Stream_ that leaks every stream. It now runs with nvcc only,
  where each CUDA stream holds about 160 KB; the HIP target keeps the
  sharing, lifetime and user stream checks.

Reported by the NVIDIA-side review of PR #401 and by the review of this
branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
- hip_init.cmake set ENABLE_HIP OFF in the cache with FORCE when no HIP
  compiler was found, so configuring the same build directory again
  with -DCMAKE_HIP_COMPILER=... left HIP disabled. CMakeLists.txt now
  runs check_language(HIP) and defines the ENABLE_HIP option only when
  a compiler is found, as it does for CUDA; otherwise ENABLE_HIP is a
  plain OFF variable for that configure, and an ENABLE_HIP the user put
  in the cache is left alone.
- On a machine with HIP for NVIDIA GPUs, check_language(HIP) could pick
  nvcc as the HIP compiler and the _hip targets would then fail to
  build. FKL's HIP backend is ParArch::GPU_AMD (CUDA is the NVIDIA
  backend), so CMAKE_HIP_PLATFORM defaults to "amd" before the check,
  which then only considers compilers for AMD GPUs, and HIP is enabled
  only for that platform.

Checked: the build-hip and build-verify trees reconfigure unchanged; a
new build directory with HIPCXX=g++ disables HIP (no _hip targets, no
ENABLE_HIP in the cache) and enables it when configured again with
-DCMAKE_HIP_COMPILER=amdclang++; -DCMAKE_HIP_PLATFORM=nvidia
-DENABLE_HIP=ON disables HIP without errors.

Reported by the NVIDIA-side review of PR #401 (FORCE) and by the review
of this branch (HIP platform).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
The CUDA and HIP executors computed their grids as
ceil(activeThreads.x / static_cast<float>(block.x)) (and .y), and
TransformDPP::build_details the thread fusion thread space as
ceil(width / static_cast<float>(elems_per_thread)). The uint is rounded
to a float first, so above 2^24 the result can be one too small and the
last elements are silently never written: a 1D Ptr of 2^24 + 1 elements
lost its last element without thread fusion, and one of 2^26 + 4 uchar
its last 4 with it, on the GPU and, for thread fusion, on the CPU too.

- active_threads.h: ceilDiv(a, b), a / b rounded up with integers.
- executors.h: computeGrid(activeThreads, block), shared by the CUDA
  and HIP executors (TransformDPP and DivergentBatchTransformDPP, 6
  grids), replaces the float ceil.
- data_parallel_patterns.h: build_details uses ceilDiv.

Host code only: the kernels are unchanged, and below 2^24 the sizes are
the same as before. tests/thread_fusion/test_large_grids.h writes
2^24 + 1 elements without thread fusion and 2^26 + 4 (and 2^24) with
it and counts the elements not written. On the RX 9060 the old code
misses 1 and 4 elements in test_large_grids_hip and 4 in
test_large_grids_cpp; the new code misses none.

Reported by the NVIDIA-side review of PR #401 (also on main and on
LTS-C++17 before this branch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
- The deleter of the cudaStream_t discarded the result of
  cudaStreamDestroy, but a failed destroy leaves its error pending, and
  the next cudaGetLastError() reports it for unrelated work: after
  "{ fk::Stream s; cudaDeviceReset(); }" the next launch threw "invalid
  device context" in its gpuErrchk (it did not on 1a9bfe8). The deleter
  now clears the error of the destroy, unless an error was already
  pending before it, which is left for its owner. The HIP deleter does
  the same with hipPeekAtLastError/hipGetLastError. On ROCm 10.1 that
  case leaves no error (hipStreamDestroy after hipDeviceReset succeeds),
  so for HIP this keeps the backends alike.
- The comment of Stream_<GPU_AMD>, copied from main, said that
  hipStreamDestroy releases the stream when its pending work has
  finished. On ROCm it blocks the host until that work has finished
  (93.6 ms for a pending 94 ms kernel on the RX 9060); the comment says
  so.

Reported by the NVIDIA-side review of PR #401 (error) and by the review
of this branch (comment).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
In a CUDA or HIP build, setTo with a Stream_<ParArch::CPU> on a Ptr in
Device or DeviceAndPinned memory ran the CPU executor on ptr(), a
device pointer, from the CPU: a segfault with CUDA. It now throws
std::runtime_error, as main's CPU_MULTITHREAD setTo does for those
memory types. Host and HostPinned memory are filled by the CPU as
before.

test_ptr_nd checks that it throws for Device and DeviceAndPinned memory
(without the check, the _hip target fails it).

Reported by the NVIDIA-side review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
raw_image.h names uchar3, ushort3 and float3 (ColorDepthPixelTypes) but
did not include vector_types.h. Under HIP, when it was the first FKL
header, those names found HIP's global vector types (hip_runtime.h comes
in through rawptr.h), not fk::uchar3..., and a ReadYUV into a
Ptr2D<fk::uchar3> failed with "no known conversion from
HIP_vector_type<unsigned char,3> to fk::Uchar3". On the CPU the same
include order did not compile. With the include, a translation unit
that includes raw_image.h first compiles in both, and
ColorDepthPixelType<p8bit> is fk::uchar3.

Reported by the review of this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
…ribute

Under HIP, __grid_constant__ was defined only when clang supports the
grid_constant attribute; with a clang that does not, the kernels in
executor_kernels.h failed with "unknown type name __grid_constant__".
On AMD GPUs kernel parameters are always in the constant kernel
argument segment, so it is now defined empty there. No change with
ROCm 10.1, whose clang supports the attribute.

Reported by the review of this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
clang accepts some C++20 features in -std=c++17 mode as extensions,
with a warning (designated initializers, uninitialized variables in
constexpr functions...); e354f0e removed the ones LTS had, but nothing
kept new ones out. HIP sources are now compiled with
-Werror=c++20-extensions (which covers -Wc++20-designator), so the HIP
build keeps the code C++17. The CUDA and CPU flags are unchanged.

Also drops the stale "#todo: add hip support" in
benchmarks/CMakeLists.txt: the benchmarks have _hip targets.

Suggested by the review of this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
morousg and others added 24 commits October 29, 2026 13:57
- README: the sample and the snippets that explain it used uchar3 and
  float3 with "using namespace fk;", which is ambiguous under HIP
  (HIP's global types and fk::uchar3...). The code now uses the fk::
  names, like tests/examples/readme_test_code.h. The prose that names
  the resulting types is unchanged.
- README, Backends and building: the __ONLY_* markers and
  ENABLE_BENCHMARK decide which targets are built; the HIP build uses
  the default C++ compiler (it said g++ but did not select it); the
  example no longer enables the benchmarks, and says how to skip them
  in ctest (benchmark_thread_fusion_cpp alone takes more than 10
  minutes).
- fkl-build-and-test skill: the loop over *_cu *_hip *_cpp printed
  "*_hip FAIL" in every build without HIP (an unmatched glob stays
  literal). It sets nullglob first.

Reported by the review of this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
In the first CI run of PR #401 only 2 of the 12 jobs (ARM64, CUDA 12.9)
built CUDA: the others pointed CUDACXX at toolkits the runners no longer
have (/usr/local/cuda-13.3 on minisforumwsl and jetsonagxorin, 12.9 and
13.3 on MINISFORUM), so check_language(CUDA) found nothing, CUDA was
disabled, and the jobs passed with the CPU targets only ("CUDA compiler
not found, CUDA support will be disabled", 34 tests instead of 73).

- The Linux x64 and ARM64 jobs use CUDA 13.4, the version main's CUDA
  workflows use on the same runners, instead of 13.3 (ARM64 keeps its
  CUDA 12.9 jobs, which work).
- The Linux workflows pass -DCMAKE_CUDA_COMPILER, as main's do, so a
  toolkit that is not installed fails the configure step ("Compiler
  requires the CUDA toolkit") instead of silently testing the CPU
  backend only.

The Windows workflow is not changed here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
…stPinned

- The Host memory case of test_setTo_stream_order downloaded into
  pageable memory, but a copy into pageable memory returns only when it
  has finished (CUDA documents it, and on the RX 9060 downloadTo blocked
  until the spin kernel and the copy were done), so the case passed
  without the stream.sync() that c5ada13 added to setTo. The earlier
  work is now a host function (cudaLaunchHostFunc/hipLaunchHostFunc)
  enqueued behind the spin kernel that writes the memory. On the RX 9060
  a setTo without the sync fails the case 3/3; the current one passes.
- setTo with a CPU stream fills HostPinned memory from the CPU in the
  GPU builds: checked next to the Device and DeviceAndPinned cases that
  throw.

Found by the round 3 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
…error

25c8559 makes the Stream_ deleter clear the error of a failed
cudaStreamDestroy (unless one was already pending), so that the next
cudaGetLastError() does not report it for other work. With nvcc the
utest now checks it last: a GPUStream destroyed after cudaDeviceReset
must leave no error pending. Under HIP there is nothing to check:
hipStreamDestroy after hipDeviceReset succeeds on ROCm 10.1.

Compiled with nvcc 13.4 (g++-13 and g++-15 as host compilers); it runs
only on a CUDA machine.

Found by the round 3 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
A compile time test for 75411b2: a translation unit whose first FKL
header is raw_image.h must see FKL's vector types, so
ColorDepthPixelType<p8bit/p10bit/f24bit> are fk::uchar3, fk::ushort3
and fk::float3, also under HIP, where hip_runtime.h declares its own
global uchar3... With the raw_image.h of before 75411b2 neither target
compiles (5 errors in the _hip one, 25 in the _cpp one).

Found by the round 3 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
- README and the build-and-test skill presented -DHIP_ARCH as a way to
  choose the HIP architectures, but hip_init.cmake uses it only when
  CMAKE_HIP_ARCHITECTURES is not cached yet: they now say "on the first
  configure of a build directory", and -DCMAKE_HIP_ARCHITECTURES to
  change it later.
- The skill's CI section said CUDA 12.9 and 13.3: since 0a0ed04 the
  Linux workflows use 12.9 and 13.4. It also says that CI builds the
  benchmarks without running them, and that no workflow builds HIP.

Found by the round 3 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
7309118 replaced __HIPCC__ with FK_HIP_BACKEND in every header,
including the new comment that explains why __HIPCC__ can not select
the HIP backend, which then said that FK_HIP_BACKEND does not identify
it. Comment only.

Found by the round 3 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
The DivergentBatch executors build their block as
min(activeThreads.x, 32) x min(activeThreads.y, 8), which is 0 wide (or
high) for an empty thread space. 2ce1f46 computes the grid with
ceilDiv(activeThreads.x, block.x), an integer division by zero there: on
x86-64 the process died with SIGFPE (both CUDA and HIP), where the float
formula before it gave a grid the launch rejected, so gpuErrchk threw
"invalid configuration argument". ceilDiv(a, 0) is now 0, so the launch
fails and throws as before. The TransformDPP executors are not
affected (getDefaultBlockSize never returns 0).

test_circular_tensor updates a CircularTensor that is not allocated with
an empty input and checks that it throws: without the fix the _hip
target dies with SIGFPE (exit 136).

Found by the round 3 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
ENABLE_HIP_LINE_INFO (ON by default) added -gline-tables-only to every
HIP compile, after the -g of CMAKE_HIP_FLAGS_DEBUG and of
add_hip_debug_support_to_target. clang keeps the last -g* flag, so a
Debug or ENABLE_DEBUG build of a _hip target only had line tables (no
variables in rocgdb). As add_cuda_lineinfo_to_target does for
-lineinfo, the line tables are now added only without ENABLE_DEBUG, and
only for configurations other than Debug: a Debug test_ptr_nd_hip
compiles with -g, a Release one with -gline-tables-only as before.

ENABLE_DEBUG was declared only by cmake/libs/cuda/debug.cmake, so a
configure without CUDA did not offer it: hip/debug.cmake declares it
too.

Found by the round 3 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
The LTS workflows test CPU and CUDA, but ENABLE_HIP defaults to ON when
check_language(HIP) finds a HIP compiler for AMD GPUs: a runner that
also has ROCm installed would build every _hip target and ctest would
run them. The three Linux workflows now pass -DENABLE_HIP=OFF, as main's
CUDA workflows do. On this machine (ROCm 10.1 installed) a configure
with it has no _hip targets.

Found by the round 3 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
…aviour, segfaults on CUDA 13.4)

5e4c0ea checked that a GPUStream destroyed after cudaDeviceReset leaves
no pending error. On the RTX 4090 (CUDA 13.4, driver R610)
utest_stream_ownership_cu then segfaults (exit 139, 3/3 runs): gdb shows
SIGSEGV in libcuda under cuStreamDestroy_v2, from cudaStreamDestroy in
the owned-stream deleter, on the destructor of that GPUStream.
cudaDeviceReset destroys all the resources of the context, and passing
a stream from before the reset to a later CUDA call is undefined:
whether the destroy returns an error or crashes depends on the state of
the process, so no ordering makes the check reliable.

The check is removed, with a comment that says why; the deleter of
25c8559 is unchanged. With the patch the test prints "GPU stream
ownership OK" 3/3 and is compute-sanitizer clean on the 4090.

Found by the NVIDIA agent's verification of a6ae66c (patch from it,
through fkl-paper).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
The e1a094b assert static_assert(std::is_same_v<fk::double4, ::double4>)
was the only new warning site under nvcc: CUDA 13 deprecates ::double4
("use double4_16a or double4_32a"). It checks fk::double3 instead,
which shows the same thing (a using-declaration of the global double
vector type) without the warning.

Reported by the NVIDIA agent's verification of 0a0ed04.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
6724ed1 fixed Ref::operator=, which took the counter of the other
object without releasing its own (and made the counter atomic), but no
test checked the counts: utest_stream_ownership only checks the native
GPU streams, now owned by a shared_ptr. Every Stream_ derives from Ref,
so the new utest checks the counts through Stream_<ParArch::CPU> in all
three backends: copies share a counter, an assignment leaves the old
counter (c drops to 1) and shares the new one, assigning the same
counter or the object itself changes nothing, and a copy that goes
away releases its count.

With the ref_class.h of origin/LTS-C++17 the _cpp target reports
"a 3, c 2 WRONG" and exits -1; with the fix all pass (_cpp and _hip
run here, _cu compiled with nvcc 13.4).

Found by the round 4 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
- test_setTo_host said the device can not write the memory and the
  values must be written from the CPU; since c5ada13 that holds for Host
  memory only, and HostPinned memory is filled by a kernel on the GPU
  stream. Comment only.
- README: the __ONLY_* markers apply to test and utest sources;
  benchmark discovery does not read them.

Found by the round 4 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
42b4f0f dropped -gline-tables-only both in Debug and whenever
ENABLE_DEBUG is ON, but add_hip_debug_support_to_target adds -g only in
the Debug configuration: a Release configure with ENABLE_DEBUG=ON
compiled the HIP objects with neither -g nor line tables, less than a
plain Release build. The line tables now depend only on the
configuration: Debug (with or without ENABLE_DEBUG) gets -g, every other
configuration -gline-tables-only. The comment said ENABLE_DEBUG gives
-g outside Debug; it does not.

Found by the round 4 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
hip_init.cmake put $HIP_PATH and $ROCM_PATH before
CMAKE_HIP_COMPILER_ROCM_ROOT in CMAKE_PREFIX_PATH. With two ROCm
installations, the environment pointing at one and the HIP compiler
coming from the other, find_package(hip) took the environment's
package, and hip::host linked a libamdhip64 other than the one the HIP
language links for the compiler's ROCm. The compiler's ROCm root now
comes first. No change with a single ROCm installation (on this machine
both resolve to /opt/rocm/core-10.1/lib/cmake/hip).

Found by the round 4 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
With -DENABLE_BENCHMARK=ON, the configure that CI and the README use,
the loop over *_cu *_hip *_cpp also ran the benchmark_* binaries, and
benchmark_thread_fusion_cpp alone takes more than 10 minutes, not the
"few minutes" the skill promises. The loop skips them, as ctest -E
"^benchmark_" does.

Found by the round 5 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
- 0373c8a added -gline-tables-only in every configuration but Debug,
  and said Debug was the only one with -g. RelWithDebInfo has it too
  (CMAKE_HIP_FLAGS_RELWITHDEBINFO is -O2 -g -DNDEBUG, and
  cmake_init.cmake lists RelWithDebInfo in CMAKE_CONFIGURATION_TYPES),
  so a RelWithDebInfo HIP build only had line tables. The line tables
  are now added outside Debug and RelWithDebInfo: Debug -g,
  RelWithDebInfo -O2 -g, Release -O3 and MinSizeRel -Os with
  -gline-tables-only.
- The comment of the ENABLE_DEBUG declaration in hip/debug.cmake says
  that the option changes nothing for HIP: Debug builds already compile
  host and device code with -g and without optimizations.

Found by the round 5 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
c5ada13 fills Host (pageable) memory from the CPU after stream.sync()
when the stream is a GPU stream. On a stream that is being captured
into a graph that sync is not allowed: gpuErrchk threw "operation not
permitted when stream is capturing", and the capture was invalidated,
so the work captured before it was lost too (hipStreamEndCapture:
"operation failed due to a previous error during capture"; CUDA
documents the same for cudaStreamSynchronize during capture). setTo now
checks cudaStreamIsCapturing/hipStreamIsCapturing first and throws
std::runtime_error, leaving the capture as it was.

The comment also says that the GPU can write pageable memory on systems
that give GPUs access to it (HMM/ATS). On those, LTS-C++17 before this
branch filled Host memory with a kernel, also in a capture; now the CPU
fills it, with the same result outside a capture.

test_ptr_nd captures a setTo on Device memory and then a setTo on Host
memory: the second must throw, and the capture must end with a valid
graph of 1 node. Without the check the _hip target gets "capture valid
0, 0 nodes" and fails; with it, "threw 1, capture valid 1, 1 nodes".
The _cu target compiles with nvcc 13.4 (g++-13 and g++-15).

Found by the round 5 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
FKL is header only, and its templates and inline functions get a
different definition in each backend under the same name: a program
that links a .cpp translation unit compiled for the CPU and a .hip one
that both instantiate Ptr2D<fk::uchar3> keeps one of the definitions
(the round 5 reviewer's probe aborted with "Device allocation not
supported in non-CUDA compilation" or worked, depending on the link
order). With CUDA this already held for scalar element types; under
HIP the fk:: vector types are the same types as on the CPU, so it also
holds for them. Every FKL target is built for one backend; the README
now says not to mix them in one program.

Found by the round 5 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
The spin kernel of the stream order tests waited for 50M clock64()
cycles. Under HIP clock64() is __builtin_readcyclecounter(), which on
gfx11 (RDNA3: RX 7000, W7000, Phoenix/Strix APUs) reads
hwreg(HW_REG_SHADER_CYCLES, 0, 20), a 20 bit counter that wraps: the
difference never reaches 50M, so test_ptr_nd_hip would never finish
there (gfx1200 reads the 64 bit SHADER_CYCLES_HI/LO, so it passed on the
RX 9060).

Under HIP the kernel now waits for 20 ms of wall_clock64() ticks
(hipDeviceAttributeWallClockRate, in kHz), which on gfx1100 and gfx1200
is the 64 bit realtime counter (s_sendmsg_rtn_b64 MSG_RTN_GET_REALTIME).
The CUDA kernel is unchanged: clock64() is 64 bit there.

On the RX 9060 all the cases pass 3/3, and with the non-Device memory
filled from the CPU without a sync (as before c5ada13) the HostPinned
download, Host host function and HostPinned upload cases still fail
3/3. The gfx1100 code of the kernel has no SHADER_CYCLES read. The _cu
target compiles with nvcc 13.4.

Found by the round 6 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
.github/copilot-instructions.md still described the branch before this
PR: CUDA and CPU backends only, vector types outside the fk namespace,
only the ONLY_CU and ONLY_CPU markers and .cpp/.cu targets, CI runners
without the cuda label, CUDA 12.9/13.3 and compilers "not as CMake -D
flags". An agent following it would write tests with the global float3
(HIP's own type in the _hip targets) or mark a GPU test ONLY_CU. It now
matches the README and the fkl-build-and-test skill: three backends and
the fk:: vector type names, the HIP requirements and options
(ENABLE_HIP, HIP_ARCH), the _hip targets and the ONLY_GPU marker,
FK_HIP_BACKEND, the HIP stream wrapper, and the CI workflows as they
are now (cuda label, 12.9/13.4, -DCMAKE_CUDA_COMPILER, -DENABLE_HIP=OFF,
benchmarks built but not run, Windows CPU only, no HIP job).

Found by the round 6 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
divergent_operate passed the function pointer launchTransformDPP<IOps...>
to apply_d. Without optimizations, as in HIP Debug builds (-g, no -O),
clang keeps that as an indirect call (a readfirstlane loop and
s_swappc_b64), and the static scratch size of the kernel does not
include the callee's frame: the runtime gives it the default
hipLimitStackSize of 1024 bytes per lane, while the CircularTensor
update kernel needs about 3256 and the test_divergent_hf_executor one
about 2120. In a Debug build test_circular_tensor_hip aborted with
HSA_STATUS_ERROR_MEMORY_FAULT on its first update (private_seg_size
1024), and test_divergent_hf_executor_hip passed only by luck; at -O1
and above both pass. The line is older than the HIP backend; nvcc -G
resolves the call statically.

A generic lambda that calls launchTransformDPP is a direct call the
compiler can see through, as executors.h already does with apply. In a
Debug HIP build both tests now pass. In the Release build the gfx1200
device code of both tests is unchanged (the same instructions; only
debug line info differs), and nvcc 13.4 compiles both with -O3 and -G
without stack size warnings.

Found by the round 7 review of PR #401.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
From the NVIDIA agent's capture probes on fd04605, documented in the
setTo comment (comment only):
- the stream.sync() of a Host fill on a stream that is not capturing
  still invalidates a capture the same thread makes on another stream in
  global mode, as any synchronization does (relaxed mode works);
- with an fk::Stream wrapping the legacy default stream while another
  stream captures, cudaStreamIsCapturing returns
  cudaErrorStreamCaptureImplicit and gpuErrchk throws (the other capture
  survives).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iTqfS8rWiAyRMrM4uU64n
@morousg
morousg merged commit b984f10 into LTS-C++17 Oct 29, 2026
16 checks passed
@morousg
morousg deleted the LTS-C++17-rocm branch October 29, 2026 18:38
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