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
16 changes: 16 additions & 0 deletions .github/workflows/c-cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,19 @@ jobs:
run: make clean && make asan=1 -j2 debug=1
- name: asan
run: export DEVICE=cpu && make test
macos_cpu:
name: macOS Apple Silicon (CPU)
runs-on: macos-14
steps:
- uses: actions/checkout@v2
- name: build
run: make -j3 debug=1
- name: test
run: make test
macos_metal:
name: macOS Apple Silicon (Metal)
runs-on: macos-14
steps:
- uses: actions/checkout@v2
- name: build metal=1
run: make -j3 metal=1 debug=1
52 changes: 34 additions & 18 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ STATICLIB = $(BUILD_DIR)/libopenfish.a
OBJ = $(BUILD_DIR)/misc.o \
$(BUILD_DIR)/error.o \
$(BUILD_DIR)/decode_cpu.o \
$(BUILD_DIR)/nn_cpu.o \
$(BUILD_DIR)/openfish.o \
$(BUILD_DIR)/beam_search.o \

Expand All @@ -34,7 +33,7 @@ endif
ifdef cuda
CUDA_ROOT ?= /usr/local/cuda
CUDA_LIB ?= $(CUDA_ROOT)/lib64
CUDA_OBJ += $(BUILD_DIR)/decode_cuda.o $(BUILD_DIR)/nn_cuda.o
CUDA_OBJ += $(BUILD_DIR)/decode_cuda.o
NVCC ?= $(CUDA_ROOT)/bin/nvcc
CUDA_CFLAGS += -g -O2 -lineinfo $(CUDA_ARCH) -Xcompiler -Wall
CUDA_LDFLAGS = -L$(CUDA_LIB) -lcudart_static -lrt -ldl
Expand All @@ -50,12 +49,23 @@ else ifdef rocm
# ifneq (,$(findstring gfx1150,$(ROCM_ARCH)))
# ROCM_CFLAGS += -D__AMDGCN_WAVEFRONT_SIZE=32
# endif
ROCM_OBJ += $(BUILD_DIR)/decode_hip.o $(BUILD_DIR)/nn_hip.o
ROCM_OBJ += $(BUILD_DIR)/decode_hip.o
GPU_LIB = $(BUILD_DIR)/hip_code.a
ROCM_LDFLAGS = -L$(ROCM_LIB) -lamdhip64 -lrt -ldl
CPPFLAGS += -DHAVE_ROCM=1
MAIN_CC = $(HIPCC)
MAIN_CFLAGS = -x hip $(ROCM_CFLAGS) -fPIC
else ifdef metal
# Apple Silicon GPU backend. Objective-C++ glue is built with Apple clang (xcrun),
# and the .metal shaders are compiled at runtime via newLibraryWithSource:, so no
# offline metal toolchain (full Xcode) is required — only the Metal runtime framework.
METAL_CXX ?= xcrun clang++
METAL_OBJ += $(BUILD_DIR)/decode_metal.o
GPU_LIB = $(BUILD_DIR)/metal_code.a
METAL_LDFLAGS = -framework Metal -framework Foundation -framework CoreFoundation -lc++ -lobjc
CPPFLAGS += -DHAVE_METAL=1
MAIN_CC = $(CC)
MAIN_CFLAGS = $(CFLAGS)
else
GPU_LIB = $(BUILD_DIR)/cpu_decoy.a
MAIN_CC = $(CC)
Expand All @@ -68,13 +78,12 @@ endif

ifdef debug
CPPFLAGS += -DDEBUG=1
CFLAGS += -fopenmp
endif

.PHONY: clean distclean test

$(BINARY): $(BUILD_DIR)/main.o $(STATICLIB)
$(CC) $(CFLAGS) $(BUILD_DIR)/main.o $(STATICLIB) $(LDFLAGS) $(CUDA_LDFLAGS) $(ROCM_LDFLAGS) -o $@
$(CC) $(CFLAGS) $(BUILD_DIR)/main.o $(STATICLIB) $(LDFLAGS) $(CUDA_LDFLAGS) $(ROCM_LDFLAGS) $(METAL_LDFLAGS) -o $@

$(STATICLIB): $(OBJ) $(GPU_LIB)
cp $(GPU_LIB) $@
Expand All @@ -89,25 +98,21 @@ $(BUILD_DIR)/misc.o: src/misc.c src/misc.h
$(BUILD_DIR)/error.o: src/error.c include/openfish/openfish_error.h
$(CC) $(CFLAGS) $(CPPFLAGS) $(DEPFLAGS) -c $< -o $@

