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
134 changes: 134 additions & 0 deletions .github/workflows/build-turboquant-macos.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
name: Build & Release TurboQuant (macOS ARM64)

on:
workflow_dispatch:
push:
branches:
- feature/turboquant-kv-cache
paths:
- '.github/workflows/build-turboquant-macos.yml'
- '**/CMakeLists.txt'
- '**/*.h'
- '**/*.hpp'
- '**/*.c'
- '**/*.cpp'
- '**/*.metal'

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true

env:
LLAMA_LOG_COLORS: 1
LLAMA_LOG_PREFIX: 1
LLAMA_LOG_TIMESTAMPS: 1

jobs:
macOS-arm64-metal:
runs-on: macos-latest
permissions:
contents: write

steps:
- name: Clone
uses: actions/checkout@v6

- name: Set version tag
id: version
run: |
SHORT_SHA=$(git rev-parse --short HEAD)
TAG="turboquant-macos-arm64-${SHORT_SHA}"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"

- name: Build
id: cmake_build
run: |
sysctl -a
cmake -B build \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DLLAMA_CURL=OFF \
-DLLAMA_OPENSSL=OFF \
-DGGML_METAL=ON \
-DGGML_METAL_USE_BF16=ON \
-DGGML_METAL_EMBED_LIBRARY=ON \
-DLLAMA_BUILD_SERVER=ON \
-DLLAMA_BUILD_TOOLS=ON \
-DLLAMA_BUILD_TESTS=OFF \
-DLLAMA_BUILD_EXAMPLES=OFF
cmake --build build --config Release -j $(sysctl -n hw.ncpu)

- name: Verify turbo3 support
run: |
./build/bin/llama-server --help 2>&1 | grep -A2 "cache-type-k" || true
echo "---"
file ./build/bin/llama-server
./build/bin/llama-server --version 2>&1 || true

- name: Verify static binary
run: |
echo "=== All dynamic dependencies (should be system-only) ==="
otool -L build/bin/llama-server
echo "---"
echo "=== Checking for non-system dylibs (should be empty) ==="
if otool -L build/bin/llama-server | grep -vE '/usr/lib|/System|llama-server' | grep '\.dylib'; then
echo "ERROR: Found non-system dynamic dependency!"
exit 1
fi
echo "OK: Only system dylibs — fully self-contained binary"
echo "---"
echo "=== Binary size ==="
ls -lh build/bin/llama-server

- name: Prepare release archive
run: |
mkdir -p release/build/bin
cp build/bin/llama-server release/build/bin/
cp build/bin/llama-cli release/build/bin/ 2>/dev/null || true
cp build/bin/llama-bench release/build/bin/ 2>/dev/null || true
cp build/bin/llama-perplexity release/build/bin/ 2>/dev/null || true
find build -name "*.dylib" -exec cp {} release/build/bin/ \; 2>/dev/null || true
find build -name "*.metal" -path "*/bin/*" -exec cp {} release/build/bin/ \; 2>/dev/null || true
cd release
tar -czf ../llama-turboquant-macos-arm64.tar.gz .
cd ..
ls -lh llama-turboquant-macos-arm64.tar.gz

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: llama-turboquant-macos-arm64
path: release/
retention-days: 30

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag }}
target_commitish: ${{ github.sha }}
name: "TurboQuant macOS ARM64 (${{ steps.version.outputs.short_sha }})"
body: |
## TurboQuant KV Cache — macOS ARM64 (Metal)

Built from `feature/turboquant-kv-cache` branch at commit `${{ steps.version.outputs.short_sha }}`.

### What's included
- `llama-server` with `--cache-type-k turbo3` / `turbo4` support
- `llama-cli`, `llama-bench`, `llama-perplexity`
- Metal backend with BF16 + embedded shader library

### Usage
```bash
tar -xzf llama-turboquant-macos-arm64.tar.gz
./build/bin/llama-server -m model.gguf --cache-type-k turbo3 --cache-type-v turbo3
```

