From 3f217037f0666ab14533d1cfee042f38a19d8dd1 Mon Sep 17 00:00:00 2001 From: whatghost Date: Thu, 30 Jul 2026 01:39:04 +0000 Subject: [PATCH 1/5] add rocm/hip backend and add pic for build of rocm/hip --- CMakeLists.txt | 14 ++++++++++++++ README.md | 11 +++++++++++ scripts/ci/check_lane_mirror.py | 1 + src/transcribe-backend.cpp | 6 +++++- src/transcribe-backend.h | 5 +++-- tests/backend_classification_unit.cpp | 2 ++ 6 files changed, 36 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d1eb84d..a10f92eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -124,6 +124,7 @@ option(TRANSCRIBE_ENABLE_VALIDATION_HOOKS option(TRANSCRIBE_METAL "Enable Metal backend" ${TRANSCRIBE_IS_APPLE_SILICON}) option(TRANSCRIBE_VULKAN "Enable Vulkan backend" OFF) # post-v1 option(TRANSCRIBE_CUDA "Enable CUDA backend" OFF) # opt-in; Linux + nvcc +option(TRANSCRIBE_HIP "Enable HIP/ROCm backend" OFF) # opt-in; Linux + ROCm 6.1+ option(TRANSCRIBE_SANITIZE "Enable ASan + UBSan" OFF) option(TRANSCRIBE_LTO "Enable LTO in Release" OFF) @@ -277,7 +278,20 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(GGML_METAL ${TRANSCRIBE_METAL} CACHE BOOL "" FORCE) set(GGML_VULKAN ${TRANSCRIBE_VULKAN} CACHE BOOL "" FORCE) set(GGML_CUDA ${TRANSCRIBE_CUDA} CACHE BOOL "" FORCE) +set(GGML_HIP ${TRANSCRIBE_HIP} CACHE BOOL "" FORCE) set(GGML_BLAS OFF CACHE BOOL "" FORCE) + +# HIP needs position-independent code even in the default static posture. +# hipcc does not pick up the distro toolchain's default-PIE codegen the way the +# system g++ does: its objects keep absolute relocations (R_X86_64_32 against +# .rodata), and ggml only sets POSITION_INDEPENDENT_CODE when BUILD_SHARED_LIBS +# is on. Linking that static archive into the PIE executables every default-PIE +# distro produces then fails at link time. Set the directory-scope default +# before add_subdirectory(ggml) so every target — ours and ggml's — is PIC. +if(TRANSCRIBE_HIP) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) +endif() + # tinyBLAS (Justine Tunney's llamafile_sgemm CPU kernels): ~29% faster encoder on # CPU (q8_0 GEMM), numerically WER-equivalent. On by default; CPU-backend only. set(GGML_LLAMAFILE ON CACHE BOOL "" FORCE) diff --git a/README.md b/README.md index 7f13aac4..5a2bc2af 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,17 @@ cmake -B build -DTRANSCRIBE_CUDA=ON cmake --build build ``` +For HIP/ROCm (Linux + AMD GPU), which needs ROCm 6.1 or newer: + +```bash +cmake -B build -DTRANSCRIBE_HIP=ON -DAMDGPU_TARGETS=gfx1201 +cmake --build build +``` + +Replace `gfx1201` with your GPU architecture — `rocminfo | grep gfx` prints it. +Pass a semicolon-separated list for several architectures. ROCm devices report +as the `rocm` backend kind and are picked up by the default `auto` backend. + `libopenblas-dev` is optional but recommended. It accelerates the host-side decoder ~10-15x. Without it the build falls back to a scalar path automatically. tinyBLAS (Justine Tunney's `llamafile_sgemm` kernels) is on by default. diff --git a/scripts/ci/check_lane_mirror.py b/scripts/ci/check_lane_mirror.py index 0cc76646..4c4e0172 100644 --- a/scripts/ci/check_lane_mirror.py +++ b/scripts/ci/check_lane_mirror.py @@ -90,6 +90,7 @@ def derive(posture: dict[str, str]) -> dict[str, str]: "TRANSCRIBE_METAL": [("GGML_METAL", None)], "TRANSCRIBE_VULKAN": [("GGML_VULKAN", None)], "TRANSCRIBE_CUDA": [("GGML_CUDA", None)], + "TRANSCRIBE_HIP": [("GGML_HIP", None)], # BACKEND_DL forces GGML_BACKEND_DL and GGML_NATIVE=OFF. "TRANSCRIBE_GGML_BACKEND_DL": [("GGML_BACKEND_DL", None), ("GGML_NATIVE", "OFF")], diff --git a/src/transcribe-backend.cpp b/src/transcribe-backend.cpp index 1be28c45..9858d0d8 100644 --- a/src/transcribe-backend.cpp +++ b/src/transcribe-backend.cpp @@ -24,6 +24,8 @@ const char * kind_name(BackendKind kind) { return "vulkan"; case BackendKind::Cuda: return "cuda"; + case BackendKind::Rocm: + return "rocm"; case BackendKind::Sycl: return "sycl"; case BackendKind::Accel: @@ -38,7 +40,7 @@ const char * kind_name(BackendKind kind) { // Return true if `reg_name` (the ggml backend registry name) starts // with the given prefix. ggml's registry names look like "MTL", -// "Vulkan", "CUDA", "SYCL", "BLAS", "CPU", etc. Prefix matching is +// "Vulkan", "CUDA", "ROCm", "SYCL", "BLAS", "CPU", etc. Prefix matching is // intentional: registry names can get version suffixes or device // index suffixes in some ggml builds. static bool reg_name_is(const char * reg_name, const char * prefix) { @@ -65,6 +67,8 @@ BackendKind classify_backend_type(enum ggml_backend_dev_type dev_type, const cha return BackendKind::Vulkan; } else if (reg_name_is(reg_name, "CUDA")) { return BackendKind::Cuda; + } else if (reg_name_is(reg_name, "ROCm")) { + return BackendKind::Rocm; } else if (reg_name_is(reg_name, "SYCL")) { return BackendKind::Sycl; } diff --git a/src/transcribe-backend.h b/src/transcribe-backend.h index dbea8aed..1d6b707f 100644 --- a/src/transcribe-backend.h +++ b/src/transcribe-backend.h @@ -34,6 +34,7 @@ enum class BackendKind { Metal, // Apple Metal Vulkan, // Vulkan compute Cuda, // NVIDIA CUDA + Rocm, // AMD ROCm Sycl, // Intel oneAPI / SYCL Accel, // BLAS / AMX / other host-memory accelerator OtherGpu, // GPU/IGPU device we don't have a special case for @@ -48,8 +49,8 @@ BackendKind classify_backend_type(enum ggml_backend_dev_type dev_type, const cha // Classify a backend device into a BackendKind. Uses // ggml_backend_dev_type for the GPU/IGPU/ACCEL/CPU dimension and the -// reg name ("MTL", "Vulkan", "CUDA", "SYCL", ...) to resolve the -// vendor. Never returns Unknown for a valid device pointer. +// reg name ("MTL", "Vulkan", "CUDA", "ROCm", "SYCL", ...) to resolve +// the vendor. Never returns Unknown for a valid device pointer. BackendKind classify_device(ggml_backend_dev_t dev); // Probe order for GPU device selection. `dev_types` holds the ggml diff --git a/tests/backend_classification_unit.cpp b/tests/backend_classification_unit.cpp index 9618b93e..7a0024e9 100644 --- a/tests/backend_classification_unit.cpp +++ b/tests/backend_classification_unit.cpp @@ -38,6 +38,8 @@ int main() { CHECK_EQ(classify_backend_type(GGML_BACKEND_DEVICE_TYPE_GPU, "Metal"), BackendKind::Metal); CHECK_EQ(classify_backend_type(GGML_BACKEND_DEVICE_TYPE_IGPU, "Vulkan"), BackendKind::Vulkan); CHECK_EQ(classify_backend_type(GGML_BACKEND_DEVICE_TYPE_GPU, "CUDA"), BackendKind::Cuda); + CHECK_EQ(classify_backend_type(GGML_BACKEND_DEVICE_TYPE_GPU, "ROCm"), BackendKind::Rocm); + CHECK_EQ(classify_backend_type(GGML_BACKEND_DEVICE_TYPE_GPU, "ROCm0"), BackendKind::Rocm); CHECK_EQ(classify_backend_type(GGML_BACKEND_DEVICE_TYPE_GPU, "SYCL"), BackendKind::Sycl); CHECK_EQ(classify_backend_type(GGML_BACKEND_DEVICE_TYPE_GPU, "WebGPU"), BackendKind::OtherGpu); CHECK_EQ(classify_backend_type(GGML_BACKEND_DEVICE_TYPE_GPU, nullptr), BackendKind::OtherGpu); From ab64130fd3399c4b4573096525af7f9ba25802e2 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Thu, 30 Jul 2026 15:46:37 +0800 Subject: [PATCH 2/5] ROCm install linking and backend naming --- .../python/src/transcribe_cpp/_library.py | 4 +- .../python/tests/test_provider_discovery.py | 1 + cmake/transcribe-backend-kinds.cmake | 5 ++- cmake/transcribe-install.cmake | 38 +++++++++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/bindings/python/src/transcribe_cpp/_library.py b/bindings/python/src/transcribe_cpp/_library.py index 5c299fe1..b0e7597e 100644 --- a/bindings/python/src/transcribe_cpp/_library.py +++ b/bindings/python/src/transcribe_cpp/_library.py @@ -27,7 +27,7 @@ Selecting a provider picks which native artifact loads into the process; the per-model ``backend=`` request is a *separate* axis resolved inside it. Selection policy: explicit ``provider`` argument → ``TRANSCRIBE_NATIVE_PROVIDER`` env var → -best accelerated (CUDA/Metal, then Vulkan) → CPU. A discovered provider whose +best accelerated (CUDA/ROCm/Metal, then Vulkan) → CPU. A discovered provider whose declared version or header hash disagrees with this binding is a hard error *before* dlopen — pip pins are not enough; this runtime check is the backstop. @@ -55,7 +55,7 @@ #: Backend-kind preference when auto-selecting among installed providers. Higher #: wins; a provider's rank is the max over the kinds it advertises. -_BACKEND_RANK = {"cuda": 3, "metal": 3, "vulkan": 2, "cpu_accel": 1, "cpu": 1} +_BACKEND_RANK = {"cuda": 3, "rocm": 3, "metal": 3, "vulkan": 2, "cpu_accel": 1, "cpu": 1} #: Set after a successful load so the package can surface it for diagnostics. #: None means the library came from the dev-tree / explicit-path fallback. diff --git a/bindings/python/tests/test_provider_discovery.py b/bindings/python/tests/test_provider_discovery.py index 563a3620..44e41e3a 100644 --- a/bindings/python/tests/test_provider_discovery.py +++ b/bindings/python/tests/test_provider_discovery.py @@ -84,6 +84,7 @@ def test_matches_request(request_str, expected): def test_rank_orders_accelerated_above_cpu(): assert make("m", ["metal", "cpu"]).rank == 3 + assert make("r", ["rocm", "cpu"]).rank == 3 assert make("v", ["vulkan"]).rank == 2 assert make("c", ["cpu"]).rank == 1 assert make("u", ["wat"]).rank == 0 diff --git a/cmake/transcribe-backend-kinds.cmake b/cmake/transcribe-backend-kinds.cmake index cabafeed..bd8d2d73 100644 --- a/cmake/transcribe-backend-kinds.cmake +++ b/cmake/transcribe-backend-kinds.cmake @@ -31,12 +31,15 @@ function(transcribe_backend_kinds out_kinds out_targets) list(APPEND _targets ${_backend}) string(REGEX REPLACE "^ggml-" "" _kind "${_backend}") string(REGEX REPLACE "^cpu-.*$" "cpu" _kind "${_kind}") + if(_kind STREQUAL "hip") + set(_kind "rocm") + endif() list(APPEND _kinds "${_kind}") endforeach() list(REMOVE_DUPLICATES _kinds) set(_ordered "") - foreach(_kind IN ITEMS cuda metal vulkan) + foreach(_kind IN ITEMS cuda rocm metal vulkan) if(_kind IN_LIST _kinds) list(APPEND _ordered "${_kind}") list(REMOVE_ITEM _kinds "${_kind}") diff --git a/cmake/transcribe-install.cmake b/cmake/transcribe-install.cmake index 586799cb..2c590135 100644 --- a/cmake/transcribe-install.cmake +++ b/cmake/transcribe-install.cmake @@ -137,6 +137,44 @@ if(NOT TRANSCRIBE_BUILD_SHARED) if("vulkan" IN_LIST _kinds) list(APPEND _system_libs vulkan) endif() + if("rocm" IN_LIST _kinds) + # ggml-hip is an archive in this posture, but its HIP/rocBLAS/hipBLAS + # dependencies are shared ROCm SDK libraries. CMake propagates those + # imported targets while linking in-tree executables; an installed + # non-CMake consumer has only this manifest, so record absolute paths + # rather than assuming /opt/rocm/lib is in its linker search path. + if(DEFINED ENV{ROCM_PATH} AND EXISTS "$ENV{ROCM_PATH}") + set(_rocm_root "$ENV{ROCM_PATH}") + elseif(EXISTS "/opt/rocm") + set(_rocm_root "/opt/rocm") + else() + set(_rocm_root "/usr") + endif() + foreach(_rocm_lib IN ITEMS amdhip64 rocblas hipblas) + unset(_transcribe_rocm_library CACHE) + unset(_transcribe_rocm_library) + find_library(_transcribe_rocm_library + NAMES ${_rocm_lib} + HINTS "${_rocm_root}/lib" "${_rocm_root}/lib64") + if(NOT _transcribe_rocm_library) + message(FATAL_ERROR + "transcribe install: could not resolve ROCm library " + "${_rocm_lib} for the static link manifest") + endif() + list(APPEND _library_paths "${_transcribe_rocm_library}") + endforeach() + + # Some HIP packages add compiler-rt builtins directly to hip::host's + # interface. Preserve any such absolute artifacts as well; the + # amdhip64 target itself is already represented above. + get_target_property(_hip_host_links hip::host INTERFACE_LINK_LIBRARIES) + foreach(_hip_host_link IN LISTS _hip_host_links) + if(IS_ABSOLUTE "${_hip_host_link}" AND EXISTS "${_hip_host_link}") + list(APPEND _library_paths "${_hip_host_link}") + endif() + endforeach() + list(REMOVE_DUPLICATES _library_paths) + endif() if(_frameworks) list(REMOVE_DUPLICATES _frameworks) endif() From 45ea05f213b0f72e653ec86d1ee3427562b867dc Mon Sep 17 00:00:00 2001 From: whatghost Date: Thu, 30 Jul 2026 08:30:08 +0000 Subject: [PATCH 3/5] document rocm in the public device kind list --- include/transcribe.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/transcribe.h b/include/transcribe.h index 7194efbb..d16c7b31 100644 --- a/include/transcribe.h +++ b/include/transcribe.h @@ -818,8 +818,8 @@ typedef enum { * * kind is the library's vendor classification, one of: "cpu", "accel" (a * host-memory accelerator such as BLAS/AMX), "metal", "vulkan", "cuda", - * "sycl", "gpu" (an unrecognized GPU), or "unknown". device_type is the - * orthogonal CPU/GPU/IGPU/ACCEL axis. + * "rocm", "sycl", "gpu" (an unrecognized GPU), or "unknown". device_type is + * the orthogonal CPU/GPU/IGPU/ACCEL axis. * * device_id is a stable hardware identifier when the backend reports one * (for PCI devices the lower-case bus id "domain:bus:device.function", e.g. From b812378dd1c1c56a5b1e1c7b11e5403b4cf561f9 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Thu, 30 Jul 2026 16:42:52 +0800 Subject: [PATCH 4/5] add first-class ROCm backend selection --- Cargo.toml | 3 +- README.md | 3 +- .../python/src/transcribe_cpp/__init__.py | 5 ++- .../python/src/transcribe_cpp/_generated.py | 3 +- bindings/python/tests/test_backends.py | 6 +-- bindings/rust/sys/README.md | 18 +++++++-- bindings/rust/sys/build.rs | 4 ++ bindings/rust/sys/src/transcribe_sys.rs | 5 ++- bindings/rust/transcribe-cpp/Cargo.toml | 1 + bindings/rust/transcribe-cpp/README.md | 2 +- .../transcribe-cpp/examples/backend-select.rs | 19 ++++++++-- bindings/rust/transcribe-cpp/src/backend.rs | 2 +- bindings/rust/transcribe-cpp/src/types.rs | 3 ++ .../rust/transcribe-cpp/tests/degradation.rs | 5 ++- .../swift/Sources/TranscribeCpp/ABIHash.swift | 2 +- .../swift/Sources/TranscribeCpp/Backend.swift | 6 ++- .../swift/Sources/backend-select/main.swift | 2 +- bindings/typescript/README.md | 4 +- .../typescript/examples/backend-select.mjs | 10 ++++- bindings/typescript/src/_generated.ts | 3 +- bindings/typescript/src/index.ts | 1 + bindings/typescript/src/types.ts | 2 +- bindings/typescript/test/no-model.test.mjs | 1 + cmake/transcribe-install.cmake | 31 ++++++++++++---- examples/bench/batch_bench.cpp | 3 ++ examples/cli/main.cpp | 6 ++- include/transcribe.abihash | 2 +- include/transcribe.h | 21 +++++++---- scripts/bench/run.py | 37 ++++++++++++++----- scripts/ci/wheel_smoke.py | 2 +- src/transcribe-load-common.cpp | 8 ++++ src/transcribe-load-common.h | 4 +- src/transcribe.cpp | 4 ++ tests/CMakeLists.txt | 2 +- tests/api_smoke.c | 1 + tests/backend_init_throw_unit.cpp | 1 + tests/backend_init_unit.cpp | 25 +++++++++++-- tools/transcribe-bench/main.cpp | 8 +++- 38 files changed, 197 insertions(+), 68 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b0c5bdb0..1a7f8539 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,13 +64,14 @@ default = ["metal"] metal = [] vulkan = [] cuda = [] +rocm = [] openmp = [] # `shared` links a shared libtranscribe (.so/.dylib/.dll) loaded at runtime # instead of statically baking it into the consumer binary. The default is a # self-contained static link. (Renamed from `dylib`.) shared = [] # `dynamic-backends` additionally ships each compute backend (the per-ISA CPU -# tiers, Vulkan, CUDA, ...) as a loadable module next to the library, selected at +# tiers, Vulkan, CUDA, ROCm, ...) as a loadable module next to the library, selected at # runtime by transcribe_init_backends(). Requires a shared library, so it implies # `shared`. See notes/rust-dynamic-backends-plan.md. dynamic-backends = ["shared"] diff --git a/README.md b/README.md index 5a2bc2af..f8519a80 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,8 @@ cmake --build build Replace `gfx1201` with your GPU architecture — `rocminfo | grep gfx` prints it. Pass a semicolon-separated list for several architectures. ROCm devices report -as the `rocm` backend kind and are picked up by the default `auto` backend. +as the `rocm` backend kind, are picked up by the default `auto` backend, and can +be required explicitly with `--backend rocm`. `libopenblas-dev` is optional but recommended. It accelerates the host-side decoder ~10-15x. Without it the build falls back to a scalar path automatically. diff --git a/bindings/python/src/transcribe_cpp/__init__.py b/bindings/python/src/transcribe_cpp/__init__.py index 946cf10f..0cf68086 100644 --- a/bindings/python/src/transcribe_cpp/__init__.py +++ b/bindings/python/src/transcribe_cpp/__init__.py @@ -49,7 +49,7 @@ __version__ = "0.2.0" # String-enum types, exported so callers (and type checkers) can name them. -Backend = Literal["auto", "cpu", "metal", "vulkan", "cpu_accel", "cuda"] +Backend = Literal["auto", "cpu", "metal", "vulkan", "cpu_accel", "cuda", "rocm"] KVType = Literal["auto", "f32", "f16"] Task = Literal["transcribe", "translate"] Timestamps = Literal["none", "auto", "segment", "word", "token"] @@ -187,6 +187,7 @@ "vulkan": _generated.TRANSCRIBE_BACKEND_VULKAN, "cpu_accel": _generated.TRANSCRIBE_BACKEND_CPU_ACCEL, "cuda": _generated.TRANSCRIBE_BACKEND_CUDA, + "rocm": _generated.TRANSCRIBE_BACKEND_ROCM, } _KV_TYPES = { "auto": _generated.TRANSCRIBE_KV_TYPE_AUTO, @@ -271,7 +272,7 @@ class BackendDevice: name: str description: str - kind: str # "cpu" | "accel" | "metal" | "vulkan" | "cuda" | "sycl" | "gpu" | "unknown" + kind: str # "cpu" | "accel" | "metal" | "vulkan" | "cuda" | "rocm" | "sycl" | "gpu" | "unknown" # Vendor-agnostic class: "cpu" | "gpu" | "igpu" | "accel", or "unknown" for a # value reported by a runtime newer than this binding (tell such devices # apart by device_id / name, not by this axis). diff --git a/bindings/python/src/transcribe_cpp/_generated.py b/bindings/python/src/transcribe_cpp/_generated.py index 9c133653..e6d42931 100644 --- a/bindings/python/src/transcribe_cpp/_generated.py +++ b/bindings/python/src/transcribe_cpp/_generated.py @@ -13,7 +13,7 @@ # Stable digest of the ABI surface below (structs, enums, macros, layout, # prototypes). A native provider package echoes this back so the API # package can reject an ABI-mismatched provider before dlopen. -PUBLIC_HEADER_HASH = "fb2e64791dcbb70a" +PUBLIC_HEADER_HASH = "7896d8d4c2a46147" # === enum constants === TRANSCRIBE_OK = 0 @@ -83,6 +83,7 @@ TRANSCRIBE_BACKEND_VULKAN = 3 TRANSCRIBE_BACKEND_CPU_ACCEL = 4 TRANSCRIBE_BACKEND_CUDA = 5 +TRANSCRIBE_BACKEND_ROCM = 6 TRANSCRIBE_DEVICE_TYPE_CPU = 0 TRANSCRIBE_DEVICE_TYPE_GPU = 1 TRANSCRIBE_DEVICE_TYPE_IGPU = 2 diff --git a/bindings/python/tests/test_backends.py b/bindings/python/tests/test_backends.py index 6ec98369..4d1bb3fa 100644 --- a/bindings/python/tests/test_backends.py +++ b/bindings/python/tests/test_backends.py @@ -21,7 +21,7 @@ def test_at_least_one_device_registered(): for dev in devices: assert dev.name assert dev.kind in { - "cpu", "accel", "metal", "vulkan", "cuda", "sycl", "gpu", "unknown" + "cpu", "accel", "metal", "vulkan", "cuda", "rocm", "sycl", "gpu", "unknown" } @@ -66,8 +66,8 @@ def test_available_kinds_match_device_list(): # answer. (cpu_accel is satisfied by a CPU device per the C contract.) kinds = {d.kind for d in t.backends()} for request, device_kind in (("metal", "metal"), ("vulkan", "vulkan"), - ("cuda", "cuda"), ("cpu", "cpu"), - ("cpu_accel", "cpu")): + ("cuda", "cuda"), ("rocm", "rocm"), + ("cpu", "cpu"), ("cpu_accel", "cpu")): assert t.backend_available(request) == (device_kind in kinds), request diff --git a/bindings/rust/sys/README.md b/bindings/rust/sys/README.md index ee516317..b992545b 100644 --- a/bindings/rust/sys/README.md +++ b/bindings/rust/sys/README.md @@ -25,17 +25,29 @@ vcpkg setup is required on any platform. The static link is the default; the ## Features -- `metal` (default on Apple), `vulkan`, `cuda`, `openmp` — each forwards to the - matching `TRANSCRIBE_*` CMake option. +- `metal` (default on Apple), `vulkan`, `cuda`, `rocm`, `openmp` — each forwards + to the matching `TRANSCRIBE_*` CMake option (`rocm` enables `TRANSCRIBE_HIP`). - `shared` — link a shared `libtranscribe` (`.so`/`.dylib`/`.dll`) loaded at runtime instead of statically baking it in. The default is a self-contained static link. - `dynamic-backends` — additionally ship each compute backend (the per-ISA CPU - tiers, Vulkan, CUDA, …) as a loadable module next to the library, selected at + tiers, Vulkan, CUDA, ROCm, …) as a loadable module next to the library, selected at runtime by `transcribe_init_backends_default()` when the modules sit next to `libtranscribe`, or `transcribe_init_backends(dir)` for a custom provider directory. Implies `shared`. +## ROCm builds + +Install ROCm 6.1 or newer, then enable the first-class `rocm` feature: + +```sh +cargo build --no-default-features --features rocm +``` + +The build detects the attached AMD GPU. To target a specific architecture, pass +it through CMake, for example +`TRANSCRIBE_CMAKE_ARGS="-DAMDGPU_TARGETS=gfx1201"`. + ## Windows Vulkan builds The `vulkan` feature requires the diff --git a/bindings/rust/sys/build.rs b/bindings/rust/sys/build.rs index 8611200d..5cf529d7 100644 --- a/bindings/rust/sys/build.rs +++ b/bindings/rust/sys/build.rs @@ -14,6 +14,7 @@ //! `metal` -> TRANSCRIBE_METAL=ON (Apple targets only; no-op elsewhere) //! `vulkan` -> TRANSCRIBE_VULKAN=ON //! `cuda` -> TRANSCRIBE_CUDA=ON +//! `rocm` -> TRANSCRIBE_HIP=ON //! `openmp` -> TRANSCRIBE_USE_OPENMP=ON //! Official-artifact hygiene flags (OpenMP/BLAS off) are deliberately NOT //! forced here: a source build is the consumer's build (same philosophy as @@ -160,6 +161,9 @@ fn main() { if feature("CUDA") { cfg.define("TRANSCRIBE_CUDA", "ON"); } + if feature("ROCM") { + cfg.define("TRANSCRIBE_HIP", "ON"); + } // Keep OpenMP OFF unless explicitly opted in. TRANSCRIBE_USE_OPENMP already // defaults OFF in CMake (the native ggml threadpool is the default path); we // set it explicitly here so `--features openmp` is the single switch. We keep diff --git a/bindings/rust/sys/src/transcribe_sys.rs b/bindings/rust/sys/src/transcribe_sys.rs index bb3b5a39..eebcf3b0 100644 --- a/bindings/rust/sys/src/transcribe_sys.rs +++ b/bindings/rust/sys/src/transcribe_sys.rs @@ -1,11 +1,11 @@ // @generated by `cargo xtask bindgen` from include/transcribe/extensions.h // DO NOT EDIT BY HAND. Regenerate: `cargo xtask bindgen`. -// Pinned to include/transcribe.abihash = fb2e64791dcbb70a +// Pinned to include/transcribe.abihash = 7896d8d4c2a46147 /// The public-ABI digest these bindings were generated against /// (sha256/16 over the normalized FFI surface). The load-time version /// gate and the CI drift check both anchor on this value. -pub const PUBLIC_HEADER_HASH: &str = "fb2e64791dcbb70a"; +pub const PUBLIC_HEADER_HASH: &str = "7896d8d4c2a46147"; /* automatically generated by rust-bindgen 0.72.1 */ @@ -200,6 +200,7 @@ impl transcribe_backend_request { pub const TRANSCRIBE_BACKEND_CPU_ACCEL: transcribe_backend_request = transcribe_backend_request(4); pub const TRANSCRIBE_BACKEND_CUDA: transcribe_backend_request = transcribe_backend_request(5); + pub const TRANSCRIBE_BACKEND_ROCM: transcribe_backend_request = transcribe_backend_request(6); } #[repr(transparent)] #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] diff --git a/bindings/rust/transcribe-cpp/Cargo.toml b/bindings/rust/transcribe-cpp/Cargo.toml index 20d94392..f3774ee9 100644 --- a/bindings/rust/transcribe-cpp/Cargo.toml +++ b/bindings/rust/transcribe-cpp/Cargo.toml @@ -26,6 +26,7 @@ default = ["metal"] metal = ["transcribe-cpp-sys/metal"] vulkan = ["transcribe-cpp-sys/vulkan"] cuda = ["transcribe-cpp-sys/cuda"] +rocm = ["transcribe-cpp-sys/rocm"] openmp = ["transcribe-cpp-sys/openmp"] # `shared` links a shared libtranscribe; `dynamic-backends` additionally ships # each compute backend as a runtime-loaded module (and implies `shared`, both diff --git a/bindings/rust/transcribe-cpp/README.md b/bindings/rust/transcribe-cpp/README.md index ea2eab92..81e2c85e 100644 --- a/bindings/rust/transcribe-cpp/README.md +++ b/bindings/rust/transcribe-cpp/README.md @@ -48,7 +48,7 @@ is the safe wrapper. ## Backends Backends are selected with cargo features forwarded to `transcribe-cpp-sys`: -`metal` (default on Apple), `vulkan`, `cuda`, and `openmp`. +`metal` (default on Apple), `vulkan`, `cuda`, `rocm`, and `openmp`. On Windows, `vulkan` requires the Vulkan SDK. Deep Cargo output paths are shortened automatically during the native build; see the diff --git a/bindings/rust/transcribe-cpp/examples/backend-select.rs b/bindings/rust/transcribe-cpp/examples/backend-select.rs index 3c9c7f9a..cc7d303b 100644 --- a/bindings/rust/transcribe-cpp/examples/backend-select.rs +++ b/bindings/rust/transcribe-cpp/examples/backend-select.rs @@ -26,16 +26,27 @@ fn main() -> Result<(), Box> { println!(" {} [{}] — {}", d.name, d.kind, d.description); } println!("\nbackend availability:"); - for b in [Backend::Cpu, Backend::Metal, Backend::Vulkan, Backend::Cuda] { + for b in [ + Backend::Cpu, + Backend::Metal, + Backend::Vulkan, + Backend::Cuda, + Backend::Rocm, + ] { println!(" {b:?} = {}", backend_available(b)); } // The first optional backend this build does NOT have — used below to show // a request that cannot be satisfied. (CPU is always present, so it is not // a candidate.) - let unavailable = [Backend::Cuda, Backend::Vulkan, Backend::Metal] - .into_iter() - .find(|&b| !backend_available(b)); + let unavailable = [ + Backend::Cuda, + Backend::Rocm, + Backend::Vulkan, + Backend::Metal, + ] + .into_iter() + .find(|&b| !backend_available(b)); let mut args = std::env::args().skip(1); let Some(model_path) = common::model_path(args.next().as_deref()) else { diff --git a/bindings/rust/transcribe-cpp/src/backend.rs b/bindings/rust/transcribe-cpp/src/backend.rs index 74941855..e75296f9 100644 --- a/bindings/rust/transcribe-cpp/src/backend.rs +++ b/bindings/rust/transcribe-cpp/src/backend.rs @@ -59,7 +59,7 @@ pub struct Device { /// Human-readable description, e.g. "Apple M4 Max". pub description: String, /// Classified vendor kind: "cpu", "accel", "metal", "vulkan", "cuda", - /// "sycl", "gpu", or "unknown". + /// "rocm", "sycl", "gpu", or "unknown". pub kind: String, /// The CPU/GPU/IGPU/ACCEL axis, orthogonal to [`Device::kind`]. pub device_type: DeviceType, diff --git a/bindings/rust/transcribe-cpp/src/types.rs b/bindings/rust/transcribe-cpp/src/types.rs index d4cd37f4..c390af7c 100644 --- a/bindings/rust/transcribe-cpp/src/types.rs +++ b/bindings/rust/transcribe-cpp/src/types.rs @@ -175,6 +175,8 @@ pub enum Backend { Vulkan, /// Require CUDA; errors if this build has no CUDA. Cuda, + /// Require ROCm; errors if this build has no ROCm. + Rocm, } impl Backend { @@ -187,6 +189,7 @@ impl Backend { Backend::Metal => B::TRANSCRIBE_BACKEND_METAL, Backend::Vulkan => B::TRANSCRIBE_BACKEND_VULKAN, Backend::Cuda => B::TRANSCRIBE_BACKEND_CUDA, + Backend::Rocm => B::TRANSCRIBE_BACKEND_ROCM, } } } diff --git a/bindings/rust/transcribe-cpp/tests/degradation.rs b/bindings/rust/transcribe-cpp/tests/degradation.rs index 1e311257..70e4b3a4 100644 --- a/bindings/rust/transcribe-cpp/tests/degradation.rs +++ b/bindings/rust/transcribe-cpp/tests/degradation.rs @@ -42,13 +42,14 @@ fn cpu_is_the_floor() { #[test] fn unavailable_backend_probes_cleanly() { init_backends_default().expect("init_backends_default"); - // In a CPU/Metal build Vulkan and CUDA are not compiled in. The probe must - // answer (true/false) without crashing — this is what lets a host turn an + // In a CPU/Metal build Vulkan, CUDA, and ROCm are not compiled in. The probe + // must answer (true/false) without crashing — this is what lets a host turn an // explicit Backend::Vulkan request into a clear error instead of a failed // model load. We assert it does not panic and is internally consistent; // CPU is the one backend we can assert positively on every runner. let _vulkan = backend_available(Backend::Vulkan); let _cuda = backend_available(Backend::Cuda); + let _rocm = backend_available(Backend::Rocm); assert!(backend_available(Backend::Cpu)); } diff --git a/bindings/swift/Sources/TranscribeCpp/ABIHash.swift b/bindings/swift/Sources/TranscribeCpp/ABIHash.swift index 500fbf03..aa501f65 100644 --- a/bindings/swift/Sources/TranscribeCpp/ABIHash.swift +++ b/bindings/swift/Sources/TranscribeCpp/ABIHash.swift @@ -13,7 +13,7 @@ import CTranscribe extension Transcribe { /// sha256/16 of the normalized public FFI surface, pinned to the value in /// include/transcribe.abihash at the time this binding was last reviewed. - public static let pinnedHeaderHash = "fb2e64791dcbb70a" + public static let pinnedHeaderHash = "7896d8d4c2a46147" /// The public-ABI digest this binding was reviewed against (16 hex chars). public static func headerHash() -> String { pinnedHeaderHash } diff --git a/bindings/swift/Sources/TranscribeCpp/Backend.swift b/bindings/swift/Sources/TranscribeCpp/Backend.swift index a52440f5..3298887f 100644 --- a/bindings/swift/Sources/TranscribeCpp/Backend.swift +++ b/bindings/swift/Sources/TranscribeCpp/Backend.swift @@ -1,7 +1,7 @@ import CTranscribe /// A backend request. `auto` always succeeds (CPU is the final fallback); -/// `metal`/`vulkan`/`cuda` require that backend to be present in the build. +/// Explicit GPU choices require that backend to be present in the build. public enum Backend: Sendable, Equatable { case auto case cpu @@ -9,6 +9,7 @@ public enum Backend: Sendable, Equatable { case metal case vulkan case cuda + case rocm var cValue: transcribe_backend_request { switch self { @@ -18,6 +19,7 @@ public enum Backend: Sendable, Equatable { case .metal: return TRANSCRIBE_BACKEND_METAL case .vulkan: return TRANSCRIBE_BACKEND_VULKAN case .cuda: return TRANSCRIBE_BACKEND_CUDA + case .rocm: return TRANSCRIBE_BACKEND_ROCM } } } @@ -54,7 +56,7 @@ public struct Device: Sendable, Equatable { public let name: String /// Human-readable description, e.g. "Apple M4 Max". public let description: String - /// Classified vendor kind string, e.g. "cpu", "metal", "vulkan", "cuda". + /// Classified vendor kind string, e.g. "cpu", "metal", "vulkan", "cuda", "rocm". public let kind: String /// The CPU/GPU/IGPU/ACCEL axis, orthogonal to `kind`. public let deviceType: DeviceType diff --git a/bindings/swift/Sources/backend-select/main.swift b/bindings/swift/Sources/backend-select/main.swift index 308d67c3..b3df0117 100644 --- a/bindings/swift/Sources/backend-select/main.swift +++ b/bindings/swift/Sources/backend-select/main.swift @@ -8,7 +8,7 @@ for device in Transcribe.devices() { } print("\nbackend availability:") -for backend in [Backend.cpu, .metal, .vulkan, .cuda] { +for backend in [Backend.cpu, .metal, .vulkan, .cuda, .rocm] { print(" \(backend): \(Transcribe.backendAvailable(backend))") } diff --git a/bindings/typescript/README.md b/bindings/typescript/README.md index bdc831b3..2998ee77 100644 --- a/bindings/typescript/README.md +++ b/bindings/typescript/README.md @@ -102,9 +102,9 @@ the model lease). Disposal is idempotent and order-independent. import { getAvailableBackends, backendAvailable } from "transcribe-cpp"; getAvailableBackends(); // [{ kind: "metal", name: "MTL0", description: "…" }, …] -backendAvailable("cuda"); // boolean — never throws +backendAvailable("rocm"); // boolean — never throws -const model = await TranscribeModel.load("model.gguf", { backend: "metal" }); +const model = await TranscribeModel.load("model.gguf", { backend: "rocm" }); ``` `backend` defaults to `"auto"` (best accelerator, else CPU). A missing Vulkan diff --git a/bindings/typescript/examples/backend-select.mjs b/bindings/typescript/examples/backend-select.mjs index 3d73f232..f266fdc7 100644 --- a/bindings/typescript/examples/backend-select.mjs +++ b/bindings/typescript/examples/backend-select.mjs @@ -13,7 +13,7 @@ for (const d of getAvailableBackends()) { console.log(` ${d.kind.padEnd(7)} ${d.name} — ${d.description}`); } console.log("\nbackend availability:"); -for (const b of ["cpu", "metal", "vulkan", "cuda"]) { +for (const b of ["cpu", "metal", "vulkan", "cuda", "rocm"]) { console.log(` ${b.padEnd(7)} ${backendAvailable(b)}`); } @@ -21,7 +21,13 @@ const path = model("TRANSCRIBE_SMOKE_MODEL"); if (!path) skip("\nno model — device discovery only (set TRANSCRIBE_SMOKE_MODEL to load)"); // Prefer an accelerator, fall back to CPU on a clean failure. -const preferred = backendAvailable("metal") ? "metal" : backendAvailable("cuda") ? "cuda" : "cpu"; +const preferred = backendAvailable("metal") + ? "metal" + : backendAvailable("cuda") + ? "cuda" + : backendAvailable("rocm") + ? "rocm" + : "cpu"; console.log(`\nrequesting backend: ${preferred}`); let m; try { diff --git a/bindings/typescript/src/_generated.ts b/bindings/typescript/src/_generated.ts index 6d316b12..ab5d280d 100644 --- a/bindings/typescript/src/_generated.ts +++ b/bindings/typescript/src/_generated.ts @@ -11,7 +11,7 @@ // Stable digest of the ABI surface (structs, enums, macros, layout, // prototypes), computed by the Python oracle and pinned here so a header // ABI change turns this binding's drift check red for conscious review. -export const PUBLIC_HEADER_HASH = "fb2e64791dcbb70a"; +export const PUBLIC_HEADER_HASH = "7896d8d4c2a46147"; // === enum constants === export const TRANSCRIBE_OK = 0; @@ -81,6 +81,7 @@ export const TRANSCRIBE_BACKEND_METAL = 2; export const TRANSCRIBE_BACKEND_VULKAN = 3; export const TRANSCRIBE_BACKEND_CPU_ACCEL = 4; export const TRANSCRIBE_BACKEND_CUDA = 5; +export const TRANSCRIBE_BACKEND_ROCM = 6; export const TRANSCRIBE_DEVICE_TYPE_CPU = 0; export const TRANSCRIBE_DEVICE_TYPE_GPU = 1; export const TRANSCRIBE_DEVICE_TYPE_IGPU = 2; diff --git a/bindings/typescript/src/index.ts b/bindings/typescript/src/index.ts index 970ba670..9b60cf00 100644 --- a/bindings/typescript/src/index.ts +++ b/bindings/typescript/src/index.ts @@ -68,6 +68,7 @@ const BACKENDS: Record = { vulkan: g.TRANSCRIBE_BACKEND_VULKAN, cpu_accel: g.TRANSCRIBE_BACKEND_CPU_ACCEL, cuda: g.TRANSCRIBE_BACKEND_CUDA, + rocm: g.TRANSCRIBE_BACKEND_ROCM, }; const KV_TYPES: Record = { auto: g.TRANSCRIBE_KV_TYPE_AUTO, diff --git a/bindings/typescript/src/types.ts b/bindings/typescript/src/types.ts index 9c087d72..71a40261 100644 --- a/bindings/typescript/src/types.ts +++ b/bindings/typescript/src/types.ts @@ -2,7 +2,7 @@ import type { TranscribeError } from "./errors.js"; -export type Backend = "auto" | "cpu" | "cpu_accel" | "cuda" | "vulkan" | "metal"; +export type Backend = "auto" | "cpu" | "cpu_accel" | "cuda" | "rocm" | "vulkan" | "metal"; export type KvType = "auto" | "f32" | "f16"; export type Task = "transcribe" | "translate"; export type TimestampKind = "none" | "auto" | "segment" | "word" | "token"; diff --git a/bindings/typescript/test/no-model.test.mjs b/bindings/typescript/test/no-model.test.mjs index ca8510f3..a92b4acc 100644 --- a/bindings/typescript/test/no-model.test.mjs +++ b/bindings/typescript/test/no-model.test.mjs @@ -81,6 +81,7 @@ test("each backend device has a well-formed, index-aligned shape", () => { test("backendAvailable is a clean boolean probe", () => { assert.equal(backendAvailable("cpu"), true); assert.equal(typeof backendAvailable("cuda"), "boolean"); + assert.equal(typeof backendAvailable("rocm"), "boolean"); }); test("invalid backend string is rejected, not silently coerced", () => { diff --git a/cmake/transcribe-install.cmake b/cmake/transcribe-install.cmake index 2c590135..4d9f06cb 100644 --- a/cmake/transcribe-install.cmake +++ b/cmake/transcribe-install.cmake @@ -165,14 +165,31 @@ if(NOT TRANSCRIBE_BUILD_SHARED) endforeach() # Some HIP packages add compiler-rt builtins directly to hip::host's - # interface. Preserve any such absolute artifacts as well; the - # amdhip64 target itself is already represented above. - get_target_property(_hip_host_links hip::host INTERFACE_LINK_LIBRARIES) - foreach(_hip_host_link IN LISTS _hip_host_links) - if(IS_ABSOLUTE "${_hip_host_link}" AND EXISTS "${_hip_host_link}") - list(APPEND _library_paths "${_hip_host_link}") + # interface. hip::host is created in ggml-hip's directory and imported + # targets are directory-scoped, so it normally is not visible here. + # Query it when a package promotes it to global; otherwise reproduce + # hip-config.cmake's compiler query using CMake's HIP compiler. + if(TARGET hip::host) + get_target_property(_hip_host_links hip::host INTERFACE_LINK_LIBRARIES) + foreach(_hip_host_link IN LISTS _hip_host_links) + if(IS_ABSOLUTE "${_hip_host_link}" AND EXISTS "${_hip_host_link}") + list(APPEND _library_paths "${_hip_host_link}") + endif() + endforeach() + elseif(CMAKE_HIP_COMPILER) + execute_process( + COMMAND "${CMAKE_HIP_COMPILER}" + -print-libgcc-file-name --rtlib=compiler-rt + OUTPUT_VARIABLE _hip_compiler_rt + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + RESULT_VARIABLE _hip_compiler_rt_result) + if(_hip_compiler_rt_result EQUAL 0 + AND IS_ABSOLUTE "${_hip_compiler_rt}" + AND EXISTS "${_hip_compiler_rt}") + list(APPEND _library_paths "${_hip_compiler_rt}") endif() - endforeach() + endif() list(REMOVE_DUPLICATES _library_paths) endif() if(_frameworks) diff --git a/examples/bench/batch_bench.cpp b/examples/bench/batch_bench.cpp index 8a0fbefe..f0d09e39 100644 --- a/examples/bench/batch_bench.cpp +++ b/examples/bench/batch_bench.cpp @@ -49,6 +49,9 @@ transcribe_backend_request parse_backend(const char * s) { if (!std::strcmp(s, "cuda")) { return TRANSCRIBE_BACKEND_CUDA; } + if (!std::strcmp(s, "rocm")) { + return TRANSCRIBE_BACKEND_ROCM; + } return TRANSCRIBE_BACKEND_AUTO; } diff --git a/examples/cli/main.cpp b/examples/cli/main.cpp index df820403..a4d0ed13 100644 --- a/examples/cli/main.cpp +++ b/examples/cli/main.cpp @@ -288,7 +288,7 @@ void print_usage(const char * argv0) { " max, >max is clamped down. Lowers the effective\n" " max audio.\n" " --kv-type TYPE flash-attn KV type: auto, f32, f16 (default: auto)\n" - " --backend TYPE compute backend: auto, cpu, cpu_accel, metal, vulkan, cuda\n" + " --backend TYPE compute backend: auto, cpu, cpu_accel, metal, vulkan, cuda, rocm\n" " (default: auto)\n" " --device N GPU device index from --list-devices: 0 = auto\n" " (first of kind), >0 selects that registry index\n" @@ -476,8 +476,10 @@ bool parse_args(int argc, char ** argv, cli_args & out) { out.backend = TRANSCRIBE_BACKEND_VULKAN; } else if (vs == "cuda") { out.backend = TRANSCRIBE_BACKEND_CUDA; + } else if (vs == "rocm") { + out.backend = TRANSCRIBE_BACKEND_ROCM; } else { - std::fprintf(stderr, "error: --backend must be auto, cpu, cpu_accel, metal, vulkan, or cuda\n"); + std::fprintf(stderr, "error: --backend must be auto, cpu, cpu_accel, metal, vulkan, cuda, or rocm\n"); return false; } } else if (a == "--device") { diff --git a/include/transcribe.abihash b/include/transcribe.abihash index 8513799e..bd8c2750 100644 --- a/include/transcribe.abihash +++ b/include/transcribe.abihash @@ -1 +1 @@ -fb2e64791dcbb70a +7896d8d4c2a46147 diff --git a/include/transcribe.h b/include/transcribe.h index d16c7b31..e789bbd7 100644 --- a/include/transcribe.h +++ b/include/transcribe.h @@ -675,7 +675,7 @@ TRANSCRIBE_API bool transcribe_model_accepts_ext_kind(const struct transcribe_mo * that successfully initializes, probing every discrete GPU * before any integrated GPU; within a tier, devices are tried * in ggml's device registry order — which is build-time - * prioritized (Metal on Apple, Vulkan / CUDA / SYCL on + * prioritized (Metal on Apple, Vulkan / CUDA / ROCm / SYCL on * Linux, …). An integrated GPU is selected only when no * discrete GPU initializes. Host-memory accelerators (BLAS, * AMX, …) are additionally layered onto the scheduler when @@ -708,6 +708,12 @@ TRANSCRIBE_API bool transcribe_model_accepts_ext_kind(const struct transcribe_mo * if Vulkan is not available in this build. Host-memory * accelerators are still layered on when present. * + * CUDA Require the NVIDIA CUDA backend. Returns TRANSCRIBE_ERR_BACKEND + * if CUDA is not available in this build. + * + * ROCM Require the AMD ROCm backend. Returns TRANSCRIBE_ERR_BACKEND + * if ROCm is not available in this build. + * * Callers that need to know which backend they actually landed on * can query transcribe_model_backend() after load. */ @@ -718,6 +724,7 @@ typedef enum { TRANSCRIBE_BACKEND_VULKAN = 3, TRANSCRIBE_BACKEND_CPU_ACCEL = 4, TRANSCRIBE_BACKEND_CUDA = 5, + TRANSCRIBE_BACKEND_ROCM = 6, } transcribe_backend_request; /* ----------------------------------------------------------------------- */ @@ -797,7 +804,7 @@ TRANSCRIBE_API int transcribe_backend_device_count(void); /* * Device type: ggml's vendor-agnostic classification of a device, * orthogonal to `kind` below (which carries the vendor: metal/vulkan/cuda/ - * ...). Backends report this classification themselves, so treat it as a + * rocm/...). Backends report this classification themselves, so treat it as a * runtime hint about CPU/GPU/IGPU/ACCEL placement rather than a portable * hardware-memory taxonomy. The numeric values mirror ggml's device-type * enum. @@ -861,8 +868,8 @@ TRANSCRIBE_API transcribe_status transcribe_get_backend_device(int index, struct /* * Whether a backend request can be satisfied by some registered device: * AUTO whenever any device exists; CPU and CPU_ACCEL when a CPU device - * exists; METAL / VULKAN / CUDA when a device of that kind exists. Unknown - * or invalid request values answer false (never an error). This is the + * exists; METAL / VULKAN / CUDA / ROCM when a device of that kind exists. + * Unknown or invalid request values answer false (never an error). This is the * probe a binding uses to turn `backend="vulkan"` on a machine without * Vulkan into a clear exception instead of a failed model load. */ @@ -915,7 +922,7 @@ TRANSCRIBE_API transcribe_status transcribe_model_get_device(const struct transc * * gpu_device: Multi-GPU selector. 0 (the default) means "auto / the first * device of the chosen kind": AUTO picks the first GPU that - * initializes, and explicit METAL/VULKAN/CUDA requests pick the + * initializes, and explicit METAL/VULKAN/CUDA/ROCM requests pick the * first matching device — in both cases probing every discrete * GPU before any integrated GPU, in ggml's registry order * within each tier. @@ -925,7 +932,7 @@ TRANSCRIBE_API transcribe_status transcribe_model_get_device(const struct transc * enumerates, so enumerate first to choose one. The selected * device becomes the model's primary backend, validated against * `backend`: it must be a GPU/IGPU, and for an explicit - * METAL/VULKAN/CUDA request it must be that vendor. The index is + * METAL/VULKAN/CUDA/ROCM request it must be that vendor. The index is * order-dependent — ggml's registry order can shift across driver * updates or hosts, so treat it as a runtime selection, not a * stable identifier; correlate via the enumerated device's name / @@ -1353,7 +1360,7 @@ TRANSCRIBE_API bool transcribe_model_supports(const struct transcribe_model * mo * and the architecture handler had no default to substitute. * * transcribe_model_backend(): the runtime backend currently bound - * to this model, e.g. "cpu", "metal", "vulkan", "cuda". This is + * to this model, e.g. "cpu", "metal", "vulkan", "cuda", "ROCm". This is * the mechanism for detecting CPU fallback when GPU was requested. * * Returns an empty string when no runtime backend is currently diff --git a/scripts/bench/run.py b/scripts/bench/run.py index 9ee01a53..3e103bee 100755 --- a/scripts/bench/run.py +++ b/scripts/bench/run.py @@ -46,7 +46,7 @@ --iters N measured iterations per cell (default: 2) --warmup N warmup iterations per cell (default: 1) --backends B[,B...]|all backends to bench (default: all, auto-detected) - valid: metal,cpu,cpu_accel,vulkan,cuda,all + valid: metal,cpu,cpu_accel,vulkan,cuda,rocm,all --name LABEL stable label for named baselines; when set, output filenames use instead of and are overwritten on re-run @@ -67,6 +67,9 @@ cuda -> repo/build/bin/transcribe-bench --backend cuda (falls through to repo/build-cuda/bin/transcribe-bench only when build/bin/transcribe-bench does not exist) + rocm -> repo/build/bin/transcribe-bench --backend rocm + (falls through to repo/build-rocm/bin/transcribe-bench + only when build/bin/transcribe-bench does not exist) Auto-detection (when --backends is unset or 'all'): metal available if build/bin/transcribe-bench exists AND sys.platform=='darwin' @@ -78,6 +81,8 @@ cuda available only if build-cuda/bin/transcribe-bench exists (same caveat as vulkan; pass --backends cuda explicitly to bench CUDA from a unified build) + rocm available only if build-rocm/bin/transcribe-bench exists + (same caveat; pass --backends rocm explicitly for a unified build) Output: one aggregated JSON per (variant, backend) at reports/perf//__.json @@ -101,7 +106,7 @@ DEFAULT_QUANTS = ["f16", "q8_0", "q4_k_m"] DEFAULT_SAMPLES = ["jfk", "dots"] -KNOWN_BACKENDS = ["metal", "cpu", "cpu_accel", "vulkan", "cuda"] +KNOWN_BACKENDS = ["metal", "cpu", "cpu_accel", "vulkan", "cuda", "rocm"] @dataclass(slots=True) @@ -367,22 +372,27 @@ def _cuda_binary(repo: Path) -> Path: return repo / "build-cuda/bin/transcribe-bench" +def _rocm_binary(repo: Path) -> Path: + """Prefer build/bin/transcribe-bench, then the legacy ROCm build tree.""" + primary = repo / "build/bin/transcribe-bench" + if primary.exists(): + return primary + return repo / "build-rocm/bin/transcribe-bench" + + def auto_detect_backends(repo: Path) -> list[str]: """Return list of backend ids that appear usable on this host. - Auto-detect is conservative for vulkan/cuda: we cannot tell from a - path alone whether build/bin/transcribe-bench was compiled with - either backend, and a --backends all that silently included a - binary missing the backend would hard-fail. So vulkan/cuda only - auto-detect when the user has the legacy build-vulkan/ or - build-cuda/ tree (a strong signal they built that backend - separately). To benchmark a GPU backend from a unified build, pass - --backends vulkan or --backends cuda explicitly. + Auto-detect is conservative for vulkan/cuda/rocm: we cannot tell from a + path alone whether build/bin/transcribe-bench was compiled with one of + those backends. They auto-detect only from their legacy split-build tree. + To benchmark a GPU backend from a unified build, pass it explicitly. """ found: list[str] = [] metal_bin = _metal_binary(repo) legacy_vulkan_bin = repo / "build-vulkan/bin/transcribe-bench" legacy_cuda_bin = repo / "build-cuda/bin/transcribe-bench" + legacy_rocm_bin = repo / "build-rocm/bin/transcribe-bench" if metal_bin.exists() and sys.platform == "darwin": found.append("metal") if metal_bin.exists(): @@ -391,6 +401,8 @@ def auto_detect_backends(repo: Path) -> list[str]: found.append("vulkan") if legacy_cuda_bin.exists(): found.append("cuda") + if legacy_rocm_bin.exists(): + found.append("rocm") return found @@ -459,6 +471,11 @@ def resolve_backends(repo: Path, requested: str | None, backend_arg = "cuda" if not binary.exists(): missing.append(f"cuda: {binary}") + elif name == "rocm": + binary = bench_bin_override or _rocm_binary(repo) + backend_arg = "rocm" + if not binary.exists(): + missing.append(f"rocm: {binary}") else: # pragma: no cover continue specs.append(BackendSpec(name=name, binary=binary, backend_arg=backend_arg)) diff --git a/scripts/ci/wheel_smoke.py b/scripts/ci/wheel_smoke.py index b1c84465..836b0648 100644 --- a/scripts/ci/wheel_smoke.py +++ b/scripts/ci/wheel_smoke.py @@ -126,7 +126,7 @@ def main() -> int: # Intel macOS ships a CPU-only wheel (no Metal — Intel-Mac GPUs are # out of scope; Metal / tuned CPU is reachable via the sdist). The # bundled artifact must therefore expose only CPU. - for accel in ("metal", "vulkan", "cuda"): + for accel in ("metal", "vulkan", "cuda", "rocm"): assert not t.backend_available(accel), ( f"{accel} unexpectedly available in the CPU-only x86 macOS wheel" ) diff --git a/src/transcribe-load-common.cpp b/src/transcribe-load-common.cpp index 74fc0f61..157add2c 100644 --- a/src/transcribe-load-common.cpp +++ b/src/transcribe-load-common.cpp @@ -187,6 +187,7 @@ bool valid_backend_request(int raw) { case TRANSCRIBE_BACKEND_VULKAN: case TRANSCRIBE_BACKEND_CPU_ACCEL: case TRANSCRIBE_BACKEND_CUDA: + case TRANSCRIBE_BACKEND_ROCM: return true; } return false; @@ -230,6 +231,9 @@ transcribe_status init_backends_explicit_index(transcribe_backend_request reques case TRANSCRIBE_BACKEND_CUDA: wanted = BackendKind::Cuda; break; + case TRANSCRIBE_BACKEND_ROCM: + wanted = BackendKind::Rocm; + break; case TRANSCRIBE_BACKEND_AUTO: break; default: @@ -349,6 +353,7 @@ transcribe_status init_backends(transcribe_backend_request requested, case TRANSCRIBE_BACKEND_METAL: case TRANSCRIBE_BACKEND_VULKAN: case TRANSCRIBE_BACKEND_CUDA: + case TRANSCRIBE_BACKEND_ROCM: { // Specific GPU backend request: must find a matching device // or fail. ACCEL is still layered on because it's host-memory @@ -364,6 +369,9 @@ transcribe_status init_backends(transcribe_backend_request requested, case TRANSCRIBE_BACKEND_CUDA: wanted = BackendKind::Cuda; break; + case TRANSCRIBE_BACKEND_ROCM: + wanted = BackendKind::Rocm; + break; default: break; } diff --git a/src/transcribe-load-common.h b/src/transcribe-load-common.h index fd0f72f2..ddd71111 100644 --- a/src/transcribe-load-common.h +++ b/src/transcribe-load-common.h @@ -59,7 +59,7 @@ bool metal_backend_lacks_simdgroup_mm(ggml_backend_t be, ggml_backend_dev_t dev) // device at that global ggml registry index (the same index // space transcribe_get_backend_device() enumerates) as the // primary, validated against `requested`: the device must be -// a GPU/IGPU, and for a specific METAL/VULKAN/CUDA request it +// a GPU/IGPU, and for a specific METAL/VULKAN/CUDA/ROCM request it // must be that vendor. gpu_device is not valid for a // CPU / CPU_ACCEL request (there is no GPU to pick) nor // negative; both return TRANSCRIBE_ERR_INVALID_ARG. @@ -77,7 +77,7 @@ bool metal_backend_lacks_simdgroup_mm(ggml_backend_t be, ggml_backend_dev_t dev) // vendor doesn't match a specific GPU request, or // is non-zero for a CPU request. // TRANSCRIBE_ERR_BACKEND if the caller asked for a specific -// backend (METAL / VULKAN) that could not +// backend (METAL / VULKAN / CUDA / ROCM) that could not // be initialized, or if the CPU backend // itself fails to initialize (there is no // fallback past CPU). diff --git a/src/transcribe.cpp b/src/transcribe.cpp index 554fb32d..d3aba582 100644 --- a/src/transcribe.cpp +++ b/src/transcribe.cpp @@ -1054,6 +1054,9 @@ static bool transcribe_backend_available_impl(int raw) { case TRANSCRIBE_BACKEND_CUDA: want = transcribe::BackendKind::Cuda; break; + case TRANSCRIBE_BACKEND_ROCM: + want = transcribe::BackendKind::Rocm; + break; default: return false; } @@ -1447,6 +1450,7 @@ static transcribe_status transcribe_model_load_file_impl(const char * case TRANSCRIBE_BACKEND_VULKAN: case TRANSCRIBE_BACKEND_CPU_ACCEL: case TRANSCRIBE_BACKEND_CUDA: + case TRANSCRIBE_BACKEND_ROCM: break; default: transcribe::log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "transcribe_model_load_file: invalid backend request %d", diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e4b12330..1f0ad78c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -128,7 +128,7 @@ add_test(NAME transcribe_backend_probe_order_unit # # Covers the runtime backend-selection matrix: strict CPU invariants, # conditional explicit-backend error paths, AUTO fallback, and invalid -# enum rejection. Hardware-dependent cases (Metal/Vulkan present or +# enum rejection. Hardware-dependent cases (vendor GPU present or # absent) are probed at runtime inside the test binary — the test # always runs but exercises different branches depending on what the # build configured and what hardware is available. diff --git a/tests/api_smoke.c b/tests/api_smoke.c index b6602c06..2e1ad6e0 100644 --- a/tests/api_smoke.c +++ b/tests/api_smoke.c @@ -176,6 +176,7 @@ static void test_backend_devices(void) { CHECK(transcribe_backend_available(TRANSCRIBE_BACKEND_AUTO)); CHECK(transcribe_backend_available(TRANSCRIBE_BACKEND_CPU)); + (void) transcribe_backend_available(TRANSCRIBE_BACKEND_ROCM); } /* Out-of-range probes answer cleanly: error status or false, never UB. */ diff --git a/tests/backend_init_throw_unit.cpp b/tests/backend_init_throw_unit.cpp index 32902469..f3facaa2 100644 --- a/tests/backend_init_throw_unit.cpp +++ b/tests/backend_init_throw_unit.cpp @@ -109,6 +109,7 @@ void test_specific_gpu_request_fails_cleanly_when_every_device_throws() { TRANSCRIBE_BACKEND_METAL, TRANSCRIBE_BACKEND_VULKAN, TRANSCRIBE_BACKEND_CUDA, + TRANSCRIBE_BACKEND_ROCM, }; for (const auto kind : kinds) { transcribe::BackendPlan plan; diff --git a/tests/backend_init_unit.cpp b/tests/backend_init_unit.cpp index 8e45e803..20f7084a 100644 --- a/tests/backend_init_unit.cpp +++ b/tests/backend_init_unit.cpp @@ -6,7 +6,7 @@ // - Strict CPU: primary_kind == Cpu, scheduler_list.size() == 1, // and the sole scheduler handle classifies as Cpu via // ggml_backend_get_device() + classify_device(). -// - Explicit Metal/Vulkan: asserts based on what init_backends() +// - Explicit vendor GPU requests: assert based on what init_backends() // actually returns, not on registry probes — a device can be // registered but fail initialization. // - AUTO: always succeeds; asserts based on the returned primary_kind. @@ -176,7 +176,24 @@ int main() { } // --------------------------------------------------------------- - // 5. AUTO — always succeeds (falls back to CPU at minimum) + // 5. Explicit ROCm — same pattern as other GPU backends + // --------------------------------------------------------------- + { + BackendPlan plan; + transcribe_status st = init_backends(TRANSCRIBE_BACKEND_ROCM, 0, "test-rocm", plan); + + if (st == TRANSCRIBE_OK) { + CHECK_EQ(plan.primary_kind, BackendKind::Rocm); + CHECK(plan.primary != nullptr); + CHECK(plan.scheduler_list.size() >= 2); + free_plan(plan); + } else { + CHECK_EQ(st, TRANSCRIBE_ERR_BACKEND); + } + } + + // --------------------------------------------------------------- + // 6. AUTO — always succeeds (falls back to CPU at minimum) // --------------------------------------------------------------- // // AUTO probes GPU/IGPU devices and falls back to CPU if none @@ -203,7 +220,7 @@ int main() { } // --------------------------------------------------------------- - // 6. Invalid enum — returns TRANSCRIBE_ERR_INVALID_ARG + // 7. Invalid enum — returns TRANSCRIBE_ERR_INVALID_ARG // --------------------------------------------------------------- { BackendPlan plan; @@ -212,7 +229,7 @@ int main() { } // --------------------------------------------------------------- - // 7. Explicit gpu_device validation + // 8. Explicit gpu_device validation // --------------------------------------------------------------- { BackendPlan plan; diff --git a/tools/transcribe-bench/main.cpp b/tools/transcribe-bench/main.cpp index 3543d40c..2516f863 100644 --- a/tools/transcribe-bench/main.cpp +++ b/tools/transcribe-bench/main.cpp @@ -52,7 +52,7 @@ void print_usage(const char * argv0) { " --quiet suppress progress lines on stderr\n" " --threads N CPU threads (default 0 = library default)\n" " --backend KIND request a specific backend:\n" - " auto|cpu|cpu_accel|metal|vulkan|cuda (default auto)\n" + " auto|cpu|cpu_accel|metal|vulkan|cuda|rocm (default auto)\n" " cpu is strict CPU (no GPU, no BLAS/AMX).\n" " cpu_accel is CPU + host-memory accelerators\n" " (BLAS/AMX) when the build includes them.\n" @@ -95,9 +95,13 @@ bool parse_backend_kind(const char * s, transcribe_backend_request & out) { out = TRANSCRIBE_BACKEND_CUDA; return true; } + if (std::strcmp(s, "rocm") == 0) { + out = TRANSCRIBE_BACKEND_ROCM; + return true; + } std::fprintf(stderr, "error: --backend value '%s' not recognized " - "(expected one of: auto, cpu, cpu_accel, metal, vulkan, cuda)\n", + "(expected one of: auto, cpu, cpu_accel, metal, vulkan, cuda, rocm)\n", s); return false; } From 7cc57372ad0cb19e120450550c1509a7483fa6ff Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Thu, 30 Jul 2026 18:44:17 +0800 Subject: [PATCH 5/5] fix rust build script --- bindings/rust/sys/build.rs | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/bindings/rust/sys/build.rs b/bindings/rust/sys/build.rs index 5cf529d7..0576ba9b 100644 --- a/bindings/rust/sys/build.rs +++ b/bindings/rust/sys/build.rs @@ -350,6 +350,27 @@ fn find_manifest(prefix: &Path) -> Option { None } +/// Convert a conventional absolute Unix library filename into the Cargo native +/// library kind/name pair. `rustc-link-lib` propagates through dependent Rust +/// crates; a raw `rustc-link-arg=/path/to/lib` does not. +fn cargo_library_name(path: &Path) -> Option<(&'static str, String)> { + let file = path.file_name()?.to_str()?; + let file = file.strip_prefix("lib")?; + + if let Some(name) = file.strip_suffix(".dll.a") { + return Some(("dylib", name.to_owned())); + } + if let Some(name) = file.strip_suffix(".a") { + return Some(("static", name.to_owned())); + } + for suffix in [".so", ".dylib", ".tbd"] { + if let Some(name) = file.strip_suffix(suffix) { + return Some(("dylib", name.to_owned())); + } + } + None +} + fn emit_link_lines(prefix: &Path, manifest_path: &Path) { let text = std::fs::read_to_string(manifest_path).expect("read transcribe-link.json"); let json: serde_json::Value = serde_json::from_str(&text).expect("parse transcribe-link.json"); @@ -377,9 +398,21 @@ fn emit_link_lines(prefix: &Path, manifest_path: &Path) { println!("cargo:rustc-link-lib={kind}={name}"); } - // Absolute library paths (e.g. a find_package(BLAS) result): link the file. + // Absolute library paths (e.g. ROCm SDK libraries, compiler-rt, or a + // find_package(BLAS) result). Express conventional library files as + // search-dir + rustc-link-lib rather than a raw rustc-link-arg: native + // library directives propagate from this -sys crate to final downstream + // binaries, while link arguments only affect this crate's own artifacts. + // Keep the exact-path fallback for an unusual linker input that cannot be + // represented as a conventional native library. for path in strs("library_paths") { - println!("cargo:rustc-link-arg={path}"); + let path = Path::new(&path); + if let (Some(parent), Some((kind, name))) = (path.parent(), cargo_library_name(path)) { + println!("cargo:rustc-link-search=native={}", parent.display()); + println!("cargo:rustc-link-lib={kind}={name}"); + } else { + println!("cargo:rustc-link-arg={}", path.display()); + } } // System libraries the C++/backend archives drag in. for name in strs("system_libs") {