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
26 changes: 24 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,42 @@ jobs:
echo "hemlock_available=false" >> $GITHUB_OUTPUT
fi

- name: Run tests
- name: Run basic tests (no model required)
if: steps.install-hemlock.outputs.hemlock_available == 'true'
run: |
export LD_LIBRARY_PATH="$(pwd)/lib:$LD_LIBRARY_PATH"
export DYLD_LIBRARY_PATH="$(pwd)/lib:$DYLD_LIBRARY_PATH"
hemlock test_basic.hml

- name: Download tiny test model
if: steps.install-hemlock.outputs.hemlock_available == 'true'
run: |
chmod +x scripts/download_test_model.sh
./scripts/download_test_model.sh

- name: Run model tests
if: steps.install-hemlock.outputs.hemlock_available == 'true'
run: |
export LD_LIBRARY_PATH="$(pwd)/lib:$LD_LIBRARY_PATH"
export DYLD_LIBRARY_PATH="$(pwd)/lib:$DYLD_LIBRARY_PATH"
hemlock test_model.hml models/tinyllama-stories-15m-q4_0.gguf

- name: Validate Hemlock source files
run: |
echo "Checking Hemlock source files exist..."
test -f llama.hml && echo "llama.hml: OK"
test -f test_basic.hml && echo "test_basic.hml: OK"
test -f test_model.hml && echo "test_model.hml: OK"
test -f examples/simple.hml && echo "examples/simple.hml: OK"
test -f examples/chat.hml && echo "examples/chat.hml: OK"
test -f examples/oneshot.hml && echo "examples/oneshot.hml: OK"
test -f examples/streaming.hml && echo "examples/streaming.hml: OK"
test -f examples/chat_template.hml && echo "examples/chat_template.hml: OK"
test -f examples/grammar.hml && echo "examples/grammar.hml: OK"
test -f examples/embeddings.hml && echo "examples/embeddings.hml: OK"
test -f examples/tokenizer.hml && echo "examples/tokenizer.hml: OK"
test -f examples/infill.hml && echo "examples/infill.hml: OK"
test -f examples/model_info.hml && echo "examples/model_info.hml: OK"
echo "All source files present!"

lint:
Expand All @@ -89,8 +110,9 @@ jobs:

- name: Check shell scripts
run: |
# Check build.sh for syntax errors
# Check build.sh and helper scripts for syntax errors
bash -n build.sh
bash -n scripts/download_test_model.sh
echo "Shell scripts OK"

- name: Check for common issues
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ llama.cpp/build/
# Test outputs
*.log

# Downloaded models
models/

# Editor/IDE
.vscode/
*.swp
Expand Down
97 changes: 93 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,33 @@ llama.backend_free();
- `sample(sampler, ctx, idx)` - Sample a token
- `sampler_accept(sampler, token)` - Accept a token (for penalties)

### Advanced Samplers

- `sampler_init_grammar(model, grammar_str, root)` - Grammar-constrained sampling (GBNF)
- `sampler_init_logit_bias(model, biases)` - Logit bias (`[{ token, bias }]`)
- `sampler_init_infill(model)` - Fill-in-the-middle sampling
- `sampler_init_top_k(k)` - Top-k sampler
- `sampler_init_top_p(p, min_keep)` - Nucleus sampler
- `sampler_init_min_p(p, min_keep)` - Min-p sampler
- `sampler_init_temp(t)` - Temperature sampler
- `sampler_init_temp_ext(t, delta, exp)` - Dynamic temperature
- `sampler_init_typical(p, min_keep)` - Typical probability
- `sampler_init_mirostat(n_vocab, seed, tau, eta, m)` - Mirostat v1
- `sampler_init_mirostat_v2(seed, tau, eta)` - Mirostat v2
- `sampler_init_penalties(last_n, repeat, freq, presence)` - Repetition penalties
- `sampler_init_greedy()` - Greedy selection
- `sampler_init_dist(seed)` - Distribution-based selection

### Sampler Chain

- `sampler_chain_init()` - Create empty sampler chain
- `sampler_chain_add(chain, sampler)` - Add sampler to chain
- `sampler_chain_n(chain)` - Number of samplers in chain
- `sampler_chain_get(chain, i)` - Get sampler at index
- `sampler_chain_remove(chain, i)` - Remove sampler at index
- `sampler_clone(sampler)` - Clone a sampler
- `sampler_name(sampler)` - Get sampler name

### Inference

- `decode(ctx, tokens, pos)` - Decode tokens
Expand All @@ -186,30 +213,92 @@ llama.backend_free();
- `kv_cache_seq_pos_min(ctx, seq_id)` - Get min position
- `kv_cache_seq_pos_max(ctx, seq_id)` - Get max position

### Embeddings

- `encode(ctx, tokens)` - Encode tokens for embedding models
- `get_embeddings(ctx)` - Get all embeddings
- `get_embeddings_ith(ctx, i)` - Get embedding at index
- `embed(ctx, model, text)` - Convenience: tokenize, encode, extract embedding
- `pooling_type(ctx)` - Get pooling type (none/mean/cls/last)

### LoRA Adapters

- `lora_adapter_init(model, path)` - Load a LoRA adapter
- `lora_adapter_free(adapter)` - Free a LoRA adapter
- `lora_adapter_set(ctx, adapter, scale)` - Apply adapter (scale: 0.0-1.0)
- `lora_adapter_remove(ctx, adapter)` - Remove a specific adapter
- `lora_adapter_clear(ctx)` - Remove all adapters

### State Save/Load

- `state_save(ctx)` - Save full context state
- `state_load(ctx, state)` - Restore full context state
- `state_free(state)` - Free saved state
- `state_seq_save(ctx, seq_id)` - Save single sequence
- `state_seq_load(ctx, state, seq_id)` - Restore single sequence
- `state_get_size(ctx)` - Get state buffer size

