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
20 changes: 6 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ graph.hard_reset();

- LLM: Gemma-4-E2B-CQ4 (1k-context prefill / decode for 100 tokens)
- VLM: Gemma-4-E2B-CQ4 (256px image encode time / decode)
- Transcribe: Parakeet-TDT-0.6B-CQ4 (20s audio ens-to-end transcribe time)
- Transcribe: Parakeet-TDT-0.6B-CQ4 (20s audio end-to-end transcribe time)
- 1k-Context RAM: peak MB during the LLM benchmark

Command: `cactus benchmark` [optional `--ios` or `--android`]
Expand Down Expand Up @@ -208,8 +208,7 @@ Command: `cactus benchmark` [optional `--ios` or `--android`]
│ │
│ cactus run [model|path] run a model (downloads if needed) │
│ --bits 1|2|3|4 CQ quantization (default: 4) │
│ --weights general|apple weights bundle variant (default: general) │
│ --backend auto|cpu|metal inference backend (default: auto) │
│ --backend cpu|metal inference backend (default: auto) │
│ --image <path> image file for VLM inference │
│ --audio <path> audio file for audio chat │
│ --system <prompt> system prompt │
Expand All @@ -222,20 +221,17 @@ Command: `cactus benchmark` [optional `--ios` or `--android`]
│ --file <audio.wav> audio file to transcribe (WAV) │
│ --language <code> language code (default: en) │
│ --bits 1|2|3|4 CQ quantization (default: 4) │
│ --weights general|apple weights bundle variant (default: general) │
│ --token <token> HuggingFace token (gated models) │
│ --reconvert force local rebuild from source │
│ │
│ cactus download [model] get a bundle (prebuilt, else build) │
│ --bits 1|2|3|4 CQ quantization (default: 4) │
│ --weights general|apple weights bundle variant (default: general) │
│ --token <token> HuggingFace token (gated models) │
│ --reconvert force local rebuild from source │
│ │
│ cactus convert <model> [dir] HuggingFace -> runnable cactus bundle │
│ (CQ weights + runtime graph) │
│ --bits 1|2|3|4 CQ quantization (default: 4) │
│ --weights general|apple weights bundle variant (default: general) │
│ --token <token> HuggingFace token (gated models) │
│ --reconvert force local rebuild from source │
│ --lora <path> merge a LoRA adapter before converting │
Expand All @@ -246,8 +242,7 @@ Command: `cactus benchmark` [optional `--ios` or `--android`]
│ --host <addr> bind address (default: 127.0.0.1) │
│ --port <port> port (default: 8080) │
│ --bits 1|2|3|4 CQ quantization (default: 4) │
│ --weights general|apple weights bundle variant (default: general) │
│ --backend auto|cpu|metal inference backend (default: auto) │
│ --backend cpu|metal inference backend (default: auto) │
│ --token <token> HuggingFace token (gated models) │
│ --reconvert force local rebuild from source │
│ --no-cloud-handoff disable automatic cloud handoff │
Expand All @@ -257,8 +252,7 @@ Command: `cactus benchmark` [optional `--ios` or `--android`]
│ cactus code run the AI coding agent (TUI / print) │
│ --serve-model <id> auto-start a server with this model │
│ --bits 1|2|3|4 CQ quantization (default: 4) │
│ --weights general|apple weights bundle variant (default: general) │
│ --backend auto|cpu|metal inference backend (default: auto) │
│ --backend cpu|metal inference backend (default: auto) │
│ --token <token> HuggingFace token (gated models) │
│ --reconvert force local rebuild from source │
│ --host <addr> server address (default: 127.0.0.1) │
Expand All @@ -282,8 +276,7 @@ Command: `cactus benchmark` [optional `--ios` or `--android`]
│ --model <hf-id> default: google/gemma-4-E2B-it │
│ --transcription-model <hf-id> default: nvidia/parakeet-tdt-0.6b-v3 │
│ --bits 1|2|3|4 CQ quantization (default: 4) │
│ --weights general|apple weights bundle variant (default: general) │
│ --backend auto|cpu|metal inference backend (default: auto) │
│ --backend cpu|metal inference backend (default: auto) │
│ --token <token> HuggingFace token (gated models) │
│ --reconvert force local rebuild of test models │
│ --suite <name> run a single test suite by name │
Expand All @@ -298,8 +291,7 @@ Command: `cactus benchmark` [optional `--ios` or `--android`]
│ --model <hf-id> default: google/gemma-4-E2B-it │
│ --transcription-model <hf-id> default: nvidia/parakeet-tdt-0.6b-v3 │
│ --bits 1|2|3|4 CQ quantization (default: 4) │
│ --weights general|apple weights bundle variant (default: general) │
│ --backend auto|cpu|metal inference backend (default: auto) │
│ --backend cpu|metal inference backend (default: auto) │
│ --ios run on connected iPhone │
│ --android run on connected Android │
│ │
Expand Down
2 changes: 1 addition & 1 deletion apple/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ if(BUILD_SHARED_LIBS)
)