### For Atomic Chat integration
Replace the binary at:
```
~/Library/Application Support/Atomic Chat/data/llamacpp/backends/<version>/macos-arm64/build/bin/llama-server
```
files: llama-turboquant-macos-arm64.tar.gz
draft: false
prerelease: true
23 changes: 12 additions & 11 deletions common/reasoning-budget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,11 @@ static void common_reasoning_budget_accept(struct llama_sampler * smpl, llama_to
break;
}
case REASONING_BUDGET_FORCING:
// force_pos is advanced in apply(), not here.
// This ensures the first forced token isn't skipped when the sampler
// is initialized directly in FORCING state (e.g. COUNTING + budget=0)
ctx->force_pos++;
if (ctx->force_pos >= ctx->forced_tokens.size()) {
ctx->state = REASONING_BUDGET_DONE;
LOG_INF("reasoning-budget: forced sequence complete, done\n");
}
break;
case REASONING_BUDGET_DONE:
break;
Expand All @@ -144,14 +146,6 @@ static void common_reasoning_budget_apply(struct llama_sampler * smpl, llama_tok
cur_p->data[i].logit = -INFINITY;
}
}

// advance to next forced token (done here rather than in accept so that
// the first forced token isn't skipped when starting in FORCING state)
ctx->force_pos++;
if (ctx->force_pos >= ctx->forced_tokens.size()) {
ctx->state = REASONING_BUDGET_DONE;
LOG_INF("reasoning-budget: forced sequence complete, done\n");
}
}

