modelc
modelc is a Rust-based CLI that packages model files into single, self-contained artifacts optimized for size, footprint, and performance. It supports multiple model formats and provides an inference experience similar to Ollama, vLLM, and SGLang — run models locally with minimal setup.
Why modelc
- Single-file packaging. One file contains all model data and can be loaded directly by the CLI. No external weight trees, no path coordination, no "wrong checkpoint" drift.
- Size and performance optimized. The packaged format is designed for minimal footprint and fast loading. Parsing and tensor layout happen at package time, so runtime overhead is low.
- Cross-platform. Runs on macOS, Windows, and Linux from the same toolchain.
- Apple Silicon acceleration. Native support for Apple Silicon M-series chips via Metal acceleration.
- Multiple format support. Compatible with Safetensors, GGUF, ONNX, and PyTorch checkpoints.
- Simple operations.
modelc run,modelc inspect,modelc compile— a minimal command set for packaging, inspection, and serving.
When weight files remain the better fit: rapid A/B swaps without repackaging, very large checkpoints where embedding blows up artifacts, multitenant “one server, many paths,” or ecosystems that assume on-disk formats (mmap, GGUF loaders, ONNX Runtime with external weights).
See SPEC.md for scope and limits.
Prerequisites
- Rust toolchain with
cargoon yourPATH(the compiler runscargo buildon a generated project).
Build
Produce the modelc binary:
Binary path: ./target/release/modelc (or target/debug/modelc without --release). To put modelc on your PATH:
Test
Examples
Runnable programs live under examples/; see examples/README.md for a table and quick flows. Typical invocations:
Usage
Examples use the modelc command. Put it on your PATH with cargo install --path ., or after cargo build --release call the binary by path (see Build).
Core workflow
Pack models into .modelc single-file artifacts:
Run inference from local artifacts:
List locally stored models:
Remove a model from the local store:
Copy a model in the local store:
Pull models from remote sources:
Chat / completion inference:
# HTTP API: POST /chat with messages
# HTTP API: POST /complete with prompt
# HTTP API: POST /chat/stream for SSE streaming
Legacy compile workflow
Inspect weights (tensor names, shapes, dtypes, sizes):
Compile to a standalone binary (default output: <stem>_serve next to the input):
From the built artifact in this repository (no install):
modelc --help and modelc --version show subcommands and semver plus a short git revision (from build.rs when .git is present).
Pack flags
--compress— use zstd compression to reduce artifact size (produces version 2 format).--arch— optional hint (llama,gpt2,mlp, …) stored in the model.--format/-f— weight format when extensions or magic-byte sniffing are ambiguous.-o— output path for the.modelcartifact.
Run flags
--port— HTTP server port (default8080).--bind— HTTP server bind address (default0.0.0.0).--api-key <key>— RequireAuthorization: Bearer <key>on all endpoints except/healthand/info.--rate-limit <N>— Max requests per minute per client IP (default: unlimited).--max-concurrent <N>— Max concurrent inference requests. Excess requests receive503 Service Unavailable. Exempts/health,/info, and/metrics.--max-context <N>— Hard context limit; triggers sliding-window KV eviction when exceeded.--anchor-tokens <N>— Preserve the first N tokens during context shifting (StreamingLLM-style).--temperature <T>— Sampling temperature (default1.0).--max-tokens <N>— Max tokens to generate (default256).--top-p <P>— Nucleus sampling threshold (default0.0, disabled).--min-p <P>— Min-p sampling threshold (default0.0, disabled).--repetition-penalty <F>— Multiplicative repetition penalty (default1.0, disabled).--presence-penalty <F>— OpenAI-style presence penalty (default0.0, disabled).--frequency-penalty <F>— OpenAI-style frequency penalty (default0.0, disabled).--grammar <pattern>— Regex grammar constraint applied during sampling.--profile— Print per-step inference timing.
Compile flags (legacy)
The generated model-serve binary binds to --bind (IP, default 0.0.0.0) plus --port (default 8080), unless --listen ADDR:PORT is set, which wins and is embedded verbatim (IPv6 literals such as [::1]:8080 are supported).
Other compile flags:
--arch— optional hint (llama,gpt2, …) stored in the model and surfaced in/info.--format/-f— weight format when extensions or magic-byte sniffing are ambiguous.--target— passed through tocargo build --target.--debug— builds the generated crate with Cargo’s debug profile instead of--release(release is the default).
Supported input formats (-f when needed):
| Flag value | Typical extensions |
|---|---|
safetensors |
.safetensors |
gguf |
.gguf, .bin with sniff / name heuristics |
onnx |
.onnx |
pytorch |
.pt, .pth, name-heuristic .bin |
Ambiguous files (e.g. extensionless or generic .bin): the CLI may sniff GGUF / zip (PyTorch-ish) / small Safetensors blobs (see SPEC.md).
HTTP API (run + model-serve)
| Method | Path | Body / response |
|---|---|---|
GET |
/info |
JSON: name, architecture, total_params, total_bytes, tensors (names). |
GET |
/health |
JSON: status, model, architecture — liveness probe. |
POST |
/infer |
Request JSON: { "input": [f32, ...] } or { "inputs": [[f32, ...], ...] } for batch. Response: { "output": [f32, ...] } or { "outputs": [[f32, ...], ...] }. |
POST |
/chat |
Request JSON: { "messages": [{"role": "user", "content": "..."}] }. Response: { "message": {"role": "assistant", "content": "..."} }. |
POST |
/chat/stream |
SSE stream of { "delta": "...", "done": bool } chunks. |
POST |
/complete |
Request JSON: { "prompt": "..." }. Response: { "completion": "..." }. |
POST |
/embeddings |
Request JSON: { "input": "..." }. Response: { "embedding": [f32, ...], "model": "..." }. |
POST |
/v1/embeddings |
OpenAI-compatible embeddings. Accepts encoding_format: "float" or "base64". Returns dimensions, index, and usage. |
POST |
/tokenize |
Request JSON: { "input": "..." } or { "inputs": [...] }. Response: { "tokens": [id, ...], "count": N } (batch: tokens_batch). |
GET |
/api/version |
JSON: version (semver), git_sha — for orchestration/health checks. |
GET |
/v1/system |
System/hardware info: CPU cores, OS, arch, Metal availability, total memory. |
GET |
/v1/models |
OpenAI-compatible model list. |
POST |
/v1/chat/completions |
OpenAI-compatible chat completion (streaming + non-streaming). |
POST |
/v1/completions |
OpenAI-compatible legacy text completion (streaming + non-streaming). |
Inference backends (priority order):
- ONNX execution plan — if the model metadata contains
onnx.execution_plan, ops are executed via the runtime tensor engine (MatMul, Gemm, Add, Mul, Div, Sub, Relu, Softmax, LayerNorm, Transpose, Reshape, Sigmoid, Tanh, Identity, Cast). - Transformer forward — when
architecture == "gpt2"or"llama", runs the full FP32 transformer forward (layer norm / RMS norm, GeLU / SwiGLU, RoPE, single-token causal attention, output projection). Inputs are resized to the model hidden size; numerics match thecompile-emitted server. - MLP GEMV — when
architecture == "mlp", emits a stacked GEMV + bias (+ ReLU between hidden layers) usinglayerN.weight/layerN.biasor a singleweight/bias. - Echo fallback — returns input unchanged when no execution plan matches.
All JSON responses are application/json; SSE uses text/event-stream.
crates.io checklist
Before the first (modelc) publish:
- Bump
Cargo.tomlversion, tagvX.Y.Z, runcargo publish --dry-run. - Snapshot
modelc --help/ subcommand help after any CLI churn (crate README can embed the summary). - After the first publish, add a crates.io version badge (shields.io versioning; crate URL https://crates.io/crates/modelc once live).
- Maintain a concise changelog (
CHANGELOG.mdoptional) noting parser/format support changes.
Format references (parsers / exports)
- Safetensors — huggingface/safetensors
- GGUF — GGML GGUF notes
- ONNX — onnx.ai
- PyTorch checkpoints — accepts mislabeled standalone Safetensors bytes and Torch ZIP containers that nest
*.safetensorspayloads; pickled-only checkpoints should be exported to Safetensors, ONNX, or GGUF externally.
Features
- ONNX graph execution — parses ONNX graph nodes into an execution plan (MatMul, Gemm, Add, Mul, Div, Sub, Relu, Softmax, LayerNorm, Transpose, Reshape, Sigmoid, Tanh, Identity, Cast) and runs inference via the tensor runtime.
- LoRA adapter support — load and apply LoRA adapters on top of a base model at runtime (
src/lora.rs). - INT4 quantization + weight pruning — pack-time
--quantize int4and--prune <threshold>for extreme size reduction. - Docker/OCI image generation —
modelc containerize <artifact>emits a minimal Dockerfile + entrypoint. - Model versioning — store multiple versions and
modelc switch <name> <version>. - Shell completions — generate bash/zsh/fish completions for all subcommands.
- Per-op profiling —
--profileflag onrunprints timing per inference step. - KV cache —
KvCachestores per-layer Key/Value vectors for autoregressive generation.forward_gpt2_cached/forward_llama_cachedappend current K/V and compute attention over all cached positions, eliminating redundant recomputation. - Autoregressive text generation —
src/generate.rsprovidesgenerate()with greedy, temperature, and top-p (nucleus) sampling, KV cache reuse, and token embedding lookup. Wires into the HTTP server so/chat,/complete, and/v1/chat/completionsreturn real generated text for GPT-2 / LLaMA models. Accepts per-requestmax_tokens,temperature, andtop_poverrides on all text generation endpoints. - Byte-level BPE tokenizer —
src/tokenizer.rsprovides encode/decode with greedy merge algorithm. Foundation for real text-in/text-out transformer inference. - GGUF tokenizer metadata extraction — reads vocab, merges, and BOS/EOS token IDs directly from GGUF KV metadata (
extract_tokenizer_metadata). Constructs aBpeTokenizerfrom in-file tokenizer data without external files. - Chat template rendering —
src/chat_template.rsreads Jinja2 templates from GGUF metadata (tokenizer.chat_template) and formats messages before tokenization usingminijinja. Falls back to simple concatenation when no template is present. - GGUF quantization inference — Q4_0, Q5_0, Q8_0, Q4_K, Q6_K block-quantized tensors are preserved in IR and dequantized on-the-fly by
Runtime::from_raw(src/runtime/serve.rs). Reduces.modelcartifact size ~8x for Q4_0 models while keeping the transformer forward pass functional. Codegen path (compile) callsdequantize_in_placebefore emitting the server so generated binaries still receive FP32 weights. - Structured output / JSON mode —
POST /v1/chat/completionsacceptsresponse_format: { type: "json_object" }to constrain output to valid JSON. Injects a system prompt requesting JSON-only responses and post-processes generated text withextract_json_objectto extract the first well-formed JSON object or array. Falls back to raw text if no valid JSON is found. - Function calling / tool use —
POST /v1/chat/completionsaccepts an OpenAI-compatibletoolsarray. When tools are present, a system prompt describing available tools is injected into the conversation. Generated output is parsed for atool_callsJSON array; if found, the response returnstool_callswithfinish_reason: "tool_calls". Otherwise falls back to regular text content. Supports both inline JSON arguments and pre-serialized argument strings. - Continuous batching (MLP) —
POST /inferwith multipleinputsand MLP architecture routes torun_mlp_forward_batched(src/serve/infer.rs), which computes the entire batch in a single pass viabatched_gemv_bias. Eliminates redundant weight traversal and improves CPU cache locality compared to serial per-item inference. Transformer generation batching is future work. - Speculative decoding —
generate()supportsconfig.gamma > 0to enable speculative decoding with an n-gram draft model (src/generate.rs). The draft model proposesgammacandidate tokens by looking up trigram continuations from the prefix; the target model verifies each candidate in a loop using the KV cache. Accepted tokens skip the sampling step. Now supports prefix caching (restores cached K/V before priming), context shifting (max_context), and grammar constraints (sample_constrained). Disabled by default (gamma: 0). Establishes infrastructure for future faster draft models (e.g., smaller transformer or prompt lookup decoding). - Streaming token generation —
/chat/streamand/v1/chat/completionswithstream: truegenerate token IDs viagenerate_token_ids(), then decode and emit each token incrementally through SSE. After each new token, the cumulative sequence is decoded and only the text delta is sent. The OpenAI-compatible stream emitschat.completion.chunkobjects withdelta.roleon the first chunk anddelta.contentper token, ending withfinish_reason: "stop"and a[DONE]sentinel. Non-transformer models fall back to a single chunk. - Batched embeddings —
POST /embeddingsacceptsinputs: ["...", "..."]and returnsembeddings: [{"embedding": [...], "index": 0}, ...]. Single-inputinput: "..."remains backward compatible and returnsembedding: [...]. - Token-level logprobs —
POST /v1/chat/completionsaccepts OpenAI-compatiblelogprobs: trueandtop_logprobs: N(0–20).generate_with_logprobs()(src/generate.rs) recordsln(P(token))from the raw pre-temperature softmax plus up to N top alternatives per position, so logprobs are deterministic regardless of sampling temperature. The response carrieschoices[].logprobs.contentwith per-tokentoken,logprob, raw UTF-8bytes, andtop_logprobsentries. The field isnullwhen not requested (matching OpenAI); non-transformer models return an emptycontentarray. - Quantized KV cache —
KvLayer::Int8(src/runtime/transformer.rs) stores per-token K/V vectors as INT8 with per-token scales, giving ~4× memory reduction vs FP32.KvCache::new_quantized(n_layers, hidden, use_int8)selects INT8 or FP32 at creation;GenerationConfig.use_int8_kvwires the choice throughgenerate_token_ids,generate_with_logprobs, andspeculative_generate. Dequantization is transparent viak_all/v_all, so attention and generation logic is unchanged. - Prefix caching —
PrefixCache(src/prefix_cache.rs) storesKvCachesnapshots keyed by token sequence. Longest-prefix matching lets later requests whose prompt starts with a cached sequence restore the saved K/V and only process the divergent suffix. Wired intogenerate_core,generate_token_ids_with_cache, andgenerate_with_logprobs_with_cache. The serve layer locks a per-state cache and passes it through. LRU eviction bounds memory to 32 entries. - LoRA loading at runtime via HTTP —
POST /lora/load { path: "...", alpha: 1.0 }clones the base model tensors, applies a Safetensors LoRA adapter (src/lora.rs), and atomically swaps the runtime viaRwLock<Runtime>.POST /lora/unloadrestores the base model from the storedbase_tensorswithout restarting the server. - Grammar-based constrained decoding —
GenerationConfig.constraintholds an optionalArc<dyn Constraint>.RegexConstraint(src/constraint.rs) wraps a regex pattern and masks invalid tokens to-infbefore sampling. Thesample_constrainedhelper ingenerate.rsdecodes the partial output, builds a per-token validity mask, and applies it. HTTP endpoints (/chat,/complete,/v1/chat/completions) accept an optionalgrammarstring (regex pattern) per request. The heuristic is permissive (tries common suffixes to avoid false negatives) rather than exact, which is acceptable for a minimal implementation. - Prometheus metrics —
GET /metricsrenders request counts, inference latency histograms (buckets: 0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, +Inf), active request gauge, and tokens-generated counter in Prometheus text format. Handlers useActiveRequestGuardandInferenceTimerRAII structs so instrumentation is automatic and zero-overhead when not scraped. - JSON Schema constrained generation —
POST /chat,/complete, and/v1/chat/completionsaccept an optionaljson_schemaobject per request. Generated text is parsed as JSON and validated against the schema using thejsonschemacrate. If invalid, generation is retried up to 3 times with increasing temperature to encourage diversity. More robust than the post-hocjson_objectmode (response_format: { type: "json_object" }), which still works for backward compatibility. - Context shifting (sliding window KV cache) —
GenerationConfig.max_contextsets a hard context limit. When total tokens exceed it during generation,generate_coreshifts both theKvCache(KvLayer::shiftremoves oldest K/V vectors from every layer) andtoken_idsleft, keeping roughly the newest 75%.prompt_lenis adjusted so generation continues seamlessly. Enables effectively infinite context length at the cost of losing distant history. - Mixed-precision KV cache —
KvLayer::Mixedstores recent tokens in FP32 and automatically moves older tokens to INT8 cold storage when the hot window (default 64 tokens) overflows. Preserves accuracy for the actively attended context while reducing memory footprint vs pure FP32. Compatible with prefix caching, context shifting, and speculative decoding. - Anchor token preservation (StreamingLLM-style) —
GenerationConfig.anchor_tokens(default 0, set via--anchor-tokens N) preserves the first N initial tokens during context shifting. The first few tokens act as "attention sinks"; evicting them degrades generation quality.KvLayer::shift_anchoredremoves tokens from the middle (after anchors) rather than from the beginning. Works with all KV cache variants (FP32, INT8, Mixed). - Concurrent transformer generation — The prefix cache was a
Mutexthat serialized all transformer requests. Refactored toRwLockwith brief lock hold times: read-lock for lookup, release during generation, write-lock for insertion. Multiple transformer requests now run in parallel, limited only by CPU cores. Covered byrun_server_handles_concurrent_transformer_requeststest. - Attention allocation optimization —
attention_kvpreviously allocated ~3×n_heads buffers per attention call (e.g., ~432 allocations per token for a 12-layer, 12-head model). Rewrote to use a single pre-allocated scratch buffer reused across heads, plussoftmax_inplacewith zero allocations. Eliminates the dominant allocation hotspot in the transformer forward pass. - OpenAI
/v1/completionsendpoint — Legacy (non-chat) completions API:POST /v1/completions { model, prompt, max_tokens, temperature, top_p, stop, stream }. Returnstext_completionobjects withchoices[].text. Supports streaming SSE,logprobs,top_logprobs,grammar, andjson_schema. - EAGLE3 / neural speculative decoding —
src/draft.rsintroduces aDraftModeltrait and anMlpDraftModel: a tiny 2-layer MLP (embedding → FC1 + ReLU → FC2 → logits) that is orders of magnitude faster than the full transformer. Falls back toPromptLookupDraftModel(weight-free, searches the token context for repeated substrings) when no neural draft tensors are present. Wired into all serve endpoints automatically. - API key authentication + rate limiting —
modelc runaccepts--api-key <key>(requiresAuthorization: Bearer <key>on all endpoints except/healthand/info) and--rate-limit <N>(max requests per minute per client IP). Returns standard HTTP status codes (401 Unauthorized,429 Too Many Requests). - Stop sequences —
GenerationConfig.stopholds a list of strings that halt generation when any appear in the decoded output. Checked after each token; output is truncated to just before the matched sequence. Wired through all text generation endpoints. - Repetition / presence / frequency penalties — three sampling penalties discourage token reuse.
repetition_penalty(multiplicative, transformers-style) and OpenAI-standardpresence_penalty(once per seen token) /frequency_penalty(scales with count) adjust logits before sampling. Wired through all text-generation endpoints (/chat,/complete,/v1/chat/completions,/v1/completions) as optional request fields and the CLI (--repetition-penalty,--presence-penalty,--frequency-penalty). They share a fast path that skips the per-token allocation when all are at their defaults. - Logit bias — OpenAI-standard
logit_biasparameter: a JSON object mapping token ID to additive bias (-100 to 100). Positive values boost a token's likelihood; negative values suppress it. Applied after penalties but before grammar constraints. Wired through all text-generation endpoints as an optionallogit_biasrequest field. Empty map = fast path (no allocation). echoandnfor/v1/completions— OpenAI-standard parameters:echo: trueprepends the prompt to the completion in the response (streaming emits the prompt as the first chunk);n: Ngenerates N independent completions (clamped 1–20) with incrementingindexinchoices.nis also supported on/v1/chat/completions./propsserver properties endpoint —GET /props(llama.cpp-compatible) exposes model name, architecture, total params/bytes, chat template, and default generation parameters so clients can auto-configure without trial-and-error. Exempt from auth and backpressure./detokenizeendpoint —POST /detokenize(llama.cpp-compatible) decodes token IDs back to text, complementing/tokenizefor round-trip token management.suffixfor/v1/completions— OpenAI-standardsuffixparameter appends text after the generated completion. Withecho: true, output isprompt + completion + suffix.stream_options.include_usage— OpenAI-standard streaming option for/v1/chat/completionsand/v1/completions. Whenstream_options: { include_usage: true }is set, the SSE stream emits a final chunk with emptychoicesand ausageobject before[DONE].GET /v1/models/:id— OpenAI-standard model retrieval endpoint. Returns a single model object by ID, or404 model_not_foundfor unknown IDs.best_offor/v1/completions— OpenAI-standardbest_ofgenerates N completions server-side (clamped 1–20), picks the best by token count, and returns the topn.userfield — OpenAI-standarduserparameter accepted on both/v1/chat/completionsand/v1/completionsfor abuse tracking. Accepted but ignored.response_format: { type: "json_schema" }— OpenAI-standard Structured Outputs for/v1/chat/completions. Injects the schema as a system prompt and validates/retries generated output against it.tool_choicefor/v1/chat/completions— OpenAI-standard tool choice control:"auto"(default),"none"(no tools),"required"(must call a tool), or{"type": "function", "function": {"name": "..."}}(must call named tool).namefield on messages — OpenAI-standardnamefield on chat message objects, accepted and serialized in responses.max_completion_tokens— OpenAI's newer parameter name formax_tokens(o1+ models). Accepted on both endpoints; takes precedence overmax_tokenswhen both are provided.- Batch
promptfor/v1/completions— OpenAI-standard batch mode. Whenpromptis an array of strings, generates completions for each prompt with incrementingindex. - Min-p sampling —
min_pkeeps only tokens whose probability is at leastmin_pfraction of the max probability, then renormalizes. The threshold scales with the model's confidence per step, making it simpler and often more effective than top-p (nucleus). Can combine withtop_p(min-p runs after). Wired through all text-generation endpoints asmin_pand the CLI (--min-p). - Tokenize endpoint —
POST /tokenizereturns token IDs and count for a prompt (singleinputor batchinputs), using the same byte-level BPE tokenizer as/chat. Standard in Ollama and llama.cpp server; useful for prompt budgeting and management. - System info endpoint —
GET /v1/systemexposes best-effort hardware info (CPU cores, OS, arch, pointer width, Metal availability, total memory) for orchestration and debugging. No added dependencies (stdlib +/proc/meminfo/sysctl). - Quantization size preview —
modelc inspect --quant-sizespreviews the artifact size under each format (fp32/fp16/int8/int4/q4_0) and the savings vs the current size, without actually quantizing. Computed from element counts, so it's accurate regardless of current dtype. - Request cancellation on disconnect — when an SSE streaming client (
/chat/stream,/v1/chat/completions,/v1/completionswithstream: true) disconnects, the server trips a cooperative cancel flag checked once per token, aborting the generation loop instead of running to completion. Saves CPU when users stop a response mid-stream.
Repository layout
src/— CLI, parsers,ModelIR, codegen, runtime helpers, ONNX execution engine.src/parsers/— format-specific parsers, modularized by format (gguf/,onnx/subdirectories).src/onnx_exec/— ONNX graph execution plan builder and executor (mod.rs,helpers.rs).src/codegen/native/— native code generator, modularized (mod.rs,forward.rs,helpers.rs).src/serve/— HTTP inference server, modularized (mod.rs,handlers.rs,infer.rs).src/tokenizer.rs— byte-level BPE tokenizer (encode/decode with greedy merge algorithm).src/chat_template.rs— Jinja2 chat template rendering for message formatting before tokenization.
examples/— runnablecargo --exampleprograms (examples/README.md).tests/— integration tests.
See SPEC.md, ARCHITECTURE.md, and TODO.md.
License
Licensed under the Apache License, Version 2.0 (LICENSE).