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
76 changes: 76 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
stub-build:
name: Stub Build & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: Build (stub, no CGO)
run: go build ./...
- name: Test (stub)
run: go test -v ./...

cgo-build:
name: CGO Build (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
platform: linux-amd64
- os: macos-13
platform: darwin-amd64
# - os: macos-latest # enable when darwin-arm64 prebuilt .a files are added
# platform: darwin-arm64
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: Build with CGO (llamacpp tag)
run: CGO_ENABLED=1 go build -tags llamacpp ./ggml/llamacpp/...

build-libs:
name: Build Libraries from Source (${{ matrix.os }})
runs-on: ${{ matrix.os }}
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
strategy:
matrix:
include:
- os: ubuntu-latest
platform: linux-amd64
- os: macos-13
platform: darwin-amd64
# - os: macos-latest
# platform: darwin-arm64
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: Install build tools
run: |
if [ "$RUNNER_OS" = "Linux" ]; then
sudo apt-get update && sudo apt-get install -y cmake
elif [ "$RUNNER_OS" = "macOS" ]; then
brew install cmake
fi
- name: Build static libraries from source
run: make build-libs
- name: Verify CGO build with fresh libraries
run: CGO_ENABLED=1 go build -tags llamacpp ./ggml/llamacpp/...
- name: Upload prebuilt artifacts
uses: actions/upload-artifact@v4
with:
name: prebuilt-${{ matrix.platform }}
path: third_party/llama.cpp/prebuilt/${{ matrix.platform }}/*.a
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Build artifacts (source downloads, cmake dirs)
third_party/llama.cpp/src/
third_party/whisper.cpp/src/
third_party/**/build/
out/

# Prebuilt .a files and headers ARE committed — do not ignore them
# They enable: go get + go build -tags llamacpp (just works)
44 changes: 44 additions & 0 deletions Dockerfile.libs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Dockerfile.libs — build linux-amd64 static libraries for llama.cpp
#
# Usage:
# docker build -f Dockerfile.libs -o ./out .
#
# This extracts prebuilt .a files + headers into ./out/ on the host.

FROM golang:1.24-bullseye AS builder

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential cmake wget && \
rm -rf /var/lib/apt/lists/*

WORKDIR /src

# Copy version files so we can read versions from Go
COPY go.mod ./
COPY version.go ./
COPY cmd/versioncmd/ ./cmd/versioncmd/

# Read versions and download sources
RUN LLAMA_VERSION=$(go run ./cmd/versioncmd llama.cpp) && \
echo "Downloading llama.cpp ${LLAMA_VERSION}..." && \
wget -qO llama.cpp.tar.gz "https://github.com/ggerganov/llama.cpp/archive/refs/tags/${LLAMA_VERSION}.tar.gz" && \
mkdir -p llama-src && \
tar xzf llama.cpp.tar.gz --strip-components=1 -C llama-src && \
rm llama.cpp.tar.gz

# Build llama.cpp
RUN cd llama-src && \
cmake -B build -DBUILD_SHARED_LIBS=OFF && \
cmake --build build --config Release -j$(nproc)

# Collect artifacts
RUN mkdir -p /out/llama.cpp/linux-amd64 /out/llama.cpp/include /out/llama.cpp/ggml/include /out/llama.cpp/common && \
find llama-src/build -name "*.a" -exec cp {} /out/llama.cpp/linux-amd64/ \; && \
cp llama-src/include/*.h /out/llama.cpp/include/ && \
cp llama-src/ggml/include/*.h /out/llama.cpp/ggml/include/ && \
cp llama-src/common/common.h /out/llama.cpp/common/ && \
cp llama-src/common/sampling.h /out/llama.cpp/common/

# Output stage — docker build -o extracts from here
FROM scratch
COPY --from=builder /out/ /
98 changes: 98 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Makefile — build prebuilt static libraries for go-nativeml
#
# For consumers:
# go get github.com/footprintai/go-nativeml
# go build -tags llamacpp ./... # just works — prebuilt .a files are in the module
#
# For maintainers (rebuild .a files from source):
# make build-libs # Build all libraries for current platform
# make build-libs-llama # Build llama.cpp only
# make build-libs-linux # Build linux-amd64 .a files via Docker
# make build-libs-all # Build native + linux-amd64
# make clean # Remove temp build dirs (keeps prebuilt .a + headers)

# Version sync via Go toolchain (single source of truth: version.go)
LLAMA_VERSION := $(shell go run ./cmd/versioncmd llama.cpp)
WHISPER_VERSION := $(shell go run ./cmd/versioncmd whisper.cpp)

# Platform detection
PLATFORM := $(shell go env GOOS)-$(shell go env GOARCH)

# Parallel build cores
NPROC := $(shell if which nproc > /dev/null 2>&1; then nproc; elif [ "$$(uname)" = "Darwin" ]; then sysctl -n hw.ncpu; else echo 4; fi)

# Paths
THIRD_PARTY := third_party
LLAMA_DIR := $(THIRD_PARTY)/llama.cpp
LLAMA_SRC := $(LLAMA_DIR)/src
LLAMA_PREBUILT := $(LLAMA_DIR)/prebuilt/$(PLATFORM)

.PHONY: build-libs build-libs-llama build-libs-linux build-libs-all clean verify

build-libs: build-libs-llama

# Build both native platform and linux-amd64 (via Docker)
build-libs-all: build-libs build-libs-linux

# ============================================================================
# llama.cpp
# ============================================================================
build-libs-llama: $(LLAMA_PREBUILT)

$(LLAMA_PREBUILT): $(LLAMA_SRC)
@echo "==> Building llama.cpp $(LLAMA_VERSION) for $(PLATFORM)..."
cd $(LLAMA_SRC) && cmake -B build -DBUILD_SHARED_LIBS=OFF && \
cmake --build build --config Release -j$(NPROC)
@mkdir -p $(LLAMA_PREBUILT)
find $(LLAMA_SRC)/build -name "*.a" -exec cp {} $(LLAMA_PREBUILT)/ \;
@echo "==> Copying llama.cpp headers..."
@mkdir -p $(LLAMA_DIR)/include
cp $(LLAMA_SRC)/include/*.h $(LLAMA_DIR)/include/
@mkdir -p $(LLAMA_DIR)/ggml/include
cp $(LLAMA_SRC)/ggml/include/*.h $(LLAMA_DIR)/ggml/include/
@mkdir -p $(LLAMA_DIR)/common
cp $(LLAMA_SRC)/common/common.h $(LLAMA_DIR)/common/
cp $(LLAMA_SRC)/common/sampling.h $(LLAMA_DIR)/common/
@echo "==> llama.cpp $(LLAMA_VERSION) ready: $(LLAMA_PREBUILT)/"

$(LLAMA_SRC):
@echo "==> Downloading llama.cpp $(LLAMA_VERSION)..."
wget -qO llama.cpp.tar.gz https://github.com/ggerganov/llama.cpp/archive/refs/tags/$(LLAMA_VERSION).tar.gz
mkdir -p $(LLAMA_SRC)
tar xzf llama.cpp.tar.gz --strip-components=1 -C $(LLAMA_SRC)
rm llama.cpp.tar.gz

# ============================================================================
# Docker build for linux-amd64 (cross-compile from macOS)
# ============================================================================
build-libs-linux:
@echo "==> Building linux-amd64 static libraries via Docker..."
docker build -f Dockerfile.libs -o ./out .
@mkdir -p $(LLAMA_DIR)/prebuilt/linux-amd64
cp out/llama.cpp/linux-amd64/*.a $(LLAMA_DIR)/prebuilt/linux-amd64/
@# Copy headers if not already present
@mkdir -p $(LLAMA_DIR)/include $(LLAMA_DIR)/ggml/include $(LLAMA_DIR)/common
cp out/llama.cpp/include/*.h $(LLAMA_DIR)/include/
cp out/llama.cpp/ggml/include/*.h $(LLAMA_DIR)/ggml/include/
cp out/llama.cpp/common/common.h $(LLAMA_DIR)/common/
cp out/llama.cpp/common/sampling.h $(LLAMA_DIR)/common/
rm -rf out
@echo "==> linux-amd64 libraries ready"

# ============================================================================
# Verification
# ============================================================================
verify:
@echo "==> Verifying stub build (no tag)..."
go build ./ggml/llamacpp/...
@echo "==> Verifying CGO build (with tag)..."
CGO_ENABLED=1 go build -tags llamacpp ./ggml/llamacpp/...
@echo "==> Running stub tests..."
go test ./ggml/llamacpp/...
@echo "==> All checks passed"

# ============================================================================
# Cleanup
# ============================================================================
clean:
rm -rf $(LLAMA_SRC) out
20 changes: 20 additions & 0 deletions claude.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Principal: protobuf contract first, typed safer approach, and no backward compatibility.

This project provides CGO wrappers for C++ inference frameworks for Go.

## Structure

- `ggml/llamacpp/` — Go bindings for llama.cpp (build tag: `llamacpp`)
- `ggml/whispercpp/` — (future) Go bindings for whisper.cpp
- `third_party/llama.cpp/` — Upstream headers + prebuilt static libraries (keep upstream layout untouched)

## Build Tags

- Default (no tag): stub implementations that return errors
- `llamacpp`: enables CGO bindings to prebuilt llama.cpp libraries

## Adding New Platforms

1. Build llama.cpp static libraries for the target platform
2. Place `.a` files in `third_party/llama.cpp/prebuilt/<os>-<arch>/`
3. Add CGO LDFLAGS directive in `llamacpp.go`
24 changes: 24 additions & 0 deletions cmd/versioncmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main

import (
"fmt"
"os"

gonativeml "github.com/footprintai/go-nativeml"
)

func main() {
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "usage: %s <llama.cpp|whisper.cpp>\n", os.Args[0])
os.Exit(1)
}
switch os.Args[1] {
case "llama.cpp":
fmt.Print(gonativeml.LlamaCppVersion)
case "whisper.cpp":
fmt.Print(gonativeml.WhisperCppVersion)
default:
fmt.Fprintf(os.Stderr, "unknown library: %s\n", os.Args[1])
os.Exit(1)
}
}
61 changes: 61 additions & 0 deletions examples/embeddings/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//go:build llamacpp

// Example: generate embeddings using go-nativeml's llamacpp package.
//
// Usage:
//
// CGO_ENABLED=1 go run -tags llamacpp ./examples/embeddings -model /path/to/model.gguf -text "Hello, world"
package main

import (
"flag"
"fmt"
"os"

"github.com/footprintai/go-nativeml/ggml/llamacpp"
)

func main() {
modelPath := flag.String("model", "", "path to GGUF model file")
text := flag.String("text", "Hello, world", "text to embed")
gpuLayers := flag.Int("gpu-layers", 999, "number of layers to offload to GPU")
flag.Parse()

if *modelPath == "" {
fmt.Fprintln(os.Stderr, "error: -model is required")
flag.Usage()
os.Exit(1)
}

llamacpp.Init()
defer llamacpp.Shutdown()

model, err := llamacpp.LoadModel(*modelPath,
llamacpp.WithGPULayers(*gpuLayers),
)
if err != nil {
fmt.Fprintf(os.Stderr, "error loading model: %v\n", err)
os.Exit(1)
}
defer model.Close()

ctx, err := model.NewContext(
llamacpp.WithContextSize(512),
llamacpp.WithEmbeddings(),
)
if err != nil {
fmt.Fprintf(os.Stderr, "error creating context: %v\n", err)
os.Exit(1)
}
defer ctx.Close()

embeddings, err := ctx.GetEmbeddings(*text)
if err != nil {
fmt.Fprintf(os.Stderr, "error getting embeddings: %v\n", err)
os.Exit(1)
}

fmt.Printf("Text: %s\n", *text)
fmt.Printf("Embedding dim: %d\n", len(embeddings))
fmt.Printf("First 5 values: %v\n", embeddings[:min(5, len(embeddings))])
}
Loading
Loading