Skip to main content

vramen

A local inference server that keeps several models resident side by side under a fixed memory quota.

Vramen is a library, not a daemon. You import it, declare a memory budget, and name the models you want to call. It holds each one in its own process for as long as the budget allows, hands it to callers a lease at a time, and evicts the least recently used idle model when something else needs the room.

from vramen import CausalModel, EncoderModel, InferenceModelResourceManager
from vramen import qwen_chat_prompt

manager = InferenceModelResourceManager(quota_gb=24.0)

writer = CausalModel(
    "Qwen/Qwen3-8B", qwen_chat_prompt, manager, mem_required_gb=17.0
)
embedder = EncoderModel(
    "Qwen/Qwen3-Embedding-0.6B", manager, mem_required_gb=2.0
)

answer = writer.complete("You are terse.", "Name three seabirds.", max_new_tokens=64)
vectors = embedder.encode(["a passage of a story", "another passage"])

Both models load on first use and stay loaded. Nothing above starts a server, opens a port, or writes a config file.

Install

uv add vramen
pip install vramen

Requires Python 3.12 or newer. Pulls in torch, transformers, accelerate and tqdm. On macOS the torch wheel from PyPI is the one you want; on Linux with CUDA, install torch yourself from the index that matches your driver before adding vramen, and see Platforms.

To try it without adding it to a project:

uv run --with vramen python

What it does

One process per model. A model is loaded inside a child process and answers requests over a pair of queues. When it is evicted the process exits, which is the only way to be certain the weights and the allocator's arenas are actually gone. A model that crashes takes its own process down and raises ModelNotAvailable to the caller, not the host.

A quota spent in declarations. You tell each model how much room it needs via mem_required_gb, and the manager admits models while the declared sizes fit inside quota_gb. It does not measure a model and then decide. Measurement happens after the fact and is reported back for you to look at, never counted against the budget. This is what makes admission predictable: the same set of models always fits or always does not, regardless of what the allocator happened to do last time.

Leases, not timers. A call holds a lease on its model for its duration. A model under lease cannot be evicted, so a swap waits for the generation in flight rather than killing it. Idle models are the only eviction candidates, and they go oldest first.

Any transformers checkpoint. Causal, encoder and seq2seq models sit under the same quota. CausalModel.complete generates, EncoderModel.encode returns unit length pooled vectors, Seq2SeqModel.complete runs an encoder-decoder edit. They are ordinary AutoModel* loads, so anything on the Hub that transformers can open works, including embedding models whose pooled hidden states no chat API would give you.

What it is not

  • Not a router. It does not choose a model for you or fall back between providers. The caller names the model. If you want request routing across providers, that is LiteLLM or OpenRouter, and they compose with this fine.
  • Not an inference engine. No custom kernels, no paged attention, no continuous batching, no quantization. It calls model.generate. vLLM, SGLang, llama.cpp and MLX are engines; vramen is the thing that decides which of them gets to be in memory, and today it only drives transformers.
  • Not an HTTP server. No port, no OpenAI-compatible endpoint. If you need one, put your own FastAPI in front of it. It is called a server because it serves models to a process, not because it serves requests to a network.
  • Not multi-tenant. Replies carry no request ids, so one caller is in flight per model at a time. It suits a desktop application or a batch job with a handful of models, not a fleet fielding concurrent traffic.
  • Not a cluster scheduler. One machine, one quota, no replicas, no autoscaling, no Kubernetes.

How it compares

Almost everything in this space is a daemon you talk to over a socket. Vramen is the same idea shrunk to an import.

Project What it is How it differs
Ollama GGUF daemon with an HTTP API Residency is OLLAMA_MAX_LOADED_MODELS plus a per-model keep_alive idle timer, so you cap a model count and a staleness window rather than a memory budget. Chat and embedding endpoints only.
LM Studio Desktop app and server JIT loading with an idle TTL and auto-evict, default 60 minutes. GUI first, closed source, timer driven.
Lemonade Server Multi-model server The closest eviction policy of the servers: keeps several models loaded and evicts LRU at the limit. Still a server, still model-count shaped.
LocalAI OpenAI-compatible multi-backend server Has VRAM management and idle unload across backends. A whole platform where this is a module.
mlx-serve Apple silicon MLX server Hot-swaps MLX models with auto-unload on inactivity. MLX rather than torch, and over HTTP.
vLLM / TGI Throughput engines One model per server instance, tuned for concurrent traffic. The opposite problem: many requests against one model, not many models against one machine.
Triton Model repository daemon EXPLICIT model control mode makes load and unload your job through an API. Vramen decides for you, from the budget.
Ray Serve Distributed serving framework @serve.multiplexed is the nearest relative: LRU eviction of models within a replica. Bounded by max_num_models_per_replica, a count again, and it wants a Ray cluster.

The recurring difference is the budget. These tools bound residency by how many models may be loaded, or by how long an idle one may linger. Vramen bounds it by gigabytes, which is the thing that actually runs out, and it declines a model that cannot fit instead of discovering the problem during a load.

The second difference is the boundary. If your application is already Python and already holds the manuscript, the index and the request, a daemon on localhost means serializing your data out and back for every call. An import does not.