target_link_libraries(cactus_framework PUBLIC
"-framework Accelerate" "-framework CoreML" "-framework Foundation" "-framework Metal" "-framework MetalPerformanceShaders"
"-framework Accelerate" "-framework Foundation" "-framework Metal" "-framework MetalPerformanceShaders"
"-framework Security" "-framework SystemConfiguration" "-framework CFNetwork"
)

Expand Down
3 changes: 2 additions & 1 deletion bindings/rust/cactus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ pub type CactusTokenCallback = Option<unsafe extern "C" fn(token: *const c_char,
pub type CactusLogCallback = Option<unsafe extern "C" fn(level: c_int, component: *const c_char, message: *const c_char, user_data: *mut c_void)>;

#[cfg_attr(target_os = "macos", link(name = "Accelerate", kind = "framework"))]
#[cfg_attr(target_os = "macos", link(name = "CoreML", kind = "framework"))]
#[cfg_attr(target_os = "macos", link(name = "Metal", kind = "framework"))]
#[cfg_attr(target_os = "macos", link(name = "MetalPerformanceShaders", kind = "framework"))]
#[cfg_attr(target_os = "macos", link(name = "Foundation", kind = "framework"))]
#[cfg_attr(target_os = "macos", link(name = "Security", kind = "framework"))]
#[cfg_attr(target_os = "macos", link(name = "SystemConfiguration", kind = "framework"))]
Expand Down
6 changes: 3 additions & 3 deletions blog/gemma4.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ This matters because latency compounds. A pipeline that transcribes audio, then

## Performance

Cactus targets ARM across platforms: Apple Silicon Macs, iPhones, iPads, Vision Pro, and Android devices with ARM64 chipsets (Snapdragon, Dimensity, Tensor). On Apple hardware we additionally leverage the Neural Engine for vision and audio encoders, while on Android and Linux the runtime uses NEON, i8mm, and dot-product intrinsics.
Cactus targets ARM across platforms: Apple Silicon Macs, iPhones, iPads, Vision Pro, and Android devices with ARM64 chipsets (Snapdragon, Dimensity, Tensor). On Apple hardware we additionally leverage Metal GPU acceleration, while on Android and Linux the runtime uses NEON, i8mm, and dot-product intrinsics.

| Metric (M5 Mac/iPad/Vision Pro) | E2B |
|---|---|
| 4096-token prefill | 660 tok/s |
| 1024-token decode | 40 tok/s |
| 30s audio end-to-end | 0.3s |
| Image encode (ANE) | 0.7s |
| Image encode | 0.7s |

40 tok/s decode is faster than most people read. You're not waiting for the model. The model is waiting for you.

Expand Down Expand Up @@ -128,7 +128,7 @@ Cactus supports React Native, Flutter, Swift, Kotlin, Python, Rust, and C++. Pic
|---|---|---|
| React Native | [React Native bindings](/bindings/react-native/) | Native bridge over the C API |
| Flutter | [cactus-flutter](/bindings/flutter/) | Dart FFI bindings |
| Swift (iOS/macOS) | [cactus-swift](/bindings/swift/) | XCFramework with NPU support |
| Swift (iOS/macOS) | [cactus-swift](/bindings/swift/) | XCFramework with Metal support |
| Kotlin (Android) | [cactus-kotlin](/bindings/kotlin/) | JNI + Kotlin Multiplatform |
| Python | [cactus-compute](/python/) | `pip install cactus-compute` |
| Rust | [Rust bindings](/bindings/rust/) | Copy `cactus.rs`, link `libcactus_engine.a` |
Expand Down
2 changes: 1 addition & 1 deletion blog/hybrid_transcription.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ cactus transcribe --file path/to/audio.wav
## Built for the Edge, from the Ground Up
Achieving this required relying on our industry-leading on-device engine. We built Cactus from the ground up exactly for this.

Because the engine sits directly on the metal, it is optimized for edge constraints. It targets ARM-based CPUs and NPUs natively, resulting in super low RAM usage (e.g. Moonshine transcription for example, runs with sub-10MB RAM utilization). This is a critical factor for mobile and wearable devices where memory is strictly rationed by the OS.
Because the engine sits directly on the metal, it is optimized for edge constraints. It targets ARM-based CPUs natively, resulting in super low RAM usage (e.g. Moonshine transcription for example, runs with sub-10MB RAM utilization). This is a critical factor for mobile and wearable devices where memory is strictly rationed by the OS.

By keeping the footprint minimal, the engine operates silently in the background without draining battery or causing OS-level memory warnings, making it viable for continuous, ambient listening.

Expand Down
14 changes: 7 additions & 7 deletions blog/lfm2.5_350m.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ most people read at roughly 4-5 words per second, so even the Raspberry Pi's 30
Second, the RAM gap between Apple and Android: 56-85 MB versus 300-330 MB for the same 355 MB model.
That gap is not a bug. It's a direct consequence of how the two platforms handle memory-mapped files, which we'll get into in the Cactus format section below.

## Why CPU, Not GPU or NPU
## Why CPU, Not GPU or Dedicated Accelerators

It is tempting to reach for the GPU. On paper, mobile GPUs have 10-50x the FLOPS of a single CPU core.
But inference on a 350M-parameter model is not a FLOPS problem, it's a memory bandwidth problem.
Expand All @@ -55,17 +55,17 @@ That's two separate shader codepaths to maintain, with different memory models,
And GPU inference holds a persistent GPU context, which competes with the UI rendering pipeline and drains battery even when the inference workload is light.
GPUs are also energy-inefficient. On Macs and PCs this barely matters since they're plugged into a power source most of the time, and GPU inference works well there. But mobile devices run on battery, and every watt spent on GPU inference cuts directly into the user's runtime. The GPU path that works fine on a MacBook becomes a battery killer on a phone.

NPU is a more interesting target. Apple's Neural Engine and Qualcomm's Hexagon DSP both offer high-throughput inference at good power efficiency.
The problem is access. Apple's Neural Engine requires CoreML model conversion, which imposes its own quantization and graph constraints.
Dedicated AI accelerators are a more interesting target. Apple's Neural Engine and Qualcomm's Hexagon DSP both offer high-throughput inference at good power efficiency.
The problem is access. Apple's Neural Engine requires conversion into a proprietary model format, which imposes its own quantization and graph constraints.
You don't control the execution schedule, the memory layout, or the precision semantics.
The quantisation techniques available through CoreML severely lag the state-of-the-art methods used by GGML and Cactus.
The quantisation techniques available through that toolchain severely lag the state-of-the-art methods used by GGML and Cactus.
Qualcomm's QNN SDK has similar restrictions (limited quantisation scheme support) and not all Android devices ship Qualcomm chips.
Both lock you into a vendor-specific format that may not support your model architecture at all, and neither works on budget devices that lack the NPU entirely.
Both lock you into a vendor-specific format that may not support your model architecture at all, and neither works on budget devices that lack the accelerator entirely.

CPU is the universal target. Every ARM device, flagship, budget, wearable, Raspberry Pi, and most recent ones support DOTPROD or I8MM extensions.
A single well-optimized CPU codepath works everywhere, the main challenge is energy-inefficiency compared to NPU.
A single well-optimized CPU codepath works everywhere, the main challenge is energy-inefficiency compared to dedicated accelerators.

ARM is also closing this gap. The progression from NEON to DOTPROD to I8MM to SME2 reflects a deliberate push to bring matrix-processing efficiency onto the CPU itself. Each generation narrows the energy and throughput gap with dedicated NPUs. SME2, landing on upcoming ARMv9.2-A cores, adds streaming matrix operations that approach NPU-class efficiency for INT8 workloads while remaining fully programmable. The bet is that general-purpose CPU silicon will continue absorbing the capabilities that once justified a separate accelerator.
ARM is also closing this gap. The progression from NEON to DOTPROD to I8MM to SME2 reflects a deliberate push to bring matrix-processing efficiency onto the CPU itself. Each generation narrows the energy and throughput gap with dedicated accelerators. SME2, landing on upcoming ARMv9.2-A cores, adds streaming matrix operations that approach accelerator-class efficiency for INT8 workloads while remaining fully programmable. The bet is that general-purpose CPU silicon will continue absorbing the capabilities that once justified a separate accelerator.

## Why Single Core for Decode

Expand Down
34 changes: 0 additions & 34 deletions cactus-code/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 2 additions & 11 deletions cactus-engine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ set(ENGINE_SOURCES
src/constraints.cpp
src/model.cpp
src/kv_compress.cpp
src/model_npu.cpp
src/engine_image.cpp
src/index.cpp
src/index_ffi.cpp
Expand All @@ -32,14 +31,6 @@ set(ENGINE_SOURCES
src/stream.cpp
src/tokenizer.cpp
)
if(APPLE)
enable_language(OBJCXX)
list(APPEND ENGINE_SOURCES src/npu_ane.mm)
set_source_files_properties(src/npu_ane.mm PROPERTIES COMPILE_FLAGS "-fobjc-arc")
else()
list(APPEND ENGINE_SOURCES src/npu.cpp)
endif()

add_library(cactus_engine STATIC ${ENGINE_SOURCES})
set_target_properties(cactus_engine PROPERTIES
POSITION_INDEPENDENT_CODE ON
Expand All @@ -56,7 +47,7 @@ target_link_libraries(cactus_engine PUBLIC cactus_graph)

if(APPLE)
target_link_libraries(cactus_engine PUBLIC
"-framework CoreML" "-framework Foundation" "-framework Metal" "-framework MetalPerformanceShaders"
"-framework Foundation" "-framework Metal" "-framework MetalPerformanceShaders"
"-framework Security" "-framework SystemConfiguration" "-framework CFNetwork"
)

Expand Down Expand Up @@ -156,7 +147,7 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
target_link_libraries(cactus_engine_shared PRIVATE "${_curl}")
endif()
target_link_libraries(cactus_engine_shared PRIVATE
"-framework Accelerate" "-framework CoreML" "-framework Foundation" "-framework Metal" "-framework MetalPerformanceShaders"
"-framework Accelerate" "-framework Foundation" "-framework Metal" "-framework MetalPerformanceShaders"
"-framework Security" "-framework SystemConfiguration" "-framework CFNetwork"
)
else()
Expand Down
11 changes: 11 additions & 0 deletions cactus-engine/cactus_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ CACTUS_FFI_EXPORT int cactus_transcribe(
size_t pcm_buffer_size
);

CACTUS_FFI_EXPORT int cactus_preprocess_audio_features(
const char* audio_file_path,
const char* model_type,
size_t mel_bins,
float* features_buffer,
size_t buffer_size,
size_t* feature_count,
size_t* out_mel_bins,
size_t* out_frames
);

CACTUS_FFI_EXPORT cactus_stream_transcribe_t cactus_stream_transcribe_start(
cactus_model_t model,
const char* options_json
Expand Down
Loading
Loading