Commit b15d422
2026-09-08 04:11:11
Changed files (14)
exe
ext
elelem
ggml
lib
elelem
ggml
vendor
exe/elelem-ggml
@@ -0,0 +1,8 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "elelem/cli"
+
+Signal.trap("INT") { exit 1 }
+
+Elelem::CLI.new(ARGV, provider: "ggml").run
ext/elelem/ggml/elelem.cpp
@@ -0,0 +1,61 @@
+// Minimal shim over raw ggml (ggml-org/ggml), exposed to Ruby via Fiddle.
+//
+// Unlike llama.cpp, ggml has no GGUF-to-chat-model inference built in: no
+// tokenizer, no chat templates, no sampler chain, no tool-calling. This shim
+// currently only proves out the ggml_context/tensor lifecycle end to end
+// (el_open allocates a ggml context, el_close frees it) so the Ruby <-> C++
+// plumbing (Provider -> Fiddle -> this shim) can be exercised.
+//
+// TODO(real inference): to make this a working provider, this file needs to:
+// 1. Parse a GGUF file directly (ggml/include/gguf.h) to read tensor data
+// and hyperparameters -- there is no shared "model" abstraction like
+// llama_model, so this is manual per-architecture work.
+// 2. Build the computation graph for one transformer architecture (see
+// ggml/examples/gpt-2 or gpt-j for a worked reference) using
+// ggml_build_forward_expand + ggml_graph_compute.
+// 3. Implement tokenization (ggml itself doesn't provide a BPE/SPM
+// tokenizer -- llama.cpp vendors its own for this reason).
+// 4. Implement sampling (top-k/top-p/temp) over the output logits tensor.
+//
+// This is meaningfully more work than elelem-llama or elelem-candle, since
+// both of those depend on libraries that already include a full inference
+// stack; ggml is the tensor library underneath that stack, not the stack.
+
+#include <cstdlib>
+#include <cstring>
+#include <string>
+
+#include "ggml.h"
+
+struct el_handle {
+ ggml_context *ctx;
+};
+
+extern "C" void *el_open(const char *model_path, int /*n_gpu_layers*/, int /*n_ctx*/, int /*n_threads*/) {
+ (void) model_path; // TODO: gguf_init_from_file once GGUF parsing is wired up
+
+ ggml_init_params params = {};
+ params.mem_size = 16 * 1024 * 1024;
+ params.mem_buffer = nullptr;
+ params.no_alloc = false;
+
+ ggml_context *ctx = ggml_init(params);
+ if (!ctx) return nullptr;
+
+ el_handle *h = new el_handle{ctx};
+ return h;
+}
+
+extern "C" const char *el_version(void *handle) {
+ (void) handle;
+ static thread_local std::string buf;
+ buf = "elelem-ggml shim over ggml (no model loaded -- scaffold only)";
+ return buf.c_str();
+}
+
+extern "C" void el_close(void *handle) {
+ if (!handle) return;
+ el_handle *h = static_cast<el_handle *>(handle);
+ if (h->ctx) ggml_free(h->ctx);
+ delete h;
+}
ext/elelem/ggml/extconf.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require "fileutils"
+
+EXT_DIR = __dir__
+GEM_ROOT = File.expand_path("../../..", EXT_DIR)
+VENDOR = File.join(GEM_ROOT, "vendor", "ggml")
+BUILD_DIR = File.join(EXT_DIR, "build")
+NATIVE = File.join(GEM_ROOT, "ext", "elelem", "ggml", "native")
+
+class BuildError < StandardError; end
+
+def run(*cmd)
+ warn "elelem-ggml: + #{cmd.join(' ')}"
+ system(*cmd) || raise(BuildError, "build step failed: #{cmd.join(' ')}")
+end
+
+unless File.exist?(File.join(VENDOR, "CMakeLists.txt"))
+ abort "elelem-ggml: vendored ggml missing at #{VENDOR}\n run: git submodule update --init --recursive"
+end
+
+# CPU-only for now (GGML_NATIVE is ON by default -> host CPU SIMD via
+# -march=native). GPU backends (Vulkan/CUDA/HIP) can follow the same
+# detect_backend pattern as elelem-llama's extconf.rb once this skeleton
+# actually loads a model.
+run(
+ "cmake", "-S", VENDOR, "-B", BUILD_DIR,
+ "-DCMAKE_BUILD_TYPE=Release",
+ "-DBUILD_SHARED_LIBS=ON",
+ "-DGGML_BUILD_TESTS=OFF",
+ "-DGGML_BUILD_EXAMPLES=OFF"
+)
+run("cmake", "--build", BUILD_DIR, "--target", "ggml", "--parallel")
+
+libdir = File.join(BUILD_DIR, "src") # ggml's CMake puts libggml*.so here (not bin/, unlike llama.cpp)
+
+FileUtils.mkdir_p(NATIVE)
+FileUtils.rm_f(Dir.glob(File.join(NATIVE, "*.so*")))
+FileUtils.cp(Dir.glob(File.join(libdir, "*.so*")), NATIVE)
+
+cxx = ENV["CXX"] || "c++"
+run(cxx, "-std=c++17", "-O2", "-Wall", "-Wextra", "-shared", "-fPIC",
+ "-I", File.join(VENDOR, "include"),
+ File.join(EXT_DIR, "elelem.cpp"),
+ "-o", File.join(NATIVE, "libelelem_ggml.so"),
+ "-L", libdir, "-lggml", "-lggml-base",
+ "-Wl,-rpath,$ORIGIN")
+
+File.write(File.join(EXT_DIR, "Makefile"), <<~MAKE)
+ all:
+ clean:
+ install:
+ .PHONY: all clean install
+MAKE
+
+warn "elelem-ggml: built ggml + shim into #{NATIVE}"
lib/elelem/ggml/provider.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Elelem
+ module Ggml
+ class Provider
+ NATIVE = File.expand_path("../../../ext/elelem/ggml/native", __dir__)
+ SHIM = File.join(NATIVE, "libelelem_ggml.so")
+ V = Fiddle::TYPE_VOIDP
+ I = Fiddle::TYPE_INT
+
+ def self.functions
+ @functions ||= begin
+ lib = Fiddle.dlopen(SHIM)
+ {
+ open: Fiddle::Function.new(lib["el_open"], [V, I, I, I], V),
+ version: Fiddle::Function.new(lib["el_version"], [V], V),
+ close: Fiddle::Function.new(lib["el_close"], [V], Fiddle::TYPE_VOID)
+ }
+ end
+ end
+
+ def initialize(model:, n_ctx: 0, n_threads: 0, n_gpu_layers: 0)
+ @handle = self.class.functions[:open].call(model, n_gpu_layers, n_ctx, n_threads)
+ raise "ggml: failed to init context for #{model}" if @handle.null?
+
+ at_exit { self.class.functions[:close].call(@handle) }
+ end
+
+ def fetch(_messages, _tools = [], &block)
+ # TODO: real GGUF loading + forward pass + sampling (see elelem.cpp).
+ # This proves the FFI round-trip so Provider wiring can be tested end
+ # to end before real inference exists.
+ ptr = self.class.functions[:version].call(@handle)
+ text = "[elelem-ggml scaffold] #{Fiddle::Pointer.new(ptr).to_s}"
+ block&.call(type: "saying", text: text)
+ []
+ end
+ end
+ end
+end
lib/elelem/ggml/version.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Elelem
+ module Ggml
+ VERSION = "0.1.0"
+ end
+end
lib/elelem/ggml.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require "fiddle"
+require "json"
+require "elelem"
+
+require_relative "ggml/version"
+require_relative "ggml/provider"
+
+Elelem.configure do |config|
+ config.provider(:ggml) do
+ Elelem::Ggml::Provider.new(
+ model: ENV.fetch("ELELEM_GGML_MODEL") { raise "set ELELEM_GGML_MODEL=/path/to/model.gguf" },
+ n_ctx: 0,
+ n_threads: 0,
+ n_gpu_layers: 0
+ )
+ end
+end
vendor/ggml
@@ -0,0 +1,1 @@
+Subproject commit e91ded11bdcd78c42f9c8d3978ff6686eb4c1226
.gitignore
@@ -0,0 +1,15 @@
+/.bundle/
+/.yardoc
+/_yardoc/
+/coverage/
+/pkg/
+/spec/reports/
+/tmp/
+*.bundle
+*.so
+*.o
+*.a
+*.gem
+/ext/elelem/ggml/native
+/ext/elelem/ggml/build/
+/ext/elelem/ggml/Makefile
.gitmodules
@@ -0,0 +1,3 @@
+[submodule "vendor/ggml"]
+ path = vendor/ggml
+ url = https://github.com/ggml-org/ggml
elelem-ggml.gemspec
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require_relative "lib/elelem/ggml/version"
+
+Gem::Specification.new do |spec|
+ spec.name = "elelem-ggml"
+ spec.version = Elelem::Ggml::VERSION
+ spec.authors = ["mo khan"]
+ spec.email = ["[email protected]"]
+
+ spec.summary = "A raw ggml (ggml-org/ggml) plugin for elelem."
+ spec.description = "A raw ggml (ggml-org/ggml) plugin for elelem."
+ spec.homepage = "https://src.mokhan.ca/elelem/ggml"
+ spec.license = "MIT"
+ spec.required_ruby_version = ">= 4.0.0"
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
+ spec.metadata["homepage_uri"] = spec.homepage
+ spec.metadata["source_code_uri"] = spec.homepage
+
+ spec.files = [
+ "LICENSE.txt",
+ "lib/elelem/ggml.rb",
+ "lib/elelem/ggml/version.rb",
+ "lib/elelem/ggml/provider.rb",
+ "exe/elelem-ggml",
+ "ext/elelem/ggml/elelem.cpp",
+ "ext/elelem/ggml/extconf.rb",
+ ]
+ # Only what extconf.rb's cmake build actually needs to configure and
+ # compile: the ggml library itself, no tests/examples/docs.
+ vendor_exclude = %r{\Avendor/ggml/(tests|docs|examples|\.github|ci)/}
+ spec.files += Dir["vendor/ggml/**/*"].select do |path|
+ File.file?(path) && path !~ vendor_exclude
+ end
+ spec.extensions = ["ext/elelem/ggml/extconf.rb"]
+ spec.bindir = "exe"
+ spec.executables = ["elelem-ggml"]
+ spec.require_paths = ["lib"]
+
+ spec.add_dependency "elelem", "~> 0.11"
+ spec.add_dependency "fiddle", "~> 1.1"
+ spec.add_dependency "json", "~> 3.0"
+end
Gemfile
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gemspec name: "elelem-ggml"
+
+gem "irb"
+gem "rake", "~> 13.0"
LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2026 mo khan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
Rakefile
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require "bundler/gem_tasks"
+
+desc "Build the in-process ggml extension (the same build `gem install` runs)"
+task :compile do
+ ruby "ext/elelem/ggml/extconf.rb"
+end
+
+task default: %i[]
README.md
@@ -0,0 +1,42 @@
+# elelem-ggml
+
+An [elelem](https://src.mokhan.ca/elelem/elelem) provider plugin backed
+directly by [ggml-org/ggml](https://github.com/ggml-org/ggml), the tensor
+library llama.cpp itself is built on -- vendored as a git submodule at
+`vendor/ggml`, same pattern as `elelem-llama`'s `vendor/llama.cpp`.
+
+## Status: scaffold only
+
+This is currently a skeleton, and meaningfully less complete than
+`elelem-llama`. ggml is a tensor library, not an LLM inference stack: it has
+no GGUF-to-chat-model loader, no tokenizer, no chat templates, no sampler
+chain, no tool-calling -- all of the things llama.cpp itself builds on top of
+ggml. `el_open`/`el_close` in `ext/elelem/ggml/elelem.cpp` currently only
+prove out a `ggml_context` lifecycle round trip over Fiddle; there is no
+model loading or inference yet.
+
+See the TODOs in `ext/elelem/ggml/elelem.cpp` for what real inference would
+require:
+
+1. Parse a GGUF file directly (`ggml/include/gguf.h`) -- there's no shared
+ "model" abstraction, so this is manual per-architecture work.
+2. Build the computation graph for one transformer architecture (see
+ `vendor/ggml/examples/gpt-2` or `gpt-j` for worked references) via
+ `ggml_build_forward_expand` + `ggml_graph_compute`.
+3. Implement tokenization yourself (ggml doesn't ship a tokenizer).
+4. Implement sampling (top-k/top-p/temp) over the output logits.
+
+## Setup
+
+```
+git submodule update --init --recursive
+bundle install
+bundle exec rake compile
+```
+
+## Usage
+
+```
+export ELELEM_GGML_MODEL=/path/to/model.gguf
+bundle exec exe/elelem-ggml ask "hi"
+```