### Vocabulary Inspection

- `vocab_get_text(model, token)` - Get token text
- `vocab_get_score(model, token)` - Get token score
- `vocab_get_attr(model, token)` - Get token attributes
- `is_control(model, token)` - Check if control token

### Model Quantization

- `model_quantize(input, output, options)` - Quantize a model
- Options: `ftype`, `n_threads`, `allow_requantize`, `quantize_output_tensor`

### High-Level

- `generate(ctx, model, prompt, options)` - Generate text
- `generate(ctx, model, prompt, options)` - Generate text with streaming callback
- Options: `n_predict`, `sampler`, `stop_tokens`, `callback`
- `complete(model_path, prompt, options)` - One-shot completion

### Performance

- `perf_print(ctx)` - Print context performance stats
- `perf_reset(ctx)` - Reset performance stats
- `perf_sampler_print(sampler)` - Print sampler performance stats
- `perf_sampler_reset(sampler)` - Reset sampler performance stats
- `time_us()` - Get current time in microseconds

## Examples

See the `examples/` directory:

- `simple.hml` - Basic text generation
- `chat.hml` - Interactive chat interface
- `oneshot.hml` - One-shot completion
| Example | Description |
|---------|-------------|
| `simple.hml` | Basic text generation |
| `chat.hml` | Interactive chat interface |
| `oneshot.hml` | One-shot completion using `complete()` |
| `streaming.hml` | Streaming generation with performance metrics |
| `chat_template.hml` | Chat template formatting (ChatML, Llama, etc.) |
| `grammar.hml` | Grammar-constrained JSON generation |
| `embeddings.hml` | Text embeddings and similarity comparison |
| `tokenizer.hml` | Tokenizer/vocabulary exploration |
| `infill.hml` | Code infill / fill-in-the-middle (FIM) |
| `model_info.hml` | Model metadata and system inspection |

Run examples:
```bash
export LD_LIBRARY_PATH="$(pwd)/lib:$LD_LIBRARY_PATH"

# Basic generation
hemlock examples/simple.hml ~/models/llama-7b.gguf "What is the meaning of life?"

# Interactive chat
hemlock examples/chat.hml ~/models/llama-7b-chat.gguf

# Grammar-constrained JSON output
hemlock examples/grammar.hml ~/models/llama-7b.gguf "Describe a fictional character"

# Text embeddings
hemlock examples/embeddings.hml ~/models/nomic-embed-text.gguf

# Model inspection
hemlock examples/model_info.hml ~/models/llama-7b.gguf
```

## License
Expand Down
143 changes: 143 additions & 0 deletions examples/chat_template.hml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/env hemlock
// Chat template example: Use the model's built-in chat template for proper
// message formatting (supports ChatML, Llama, Mistral, etc.)
//
// Usage:
// hemlock examples/chat_template.hml <model.gguf>

import "llama" from "../llama.hml";

fn main(args) {
if (args.length < 2) {
print("Usage: hemlock examples/chat_template.hml <model.gguf>");
return 1;
}

let model_path = args[1];

print("Loading model: " + model_path);
print("");

// Initialize backend
llama.backend_init();

// Load model
let model = llama.model_load(model_path, {
n_gpu_layers: 0
});

let info = llama.model_info(model);
print("Model: " + info.description);

// Retrieve and display the model's chat template
let tmpl = llama.chat_template(model, null);
if (tmpl == "") {
print("Warning: Model has no built-in chat template.");
print("Using a default ChatML template instead.");
tmpl = null;
} else {
print("Chat template found (" + tmpl.length + " chars)");
print("");
print("--- Raw Template ---");
// Show just the first part
if (tmpl.length > 200) {
print(tmpl.slice(0, 200) + "...");
} else {
print(tmpl);
}
print("--- End Template ---");
}

print("");

// Format a multi-turn conversation
let messages = [
{ role: "system", content: "You are a helpful assistant that answers concisely." },
{ role: "user", content: "What is the capital of France?" },
{ role: "assistant", content: "Paris." },
{ role: "user", content: "And what about Germany?" }
];

// Apply the chat template
let formatted = llama.apply_chat_template(model, messages, {
add_generation_prompt: true,
template: tmpl
});

print("=== Formatted Conversation ===");
print(formatted);
print("=== End ===");
print("");

// Create context for generation
let ctx = llama.context_create(model, {
n_ctx: 2048,
n_batch: 512
});

// Create sampler
let sampler = llama.sampler_create({
temp: 0.7,
top_k: 40,
top_p: 0.9
});

// Tokenize the formatted prompt
let tokens = llama.tokenize(model, formatted, {
add_special: false, // Template already includes special tokens
parse_special: true
});

print("Prompt tokens: " + tokens.length);
print("");

// Decode prompt
let result = llama.decode(ctx, tokens, 0);
if (result != 0) {
print("Error decoding prompt: " + result);
return 1;
}

// Generate response
print("=== Model Response ===");
print("");

let n_cur = tokens.length;
let max_tokens = 256;

for (let i = 0; i < max_tokens; i = i + 1) {
let token = llama.sample(sampler, ctx, -1);

if (llama.is_eog(model, token)) {
break;
}

let piece = llama.token_to_piece(model, token, false);
print_inline(piece);

result = llama.decode(ctx, [token], n_cur);
if (result != 0) {
break;
}

n_cur = n_cur + 1;
}

print("");
print("");

// Cleanup
llama.sampler_free(sampler);
llama.context_free(ctx);
llama.model_free(model);
llama.backend_free();

return 0;
}

fn print_inline(text) {
io.write(io.stdout, text);
io.flush(io.stdout);
}

main(args);
Loading
Loading