$(BUILD_DIR)/signal_prep.o: src/signal_prep.c
$(CC) $(CFLAGS) $(CPPFLAGS) $(DEPFLAGS) -c $< -o $@

$(BUILD_DIR)/decode_cpu.o: src/decode_cpu.c
$(CC) $(CFLAGS) $(CPPFLAGS) $(DEPFLAGS) -c $< -o $@

$(BUILD_DIR)/nn_cpu.o: src/nn_cpu.c
$(CC) $(CFLAGS) $(CPPFLAGS) $(DEPFLAGS) -c $< -o $@

$(BUILD_DIR)/beam_search.o: src/beam_search.c
$(CC) $(CFLAGS) $(CPPFLAGS) $(DEPFLAGS) -c $< -o $@

$(BUILD_DIR)/openfish.o: src/openfish.c include/openfish/openfish.h
$(CC) $(CFLAGS) $(CPPFLAGS) $(DEPFLAGS) -c $< -o $@

# cpu decoy
# cpu decoy: an empty static archive that $(STATICLIB) copies as its base before
# adding $(OBJ). BSD/macOS ar cannot create an archive with no members, so write
# the archive magic directly — both GNU and BSD ar accept this as a valid empty archive.
$(BUILD_DIR)/cpu_decoy.a:
rm -f $@
$(AR) -r $@
printf '!<arch>\n' > $@

# cuda
$(BUILD_DIR)/cuda.a: $(CUDA_OBJ)
Expand All @@ -116,18 +121,29 @@ $(BUILD_DIR)/cuda.a: $(CUDA_OBJ)
$(BUILD_DIR)/decode_cuda.o: src/decode_cuda.c
$(NVCC) -x cu $(CUDA_CFLAGS) $(CPPFLAGS) $(DEPFLAGS) -c $< -o $@

$(BUILD_DIR)/nn_cuda.o: src/nn_cuda.c
$(NVCC) -x cu $(CUDA_CFLAGS) $(CPPFLAGS) $(DEPFLAGS) -c $< -o $@

# hip
$(BUILD_DIR)/hip_code.a: $(ROCM_OBJ)
$(HIPCC) $(ROCM_CFLAGS) --emit-static-lib -fPIC --hip-link $^ -o $@

$(BUILD_DIR)/decode_hip.o: src/decode_hip.c
$(HIPCC) -x hip $(ROCM_CFLAGS) $(CPPFLAGS) $(DEPFLAGS) -fPIC -c $< -o $@

$(BUILD_DIR)/nn_hip.o: src/nn_hip.c
$(HIPCC) -x hip $(ROCM_CFLAGS) $(CPPFLAGS) $(DEPFLAGS) -fPIC -c $< -o $@
# metal
$(BUILD_DIR)/metal_code.a: $(METAL_OBJ)
$(AR) rcs $@ $^

# embed the shader source as a C string (compiled at runtime with newLibraryWithSource:).
# openfish_defs.h is prepended so the shader and the host code share one copy of the constants,
# structs and arg blocks (newLibraryWithSource: can't resolve local #includes, so we concatenate
# at build time). each line is wrapped as a C string literal with a trailing \n; adjacent literals concatenate.
$(BUILD_DIR)/openfish_metal_src.h: src/openfish_defs.h src/openfish_metal.metal
printf 'static const char OPENFISH_METAL_SRC[] =\n' > $@
sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/^/"/' -e 's/$$/\\n"/' \
src/openfish_defs.h src/openfish_metal.metal >> $@
printf ';\n' >> $@

$(BUILD_DIR)/decode_metal.o: src/decode_metal.mm src/openfish_defs.h $(BUILD_DIR)/openfish_metal_src.h
$(METAL_CXX) -x objective-c++ -fobjc-arc -std=c++17 $(CFLAGS) $(CPPFLAGS) -I$(BUILD_DIR) $(DEPFLAGS) -c $< -o $@

