Plugins / Lab
elelem-llama2
llama2.c, wired into elelem.
Runs Andrej Karpathy's llama2.c inside elelem. One C file does the whole forward pass; a 161-line C++ shim streams tokens back to Ruby.
git clone https://git.mokhan.ca/elelem/llama2.gitLab. An experiment in how inference works. Built to learn from and read, not published to RubyGems.
Experimental: a story generator, not a coding assistant. No chat template, no tool calls, and replies show up as thinking text, not as the agent’s answer. Not published to RubyGems.
elelem-llama2 runs karpathy/llama2.c
inside the elelem process. llama2.c is a single C file, run.c, that
implements Llama 2 inference with no dependencies. This plugin compiles
that file as-is and calls its functions from a small C++ shim. Use it to
see the smallest complete inference loop elelem can host, on tiny models
like the TinyStories checkpoints.
Install #
The gem is not on RubyGems. Build it from source:
git clone https://git.mokhan.ca/elelem/llama2.git
cd llama2
git submodule update --init --recursive
bundle install
bundle exec rake compilellama2.c is vendored as a git submodule at vendor/llama2.c. There is no
CMake. ext/elelem/llama2/extconf.rb runs cc on run.c and c++ on the
shim, and writes ext/elelem/llama2/native/libelelem_llama2.so.
Configure #
You need a checkpoint in llama2.c’s own .bin format, made by
vendor/llama2.c/export.py or downloaded. The upstream llama2.c README
shows how to download stories15M.bin, a small TinyStories model. The vendored tokenizer.bin is used by default.
export ELELEM_LLAMA2_CHECKPOINT=/path/to/stories15M.binELELEM_LLAMA2_STEPS counts every position, prompt tokens included. Keep
it at or below the model’s seq_len: the shim does not clamp it.
Use #
bundle exec exe/elelem-llama2 ask "Once upon a time"exe/elelem-llama2 is the elelem CLI with llama2 as the default
provider. chat (the default command) gives you a REPL.
Tokens stream as they are sampled, but the plugin sends them as
thinking events, not saying. In chat you watch the story appear as
thinking text, and the recorded reply is empty. ask streams the text,
then exits with elelem: no reply.
How it works #
run.c has no header and its own main(). The build compiles it to an
object file and links it into the shim. The shim never calls main(); it
calls the building blocks directly. Because there is no .h to include,
ext/elelem/llama2/elelem.cpp copies the struct layouts and function
signatures from run.c. If upstream changes them, the shim has to change
too.
Open. el_open calls 3 functions from run.c:
build_transformerloads the checkpoint and sets up the weights, activations, and KV cache.build_tokenizerloadstokenizer.bin.build_samplerstores temperature, top-p, and the RNG seed.
Prompt. llama2.c has no chat template. The Ruby provider joins the
content of every message with newlines into one string. That includes
elelem’s system prompt and any AGENTS.md.
Generate. el_generate is a short loop over run.c’s functions that
hands each piece to a callback:
encodeturns the prompt into token IDs, with a BOS token in front.- For each position,
forwardruns one token through the network and returns logits. - While still inside the prompt, the next token is the next prompt token
(teacher forcing). After the prompt,
samplepicks one from the logits with temperature and top-p. decodeturns the token into a string, and the shim calls Ruby’s callback with it.- It stops at the step limit, or when the model emits BOS (ID 1), which llama2.c uses to separate sequences.
Ruby supplies the callback as a Fiddle::Closure::BlockCaller and calls
el_generate with need_gvl: true, so the callback can run Ruby code
from inside the C loop. Each piece is forced to UTF-8 and scrubbed before it
reaches elelem.
Every call starts again at position 0, so the KV cache is not reused across turns.
Limitations #
- Only llama2.c’s
.bincheckpoint format and the original Llama 2 architecture. No GGUF. - No chat template and no tool calls. The tool list is ignored.
- Replies arrive as
thinkingevents. The agent’s answer is always empty, soaskends withelelem: no reply. - The prompt is echoed back. The callback fires for every position, including the prompt tokens being fed in.
- The whole conversation, system prompt included, is one prompt. On a
small model with a short
seq_lenthis crowds out the reply. ELELEM_LLAMA2_STEPSis not clamped toseq_len.- No quantized models. Upstream’s
runq.cis not wired in. - The gemspec does not package
vendor/llama2.c/tokenizer.bin, so the default tokenizer path only works from a source checkout.
Development #
bin/setup
bin/compile
bin/lintbin/setup runs bundle install only; run
git submodule update --init --recursive first. There is no test suite.
Environment #
| Variable | Default | Description |
|---|---|---|
ELELEM_LLAMA2_CHECKPOINT | none | Required. Path to a llama2.c .bin checkpoint. |
ELELEM_LLAMA2_TOKENIZER | vendor/llama2.c/tokenizer.bin | Path to a llama2.c tokenizer.bin. The default is the one in the vendored submodule. |
ELELEM_LLAMA2_STEPS | 0 | Total positions to run, prompt included. 0 means the model’s seq_len. |
ELELEM_LLAMA2_TEMP | 1.0 | Sampling temperature. |
ELELEM_LLAMA2_TOPP | 0.9 | Top-p (nucleus) sampling threshold. |
ELELEM_LLAMA2_SEED | -1 | Random seed. Negative uses the current time in nanoseconds. |