Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ 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, 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.

tinyBLAS (Justine Tunney's `llamafile_sgemm` kernels) is on by default.
Expand Down
5 changes: 3 additions & 2 deletions bindings/python/src/transcribe_cpp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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).
Expand Down
3 changes: 2 additions & 1 deletion bindings/python/src/transcribe_cpp/_generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions bindings/python/src/transcribe_cpp/_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions bindings/python/tests/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}


Expand Down Expand Up @@ -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


Expand Down
1 change: 1 addition & 0 deletions bindings/python/tests/test_provider_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 15 additions & 3 deletions bindings/rust/sys/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 39 additions & 2 deletions bindings/rust/sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -346,6 +350,27 @@ fn find_manifest(prefix: &Path) -> Option<PathBuf> {
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");
Expand Down Expand Up @@ -373,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") {
Expand Down
5 changes: 3 additions & 2 deletions bindings/rust/sys/src/transcribe_sys.rs
Original file line number Diff line number Diff line change
@@ -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 */

Expand Down Expand Up @@ -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)]
Expand Down
1 change: 1 addition & 0 deletions bindings/rust/transcribe-cpp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bindings/rust/transcribe-cpp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 15 additions & 4 deletions bindings/rust/transcribe-cpp/examples/backend-select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,27 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
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 {
Expand Down
2 changes: 1 addition & 1 deletion bindings/rust/transcribe-cpp/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions bindings/rust/transcribe-cpp/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions bindings/rust/transcribe-cpp/tests/degradation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
2 changes: 1 addition & 1 deletion bindings/swift/Sources/TranscribeCpp/ABIHash.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
Loading
Loading