# pull in auto-generated header dependencies (.d files emitted by -MMD)
-include $(BUILD_DIR)/*.d
Expand Down
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# openfish

*openfish* is a library for CRF-CTC beam-search decoding used in nanopore basecalling. It supports CPU, NVIDIA GPU (CUDA) and AMD GPU (ROCm/HIP).
*openfish* is a library for CRF-CTC beam-search decoding used in nanopore basecalling. It supports CPU, NVIDIA GPU (CUDA), AMD GPU (ROCm/HIP) and Apple Silicon GPU (Metal).

The CPU implementation was adopted from the C++ beam-search implementation in [ONT Dorado](https://github.com/nanoporetech/dorado) (licensed under the [Oxford Nanopore Technologies PLC. Public License Version 1.0](https://github.com/nanoporetech/dorado/blob/release-v0.8/LICENCE.txt)) and re-written in C. GPU backends were then built on top of that C implementation.

Expand All @@ -12,6 +12,7 @@ The CPU implementation was adopted from the C++ beam-search implementation in [O
- [CPU-only](#cpu-only)
- [NVIDIA GPU (CUDA)](#nvidia-gpu-cuda)
- [AMD GPU (ROCm)](#amd-gpu-rocm)
- [Apple Silicon GPU (Metal)](#apple-silicon-gpu-metal)
- [Optional build flags](#optional-build-flags)
- [Usage](#usage)
- [Integrating as a library](#integrating-as-a-library)
Expand Down Expand Up @@ -63,6 +64,22 @@ To target a specific GPU architecture, pass the `hipcc` architecture flag:
make rocm=1 ROCM_ARCH="--offload-arch=gfx90a"
```

### Apple Silicon GPU (Metal)

Requires macOS on Apple Silicon (M-series). Only the **Xcode Command Line Tools** are needed
(`xcode-select --install`) — the full Xcode / offline Metal toolchain is *not* required, because
the shaders in `src/openfish_metal.metal` are compiled at runtime via `newLibraryWithSource:`.

```sh
make metal=1
```

The Objective-C++ glue (`src/decode_metal.mm`) is compiled with Apple clang (`xcrun clang++`) and
linked against the `Metal` and `Foundation` frameworks. All buffers use unified (shared) memory, so
there are no explicit host/device copies. Override the compiler with `METAL_CXX=... make metal=1`.

The CPU-only build (`make`) also runs natively on Apple Silicon.

### Optional build flags

| Flag | Description |
Expand Down Expand Up @@ -94,6 +111,12 @@ gcc [OPTIONS] -I path/to/openfish/include your_program.c \
path/to/openfish/lib/libopenfish.a -lz -lm -lpthread \
-L/opt/rocm/lib -lamdhip64 -lrt -ldl -o your_program

# static linking (Metal build, Apple Silicon)
clang [OPTIONS] -I path/to/openfish/include your_program.c \
path/to/openfish/lib/libopenfish.a -lz -lm -lpthread \
-framework Metal -framework Foundation -framework CoreFoundation \
-lc++ -lobjc -o your_program

```

*path/to/openfish/* is the absolute or relative path to the cloned repository.
Expand Down Expand Up @@ -133,6 +156,18 @@ scripts/gpu_quick_run.sh hac
scripts/gpu_quick_run.sh sup
```

**GPU (Metal, Apple Silicon):**

Like the CUDA/ROCm paths, the Metal backend consumes float16 scores, so `scripts/gpu_quick_run.sh`
works unchanged:

```sh
make metal=1 debug=1
scripts/gpu_quick_run.sh fast
scripts/gpu_quick_run.sh hac
scripts/gpu_quick_run.sh sup
```

We have provided some expected values on several GPUs [here](test/ref_test_vals/)

## Acknowledgements
Expand Down
27 changes: 1 addition & 26 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ free(qstring);

## GPU decoding

Requires a `cuda=1` or `rocm=1` build. Scores must be in device memory and are **float16** for the GPU path (CPU path is float32).
Requires a `cuda=1`, `rocm=1` or `metal=1` build. Scores must be in device memory and are **float16** for the GPU path (CPU path is float32). For the Metal backend a device buffer is an `MTLBuffer` (the `scores_TNC` handle returned by the harness upload helper); on Apple Silicon's unified memory the `gpubuf` result pointers alias the shared buffers directly.

```c
// Allocate persistent GPU working buffers (once per n_timesteps/batch_size/state_len combination)
Expand All @@ -111,28 +111,3 @@ openfish_gpubuf_free(gpubuf);
```

`openfish_gpubuf_size(n_timesteps, batch_size, state_len)` returns the total number of device bytes a `gpubuf` of those dimensions occupies (useful for VRAM accounting before `openfish_gpubuf_init`).

## Rotary embeddings

Used by transformer-based models; applied in place to `x`. The GPU variant requires a `cuda=1` / `rocm=1` build and device pointers.

```c
// CPU
void openfish_rotary_emb_cpu(
void *x, // [batch, seq, heads, head_dim] (modified in place)
const void *sin_buf,
const void *cos_buf,
int batch_size, int seq_len, int n_heads, int head_dim, int rotary_half,
int stride_batch, int stride_seq, int stride_head,
int n_threads
);

// GPU (all pointers are device pointers)
void openfish_rotary_emb_gpu(
void *x, // modified in place
const void *sin_gpu,
const void *cos_gpu,
int batch_size, int seq_len, int n_heads, int head_dim, int rotary_half,
int stride_batch, int stride_seq, int stride_head
);
```
113 changes: 2 additions & 111 deletions include/openfish/openfish.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,40 +46,13 @@ void openfish_decode_cpu(
char **qstring
);

// Rotary position embedding (rotate-half convention).
//
// `sincos_width` is the width of the sin/cos tables: sin_buf and cos_buf MUST be
// laid out as [seq_len, sincos_width]. The kernel rotates 2*sincos_width elements
// of each head, pairing element i with element i+sincos_width:
// out_i = x_i*cos_j - x_{i+sincos_width}*sin_j
// out_{i+sincos_width} = x_i*sin_j + x_{i+sincos_width}*cos_j (j = i, table col)
// so it rotates 2*sincos_width dims (require 2*sincos_width <= head_dim).
//
// NOTE: fused GEMM+rotary kernels elsewhere may instead expect FULL-width tables
// [seq_len, 2*sincos_width] (each column duplicated). Do not confuse the two — the
// table's second dimension must always equal the width the consumer documents.
void openfish_rotary_emb_cpu(
void *x,
const void *sin_buf,
const void *cos_buf,
int batch_size,
int seq_len,
int n_heads,
int head_dim,
int sincos_width, // width of sin/cos tables: [seq_len, sincos_width]; rotates 2*sincos_width dims
int stride_batch,
int stride_seq,
int stride_head,
int n_threads
);

size_t openfish_gpubuf_size(
int n_timesteps,
int batch_size,
int state_len
);

#if defined(HAVE_CUDA) || defined(HAVE_ROCM)
#if defined(HAVE_CUDA) || defined(HAVE_ROCM) || defined(HAVE_METAL)

void openfish_decode_gpu(
int n_timesteps,
Expand All @@ -104,89 +77,7 @@ void openfish_gpubuf_free(
openfish_gpubuf_t *gpubuf
);

// See openfish_rotary_emb_cpu: sin_gpu/cos_gpu are [seq_len, sincos_width] (rotate-half),
// rotating 2*sincos_width dims per head (require 2*sincos_width <= head_dim).
void openfish_rotary_emb_gpu(
void *x,
const void *sin_gpu,
const void *cos_gpu,
int batch_size,
int seq_len,
int n_heads,
int head_dim,
int sincos_width, // width of sin/cos tables: [seq_len, sincos_width]; rotates 2*sincos_width dims
int stride_batch,
int stride_seq,
int stride_head
);

void openfish_flstm_step_gpu(
const void* scratch,
const void* ih_t,
void* cell,
void* hh_next,
int batch_size,
int hidden_dim
);

void openfish_silu_mul_gpu(
const void *in,
void *out,
int n_tokens,
int hidden_dim
);

void openfish_rmsnorm_gpu(
const void* in,
const void* residual,
const void* weight,
void* out,
int n_tokens,
int hidden_dim,
float alpha,
float eps
);

void openfish_rmsnorm_quant_int8_gpu(
const void* in,
const void* weight,
void* residual,
void* residual_scale,
int n_tokens,
int hidden_dim,
float alpha,
float eps
);

void openfish_rmsnorm_quant_fp8_gpu(
const void* in,
const void* weight,
void* residual,
void* residual_scale,
int n_tokens,
int hidden_dim,
float alpha,
float eps
);

void openfish_quant_fp8_gpu(
const void* in, /* f16 [n_tokens, hidden_dim] input */
void* out, /* uint8[n_tokens, hidden_dim] fp8 E4M3FN output */
void* scale, /* f32 [n_tokens] per-token scale output */
int n_tokens,
int hidden_dim
);

void openfish_dequant_fp8_transpose_gpu(
const void* in, /* fp8 [n_timesteps, batch_size, n_channels] in */
void* out, /* f16 [batch_size, n_timesteps, n_channels] out (dequant × scale, transposed) */
int n_timesteps,
int batch_size,
int n_channels,
float scale
);

#endif // defined(HAVE_CUDA) || defined(HAVE_ROCM)
#endif // defined(HAVE_CUDA) || defined(HAVE_ROCM) || defined(HAVE_METAL)

#ifdef __cplusplus
}
Expand Down
Loading
Loading