Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
a7f75f1
Removed existing transpiler
ParkiratS Jun 24, 2026
a9fcf3f
Initial converter progress
ParkiratS Jun 24, 2026
cfaa176
Initial converter complete: convers text-to-text model graphs to JSON
ParkiratS Jun 25, 2026
09cb668
Inital IR simplification logic
ParkiratS Jun 26, 2026
f8b7ae3
Completed computation graph object
ParkiratS Jun 30, 2026
7a5c750
Added a new field to ValueNodes to keep track of value users
ParkiratS Jun 30, 2026
c23a00c
improving ir
ParkiratS Jul 7, 2026
88d9446
Updates to fusions base object
ParkiratS Jul 8, 2026
ecdfdaf
Fusion matching logic + attributions cleaning
ParkiratS Jul 8, 2026
5d66cdd
Updated fusion matching logic
ParkiratS Jul 8, 2026
e73b35c
Added FusionResult object for fusions
ParkiratS Jul 8, 2026
6b2069b
Added final fused graph clss
ParkiratS Jul 8, 2026
ea0207a
Added simplified graph representation + topo sort on simplified graph
ParkiratS Jul 8, 2026
c9876a8
DAG based fusion: Initial models refactoring
ParkiratS Jul 9, 2026
e6baa92
Added matchers which will check for synthetic graph and main graph eq…
ParkiratS Jul 9, 2026
6961ed4
Reset architecture to support harder fusions
ParkiratS Jul 9, 2026
edd39a5
Changes to transpiler converter
ParkiratS Jul 10, 2026
a4c7af6
Reset
ParkiratS Jul 10, 2026
802b326
File restructure
ParkiratS Jul 12, 2026
386ff84
Folder restructuring
ParkiratS Jul 12, 2026
a74d3fa
Model profiles
ParkiratS Jul 13, 2026
4d0e6a6
Added additional profiles
ParkiratS Jul 13, 2026
d54bda5
Added qwen2.5-0.5b profile
ParkiratS Jul 14, 2026
7fd1cfc
Added converter logic
ParkiratS Jul 14, 2026
3333bb1
Removed redundant objects and refactored class structure
ParkiratS Jul 17, 2026
a6311c5
Removed redundant Files class + modified existing model profiles
ParkiratS Jul 17, 2026
391b6fd
Added supported modalities to model profiles class
ParkiratS Jul 17, 2026
5898e7e
Progress to convert
ParkiratS Jul 17, 2026
8d86c94
Load files function
ParkiratS Jul 19, 2026
a0530e1
Load files function
ParkiratS Jul 19, 2026
1b6f44d
Added input loading utilities
ParkiratS Jul 20, 2026
c0ff8a0
Brute-force code for converted jsons
ParkiratS Jul 20, 2026
26e53ba
Modifications to ModelProfiles classes to add additional cache and in…
ParkiratS Jul 21, 2026
21fa95a
Streamlining convertert logic + code; still need to clean up
ParkiratS Jul 21, 2026
c886f1c
Cleaned out convert.py
ParkiratS Jul 21, 2026
0bf9dc7
Reorganizing + optimizing converter
ParkiratS Jul 22, 2026
af0797f
Updated required files for Gemma
ParkiratS Jul 22, 2026
052a2ba
Converter reorganization
ParkiratS Jul 23, 2026
198f46c
Code refactoring
ParkiratS Jul 23, 2026
1c4a1f0
Converter input_utils removal + input loading code simplification
ParkiratS Jul 23, 2026
72e6874
Removed unecessary code
ParkiratS Jul 23, 2026
de8514e
Added fusion patterns
ParkiratS Jul 24, 2026
8852af1
Initial graph classes
ParkiratS Jul 24, 2026
04b9057
Added fusion results object
ParkiratS Jul 25, 2026
9c7ed82
Future instructions
ParkiratS Jul 25, 2026
46ecc0d
Matching nodes
ParkiratS Jul 27, 2026
338b5a5
Converter testing
ParkiratS Jul 27, 2026
1e5297b
Simplification of logic
ParkiratS Jul 28, 2026
53c759e
Added graph matchers
ParkiratS Jul 28, 2026
344edc9
Simplifications to fusion matcher functions
ParkiratS Jul 28, 2026
aa231af
Added fusion definition matchers
ParkiratS Jul 28, 2026
841bdae
Added extra matchers for extraneous constraints for fusions
ParkiratS Jul 28, 2026
610d3b4
Additional extra matchers for RoPE, linear projs, and LSTMs (yet to b…
ParkiratS Jul 28, 2026
80243d3
Minor modifications to converter and input processing to support MOE …
ParkiratS Jul 28, 2026
8aa7672
Additional fusions + nodes for MOE ops
ParkiratS Jul 28, 2026
ae567c6
Added LFM MOE model profile
ParkiratS Jul 28, 2026
0cdd3d6
Additional matchers for lfm MOE
ParkiratS Jul 28, 2026
9083766
LFM with cache (prefill + decode)
ParkiratS Jul 28, 2026
f97178c
Added simplification of IR step (uses fusion matchers to determine fu…
ParkiratS Jul 29, 2026
3a51ea5
Added simplification of IR step (uses fusion matchers to determine fu…
ParkiratS Jul 29, 2026
6db22b2
Added cactus ops lowering with generator
ParkiratS Jul 29, 2026
ddcbcc8
Further LFM support
ParkiratS Jul 29, 2026
fb5963e
Additional conv-state matching logic
ParkiratS Jul 29, 2026
1d1837a
LFM convs
ParkiratS Jul 29, 2026
e07c158
Added runtime plan
ParkiratS Jul 29, 2026
3b0b9cf
New run operations with runtime plans
ParkiratS Jul 29, 2026
aa06bf6
New run operations with runtime plans
ParkiratS Jul 29, 2026
b65b667
Performance upgrades
ParkiratS Jul 30, 2026
e78aeca
performance upgrades
ParkiratS Jul 30, 2026
909da19
Performace upgrades
ParkiratS Jul 31, 2026
4c6733b
Code reformatting
ParkiratS Jul 31, 2026
fb6176c
Performance upgrades
ParkiratS Aug 3, 2026
1d90b40
Working main models
ParkiratS Aug 5, 2026
449363f
Memory optmizations
ParkiratS Aug 5, 2026
9a6b4dc
Working optmized transpiler and runtime + advancements to shared kv c…
ParkiratS Aug 5, 2026
e4f0d3e
code reduction
ParkiratS Aug 5, 2026
ee89a96
Kernel optmizations
ParkiratS Aug 6, 2026
ba1e98b
Code reduction
ParkiratS Aug 6, 2026
1ac9d72
Merge main into custom-transpiler
ParkiratS Aug 6, 2026
2b95286
Fix chunk retargeting of attention broadcasts
ParkiratS Aug 6, 2026
9e69e03
Optimize contiguous last-axis padding
ParkiratS Aug 6, 2026
dc1a58d
Tune CQ GEMV decode scheduling
ParkiratS Aug 6, 2026
4abf717
Code simplifications
ParkiratS Aug 6, 2026
e9773b1
Code cleanup
ParkiratS Aug 8, 2026
8226cfe
Visual chagnes
ParkiratS Aug 8, 2026
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
*.dylib
*.bin
*.cg
*.json
*.cactus
*.weights

!cactus-engine/libs/curl/**/*.a
!android/mbedtls/**/*.a
Expand All @@ -29,4 +32,6 @@ python/cactus/bindings/lib/
site/
site_docs/

TODO.md
explain.md
python/cactus/code/
32 changes: 13 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,16 @@ A hybrid edge-cloud AI engine for mobile devices & wearables.
└─────────────────┘
│
┌─────────────────┐
│ Cactus Graph │ ←── Zero-copy computation graph
│ Cactus Graph │ ←── Zero-copy computation graph
└─────────────────┘
│
┌─────────────────┐
│ Cactus Kernels │ ←── CPU/GPU kernels for (Apple, Samsung, Pixel, etc.)
└─────────────────┘
│
┌─────────────────┐
│ Cactus Quants │ ←── Custom rotation-based quantization technique
│ Cactus Quants │ ←── Custom rotation-based quantization technique
└─────────────────┘
│
┌─────────────────┐
│Cactus Transpiler│ ←── Transpiles custom PyTorch model to Cactus.
└─────────────────┘
```

## Quick Demo (Mac)
Expand Down Expand Up @@ -126,7 +122,7 @@ graph.hard_reset();
- VLM: Gemma-4-E2B-CQ4 (256px image encode time / decode)
- Transcribe: Parakeet-TDT-0.6B-CQ4 (20s audio end-to-end transcribe time)
- 1k-Context RAM: peak MB during the LLM benchmark
- No speculative decode or MTP, pure decode
- No speculative decode or MTP, pure decode

Command: `cactus benchmark` [optional `--ios` or `--android`]

Expand All @@ -141,14 +137,14 @@ Command: `cactus benchmark` [optional `--ios` or `--android`]

N/B: With 1k-context prefill and decode for 100 runs on M5 Max
- `LFM2.5-VL-1.6B` = 289toks/sec
- `Qwen3-1.7B` = 155toks/sec
- `Qwen3-1.7B` = 155toks/sec
- `LFM2.5-VL-450m` = 472toks/sec, image encodes in 43ms
- `LFM22.5-VL-230m` = 555toks/sec

## Output Quality
## Output Quality

- Gemma-4-E2B-it accuracy across bit widths, averaged over 3 seeds.
- CQ3.26 and CQ2.54 are mixed-precision, CQ2/CQ3/CQ4 are uniformly quantized.
- Gemma-4-E2B-it accuracy across bit widths, averaged over 3 seeds.
- CQ3.26 and CQ2.54 are mixed-precision, CQ2/CQ3/CQ4 are uniformly quantized.
- Full results in [docs/cactus_quants.md](/docs/cactus_quants.md):

| Task | F16 (Original) | CQ4 | CQ3.26 | CQ2.54 | CQ2 |
Expand All @@ -168,10 +164,10 @@ N/B: With 1k-context prefill and decode for 100 runs on M5 Max

## Supported Models

- Any HuggingFace model can be converted using `cactus convert [HF-Name]`, though experimental.
- Liquid, Gemma. whisper. parakeet and Qwen model families are especially tested.
- Any HuggingFace model can be converted using `cactus convert [HF-Name]`, though experimental.
- Liquid, Gemma. whisper. parakeet and Qwen model families are especially tested.
- Some models have been pre-uploaded [here](https://huggingface.co/Cactus-Compute), just run `cactus download [HF-Name]`.
- `cactus run [HF-Name]` albeit first downloads or convert the model if not found.
- `cactus run [HF-Name]` albeit first downloads or convert the model if not found.

## Needle

Expand All @@ -190,7 +186,6 @@ cactus run Cactus-Compute/needle [--tools my_tools.json] # OpenAI function-call
| [Cactus Kernels](/docs/cactus_kernels.md) | C++ | ARM NEON SIMD kernels for matmul, attention, convolution, quantization, DSP, image processing |
| [Cactus Quants](/docs/cactus_quants.md) | C++ | Rotation-and-codebook quantization from 4-bit to 1-bit for all weight tensors |
| [Cactus Hybrid](/docs/cactus_hybrid.md) | C/Python | Route hard queries to the cloud automatically based on local model confidence |
| [Cactus Transpiler](/docs/cactus_transpiler.md) | Python | Convert any PyTorch model to a Cactus runtime graph for on-device inference |
| [Python Package](/python/) | Python | Python package and CLI |

## Bindings
Expand Down Expand Up @@ -244,13 +239,12 @@ cactus run Cactus-Compute/needle [--tools my_tools.json] # OpenAI function-call
│ cactus download [model] get a bundle (prebuilt, else build) │
│ --bits 1|2|3|4|2.54|3.26 CQ quantization (default: 4) │
│ --token <token> HuggingFace token (gated models) │
│ --reconvert force local rebuild from source │
│ --reconvert refresh cached bundle │
│ │
│ cactus convert <model> [dir] HuggingFace -> runnable cactus bundle │
│ (CQ weights + runtime graph) │
│ cactus convert <model> [dir] HuggingFace -> Cactus CQ weights │
│ --bits 1|2|3|4 CQ quantization (default: 4) │
│ --token <token> HuggingFace token (gated models) │
│ --reconvert force local rebuild from source │
│ --reconvert force weight conversion from source │
│ --lora <path> merge a LoRA adapter before converting │
│ --weights-only stop after CQ weights (skip the graph) │
│ --artifact-dir <path> bundle output (default: weights/<model>) │
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 Foundation" "-framework Metal" "-framework MetalPerformanceShaders"
"-framework Accelerate" "-framework CoreML" "-framework Foundation" "-framework Metal" "-framework MetalPerformanceShaders"
"-framework Security" "-framework SystemConfiguration" "-framework CFNetwork"
)

Expand Down
1 change: 1 addition & 0 deletions bindings/rust/cactus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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"))]
Expand Down
4 changes: 2 additions & 2 deletions blog/lfm2.5_350m.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ 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.

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.
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 that toolchain severely lag the state-of-the-art methods used by GGML and Cactus.
Expand Down Expand Up @@ -201,4 +201,4 @@ from a Vision Pro to a Raspberry Pi 5, with no special casing and no device-spec
- [Cactus Engine API Reference](/docs/cactus_engine.md) - Full C API docs for completion, tool calling, and cloud handoff
- [LFM2-24B-A2B](/blog/lfm2_24b_a2b.md) - Running LFM2 24B MoE locally on Mac for coding use cases
- [Hybrid Transcription](/blog/hybrid_transcription.md) - On-device/cloud hybrid speech transcription with Cactus
- [Ridiculously Fast Transcription](/blog/parakeet.md) - 6M tok/sec decode speed with Parakeet CTC 1.1B
- [Ridiculously Fast Transcription](/blog/parakeet.md) - 6M tok/sec decode speed with Parakeet CTC 1.1B
13 changes: 11 additions & 2 deletions cactus-engine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ set(ENGINE_SOURCES
src/sp.cpp
src/constraints.cpp
src/model.cpp
src/model_npu.cpp
src/kv_compress.cpp
src/engine_image.cpp
src/index.cpp
Expand All @@ -31,6 +32,13 @@ 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 @@ -47,7 +55,7 @@ target_link_libraries(cactus_engine PUBLIC cactus_graph)

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

Expand Down Expand Up @@ -134,6 +142,7 @@ if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
add_library(cactus_engine_shared SHARED ${CMAKE_BINARY_DIR}/cactus_engine_shared_stub.cpp)
set_target_properties(cactus_engine_shared PROPERTIES OUTPUT_NAME cactus_engine)
add_dependencies(cactus_engine_shared ${_CACTUS_ENGINE_DEPS})
set_property(TARGET cactus_engine_shared APPEND PROPERTY LINK_DEPENDS ${_CACTUS_ENGINE_ARCHIVES})
target_link_libraries(cactus_engine_shared PRIVATE cactus_flags)

if(APPLE)
Expand All @@ -147,7 +156,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 Foundation" "-framework Metal" "-framework MetalPerformanceShaders"
"-framework Accelerate" "-framework CoreML" "-framework Foundation" "-framework Metal" "-framework MetalPerformanceShaders"
"-framework Security" "-framework SystemConfiguration" "-framework CFNetwork"
)
else()
Expand Down
43 changes: 42 additions & 1 deletion cactus-engine/src/complete.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,13 @@ std::vector<std::vector<uint32_t>> build_stop_sequences(
stop_token_sequences.push_back(tokenizer->encode(stop_seq));
}

if (!tokenizer->has_chat_template()) {
stop_token_sequences.push_back(tokenizer->encode("\nUser:"));
stop_token_sequences.push_back(tokenizer->encode("\nAssistant:"));
stop_token_sequences.push_back(tokenizer->encode("\nUser"));
stop_token_sequences.push_back(tokenizer->encode("\nAssistant"));
}

if (model_type == Config::ModelType::GEMMA4) {
stop_token_sequences.push_back(tokenizer->encode("<turn|>"));
if (has_tools) {
Expand Down Expand Up @@ -435,6 +442,7 @@ struct PreparedPrompt {
std::vector<uint32_t> tokens;
size_t context_token_count = 0;
std::vector<std::vector<CactusModelHandle::ProcessedImage>> images;
bool force_session_reset = false;

std::vector<std::vector<float>> audio_features;
size_t audio_num_frames = 0;
Expand Down Expand Up @@ -575,7 +583,7 @@ PreparedPrompt prepare_prompt(
}
}

if (prompt.model_type == Config::ModelType::GEMMA4) {
if (handle->model->audio_preprocess_strategy() == "gemma4") {
std::vector<size_t> user_indices;
for (size_t i = 0; i < prompt.messages.size(); i++) {
if (prompt.messages[i].role == "user") user_indices.push_back(i);
Expand Down Expand Up @@ -629,6 +637,23 @@ PreparedPrompt prepare_prompt(
setup_tool_constraints(handle, prompt.tools, prompt.options.force_tools, prompt.options.temperature);
}

if (tokenizer->uses_default_chat_style(prompt.messages, formatted_tools, prompt.options.enable_thinking_if_supported)) {
auto last_user = std::find_if(prompt.messages.rbegin(), prompt.messages.rend(), [](const ChatMessage& msg) {
return msg.role == "user";
});
if (last_user != prompt.messages.rend()) {
std::vector<ChatMessage> latest_turn;
for (const auto& msg : prompt.messages) {
if (msg.role == "system" || msg.role == "developer") {
latest_turn.push_back(msg);
}
}
latest_turn.push_back(*last_user);
prompt.messages = std::move(latest_turn);
prompt.force_session_reset = true;
}
}

prompt.rendered = tokenizer->format_chat_prompt(
prompt.messages,
add_generation_prompt,
Expand Down Expand Up @@ -827,6 +852,15 @@ int cactus_complete(
handle_error_response(err, response_buffer, buffer_size);
return -1;
}
const auto runtime_model_type = handle->model->get_config().model_type;
if (runtime_model_type == Config::ModelType::WHISPER ||
runtime_model_type == Config::ModelType::PARAKEET ||
runtime_model_type == Config::ModelType::PARAKEET_TDT) {
const std::string err = "Model is speech-to-text; use `cactus transcribe` instead of `cactus run`";
CACTUS_LOG_ERROR("complete", err);
handle_error_response(err, response_buffer, buffer_size);
return -1;
}
auto* tokenizer = handle->model->get_tokenizer();
auto prompt = prepare_prompt(handle, messages_json, options_json, tools_json, true, true, pcm_buffer, pcm_buffer_size);
if (prompt.options.sample_seed != 0) {
Expand Down Expand Up @@ -956,6 +990,10 @@ int cactus_complete(
uint32_t next_token;
size_t prompt_tokens;

if (prompt.force_session_reset && !handle->processed_tokens.empty()) {
reset_cache(handle);
}

if (has_audio && !handle->processed_tokens.empty()) {
auto& cache = handle->processed_tokens;
size_t common = 0;
Expand All @@ -979,6 +1017,9 @@ int cactus_complete(
if (!first_token_from_prefill) {
auto prefill_result = do_prefill(handle, prompt, prompt.tokens);
prompt_tokens = prefill_result.prefilled_count + prefill_result.remaining_tokens.size();
if (handle->model->repetition_penalty_uses_context()) {
handle->model->seed_token_history_from_context(prompt.tokens);
}
next_token = generate_first_token(handle, prefill_result, prompt, &first_token_entropy);
}

Expand Down
Loading
Loading