Using it

Sizing the quota

mem_required_gb is your estimate of a model's resident size, and the quota is what you are willing to spend in total. Leave the machine some room.

from vramen import machine_memory

manager = InferenceModelResourceManager(quota_gb=machine_memory() * 0.6)

A model whose declaration exceeds the whole quota is refused immediately with ModelNotAvailable rather than being loaded and killed.

Generating

from vramen import CausalModel, ModelNotAvailable, qwen_chat_prompt

model = CausalModel("Qwen/Qwen3-8B", qwen_chat_prompt, manager, mem_required_gb=17.0)

try:
    text = model.complete("You are an editor.", "Tighten this line.", max_new_tokens=256)
except ModelNotAvailable as failure:
    ...

The third argument to CausalModel is a prompt formatter, (system, user) -> str. qwen_chat_prompt renders Qwen's chat template with reasoning turned off; coedit_prompt renders the instruction form CoEdIT models expect. Any callable of that shape works.

Embedding

from vramen import EncoderModel

encoder = EncoderModel("Qwen/Qwen3-Embedding-0.6B", manager, mem_required_gb=2.0)
vectors = encoder.encode(["first passage", "second passage"])

Passages are encoded as they stand, in batches, pooled from the last real token of each row and normalized to unit length, so a dot product is a cosine.

Editing

from vramen import Seq2SeqModel, coedit_prompt

editor = Seq2SeqModel("grammarly/coedit-large", coedit_prompt, manager, mem_required_gb=3.0)
fixed = editor.complete("Fix grammar", "she dont know", max_new_tokens=64)

Watching it

manager.residents          # the models loaded right now
manager.memory()           # gpu_used, gpu_limit, process, as the children last reported
manager.shutdown()         # stop every serving process, hand the quota back

memory() reads what the serving processes volunteered on their own queue, so asking never queues behind a generation that runs for minutes.

Logging

from vramen import log

log.setup()

Loads, evictions, tokens per second and memory readings go to the root logger at INFO.

Adding a model kind

Subclass ModelKind and implement load. Whatever you return is handed to the requests you send through residency.

from vramen.resource_manager import ModelKind

class VisionModel(ModelKind):
    def load(self):
        return AutoModelForVision2Seq.from_pretrained(self.model_id), AutoProcessor.from_pretrained(self.model_id)

The instance is pickled into the child process, so keep its attributes picklable. The manager reference is dropped on the way across.

Platforms

macOS on Apple silicon is what this is built for and tested on. Models load onto MPS, and the memory readings come from torch.mps.

Linux is not supported out of the box today. The three model classes pass device_map="mps" to from_pretrained, so CUDA needs that changed. Everything else is portable: the process, queue and quota machinery is plain multiprocessing, and process_memory already handles the Linux units for ru_maxrss. Off MPS the GPU figures report 0.0, which does not affect admission, since the quota is spent in declarations rather than measurements.

Windows does not work. vramen.utils imports resource, which is POSIX only, so the package fails at import. WSL is the path there.

Developing

git clone https://github.com/robodatalab/vramen
cd vramen
uv sync
uv run python -m unittest discover -s tests -t . -v

uv sync installs vramen into .venv as an editable install, so import vramen resolves to src/vramen.

Releasing

Publishing runs on PyPI trusted publishing, so no API token is stored in the repository. One-time setup on PyPI, under the project's Publishing settings:

Field Value
Owner robodatalab
Repository vramen
Workflow publish.yml
Environment pypi

After that, cutting a GitHub release runs .github/workflows/publish.yml, which builds the sdist and wheel with uv build and uploads them with uv publish. The job requests a short-lived OIDC token from GitHub, PyPI verifies the claims against the configuration above and mints a scoped upload credential for that run only.

To release, bump version in pyproject.toml, commit, then tag and publish a release on GitHub. To check the artifacts first without uploading:

uv build

License

Apache 2.0. Use it freely, including commercially. Keep the copyright notice and the NOTICE file, and say so if you modify a file.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vramen-0.1.0.tar.gz (20.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vramen-0.1.0-py3-none-any.whl (20.9 kB view details)

Uploaded Python 3

File details

Details for the file vramen-0.1.0.tar.gz.

File metadata

  • Download URL: vramen-0.1.0.tar.gz
  • Upload date:
  • Size: 20.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for vramen-0.1.0.tar.gz
Algorithm Hash digest
SHA256 cdc9afed03188cb5310de850635ac4b4da42b55c198e1ff03b6072eb65bc2fe6
MD5 fcc98a161ba5f018921c82bf03e4efd3
BLAKE2b-256 b7df61b6a7df50f824f53cd5539fb3801db3194b3943daf60a5e20281db1c323

See more details on using hashes here.

File details

Details for the file vramen-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: vramen-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 20.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for vramen-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 35c40f1e2563ee23327a7dfe8db3f5170e2f9153f881d032761e5c2987fdac5f
MD5 5152295f42896ec908f16ec84861a522
BLAKE2b-256 39bb9045ebd270a2cbd8ccf3795e56e6401ed698875f2fbd49dc409b27e63600

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page