static void common_reasoning_budget_reset(struct llama_sampler * smpl) {
Expand Down Expand Up @@ -261,3 +255,10 @@ struct llama_sampler * common_reasoning_budget_init(
common_reasoning_budget_state initial_state) {
return common_reasoning_budget_init_state(vocab, start_tokens, end_tokens, forced_tokens, budget, initial_state);
}

common_reasoning_budget_state common_reasoning_budget_get_state(const struct llama_sampler * smpl) {
if (!smpl) {
return REASONING_BUDGET_IDLE;
}
return ((const common_reasoning_budget_ctx *)smpl->ctx)->state;
}
2 changes: 2 additions & 0 deletions common/reasoning-budget.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,5 @@ struct llama_sampler * common_reasoning_budget_init(
const std::vector<llama_token> & forced_tokens,
int32_t budget,
common_reasoning_budget_state initial_state);

common_reasoning_budget_state common_reasoning_budget_get_state(const struct llama_sampler * smpl);
56 changes: 46 additions & 10 deletions common/sampling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <algorithm>
#include <cctype>
#include <climits>
#include <cmath>
#include <cstring>
#include <unordered_map>
Expand Down Expand Up @@ -109,6 +110,7 @@ struct common_sampler {
common_params_sampling params;

struct llama_sampler * grmr;
struct llama_sampler * rbudget;
struct llama_sampler * chain;

ring_buffer<llama_token> prev;
Expand Down Expand Up @@ -188,6 +190,7 @@ struct common_sampler * common_sampler_init(const struct llama_model * model, st
lparams.no_perf = params.no_perf;

llama_sampler * grmr = nullptr;
llama_sampler * rbudget = nullptr;
llama_sampler * chain = llama_sampler_chain_init(lparams);

std::vector<llama_sampler *> samplers;
Expand Down Expand Up @@ -270,7 +273,7 @@ struct common_sampler * common_sampler_init(const struct llama_model * model, st
}
}

if (grmr) {
if (grmr && !params.grammar_lazy) {
try {
for (const auto & token : prefill_tokens) {
llama_sampler_accept(grmr, token);
Expand All @@ -284,15 +287,15 @@ struct common_sampler * common_sampler_init(const struct llama_model * model, st
}
}

// reasoning budget sampler — added first so it can force tokens before other samplers
if (params.reasoning_budget_tokens >= 0 && !params.reasoning_budget_forced.empty()) {
samplers.push_back(common_reasoning_budget_init(
// reasoning budget sampler
if (!params.reasoning_budget_start.empty() && !params.reasoning_budget_end.empty()) {
rbudget = common_reasoning_budget_init(
vocab,
params.reasoning_budget_start,
params.reasoning_budget_end,
params.reasoning_budget_forced,
params.reasoning_budget_tokens,
prefill_tokens));
params.reasoning_budget_tokens < 0 ? INT_MAX : params.reasoning_budget_tokens,
prefill_tokens);
}

if (params.has_logit_bias()) {
Expand Down Expand Up @@ -383,6 +386,7 @@ struct common_sampler * common_sampler_init(const struct llama_model * model, st
auto * result = new common_sampler {
/* .params = */ params,
/* .grmr = */ grmr,
/* .rbudget = */ rbudget,
/* .chain = */ chain,
/* .prev = */ ring_buffer<llama_token>(std::max(32, params.n_prev)),
/* .cur = */ {},
Expand All @@ -398,18 +402,39 @@ void common_sampler_free(struct common_sampler * gsmpl) {
}

llama_sampler_free(gsmpl->grmr);
llama_sampler_free(gsmpl->rbudget);
llama_sampler_free(gsmpl->chain);

delete gsmpl;
}

static bool grammar_should_apply(struct common_sampler * gsmpl) {
if (!gsmpl->grmr) {
return false;
}
if (!gsmpl->rbudget) {
return true;
}
if (gsmpl->params.grammar_lazy) {
// if grammar is lazy, only apply when reasoning budget is not active
const auto state = common_reasoning_budget_get_state(gsmpl->rbudget);
return state == REASONING_BUDGET_IDLE || state == REASONING_BUDGET_DONE;
}
return true;
}

void common_sampler_accept(struct common_sampler * gsmpl, llama_token token, bool accept_grammar) {
if (!gsmpl) {
return;
}

const auto tm = gsmpl->tm();

// grammar_should_apply() checks the reasoning budget state, so calculate this before we accept
accept_grammar = accept_grammar && grammar_should_apply(gsmpl);

llama_sampler_accept(gsmpl->rbudget, token);

if (gsmpl->grmr && accept_grammar) {
llama_sampler_accept(gsmpl->grmr, token);
}
Expand All @@ -431,6 +456,7 @@ struct common_sampler * common_sampler_clone(common_sampler * gsmpl) {
return new common_sampler {
/* .params = */ gsmpl->params,
/* .grmr = */ llama_sampler_clone(gsmpl->grmr),
/* .rbudget = */ llama_sampler_clone(gsmpl->rbudget),
/* .chain = */ llama_sampler_clone(gsmpl->chain),
/* .prev = */ gsmpl->prev,
/* .cur = */ gsmpl->cur,
Expand Down Expand Up @@ -500,6 +526,7 @@ llama_token common_sampler_sample(struct common_sampler * gsmpl, struct llama_co
llama_token id = LLAMA_TOKEN_NULL;

auto & grmr = gsmpl->grmr;
auto & rbudget = gsmpl->rbudget;
auto & chain = gsmpl->chain;
auto & cur_p = gsmpl->cur_p; // initialized by set_logits

Expand All @@ -511,7 +538,8 @@ llama_token common_sampler_sample(struct common_sampler * gsmpl, struct llama_co
if (id != LLAMA_TOKEN_NULL) {
LOG_DBG("%s: Backend sampler selected token: '%d'. Will not run any CPU samplers\n", __func__, id);

GGML_ASSERT(!gsmpl->grmr && "using grammar in combination with backend sampling is not supported");
GGML_ASSERT(!gsmpl->grmr && "using grammar in combination with backend sampling is not supported");
GGML_ASSERT(!gsmpl->rbudget && "using reasoning budget in combination with backend sampling is not supported");

// TODO: simplify
gsmpl->cur.resize(1);
Expand All @@ -524,15 +552,18 @@ llama_token common_sampler_sample(struct common_sampler * gsmpl, struct llama_co

gsmpl->set_logits(ctx, idx);

if (grammar_first) {
// apply reasoning budget first
llama_sampler_apply(rbudget, &cur_p);

if (grammar_first && grammar_should_apply(gsmpl)) {
llama_sampler_apply(grmr, &cur_p);
}

llama_sampler_apply(chain, &cur_p);

id = cur_p.data[cur_p.selected].id;

if (grammar_first) {
if (grammar_first || !grammar_should_apply(gsmpl)) {
return id;
}

Expand All @@ -553,7 +584,12 @@ llama_token common_sampler_sample(struct common_sampler * gsmpl, struct llama_co
// if the token is not valid, sample again, but first apply the grammar sampler and then the sampling chain
gsmpl->set_logits(ctx, idx);

llama_sampler_apply(grmr, &cur_p);
llama_sampler_apply(rbudget, &cur_p);

if (grammar_should_apply(gsmpl)) {
llama_sampler_apply(grmr, &cur_p);
}

llama_sampler_apply(chain, &cur_p);

GGML_ASSERT(cur_p.selected != -1 && "no selected token during sampling - check your sampling configuration");
Expand Down
Loading
Loading