From abdb59bc43accd899bb0fbea0ccfbc413236612d Mon Sep 17 00:00:00 2001 From: Alexis Montoison Date: Sat, 7 Nov 2026 01:53:53 -0600 Subject: [PATCH 1/2] feat(hipfft): one module per hipFFT header, hipfftxt beside hipfft CUDA Fortran ships cufft and cufftXt, the latter including cufft and the cuFFTMp interfaces, so `use cufftXt` should hipify to a module of its own. The hipfft module now holds hipfft.h only, and the new hipfftxt module hipfftXt.h, hipfftMp.h and the descriptor types of hiplibxt.h. hipfftxt `use`s hipfft whole, so `use hipfftxt` alone gives the whole API, as including hipfftXt.h does in C. The new [library] uses key names the modules a library uses whole: their functions, enums, constants and types are not redeclared, and they are re-exported. With it, own_headers also selects the functions. --explain picks, among libraries sharing a prefix, the one whose headers declare the function. --- README.md | 9 ++- src/common/assemble.jl | 3 +- src/common/clang_args.jl | 12 +++- src/common/clang_frontend.jl | 4 +- src/common/config.jl | 10 +-- src/common/header_sources.jl | 11 ++-- src/common/metadata.jl | 2 + src/explain_metadata.jl | 8 ++- src/hipfft/generator.jl | 15 ++--- src/hipfft/hipfft.toml | 5 +- src/hipfft/metadata/hipfft.toml | 18 ------ src/hipfftxt/generator.jl | 52 ++++++++++++++++ src/hipfftxt/hipfftxt.toml | 26 ++++++++ src/hipfftxt/metadata/hipfftxt.toml | 32 ++++++++++ src/test/fixtures/uses/include/ulib/ulib.h | 19 ++++++ src/test/fixtures/uses/include/ulib/ulibXt.h | 25 ++++++++ src/test/fixtures/uses/ulib.toml | 17 +++++ src/test/fixtures/uses/ulibxt.toml | 19 ++++++ src/test/runtests.jl | 65 +++++++++++++++++++- 19 files changed, 300 insertions(+), 52 deletions(-) create mode 100644 src/hipfftxt/generator.jl create mode 100644 src/hipfftxt/hipfftxt.toml create mode 100644 src/hipfftxt/metadata/hipfftxt.toml create mode 100644 src/test/fixtures/uses/include/ulib/ulib.h create mode 100644 src/test/fixtures/uses/include/ulib/ulibXt.h create mode 100644 src/test/fixtures/uses/ulib.toml create mode 100644 src/test/fixtures/uses/ulibxt.toml diff --git a/README.md b/README.md index 4336d21..695d853 100644 --- a/README.md +++ b/README.md @@ -122,13 +122,14 @@ Run from this repository: | hipSPARSE | `ROCM_LIBRARIES= ./gen.sh --overwrite hipsparse` | `bindings/fortran/hipsparse/hipsparse.F90` | | rocFFT | `ROCM_LIBRARIES= ./gen.sh --overwrite rocfft` | `bindings/fortran/rocfft/rocfft.F90` | | hipFFT | `ROCM_LIBRARIES= ./gen.sh --overwrite hipfft` | `bindings/fortran/hipfft/hipfft.F90` | +| hipFFT Xt and Mp | `ROCM_LIBRARIES= ./gen.sh --overwrite hipfftxt` | `bindings/fortran/hipfft/hipfftxt.F90` | | hipFFTW | `ROCM_LIBRARIES= ./gen.sh --overwrite hipfftw` | `bindings/fortran/hipfft/hipfftw.F90` | | rocRAND | `ROCM_LIBRARIES= ./gen.sh --overwrite rocrand` | `bindings/fortran/rocrand/rocrand.F90` | | hipRAND | `ROCM_LIBRARIES= ./gen.sh --overwrite hiprand` | `bindings/fortran/hiprand/hiprand.F90` | `` and `` are the paths of your checkouts, and the last column is relative to them. Several libraries can be named in one run -(`./gen.sh --overwrite hipfft hipfftw`). A math library reads its own headers from +(`./gen.sh --overwrite hipfft hipfftxt hipfftw`). A math library reads its own headers from the checkout and HIP's from the installed ROCm (`ROCM_INCLUDE`, default `/opt/rocm/include`); set `ROCM_SYSTEMS` as well to take HIP from a rocm-systems checkout instead. @@ -158,7 +159,7 @@ the checkout, and compile it with `bash check_mods.sh`. main() ``` - The others pass `argshook=` to add include directories or force-included headers (see `src/hipfft/generator.jl`). + The others pass `argshook=` to add include directories or force-included headers (see `src/hipfftxt/generator.jl`). 3. Add `""` to `LIBS` in `src/common/header_sources.jl` (and to `proj_of`, `repo_of` and `lib_include` there if its header directory is not `/` or it ships in rocm-systems). 4. Run `./gen.sh && bash check_mods.sh`, then add `src//metadata/.toml` as needed (see [Fixing a binding](#fixing-a-binding)). @@ -172,7 +173,8 @@ If the generator finds far fewer functions than expected, check the `prefix` spe | `[general] name` | required | the library and module name, and the metadata file name | | `[library] prefix` | required | only functions whose name starts with it are bound | | `[library] opaque_handles` | required (may be `[]`) | struct tags never emitted as derived types (handles) | -| `[library] own_headers` | `[name]` | header-path markers of the files whose enums, structs and macros are this library's | +| `[library] own_headers` | `[name]` | header-path markers of the files whose enums, structs and macros (and, with `uses`, functions) are this library's | +| `[library] uses` | `[]` | modules `use`d whole, so the module re-exports them; their headers' functions, enums, constants and types are not redeclared (hipfftxt: `["hipfft"]`, as CUDA Fortran's cufftXt includes cufft) | | `[library] reexport_enums` | `[]` | sibling libraries whose enums are `use`d from the sibling module instead of copied (rocSOLVER: `["rocblas"]`) | | `[library] keep_structs` | `[]` | HIP vector types to emit anyway, when a function takes one by value (rocRAND: `["uint4"]`) | | `[library] pointer_mode` | `false` | the handle has a pointer mode: a `const` pointer named `alpha` or `beta` is `scalar-pointer-mode` (BLAS, SPARSE) | @@ -498,6 +500,7 @@ The version is in `src/Project.toml` and follows semantic versioning; releases a | hipSOLVER | `hipsolver` | | hipSPARSE | `hipsparse` | | hipFFT | `hipfft` | +| hipFFT Xt and Mp (`hipfftXt.h`, `hipfftMp.h`) | `hipfftxt` (uses and re-exports `hipfft`) | | hipFFTW | `hipfftw` | | hipRAND | `hiprand` | diff --git a/src/common/assemble.jl b/src/common/assemble.jl index 997fff6..cb33c66 100644 --- a/src/common/assemble.jl +++ b/src/common/assemble.jl @@ -33,6 +33,7 @@ function gen_files(output_dir::String, cfg::Dict, funcs::Vector{FuncInfo}, aliases::Dict{String,String} = Dict{String,String}(), sibling_enum_mod::Dict{String,String} = Dict{String,String}(), sibling_enums::Vector{EnumInfo} = EnumInfo[], + uses::Vector{String} = String[], extra_generics::Union{Nothing,Function} = nothing, extra_wrappers::Union{Nothing,Function} = nothing, extra_procs::Dict{String,Tuple{Vector{String},Vector{String}}} = @@ -74,7 +75,7 @@ function gen_files(output_dir::String, cfg::Dict, funcs::Vector{FuncInfo}, # only withholds their _typed form. devonly = md.devonly, string_args = get(cfg["library"], "string_args", false), - own_consts, sibling_enum_mod, + own_consts, sibling_enum_mod, uses, bind_names = copy(BIND_NAMES[]), extra_procs) # Must precede all emission. diff --git a/src/common/clang_args.jl b/src/common/clang_args.jl index f01fd94..0f0d470 100644 --- a/src/common/clang_args.jl +++ b/src/common/clang_args.jl @@ -450,7 +450,9 @@ function parse_library(dir::AbstractString, include_dir::AbstractString; root = Clang.getTranslationUnitCursor(tu) prefix = cfg["library"]["prefix"] - funcs = GC.@preserve ctx tu collect_funcs(root, prefix) + # With [library] uses, the functions of a used module's headers are that module's. + fown = isempty(uses_of(cfg)) ? nothing : own_headers_of(cfg) + funcs = GC.@preserve ctx tu collect_funcs(root, prefix; own=fown) @info "Found $(length(funcs)) $(prefix)* functions" declared = [f.name for f in funcs] funcfilter === nothing || (funcs = funcfilter(funcs)) @@ -463,6 +465,7 @@ end # An empty TOML array parses as Vector{Any}, which the typed keywords reject. strlist(x) = collect(String, x) own_headers_of(cfg::Dict) = strlist(get(cfg["library"], "own_headers", [cfg["general"]["name"]])) +uses_of(cfg::Dict) = strlist(get(cfg["library"], "uses", String[])) """ library_funcs(dir; include_dir, headers, argshook, funcfilter, gen_kwargs...) -> (cfg, funcs, parsed) @@ -527,9 +530,12 @@ function run_library_generator(dir::AbstractString; @info "Found $(length(macros)) #define constants" # Each sibling constant, mapped to the module defining it, for the `use` lines. + # uses: modules `use`d whole, and so re-exported (hipfftxt uses hipfft); their + # enums, constants, types and functions are not redeclared. + uses = uses_of(cfg) sibling_enum_mod = Dict{String,String}() sibling_enums = EnumInfo[] - for sib in reexport + for sib in vcat(reexport, uses) sib_enums = GC.@preserve ctx tu collect_enums(root; own=[sib]) sib_macros = GC.@preserve ctx tu collect_macros(root; own=[sib]) append!(sibling_enums, sib_enums) @@ -545,7 +551,7 @@ function run_library_generator(dir::AbstractString; @info "Found $(length(structs)) derived types" gen_files(output_dir, cfg, funcs, enums, structs; macros=macros, aliases=aliases, - sibling_enum_mod=sibling_enum_mod, sibling_enums=sibling_enums, gen_kwargs...) + sibling_enum_mod=sibling_enum_mod, sibling_enums=sibling_enums, uses=uses, gen_kwargs...) end # BLAS Set/Get Vector/Matrix families: drop them from the bind(C) path and emit diff --git a/src/common/clang_frontend.jl b/src/common/clang_frontend.jl index 037e91a..7006c19 100644 --- a/src/common/clang_frontend.jl +++ b/src/common/clang_frontend.jl @@ -126,7 +126,8 @@ function is_ptr_array(t) :: Bool return Clang.kind(pt) == Clang.CXType_Pointer && Clang.LibClang.clang_isConstQualifiedType(pt.type) != 0 end -function collect_funcs(root, prefix::String) :: Vector{FuncInfo} +# `own`, when given, also keeps only the functions declared in those headers. +function collect_funcs(root, prefix::String; own::Union{Nothing,Vector{String}} = nothing) :: Vector{FuncInfo} reset_unmapped_cptr!() reset_func_docs!() carried = Pair{String,String}[] # the last doc block's @params (function_docs!) @@ -139,6 +140,7 @@ function collect_funcs(root, prefix::String) :: Vector{FuncInfo} Clang.kind(child) == Clang.CXCursor_FunctionDecl || continue name = Clang.spelling(child) (startswith(name, prefix) || name == bare) || continue + own === nothing || owns_decl(cursor_file(child), own) || continue # `static inline` has no exported symbol: binding it is a link error. Clang.LibClang.clang_Cursor_isFunctionInlined(child.cursor) != 0 && continue # Skip (and log) variadic functions: Fortran cannot declare `...`, and calling diff --git a/src/common/config.jl b/src/common/config.jl index 4a1be8b..cff259a 100644 --- a/src/common/config.jl +++ b/src/common/config.jl @@ -107,10 +107,11 @@ neutralize_funcs(funcs::Vector{FuncInfo}, meta::LibMeta) = [ # One module-level `use , only: ...` per sibling, naming the union of what # this library borrows. A bare `use rocblas` makes every interface's IMPORT copy # all of rocBLAS (>5.9 GB on rocsolver); a per-routine `use, only` makes gfortran -# re-read rocblas.mod thousands of times. +# re-read rocblas.mod thousands of times. The `uses` modules come first, bare, so +# that the module re-exports them. function sibling_module_uses(funcs::Vector{FuncInfo}, libname::String, meta::LibMeta; extra::Vector{String} = String[]) :: Vector{String} - isempty(meta.sibling_enum_mod) && return String[] + isempty(meta.sibling_enum_mod) && return copy(meta.uses) bymod = Dict{String,Set{String}}() for f in funcs, u in sibling_use_onlys(f, libname, meta) mod, names = split(u, ", only: "; limit = 2) @@ -121,8 +122,9 @@ function sibling_module_uses(funcs::Vector{FuncInfo}, libname::String, meta::Lib (c in meta.own_consts || !haskey(meta.sibling_enum_mod, c)) && continue push!(get!(bymod, meta.sibling_enum_mod[c], Set{String}()), c) end - return [ "$(mod), only: " * join(sort(collect(names)), ", ") - for (mod, names) in sort(collect(bymod); by = first) ] + return vcat(meta.uses, + [ "$(mod), only: " * join(sort(collect(names)), ", ") + for (mod, names) in sort(collect(bymod); by = first) if !(mod in meta.uses) ]) end function sibling_use_onlys(f::FuncInfo, libname::String, meta::LibMeta) :: Vector{String} diff --git a/src/common/header_sources.jl b/src/common/header_sources.jl index 68a337b..8b5b0af 100644 --- a/src/common/header_sources.jl +++ b/src/common/header_sources.jl @@ -28,11 +28,11 @@ # modules from its own checkout: # # ROCM_SYSTEMS ROCM_LIBRARIES generated headers -# - - all 13 the installed include dir +# - - all 14 the installed include dir # set - hip, roctx the rocm-systems checkout # - set the math libraries the rocm-libraries checkout; HIP # from the installed include dir -# set set all 13 the two checkouts +# set set all 14 the two checkouts # # The installed include dir is a positional argument, read only when ROCM_SYSTEMS # is unset: with it, HIP comes from the checkout. @@ -47,6 +47,7 @@ const LIBS = [ "hipsolver", "rocfft", "hipfft", + "hipfftxt", "hipfftw", "rocrand", "hiprand", @@ -56,8 +57,8 @@ const LIBS = [ const DEFAULT_INCLUDE = "/opt/rocm/include" # Header subdirectory (installed) / project (checkout) of a library. -proj_of(lib) = lib == "hipfftw" ? "hipfft" : - lib == "roctx" ? "rocprofiler-sdk-roctx" : lib +proj_of(lib) = lib in ("hipfftxt", "hipfftw") ? "hipfft" : + lib == "roctx" ? "rocprofiler-sdk-roctx" : lib # Upstream repository each module is committed to, in its bindings/fortran/ tree. repo_of(lib) = lib in ("hip", "roctx") ? "rocm-systems" : "rocm-libraries" @@ -197,7 +198,7 @@ end # A library's metadata file (metadata/.toml): the checkout's # /metadata/ when it exists, else the generator's own # src//metadata/. The file names carry the library name, so the modules sharing -# a binding directory (hip and roctx, hipfft and hipfftw) share one metadata/. +# a binding directory (hip and roctx; hipfft, hipfftxt and hipfftw) share one metadata/. function lib_metadata(s::HeaderSources, lib) if from_checkouts(s) d = joinpath(fortran_dir(s, lib), "metadata") diff --git a/src/common/metadata.jl b/src/common/metadata.jl index 0993705..c55d705 100644 --- a/src/common/metadata.jl +++ b/src/common/metadata.jl @@ -54,6 +54,8 @@ Base.@kwdef struct LibMeta own_consts::Set{String} = Set{String}() "Foreign enum constant => sibling module that defines it (rocSOLVER uses rocBLAS kinds)." sibling_enum_mod::Dict{String,String} = Dict{String,String}() + "`[library] uses`: modules `use`d whole, so everything they define is re-exported." + uses::Vector{String} = String[] "Fortran routine name => C symbol for `bind(C, name=...)`, when they differ." bind_names::Dict{String,String} = Dict{String,String}() "Generic => module procedures a library emits itself, (per-rank, assumed-rank) diff --git a/src/explain_metadata.jl b/src/explain_metadata.jl index 978fbae..5506977 100644 --- a/src/explain_metadata.jl +++ b/src/explain_metadata.jl @@ -95,7 +95,13 @@ end function explain(io, name, s::HeaderSources, parse) # The library binding `name`: the longest prefix that matches (hipfft before hip). owners = [lib for lib in LIBS if !isempty(lib_prefix(lib)) && startswith(name, lib_prefix(lib))] - owner = isempty(owners) ? "" : owners[argmax(length.(lib_prefix.(owners)))] + isempty(owners) || (owners = filter(l -> length(lib_prefix(l)) == maximum(length ∘ lib_prefix, owners), owners)) + # Libraries sharing a prefix (hipfft, hipfftxt): the one whose headers declare it, + # else the one whose metadata names it. + declares(lib) = (p = parse(lib); !(p isa AbstractString) && any(f -> f.name == name, first(p).funcs)) + named(lib) = applying_table(load_metadata(joinpath(lib_metadata(s, lib), "$(lib).toml")), name) !== nothing + owner = isempty(owners) ? "" : length(owners) == 1 ? only(owners) : + something(findfirst(declares, owners), findfirst(named, owners), 1) |> i -> owners[i] found = false for lib in LIBS path = joinpath(lib_metadata(s, lib), "$(lib).toml") diff --git a/src/hipfft/generator.jl b/src/hipfft/generator.jl index c66b246..209c13b 100644 --- a/src/hipfft/generator.jl +++ b/src/hipfft/generator.jl @@ -23,7 +23,8 @@ # ############################################################################### -# Fortran bindings for hipFFT. +# Fortran bindings for hipFFT (hipfft/hipfft.h). hipfftXt.h and hipfftMp.h are +# the hipfftxt module's (src/hipfftxt). # # Usage (from the repo root): # julia --project=src src/hipfft/generator.jl [include_dir] [output_dir] @@ -34,16 +35,8 @@ include("../common/fortran_gen.jl") # (explain_metadata.jl). function main(run = run_library_generator) run(@__DIR__; - # hipfftMp.h uses hipfftXtSubFormat without including hipfftXt.h, and only one - # translation unit is walked: force-include hipfftXt.h ahead of it. - # hipfftXt.h includes , a C++ header: src/hipfft/cstddef stands in. - argshook = function (args, include_dir, hdrs) - xt = joinpath(dirname(hdrs[1]), "hipfftXt.h") - isfile(xt) || error("Header not found: $xt") - push!(args, "-I$(dirname(hdrs[1]))") - push!(args, "-I$(@__DIR__)") - push!(args, "-include", xt) - end) + # hipfft.h includes , a C++ header: src/hipfft/cstddef stands in. + argshook = (args, include_dir, hdrs) -> push!(args, "-I$(dirname(hdrs[1]))", "-I$(@__DIR__)")) end if abspath(PROGRAM_FILE) == @__FILE__ diff --git a/src/hipfft/hipfft.toml b/src/hipfft/hipfft.toml index ca0e768..4362bee 100644 --- a/src/hipfft/hipfft.toml +++ b/src/hipfft/hipfft.toml @@ -8,11 +8,8 @@ opaque_handles = [ ] [headers] -# hipfftMp.h (the multi-process API) includes hipfft.h; hipfftXt.h is force-included -# ahead of it (see generator.jl). hipfftMp.h's routines exist only in a hipFFT built -# with HIPFFT_MPI_ENABLE; a bind(C) interface costs nothing until called. subdir = "hipfft" -file = "hipfftMp.h" +file = "hipfft.h" [clang] defines = [ diff --git a/src/hipfft/metadata/hipfft.toml b/src/hipfft/metadata/hipfft.toml index 195d31f..da4e502 100644 --- a/src/hipfft/metadata/hipfft.toml +++ b/src/hipfft/metadata/hipfft.toml @@ -81,21 +81,3 @@ workSize = "scalar-host" [hipfftMakePlanMany64] workSize = "scalar-host" - -[hipfftXtGetSizeMany] -workSize = "scalar-host" - -[hipfftXtMakePlanMany] -workSize = "scalar-host" - -# hipfftMp.h's routines exist only in a hipFFT built with HIPFFT_MPI_ENABLE, which the -# ROCm packages are not: a wrapper that calls one leaves an undefined symbol in a -# shared libhipfort (hipfort's shared-link test). These arrays stay type(c_ptr), so -# the routine keeps its bind(C) interface alone, which costs nothing until called. -[hipfftXtSetDistribution] -input_lower = "always-c-ptr" -input_upper = "always-c-ptr" -output_lower = "always-c-ptr" -output_upper = "always-c-ptr" -input_stride = "always-c-ptr" -output_stride = "always-c-ptr" diff --git a/src/hipfftxt/generator.jl b/src/hipfftxt/generator.jl new file mode 100644 index 0000000..028efb1 --- /dev/null +++ b/src/hipfftxt/generator.jl @@ -0,0 +1,52 @@ +############################################################################### +# Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +############################################################################### + +# Fortran bindings for hipFFT's multi-GPU and multi-process APIs (hipfft/hipfftXt.h, +# hipfft/hipfftMp.h), as CUDA Fortran's cufftXt module. It uses the hipfft module. +# +# Usage (from the repo root): +# julia --project=src src/hipfftxt/generator.jl [include_dir] [output_dir] + +include("../common/fortran_gen.jl") + +# `run` is run_library_generator, or library_funcs to parse without generating +# (explain_metadata.jl). +function main(run = run_library_generator) + run(@__DIR__; + # hipfftMp.h uses hipfftXtSubFormat without including hipfftXt.h, and only one + # translation unit is walked: force-include hipfftXt.h ahead of it. + # hipfftXt.h includes , a C++ header: src/hipfft/cstddef stands in. + argshook = function (args, include_dir, hdrs) + xt = joinpath(dirname(hdrs[1]), "hipfftXt.h") + isfile(xt) || error("Header not found: $xt") + push!(args, "-I$(dirname(hdrs[1]))") + push!(args, "-I$(joinpath(@__DIR__, "..", "hipfft"))") + push!(args, "-include", xt) + end) +end + +if abspath(PROGRAM_FILE) == @__FILE__ + main() +end diff --git a/src/hipfftxt/hipfftxt.toml b/src/hipfftxt/hipfftxt.toml new file mode 100644 index 0000000..90b5c70 --- /dev/null +++ b/src/hipfftxt/hipfftxt.toml @@ -0,0 +1,26 @@ +[general] +name = "hipfftxt" + +[library] +prefix = "hipfft" +opaque_handles = [ + "hipfftHandle", +] +# hipfftXt.h, hipfftMp.h and the descriptor types of hiplibxt.h; hipfft.h is the +# hipfft module's, used (and so re-exported) whole. +own_headers = ["hipfft/hipfftXt", "hipfft/hipfftMp", "hipfft/hiplibxt"] +uses = ["hipfft"] + +[headers] +# hipfftMp.h (the multi-process API) includes hipfft.h; hipfftXt.h is force-included +# ahead of it (see generator.jl). hipfftMp.h's routines exist only in a hipFFT built +# with HIPFFT_MPI_ENABLE; a bind(C) interface costs nothing until called. +subdir = "hipfft" +file = "hipfftMp.h" + +[clang] +defines = [ + "HIPFFT_EXPORT=", + "__HIP_PLATFORM_AMD__=1", +] +extra_include_dirs = [] diff --git a/src/hipfftxt/metadata/hipfftxt.toml b/src/hipfftxt/metadata/hipfftxt.toml new file mode 100644 index 0000000..5e8342f --- /dev/null +++ b/src/hipfftxt/metadata/hipfftxt.toml @@ -0,0 +1,32 @@ +# Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. +# SPDX-License-Identifier: MIT +# +# What the hipfftXt C headers (hipfftXt.h, hipfftMp.h) cannot express, beyond the +# defaults the generator derives from them: one table per C function, and a key +# with braces names several. A _64 or _spt variant without a table takes its +# base's. See "Fixing a binding" in the generator README; `gen.sh --explain +# ` shows what applies. + +format = 1 +# scalar-host: +# workSize stays a scalar, which is right for a plan on one GPU. On a plan spread +# over several GPUs (hipfftXtSetGPUs), hipFFT writes one work size per GPU: such a +# caller passes c_loc of an array to the bind(C) form. + +[hipfftXtGetSizeMany] +workSize = "scalar-host" + +[hipfftXtMakePlanMany] +workSize = "scalar-host" + +# hipfftMp.h's routines exist only in a hipFFT built with HIPFFT_MPI_ENABLE, which the +# ROCm packages are not: a wrapper that calls one leaves an undefined symbol in a +# shared libhipfort (hipfort's shared-link test). These arrays stay type(c_ptr), so +# the routine keeps its bind(C) interface alone, which costs nothing until called. +[hipfftXtSetDistribution] +input_lower = "always-c-ptr" +input_upper = "always-c-ptr" +output_lower = "always-c-ptr" +output_upper = "always-c-ptr" +input_stride = "always-c-ptr" +output_stride = "always-c-ptr" diff --git a/src/test/fixtures/uses/include/ulib/ulib.h b/src/test/fixtures/uses/include/ulib/ulib.h new file mode 100644 index 0000000..5f06a41 --- /dev/null +++ b/src/test/fixtures/uses/include/ulib/ulib.h @@ -0,0 +1,19 @@ +// Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. +// SPDX-License-Identifier: MIT +/* The base header of the `uses` fixture, as hipfft.h: the ulib module. */ +#ifndef ULIB_H +#define ULIB_H + +#define ULIB_FORWARD -1 + +typedef struct ulib_plan_s* ulibHandle; + +typedef enum ulib_status_t { + ULIB_SUCCESS = 0, + ULIB_FAILURE = 1 +} ulibStatus; + +ulibStatus ulibCreate(ulibHandle* plan); +ulibStatus ulibExec(ulibHandle plan, float* data, int direction); + +#endif diff --git a/src/test/fixtures/uses/include/ulib/ulibXt.h b/src/test/fixtures/uses/include/ulib/ulibXt.h new file mode 100644 index 0000000..f40528e --- /dev/null +++ b/src/test/fixtures/uses/include/ulib/ulibXt.h @@ -0,0 +1,25 @@ +// Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. +// SPDX-License-Identifier: MIT +/* The extension header of the `uses` fixture, as hipfftXt.h: the ulibXt module, + which uses ulib. */ +#ifndef ULIBXT_H +#define ULIBXT_H + +#include "ulib/ulib.h" + +#define ULIB_XT_MAX_GPUS 8 + +typedef enum ulib_xt_format_t { + ULIB_XT_FORMAT_INPUT = 0, + ULIB_XT_FORMAT_OUTPUT = 1 +} ulibXtFormat; + +typedef struct ulibXtDesc_t { + int nGPUs; + void* data[8]; +} ulibXtDesc; + +ulibStatus ulibXtMalloc(ulibHandle plan, ulibXtDesc** desc, ulibXtFormat format); +ulibStatus ulibXtFree(ulibXtDesc* desc); + +#endif diff --git a/src/test/fixtures/uses/ulib.toml b/src/test/fixtures/uses/ulib.toml new file mode 100644 index 0000000..ecb3173 --- /dev/null +++ b/src/test/fixtures/uses/ulib.toml @@ -0,0 +1,17 @@ +# Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. +# SPDX-License-Identifier: MIT + +[general] +name = "ulib" + +[library] +prefix = "ulib" +opaque_handles = ["ulibHandle"] + +[headers] +subdir = "ulib" +file = "ulib.h" + +[clang] +defines = [] +extra_include_dirs = [] diff --git a/src/test/fixtures/uses/ulibxt.toml b/src/test/fixtures/uses/ulibxt.toml new file mode 100644 index 0000000..e5967e4 --- /dev/null +++ b/src/test/fixtures/uses/ulibxt.toml @@ -0,0 +1,19 @@ +# Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved. +# SPDX-License-Identifier: MIT + +[general] +name = "ulibxt" + +[library] +prefix = "ulib" +opaque_handles = ["ulibHandle"] +own_headers = ["ulib/ulibXt"] +uses = ["ulib"] + +[headers] +subdir = "ulib" +file = "ulibXt.h" + +[clang] +defines = [] +extra_include_dirs = [] diff --git a/src/test/runtests.jl b/src/test/runtests.jl index 119fd1c..5a84cbb 100644 --- a/src/test/runtests.jl +++ b/src/test/runtests.jl @@ -765,6 +765,16 @@ overlong_fortran_lines(s::AbstractString) = # unknown names are not `use`d. @test sibling_module_uses([fe], "zz", meta; extra=["SIB_C", "OWN_C", "UNK_C"]) == ["sib, only: SIB_C, sib_pair_t"] + # [library] uses: the used module comes first, bare (re-exported whole), + # and replaces its narrow `use, only:`. + let mu = LibMeta(own_consts = meta.own_consts, sibling_enum_mod = meta.sibling_enum_mod, + uses = ["sib"]) + @test sibling_module_uses([fe], "zz", mu; extra=["SIB_C"]) == ["sib"] + @test sibling_module_uses([fe], "zz", LibMeta(uses = ["sib"])) == ["sib"] + @test sibling_module_uses([fe], "zz", LibMeta(own_consts = meta.own_consts, + sibling_enum_mod = Dict("SIB_C" => "sib", "OTH_C" => "oth"), uses = ["oth"])) == + ["oth", "sib, only: SIB_C, sib_pair_t"] + end let m2 = LibMeta(own_consts = meta.own_consts, sibling_enum_mod = Dict("SIB_C" => "sib", "SIB_BAD" => "sib")) @test sibling_module_uses([fe], "zz", m2; extra=["SIB_BAD"]) == @@ -1186,6 +1196,54 @@ overlong_fortran_lines(s::AbstractString) = @test_throws ErrorException find_headers(Dict("general" => Dict("name" => "nope")), fx) end + @testset "[library] uses: one module per header (uses/ fixture, as hipfft and hipfftxt)" begin + fx = joinpath(@__DIR__, "fixtures", "uses") + inc = joinpath(fx, "include") + tmp = mktempdir() + src = Dict{String,String}() + for lib in ("ulib", "ulibxt") + d = joinpath(tmp, lib); mkpath(d) + cp(joinpath(fx, "$(lib).toml"), joinpath(d, "$(lib).toml")) + run_library_generator(d; include_dir = inc) + src[lib] = read(joinpath(d, "output", "$(lib).F90"), String) + end + base, xt = src["ulib"], src["ulibxt"] + bound(f) = Set(m.captures[1] for m in eachmatch(r"bind\(c, name=\"(\w+)\"\)", f)) + # Each module binds the functions of its own header only. + @test bound(base) == Set(["ulibCreate", "ulibExec"]) + @test bound(xt) == Set(["ulibXtMalloc", "ulibXtFree"]) + # ulibxt uses ulib whole, so `use ulibxt` alone gives both APIs... + @test occursin("module ulibxt\n use ulib\n use, intrinsic :: iso_c_binding\n", xt) + @test !occursin("use ulib,", xt) + # ...and declares only what its header adds: its enums, constants and types. + @test occursin("ULIB_XT_FORMAT_INPUT = 0", xt) && occursin("ULIB_XT_MAX_GPUS = 8", xt) + @test occursin("type, bind(c) :: ulibXtDesc", xt) + @test !occursin("ULIB_SUCCESS =", xt) && !occursin("ULIB_FORWARD", xt) + @test !occursin("ulibxt", lowercase(base)) && !occursin("ULIB_XT", base) + @test occursin("subroutine ulibCheck", base) && !occursin("Check(", xt) + + # Both compile, and a program using ulibxt alone reaches ulib's entities. + if Sys.which("gfortran") !== nothing + write(joinpath(tmp, "ulib.F90"), base); write(joinpath(tmp, "ulibxt.F90"), xt) + write(joinpath(tmp, "prog.f90"), """ + program p + use ulibxt + type(c_ptr) :: plan + type(c_ptr) :: desc + integer(kind(ULIB_SUCCESS)) :: st + st = ulibXtMalloc(plan, desc, ULIB_XT_FORMAT_INPUT) + if (st == ULIB_SUCCESS) st = ulibCreate(plan) + call ulibCheck(st) + end program p + """) + cd(tmp) do + for f in ("ulib.F90", "ulibxt.F90", "prog.f90") + @test success(pipeline(`gfortran -std=f2018 -cpp -c $f`; stderr = devnull)) + end + end + end + end + @testset "header sources (generate_all.jl selection)" begin tmp = mktempdir() libs, sys, inst, out = (joinpath(tmp, d) for d in ("libs", "sys", "inst", "out")) @@ -1200,8 +1258,9 @@ overlong_fortran_lines(s::AbstractString) = write(joinpath(inst, "hip", "hip_version.h"), "#define HIP_VERSION_MAJOR 7\n#define HIP_VERSION_MINOR 16\n#define HIP_VERSION_PATCH 26385\n") math = filter(l -> repo_of(l) == "rocm-libraries", LIBS) - @test length(math) == 11 && filter(l -> repo_of(l) == "rocm-systems", LIBS) == ["hip", "roctx"] + @test length(math) == 12 && filter(l -> repo_of(l) == "rocm-systems", LIBS) == ["hip", "roctx"] @test repo_of("hipfftw") == "rocm-libraries" && proj_of("hipfftw") == "hipfft" + @test repo_of("hipfftxt") == "rocm-libraries" && proj_of("hipfftxt") == "hipfft" # Neither checkout: every library, from the installed dir. s, rest = header_sources(Dict{String,String}(), [inst, out]) @@ -1240,6 +1299,7 @@ overlong_fortran_lines(s::AbstractString) = mkpath(joinpath(sys, "bindings", "fortran", "metadata")) @test lib_metadata(s, "hipfft") == joinpath(libs, "bindings", "fortran", "hipfft", "metadata") @test lib_metadata(s, "hipfftw") == joinpath(libs, "bindings", "fortran", "hipfft", "metadata") + @test lib_metadata(s, "hipfftxt") == joinpath(libs, "bindings", "fortran", "hipfft", "metadata") @test lib_metadata(s, "roctx") == joinpath(sys, "bindings", "fortran", "metadata") @test lib_metadata(s, "hip") == joinpath(sys, "bindings", "fortran", "metadata") @test lib_metadata(s, "rocblas") == gen_meta("rocblas") # no metadata in the checkout @@ -1261,6 +1321,9 @@ overlong_fortran_lines(s::AbstractString) = @test_throws ArgumentError run_libs(sl, ["hip"]) # rocm-systems is not read @test overwrite_target(sl, "hipfftw") == joinpath(libs, "bindings", "fortran", "hipfft", "hipfftw.F90") + @test overwrite_target(sl, "hipfftxt") == + joinpath(libs, "bindings", "fortran", "hipfft", "hipfftxt.F90") + @test run_libs(sl, ["hipfftw", "hipfftxt", "hipfft"]) == ["hipfft", "hipfftxt", "hipfftw"] @test_throws ArgumentError check_overwrite(sl, ["rocblas"]) # no bindings/fortran/rocblas/ yet mkpath(joinpath(libs, "bindings", "fortran", "rocblas")) @test check_overwrite(sl, ["rocblas", "hipfftw"]) === nothing From 4779fed2a43a23b88be42e02171569247d9106ce Mon Sep 17 00:00:00 2001 From: Alexis Montoison Date: Sat, 7 Nov 2026 02:23:05 -0600 Subject: [PATCH 2/2] feat(hipfft): bind every hipfftXt* and hipfftMp* function in hipfftxt CUDA Fortran's cufftXt module has cufftXtSetWorkArea, but hipFFT declares hipfftXtSetWorkArea in hipfft.h, so splitting by header left it in hipfft. The functions now go by name: the new [library] function_prefixes key keeps only the functions with those prefixes, whatever header declares them, and skip_function_prefixes drops those another library binds. hipfftxt takes hipfftXt* and hipfftMp*, hipfft skips them; own_headers still decides the enums, constants and types. --- README.md | 8 +++++--- src/common/clang_args.jl | 14 ++++++++++---- src/common/clang_frontend.jl | 4 +--- src/hipfft/hipfft.toml | 3 +++ src/hipfftxt/hipfftxt.toml | 7 +++++-- src/test/fixtures/uses/include/ulib/ulib.h | 2 ++ src/test/fixtures/uses/ulib.toml | 1 + src/test/fixtures/uses/ulibxt.toml | 1 + src/test/runtests.jl | 6 ++++-- 9 files changed, 32 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 695d853..f307456 100644 --- a/README.md +++ b/README.md @@ -173,8 +173,10 @@ If the generator finds far fewer functions than expected, check the `prefix` spe | `[general] name` | required | the library and module name, and the metadata file name | | `[library] prefix` | required | only functions whose name starts with it are bound | | `[library] opaque_handles` | required (may be `[]`) | struct tags never emitted as derived types (handles) | -| `[library] own_headers` | `[name]` | header-path markers of the files whose enums, structs and macros (and, with `uses`, functions) are this library's | -| `[library] uses` | `[]` | modules `use`d whole, so the module re-exports them; their headers' functions, enums, constants and types are not redeclared (hipfftxt: `["hipfft"]`, as CUDA Fortran's cufftXt includes cufft) | +| `[library] own_headers` | `[name]` | header-path markers of the files whose enums, structs and macros are this library's | +| `[library] uses` | `[]` | modules `use`d whole, so the module re-exports them; their headers' enums, constants and types are not redeclared (hipfftxt: `["hipfft"]`, as CUDA Fortran's cufftXt includes cufft) | +| `[library] function_prefixes` | `[]` | when set, the functions bound are those starting with one of them, whatever header declares them (hipfftxt: `["hipfftXt", "hipfftMp"]`) | +| `[library] skip_function_prefixes` | `[]` | functions starting with one of them are not bound: another library binds them (hipfft: `["hipfftXt", "hipfftMp"]`) | | `[library] reexport_enums` | `[]` | sibling libraries whose enums are `use`d from the sibling module instead of copied (rocSOLVER: `["rocblas"]`) | | `[library] keep_structs` | `[]` | HIP vector types to emit anyway, when a function takes one by value (rocRAND: `["uint4"]`) | | `[library] pointer_mode` | `false` | the handle has a pointer mode: a `const` pointer named `alpha` or `beta` is `scalar-pointer-mode` (BLAS, SPARSE) | @@ -500,7 +502,7 @@ The version is in `src/Project.toml` and follows semantic versioning; releases a | hipSOLVER | `hipsolver` | | hipSPARSE | `hipsparse` | | hipFFT | `hipfft` | -| hipFFT Xt and Mp (`hipfftXt.h`, `hipfftMp.h`) | `hipfftxt` (uses and re-exports `hipfft`) | +| hipFFT Xt and Mp (`hipfftXt*`, `hipfftMp*`) | `hipfftxt` (uses and re-exports `hipfft`) | | hipFFTW | `hipfftw` | | hipRAND | `hiprand` | diff --git a/src/common/clang_args.jl b/src/common/clang_args.jl index 0f0d470..3533512 100644 --- a/src/common/clang_args.jl +++ b/src/common/clang_args.jl @@ -450,9 +450,15 @@ function parse_library(dir::AbstractString, include_dir::AbstractString; root = Clang.getTranslationUnitCursor(tu) prefix = cfg["library"]["prefix"] - # With [library] uses, the functions of a used module's headers are that module's. - fown = isempty(uses_of(cfg)) ? nothing : own_headers_of(cfg) - funcs = GC.@preserve ctx tu collect_funcs(root, prefix; own=fown) + # The functions go by name, whatever header declares them (hipfft.h declares + # hipfftXtSetWorkArea, hipfftxt's): only those of function_prefixes when set, + # less those of skip_function_prefixes, which another library binds. + fpre = strlist(get(cfg["library"], "function_prefixes", String[])) + skip = strlist(get(cfg["library"], "skip_function_prefixes", String[])) + funcs = GC.@preserve ctx tu collect_funcs(root, prefix) + starts(f, ps) = any(p -> startswith(f.name, p), ps) + isempty(fpre) || filter!(f -> starts(f, fpre), funcs) + filter!(f -> !starts(f, skip), funcs) @info "Found $(length(funcs)) $(prefix)* functions" declared = [f.name for f in funcs] funcfilter === nothing || (funcs = funcfilter(funcs)) @@ -531,7 +537,7 @@ function run_library_generator(dir::AbstractString; # Each sibling constant, mapped to the module defining it, for the `use` lines. # uses: modules `use`d whole, and so re-exported (hipfftxt uses hipfft); their - # enums, constants, types and functions are not redeclared. + # enums, constants and types are not redeclared. uses = uses_of(cfg) sibling_enum_mod = Dict{String,String}() sibling_enums = EnumInfo[] diff --git a/src/common/clang_frontend.jl b/src/common/clang_frontend.jl index 7006c19..037e91a 100644 --- a/src/common/clang_frontend.jl +++ b/src/common/clang_frontend.jl @@ -126,8 +126,7 @@ function is_ptr_array(t) :: Bool return Clang.kind(pt) == Clang.CXType_Pointer && Clang.LibClang.clang_isConstQualifiedType(pt.type) != 0 end -# `own`, when given, also keeps only the functions declared in those headers. -function collect_funcs(root, prefix::String; own::Union{Nothing,Vector{String}} = nothing) :: Vector{FuncInfo} +function collect_funcs(root, prefix::String) :: Vector{FuncInfo} reset_unmapped_cptr!() reset_func_docs!() carried = Pair{String,String}[] # the last doc block's @params (function_docs!) @@ -140,7 +139,6 @@ function collect_funcs(root, prefix::String; own::Union{Nothing,Vector{String}} Clang.kind(child) == Clang.CXCursor_FunctionDecl || continue name = Clang.spelling(child) (startswith(name, prefix) || name == bare) || continue - own === nothing || owns_decl(cursor_file(child), own) || continue # `static inline` has no exported symbol: binding it is a link error. Clang.LibClang.clang_Cursor_isFunctionInlined(child.cursor) != 0 && continue # Skip (and log) variadic functions: Fortran cannot declare `...`, and calling diff --git a/src/hipfft/hipfft.toml b/src/hipfft/hipfft.toml index 4362bee..23928c4 100644 --- a/src/hipfft/hipfft.toml +++ b/src/hipfft/hipfft.toml @@ -6,6 +6,9 @@ prefix = "hipfft" opaque_handles = [ "hipfftHandle", ] +# The hipfftxt module's, as cufftXt's in CUDA Fortran (hipfft.h declares +# hipfftXtSetWorkArea). +skip_function_prefixes = ["hipfftXt", "hipfftMp"] [headers] subdir = "hipfft" diff --git a/src/hipfftxt/hipfftxt.toml b/src/hipfftxt/hipfftxt.toml index 90b5c70..91c2427 100644 --- a/src/hipfftxt/hipfftxt.toml +++ b/src/hipfftxt/hipfftxt.toml @@ -6,10 +6,13 @@ prefix = "hipfft" opaque_handles = [ "hipfftHandle", ] -# hipfftXt.h, hipfftMp.h and the descriptor types of hiplibxt.h; hipfft.h is the -# hipfft module's, used (and so re-exported) whole. +# The enums, constants and types of hipfftXt.h, hipfftMp.h and hiplibxt.h; hipfft.h +# is the hipfft module's, used (and so re-exported) whole. own_headers = ["hipfft/hipfftXt", "hipfft/hipfftMp", "hipfft/hiplibxt"] uses = ["hipfft"] +# Every hipfftXt* and hipfftMp* function, as cufftXt's in CUDA Fortran, whatever +# header declares it (hipfft.h declares hipfftXtSetWorkArea). +function_prefixes = ["hipfftXt", "hipfftMp"] [headers] # hipfftMp.h (the multi-process API) includes hipfft.h; hipfftXt.h is force-included diff --git a/src/test/fixtures/uses/include/ulib/ulib.h b/src/test/fixtures/uses/include/ulib/ulib.h index 5f06a41..c1867cb 100644 --- a/src/test/fixtures/uses/include/ulib/ulib.h +++ b/src/test/fixtures/uses/include/ulib/ulib.h @@ -15,5 +15,7 @@ typedef enum ulib_status_t { ulibStatus ulibCreate(ulibHandle* plan); ulibStatus ulibExec(ulibHandle plan, float* data, int direction); +/* An Xt function the base header declares, as hipfft.h's hipfftXtSetWorkArea. */ +ulibStatus ulibXtSetArea(ulibHandle plan, void* area); #endif diff --git a/src/test/fixtures/uses/ulib.toml b/src/test/fixtures/uses/ulib.toml index ecb3173..37b07fb 100644 --- a/src/test/fixtures/uses/ulib.toml +++ b/src/test/fixtures/uses/ulib.toml @@ -7,6 +7,7 @@ name = "ulib" [library] prefix = "ulib" opaque_handles = ["ulibHandle"] +skip_function_prefixes = ["ulibXt"] [headers] subdir = "ulib" diff --git a/src/test/fixtures/uses/ulibxt.toml b/src/test/fixtures/uses/ulibxt.toml index e5967e4..d5490f8 100644 --- a/src/test/fixtures/uses/ulibxt.toml +++ b/src/test/fixtures/uses/ulibxt.toml @@ -9,6 +9,7 @@ prefix = "ulib" opaque_handles = ["ulibHandle"] own_headers = ["ulib/ulibXt"] uses = ["ulib"] +function_prefixes = ["ulibXt"] [headers] subdir = "ulib" diff --git a/src/test/runtests.jl b/src/test/runtests.jl index 5a84cbb..9e34f9d 100644 --- a/src/test/runtests.jl +++ b/src/test/runtests.jl @@ -1209,9 +1209,10 @@ overlong_fortran_lines(s::AbstractString) = end base, xt = src["ulib"], src["ulibxt"] bound(f) = Set(m.captures[1] for m in eachmatch(r"bind\(c, name=\"(\w+)\"\)", f)) - # Each module binds the functions of its own header only. + # The functions go by name, not by header: ulibXtSetArea, declared in ulib.h, + # is ulibxt's (function_prefixes), and ulib skips it (skip_function_prefixes). @test bound(base) == Set(["ulibCreate", "ulibExec"]) - @test bound(xt) == Set(["ulibXtMalloc", "ulibXtFree"]) + @test bound(xt) == Set(["ulibXtMalloc", "ulibXtFree", "ulibXtSetArea"]) # ulibxt uses ulib whole, so `use ulibxt` alone gives both APIs... @test occursin("module ulibxt\n use ulib\n use, intrinsic :: iso_c_binding\n", xt) @test !occursin("use ulib,", xt) @@ -1233,6 +1234,7 @@ overlong_fortran_lines(s::AbstractString) = integer(kind(ULIB_SUCCESS)) :: st st = ulibXtMalloc(plan, desc, ULIB_XT_FORMAT_INPUT) if (st == ULIB_SUCCESS) st = ulibCreate(plan) + if (st == ULIB_SUCCESS) st = ulibXtSetArea(plan, desc) call ulibCheck(st) end program p """)